Skip to content

Commit 2a37952

Browse files
labkey-ianslabkey-nicka
authored andcommitted
Security (#7)
* Adding initial support of some User/security apis * Moving CSRF token to session header * Syseng, integrating code review feedback. + security unit tests + get_roles api - roles.py * Security: add to package - remove kwargs, reorder functions * Utils: make_request, request_csrf - create_server_context by default requests the CSRF token upon creation. This can be disabled with the request_csrf_token flag.
1 parent cb5f105 commit 2a37952

File tree

12 files changed

+1129
-111
lines changed

12 files changed

+1129
-111
lines changed

labkey/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515
#
16-
from labkey import query, experiment, utils
16+
from labkey import query, experiment, security, utils
1717

1818
__title__ = 'labkey'
1919
__version__ = '0.4.3'

labkey/experiment.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
from __future__ import unicode_literals
1717
import json
1818

19-
from requests.exceptions import SSLError
19+
from requests.exceptions import SSLError, ConnectionError
2020
from labkey.utils import build_url, handle_response
21-
from labkey.exceptions import ServerContextError
21+
from labkey.exceptions import ServerContextError, ServerNotFoundError
2222

2323

2424
# EXAMPLE
@@ -81,6 +81,8 @@ def load_batch(server_context, assay_id, batch_id):
8181
loaded_batch = Batch.from_data(json_body['batch'])
8282
except SSLError as e:
8383
raise ServerContextError(e)
84+
except ConnectionError as e:
85+
raise ServerNotFoundError(e)
8486

8587
return loaded_batch
8688

labkey/query.py

Lines changed: 9 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,7 @@
4747
from __future__ import unicode_literals
4848
import json
4949

50-
from requests.exceptions import SSLError
51-
from labkey.utils import build_url, handle_response
52-
from labkey.exceptions import ServerContextError
50+
from labkey.utils import build_url, make_request
5351

5452

5553
_query_headers = {
@@ -90,9 +88,8 @@ def delete_rows(server_context, schema_name, query_name, rows, container_path=No
9088
}
9189

9290
# explicit json payload and headers required for form generation
93-
delete_rows_response = _make_request(server_context, url, json.dumps(payload, sort_keys=True),
94-
headers=_query_headers, timeout=timeout)
95-
return delete_rows_response
91+
return make_request(server_context, url, json.dumps(payload, sort_keys=True), headers=_query_headers,
92+
timeout=timeout)
9693

9794

9895
def execute_sql(server_context, schema_name, sql, container_path=None,
@@ -151,8 +148,7 @@ def execute_sql(server_context, schema_name, sql, container_path=None,
151148
if required_version is not None:
152149
payload['apiVersion'] = required_version
153150

154-
execute_sql_response = _make_request(server_context, url, payload, timeout=timeout)
155-
return execute_sql_response
151+
return make_request(server_context, url, payload, timeout=timeout)
156152

157153

158154
def insert_rows(server_context, schema_name, query_name, rows, container_path=None, timeout=_default_timeout):
@@ -175,9 +171,8 @@ def insert_rows(server_context, schema_name, query_name, rows, container_path=No
175171
}
176172

177173
# explicit json payload and headers required for form generation
178-
insert_rows_response = _make_request(server_context, url, json.dumps(payload, sort_keys=True),
179-
headers=_query_headers, timeout=timeout)
180-
return insert_rows_response
174+
return make_request(server_context, url, json.dumps(payload, sort_keys=True), headers=_query_headers,
175+
timeout=timeout)
181176

182177

183178
def select_rows(server_context, schema_name, query_name, view_name=None,
@@ -273,8 +268,7 @@ def select_rows(server_context, schema_name, query_name, view_name=None,
273268
if required_version is not None:
274269
payload['apiVersion'] = required_version
275270

276-
select_rows_response = _make_request(server_context, url, payload, timeout=timeout)
277-
return select_rows_response
271+
return make_request(server_context, url, payload, timeout=timeout)
278272

279273

280274
def update_rows(server_context, schema_name, query_name, rows, container_path=None, timeout=_default_timeout):
@@ -298,18 +292,8 @@ def update_rows(server_context, schema_name, query_name, rows, container_path=No
298292
}
299293

300294
# explicit json payload and headers required for form generation
301-
update_rows_response = _make_request(server_context, url, json.dumps(payload, sort_keys=True),
302-
headers=_query_headers, timeout=timeout)
303-
return update_rows_response
304-
305-
306-
def _make_request(server_context, url, payload, headers=None, timeout=_default_timeout):
307-
try:
308-
session = server_context['session']
309-
raw_response = session.post(url, data=payload, headers=headers, timeout=timeout)
310-
return handle_response(raw_response)
311-
except SSLError as e:
312-
raise ServerContextError(e)
295+
return make_request(server_context, url, json.dumps(payload, sort_keys=True), headers=_query_headers,
296+
timeout=timeout)
313297

314298

315299
# TODO: Provide filter generators.

labkey/security.py

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
#
2+
# Copyright (c) 2015-2016 LabKey Corporation
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#
16+
from __future__ import unicode_literals
17+
18+
from labkey.utils import build_url, make_request
19+
20+
security_controller = 'security'
21+
user_controller = 'user'
22+
23+
24+
def activate_users(server_context, target_ids, container_path=None):
25+
"""
26+
Activate user accounts
27+
:param server_context: LabKey server context
28+
:param target_ids:
29+
:param container_path:
30+
:return:
31+
"""
32+
return __make_user_api_request(server_context, target_ids=target_ids, api='activateUsers.api',
33+
container_path=container_path)
34+
35+
36+
def add_to_group(server_context, user_ids, group_id, container_path=None):
37+
"""
38+
Add user to group
39+
:param server_context: LabKey server context
40+
:param user_ids: users to add
41+
:param group_id: to add to
42+
:param container_path:
43+
:return:
44+
"""
45+
return __make_security_group_api_request(server_context, 'addGroupMember.api', user_ids, group_id, container_path)
46+
47+
48+
def add_to_role(server_context, role, user_id=None, email=None, container_path=None):
49+
"""
50+
Add user/group to security role
51+
:param server_context: LabKey server context
52+
:param role: (from get_roles) to add user to
53+
:param user_id: to add permissions role to (must supply this or email or both)
54+
:param email: to add permissions role to (must supply this or user_id or both)
55+
:param container_path: additional project path context
56+
:return:
57+
"""
58+
return __make_security_role_api_request(server_context, 'addAssignment.api', role, user_id=user_id, email=email,
59+
container_path=container_path)
60+
61+
62+
def create_user(server_context, email, container_path=None, send_email=False):
63+
"""
64+
Create new account
65+
:param server_context: LabKey server context
66+
:param email:
67+
:param container_path:
68+
:param send_email: true to send email notification to user
69+
:return:
70+
"""
71+
url = build_url(server_context, security_controller, 'createNewUser.api', container_path)
72+
payload = {
73+
'email': email,
74+
'sendEmail': send_email
75+
}
76+
77+
return make_request(server_context, url, payload)
78+
79+
80+
def deactivate_users(server_context, target_ids, container_path=None):
81+
"""
82+
Deactivate but do not delete user accounts
83+
:param server_context: LabKey server context
84+
:param target_ids:
85+
:param container_path:
86+
:return:
87+
"""
88+
# This action responds with HTML so we just check if it responds OK
89+
response = __make_user_api_request(server_context, target_ids=target_ids, api='deactivateUsers.view',
90+
container_path=container_path)
91+
if response is not None and response['status_code'] == 200:
92+
return dict(success=True)
93+
else:
94+
raise ValueError("Unable to deactivate users {0}".format(target_ids))
95+
96+
97+
def delete_users(server_context, target_ids, container_path=None):
98+
"""
99+
Delete user accounts
100+
:param server_context: LabKey server context
101+
:param target_ids:
102+
:param container_path:
103+
:return:
104+
"""
105+
# This action responds with HTML so we just check if it responds OK
106+
response = __make_user_api_request(server_context, target_ids=target_ids, api='deleteUsers.view',
107+
container_path=container_path)
108+
if response is not None and response['status_code'] == 200:
109+
return dict(success=True)
110+
else:
111+
raise ValueError("Unable to delete users {0}".format(target_ids))
112+
113+
114+
def get_roles(server_context, container_path=None):
115+
"""
116+
Gets the set of permissions and roles available from the server
117+
:param server_context:
118+
:param container_path:
119+
:return:
120+
"""
121+
url = build_url(server_context, security_controller, 'getRoles.api', container_path=container_path)
122+
return make_request(server_context, url, None)
123+
124+
125+
def get_user_by_email(server_context, email):
126+
"""
127+
Get the user with the provided email. Throws a ValueError if not found.
128+
:param server_context: LabKey server context
129+
:param email:
130+
:return:
131+
"""
132+
url = build_url(server_context, user_controller, 'getUsers.api')
133+
payload = dict(includeDeactivatedAccounts=True)
134+
result = make_request(server_context, url, payload)
135+
136+
if result is None or result['users'] is None:
137+
raise ValueError("No Users in container" + email)
138+
139+
for user in result['users']:
140+
if user['email'] == email:
141+
return user
142+
else:
143+
raise ValueError("User not found: " + email)
144+
145+
146+
def list_groups(server_context, include_site_groups=False, container_path=None):
147+
url = build_url(server_context, security_controller, 'listProjectGroups.api', container_path)
148+
149+
payload = {
150+
'includeSiteGroups': include_site_groups
151+
}
152+
153+
return make_request(server_context, url, payload)
154+
155+
156+
def remove_from_group(server_context, user_ids, group_id, container_path=None):
157+
"""
158+
Remove user from group
159+
:param server_context: LabKey server context
160+
:param user_ids:
161+
:param group_id:
162+
:param container_path:
163+
:return:
164+
"""
165+
return __make_security_group_api_request(server_context, 'removeGroupMember.api', user_ids,
166+
group_id, container_path)
167+
168+
169+
def remove_from_role(server_context, role, user_id=None, email=None, container_path=None):
170+
"""
171+
Remove user/group from security role
172+
:param server_context: LabKey server context
173+
:param role: (from get_roles) to remove user from
174+
:param user_id: to remove permissions from (must supply this or email or both)
175+
:param email: to remove permissions from (must supply this or user_id or both)
176+
:param container_path: additional project path context
177+
:return:
178+
"""
179+
return __make_security_role_api_request(server_context, 'removeAssignment.api', role, user_id=user_id, email=email,
180+
container_path=container_path)
181+
182+
183+
def reset_password(server_context, email, container_path=None):
184+
"""
185+
Change password for a user (Requires Admin privileges on the LabKey server)
186+
:param server_context:
187+
:param email:
188+
:param container_path:
189+
:return:
190+
"""
191+
url = build_url(server_context, security_controller, 'adminRotatePassword.api', container_path)
192+
193+
payload = {
194+
'email': email
195+
}
196+
197+
return make_request(server_context, url, payload)
198+
199+
200+
def __make_security_group_api_request(server_context, api, user_ids, group_id, container_path):
201+
"""
202+
Execute a request against the LabKey Security Controller Group Membership apis
203+
:param server_context: LabKey Server context
204+
:param api: Action to execute
205+
:param user_ids: user ids to apply action to
206+
:param group_id: group id to apply action to
207+
:param container_path: Additional container context path
208+
:return: Request json object
209+
"""
210+
url = build_url(server_context, security_controller, api, container_path)
211+
212+
# if user_ids is only a single scalar make it an array
213+
if not hasattr(user_ids, "__iter__"):
214+
user_ids = [user_ids]
215+
216+
payload = {
217+
'groupId': group_id,
218+
'principalIds': user_ids
219+
}
220+
221+
return make_request(server_context, url, payload)
222+
223+
224+
def __make_security_role_api_request(server_context, api, role, email=None, user_id=None, container_path=None):
225+
"""
226+
Execute a request against the LabKey Security Controller Group Membership apis
227+
:param server_context: LabKey Server context
228+
:param api: Action to execute
229+
:param user_id: user ids to apply action to
230+
:param role: (from get_roles) to remove user from
231+
:param container_path: Additional container context path
232+
:return: Request json object
233+
"""
234+
if email is None and user_id is None:
235+
raise ValueError("Must supply either/both [email] or [user_id]")
236+
237+
url = build_url(server_context, security_controller, api, container_path)
238+
239+
payload = {
240+
'roleClassName': role['uniqueName'],
241+
'principalId': user_id,
242+
'email': email
243+
}
244+
245+
return make_request(server_context, url, payload)
246+
247+
248+
def __make_user_api_request(server_context, target_ids, api, container_path=None):
249+
"""
250+
Make a request to the LabKey User Controller
251+
:param server_context: LabKey Server context
252+
:param target_ids: Array of User ids to affect
253+
:param api: action to take
254+
:param container_path: container context
255+
:return: response json
256+
"""
257+
url = build_url(server_context, user_controller, api, container_path)
258+
payload = {
259+
'userId': target_ids
260+
}
261+
262+
return make_request(server_context, url, payload)

0 commit comments

Comments
 (0)