Python3.8 related changes.
[optf/has.git] / conductor / conductor / api / controllers / v1 / plans.py
1 #
2 # -------------------------------------------------------------------------
3 #   Copyright (c) 2015-2017 AT&T Intellectual Property
4 #
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
8 #
9 #       http://www.apache.org/licenses/LICENSE-2.0
10 #
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.
16 #
17 # -------------------------------------------------------------------------
18 #
19 import six
20 import yaml
21 import base64
22 from yaml.constructor import ConstructorError
23
24 from notario import decorators
25 from notario.validators import types
26 from oslo_log import log
27 import pecan
28 from pecan_notario import validate
29
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
35
36 from conductor.api.adapters.aaf import aaf_authentication as aaf_auth
37
38 CONF = cfg.CONF
39
40 LOG = log.getLogger(__name__)
41
42 CONDUCTOR_API_OPTS = [
43     cfg.StrOpt('server_url',
44                default='',
45                help='Base URL for plans.'),
46     cfg.StrOpt('username',
47                default='',
48                help='username for plans.'),
49     cfg.StrOpt('password',
50                default='',
51                help='password for plans.'),
52     cfg.BoolOpt('basic_auth_secure',
53                 default=True,
54                 help='auth toggling.'),
55 ]
56
57 CONF.register_opts(CONDUCTOR_API_OPTS, group='conductor_api')
58
59 CREATE_SCHEMA = (
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),
68 )
69
70
71 class PlansBaseController(object):
72     """Plans Base Controller - Common Methods"""
73
74     def plan_link(self, plan_id):
75         return [
76             {
77                 "href": "%(url)s/v1/%(endpoint)s/%(id)s" %
78                         {
79                             'url': pecan.request.application_url,
80                             'endpoint': 'plans',
81                             'id': plan_id,
82                         },
83                 "rel": "self"
84             }
85         ]
86
87     def plans_get(self, plan_id=None):
88
89         auth_flag = CONF.conductor_api.basic_auth_secure or CONF.aaf_api.is_aaf_enabled
90
91         # TBD - is healthcheck properly supported?
92         if plan_id == 'healthcheck' or \
93                 not auth_flag or \
94                 (auth_flag and check_auth()):
95             return self.plan_getid(plan_id)
96
97     def plan_getid(self, plan_id):
98         ctx = {}
99         method = 'plans_get'
100         if plan_id:
101             args = {'plan_id': plan_id}
102             LOG.debug('Plan {} requested.'.format(plan_id))
103         else:
104             args = {}
105             LOG.debug('All plans requested.')
106
107         plans_list = []
108
109         client = pecan.request.controller
110         result = client.call(ctx, method, args)
111         plans = result and result.get('plans')
112
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)
117
118         if plan_id:
119             if len(plans_list) == 1:
120                 return plans_list[0]
121             else:
122                 # For a single plan, we return None if not found
123                 return None
124         else:
125             # For all plans, it's ok to return an empty list
126             return plans_list
127
128     def plan_create(self, args):
129         ctx = {}
130         method = 'plan_create'
131
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))
146
147         LOG.debug('Plan creation requested (name "{}").'.format(
148             args.get('name')))
149
150         client = pecan.request.controller
151
152         transaction_id = pecan.request.headers.get('transaction-id')
153         if transaction_id:
154             args['template']['transaction-id'] = transaction_id
155
156         result = client.call(ctx, method, args)
157         plan = result and result.get('plan')
158
159         if 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(
164                 plan_id, plan_name))
165
166         return plan
167
168     def plan_delete(self, plan):
169         ctx = {}
170         method = 'plans_delete'
171
172         plan_name = plan.get('name')
173         plan_id = plan.get('id')
174         LOG.debug('Plan {} (name "{}") deletion requested.'.format(
175             plan_id, plan_name))
176
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(
181             plan_id, plan_name))
182
183
184 class PlansItemController(PlansBaseController):
185     """Plans Item Controller /v1/plans/{plan_id}"""
186
187     def __init__(self, uuid4):
188         """Initializer."""
189         self.uuid = uuid4
190         self.plan = self.plans_get(plan_id=self.uuid)
191
192         if not self.plan:
193             error('/errors/not_found',
194                   _('Plan {} not found').format(self.uuid))
195         pecan.request.context['plan_id'] = self.uuid
196
197     @classmethod
198     def allow(cls):
199         """Allowed methods"""
200         return 'GET,DELETE'
201
202     @pecan.expose(generic=True, template='json')
203     def index(self):
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)
209
210     @index.when(method='OPTIONS', template='json')
211     def index_options(self):
212         """Options"""
213         pecan.response.headers['Allow'] = self.allow()
214         pecan.response.status = 204
215
216     @index.when(method='GET', template='json')
217     def index_get(self):
218         """Get plan"""
219         return {"plans": [self.plan]}
220
221     @index.when(method='DELETE', template='json')
222     def index_delete(self):
223         """Delete a Plan"""
224         self.plan_delete(self.plan)
225         pecan.response.status = 204
226
227
228 class PlansController(PlansBaseController):
229     """Plans Controller /v1/plans"""
230
231     @classmethod
232     def allow(cls):
233         """Allowed methods"""
234         return 'GET,POST'
235
236     @pecan.expose(generic=True, template='json')
237     def index(self):
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)
243
244     @index.when(method='OPTIONS', template='json')
245     def index_options(self):
246         """Options"""
247         pecan.response.headers['Allow'] = self.allow()
248         pecan.response.status = 204
249
250     @index.when(method='GET', template='json')
251     def index_get(self):
252         """Get all the plans"""
253         plans = self.plans_get()
254         return {"plans": plans}
255
256     @index.when(method='POST', template='json')
257     @validate(CREATE_SCHEMA, '/errors/schema')
258     def index_post(self):
259         """Create a Plan"""
260
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"
265         try:
266             parsed = yaml.load(pecan.request.text, validator.UniqueKeyLoader)
267             if 'template' in parsed:
268                 where = "Template"
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):
275             if exc.problem is \
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.
287             pass
288
289         args = pecan.request.json
290
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']))
294
295         auth_flag = CONF.conductor_api.basic_auth_secure or CONF.aaf_api.is_aaf_enabled
296
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)
301
302         if not plan:
303             error('/errors/server_error', _('Unable to create Plan.'))
304         else:
305             pecan.response.status = 201
306             return plan
307
308     @pecan.expose()
309     def _lookup(self, uuid4, *remainder):
310         """Pecan subcontroller routing callback"""
311         return PlansItemController(uuid4), remainder
312
313
314 def check_auth():
315     """
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
319     """
320
321     try:
322         if pecan.request.headers['Authorization'] and verify_user(pecan.request.headers['Authorization']):
323             LOG.debug("Authorized username and password")
324             plan = True
325         else:
326             plan = False
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]))
332     except:
333         error('/errors/basic_auth_error', _('Unauthorized: The request does not '
334                                             'provide any HTTP authentication (basic authentication)'))
335         plan = False
336
337     if not plan:
338         error('/errors/authentication_error', _('Invalid credentials: username or password is incorrect'))
339
340     return plan
341
342
343 def verify_user(authstr):
344     """
345     authenticate user as per config file or AAF authentication service
346     :param authstr:
347     :return boolean value
348     """
349     user_dict = dict()
350     auth_str = authstr
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
359
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']))
362
363     retVal = False
364
365     if CONF.aaf_api.is_aaf_enabled:
366         retVal = aaf_auth.authenticate(user_dict['username'], user_dict['password'])
367     else:
368         if username == user_dict['username'] and password == user_dict['password']:
369             retVal = True
370
371     return retVal
372