Skip to content

Commit 3891483

Browse files
committed
Added authorization to all API calls
Apply authorization rules from a local yaml file based on the identity, resource and action being performed to determine whether to allow the call to proceed. - - Implement the same rules as currently defined, i.e. users cannot modify each other's applications unless they are admin - Extend this to cover packages as well as applications PNDA-4560
1 parent 7a79978 commit 3891483

12 files changed

+546
-120
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ Example response:
159159
"status": "DEPLOYED",
160160
"version": "1.0.23",
161161
"name": "spark-batch-example-app",
162+
"user": "who-deployed-this",
162163
"defaults": {
163164
"oozie": {
164165
"example": {
@@ -193,6 +194,7 @@ DELETE /packages/<package>
193194
194195
Response Codes:
195196
202 - Accepted, poll /packages/<package>/status for status
197+
403 - Unauthorised user
196198
404 - Package not deployed
197199
500 - Server Error
198200
````

api/src/main/resources/app.py

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -69,27 +69,29 @@ def finish():
6969
if isinstance(ex, NotFound):
7070
logging.info(ex.msg)
7171
self.set_status(404)
72+
# Format already expected to be JSON when raised
7273
self.finish(ex.msg)
7374
elif isinstance(ex, ConflictingState):
7475
logging.info(ex.msg)
7576
self.set_status(409)
77+
# Format already expected to be JSON when raised
7678
self.finish(ex.msg)
7779
elif isinstance(ex, FailedValidation):
7880
logging.info(ex.msg)
7981
self.set_status(400)
80-
self.finish(ex.msg)
82+
self.finish({"information": str(ex.msg)})
8183
elif isinstance(ex, Forbidden):
8284
logging.info(ex.msg)
8385
self.set_status(403)
84-
self.finish(ex.msg)
86+
self.finish({"information": str(ex.msg)})
8587
elif isinstance(ex, FailedCreation):
8688
logging.info(ex.msg)
8789
self.set_status(500)
88-
self.finish(ex.msg)
90+
self.finish({"information": str(ex.msg)})
8991
elif isinstance(ex, FailedConnection):
9092
logging.info(ex.msg)
9193
self.set_status(503)
92-
self.finish(ex.msg)
94+
self.finish({"information": str(ex.msg)})
9395
else:
9496
self.set_status(500)
9597
if "information" in str(ex):
@@ -137,7 +139,7 @@ class EnvironmentHandler(BaseHandler):
137139
@asynchronous
138140
def get(self):
139141
def do_call():
140-
self.send_result(dm.get_environment())
142+
self.send_result(dm.get_environment(self.get_argument("user.name", default='')))
141143

142144
DISPATCHER.run_as_asynch(task=do_call, on_error=self.handle_error)
143145

@@ -151,7 +153,7 @@ def do_call():
151153
recency = 1
152154
if 'recency' in args:
153155
recency = int(args['recency'][0])
154-
self.send_result(dm.list_repository(recency))
156+
self.send_result(dm.list_repository(recency, self.get_argument("user.name", default='')))
155157

156158
DISPATCHER.run_as_asynch(task=do_call, on_error=self.handle_error)
157159

@@ -160,7 +162,7 @@ class PackagesHandler(BaseHandler):
160162
@asynchronous
161163
def get(self):
162164
def do_call():
163-
self.send_result(dm.list_packages())
165+
self.send_result(dm.list_packages(self.get_argument("user.name", default='')))
164166

165167
DISPATCHER.run_as_asynch(task=do_call, on_error=self.handle_error)
166168

@@ -169,22 +171,22 @@ class PackageHandler(BaseHandler):
169171
@asynchronous
170172
def get(self, name):
171173
def do_call():
172-
self.send_result(dm.get_package_info(name))
174+
self.send_result(dm.get_package_info(name, self.get_argument("user.name", default='')))
173175

174176
DISPATCHER.run_as_asynch(task=do_call, on_error=self.handle_error)
175177

176178
@asynchronous
177179
def put(self, name):
178180
def do_call():
179-
dm.deploy_package(name)
181+
dm.deploy_package(name, self.get_argument("user.name"))
180182
self.send_accepted()
181183

182184
DISPATCHER.run_as_asynch(task=do_call, on_error=self.handle_error)
183185

184186
@asynchronous
185187
def delete(self, name):
186188
def do_call():
187-
dm.undeploy_package(name)
189+
dm.undeploy_package(name, self.get_argument("user.name"))
188190
self.send_accepted()
189191

190192
DISPATCHER.run_as_asynch(task=do_call, on_error=self.handle_error)
@@ -194,7 +196,7 @@ class PackageApplicationsHandler(BaseHandler):
194196
@asynchronous
195197
def get(self, name):
196198
def do_call():
197-
self.send_result(dm.list_package_applications(name))
199+
self.send_result(dm.list_package_applications(name, self.get_argument("user.name", default='')))
198200

199201
DISPATCHER.run_as_asynch(task=do_call, on_error=self.handle_error)
200202

@@ -203,7 +205,7 @@ class PackageStatusHandler(BaseHandler):
203205
@asynchronous
204206
def get(self, name):
205207
def do_call():
206-
package_info = dm.get_package_info(name)
208+
package_info = dm.get_package_info(name, self.get_argument("user.name", default=''))
207209
self.send_result({
208210
"status": package_info.get("status"),
209211
"information": package_info.get("information", None)
@@ -216,7 +218,7 @@ class ApplicationsHandler(BaseHandler):
216218
@asynchronous
217219
def get(self):
218220
def do_call():
219-
self.send_result(dm.list_applications())
221+
self.send_result(dm.list_applications(self.get_argument("user.name", default='')))
220222

221223
DISPATCHER.run_as_asynch(task=do_call, on_error=self.handle_error)
222224

@@ -241,16 +243,16 @@ def do_call():
241243
def get(self, name, action):
242244
def do_call():
243245
if action == 'status':
244-
app_info = dm.get_application_info(name)
246+
app_info = dm.get_application_info(name, self.get_argument("user.name", default=''))
245247
ret = {
246248
"status": app_info["status"],
247249
"information": app_info.get("information", None)
248250
}
249251
self.send_result(ret)
250252
elif action == 'detail':
251-
self.send_result(dm.get_application_detail(name))
253+
self.send_result(dm.get_application_detail(name, self.get_argument("user.name", default='')))
252254
elif action == 'summary':
253-
self.send_result(dm.get_application_summary(name))
255+
self.send_result(dm.get_application_summary(name, self.get_argument("user.name", default='')))
254256
else:
255257
self.send_client_error("%s is not a valid query (status|detail|summary)" % action)
256258

@@ -275,15 +277,15 @@ def put(self, aname):
275277
user_name = self.get_argument("user.name")
276278
def do_call():
277279
request_body.update({'user': user_name})
278-
dm.create_application(request_body['package'], aname, request_body)
280+
dm.create_application(request_body['package'], aname, request_body, user_name)
279281
self.send_accepted()
280282

281283
DISPATCHER.run_as_asynch(task=do_call, on_error=self.handle_error)
282284

283285
@asynchronous
284286
def get(self, name):
285287
def do_call():
286-
self.send_result(dm.get_application_info(name))
288+
self.send_result(dm.get_application_info(name, self.get_argument("user.name", default='')))
287289

288290
DISPATCHER.run_as_asynch(task=do_call, on_error=self.handle_error)
289291

api/src/main/resources/application_summary_registrar.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import json
22
import logging
33
import happybase
4-
from Hbase_thrift import AlreadyExists
54
from thriftpy.transport import TTransportException
5+
from Hbase_thrift import AlreadyExists
66

77
#pylint: disable=E0602
88

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""
2+
Name: authorizer.py
3+
Purpose: Interface definition for modules that validate requests to access resources by comparing the attributes of an
4+
identity, a resource and an action against a set of rules.
5+
6+
Author: PNDA team
7+
8+
Created: 17/05/2018
9+
10+
Copyright (c) 2018 Cisco and/or its affiliates.
11+
12+
This software is licensed to you under the terms of the Apache License, Version 2.0 (the "License").
13+
You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
14+
15+
The code, technical concepts, and all information contained herein, are the property of Cisco Technology, Inc.
16+
and/or its affiliated entities, under various laws including copyright, international treaties, patent,
17+
and/or contract. Any use of the material herein must be in accordance with the terms of the License.
18+
All rights not expressly granted by the License are reserved.
19+
20+
Unless required by applicable law or agreed to separately in writing, software distributed under the
21+
License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
22+
either express or implied.
23+
"""
24+
25+
class Authorizer(object):
26+
'''
27+
Interface definition that validate requests to access resources
28+
'''
29+
def authorize(self, identity, resource, action):
30+
'''
31+
Validate a request to access a resource
32+
Parameters:
33+
- identity: dictionary of attributes defining the user performing the action
34+
- resource: dictionary of attributes defining the resource that access is required for
35+
- action: dictionary of attributes defining the action being performed
36+
'''
37+
pass
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""
2+
Name: authorizer_local.py
3+
Purpose: Validates requests to access resources by comparing the attributes of an
4+
identity, a resource and an action against rules defined in authorizer_rules.yaml
5+
6+
Author: PNDA team
7+
8+
Created: 17/05/2018
9+
10+
Copyright (c) 2018 Cisco and/or its affiliates.
11+
12+
This software is licensed to you under the terms of the Apache License, Version 2.0 (the "License").
13+
You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
14+
15+
The code, technical concepts, and all information contained herein, are the property of Cisco Technology, Inc.
16+
and/or its affiliated entities, under various laws including copyright, international treaties, patent,
17+
and/or contract. Any use of the material herein must be in accordance with the terms of the License.
18+
All rights not expressly granted by the License are reserved.
19+
20+
Unless required by applicable law or agreed to separately in writing, software distributed under the
21+
License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
22+
either express or implied.
23+
"""
24+
import logging
25+
import json
26+
import yaml
27+
from authorizer import Authorizer
28+
29+
class AuthorizerLocal(Authorizer):
30+
'''
31+
Authorizer implementation that validates requests based on locally defined rules
32+
'''
33+
def __init__(self):
34+
'''
35+
Initialise the authorizer by loading the rules file
36+
'''
37+
with open('authorizer_rules.yaml') as rules_file:
38+
self._rules = yaml.load(rules_file)
39+
40+
def authorize(self, identity, resource, action):
41+
'''
42+
Validate a request to access a resource
43+
Parameters:
44+
- identity: dictionary of attributes defining the user performing the action
45+
- resource: dictionary of attributes defining the resource that access is required for
46+
- action: dictionary of attributes defining the action being performed
47+
'''
48+
logging.debug("authorize: identity:%s, resource:%s, action:%s", json.dumps(identity), json.dumps(resource), json.dumps(action))
49+
sets = self._rules['sets']
50+
logging.debug("sets: %s", json.dumps(sets))
51+
authorize = False
52+
for grant_rule in self._rules['rules']['grant']:
53+
logging.debug("checking rule: %s", grant_rule)
54+
try:
55+
#pylint: disable=eval-used
56+
if eval(grant_rule):
57+
authorize = True
58+
logging.debug("authorize: %s for %s", authorize, grant_rule)
59+
break
60+
except KeyError, ex:
61+
logging.warning("missing attribute %s", ex)
62+
63+
if not authorize:
64+
logging.debug("authorize: %s", authorize)
65+
66+
return authorize
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#
2+
# Name: authorizer_rules.yaml
3+
# Purpose: rules defining who is allowed to perform actions on deployment manager resources
4+
#
5+
# Author: PNDA team
6+
#
7+
# Created: 17/05/2018
8+
#
9+
# Copyright (c) 2018 Cisco and/or its affiliates.
10+
#
11+
# This software is licensed to you under the terms of the Apache License, Version 2.0 (the "License").
12+
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
13+
#
14+
# The code, technical concepts, and all information contained herein, are the property of Cisco Technology, Inc.
15+
# and/or its affiliated entities, under various laws including copyright, international treaties, patent,
16+
# and/or contract. Any use of the material herein must be in accordance with the terms of the License.
17+
# All rights not expressly granted by the License are reserved.
18+
#
19+
# Unless required by applicable law or agreed to separately in writing, software distributed under the
20+
# License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
21+
# either express or implied.
22+
23+
# action, resource and identity are dictiionaries of attributes defining the action, resource and identity respectively
24+
# action
25+
# - name: the name of the action being performed e.g. 'deployment_manager:package:deploy'
26+
# resource
27+
# - type: the resource type, e.g. 'deployment_manager:package'
28+
# identity
29+
# - user: the username of the user performing this action
30+
# - groups: the (linux) groups that this user belongs to
31+
rules:
32+
grant:
33+
- "action['name'] in sets['owner_only_actions'] and (resource['owner'] == identity['user'] or 'admin' in identity['groups'])"
34+
- "action['name'] in sets['everyone_actions']"
35+
36+
# Sets allow arbitrary groups to be referenced as a whole in the rules and reused
37+
sets:
38+
owner_only_actions:
39+
- "deployment_manager:package:undeploy"
40+
- "deployment_manager:application:start"
41+
- "deployment_manager:application:stop"
42+
- "deployment_manager:application:destroy"
43+
44+
everyone_actions:
45+
- "deployment_manager:package:deploy"
46+
- "deployment_manager:application:create"
47+
- "deployment_manager:package:read"
48+
- "deployment_manager:application:read"
49+
- "deployment_manager:environment:read"
50+
- "deployment_manager:packages:read"
51+
- "deployment_manager:applications:read"
52+
- "deployment_manager:repository:read"

0 commit comments

Comments
 (0)