Skip to content

Commit 9a4daf6

Browse files
authoredMay 12, 2021
Add impersonation APIs, add json kwarg to make_request (#46)
- ServerContext.make_request: payload is now optional - Updated usages that passed None to pass nothing - ServerContext.make_request: add json kwarg - This automatically does json_dumps and sets the content-type header for you - Updated usages of make_request to use the json kwarg where applicable - Add impersonate_user and stop_impersonating to security module (including APIWrapper) - Fix imports for mock in unit tests - Remove unused test_utils file. - Add more environment variables for integration test configuration - host, port, context_path can all be overridden via env vars
1 parent 3fc21ed commit 9a4daf6

16 files changed

+184
-141
lines changed
 

‎.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ labkey\.egg-info/
1313

1414
.DS_Store
1515
.python-version
16+
.vscode/

‎CHANGE.txt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@ What's New in the LabKey 2.1.0 package
66
==============================
77

88
*Release date: TBD*
9-
Adding support for ontology based column filters ONTOLOGY_IN_SUBTREE and ONTOLOGY_NOT_IN_SUBTREE
9+
- Add support for ontology based column filters ONTOLOGY_IN_SUBTREE and ONTOLOGY_NOT_IN_SUBTREE
10+
- ServerContext.make_request: payload is now optional
11+
- ServerContext.make_request: add json kwarg
12+
- This automatically does json_dumps and sets the content-type header for you
13+
- Add impersonate_user and stop_impersonating to security module (including APIWrapper)
14+
- Add more environment variables for integration test configuration
15+
- host, port, and context_path can now be overridden via env vars
1016

1117
What's New in the LabKey 2.0.1 package
1218
==============================

‎labkey/container.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from .server_context import ServerContext
2-
from labkey.utils import json_dumps
32

43

54
def create(
@@ -23,7 +22,6 @@ def create(
2322
:param title: the title for the container.
2423
:return:
2524
"""
26-
headers = {"Content-Type": "application/json"}
2725
url = server_context.build_url("core", "createContainer.api", container_path)
2826
payload = {
2927
"description": description,
@@ -32,7 +30,7 @@ def create(
3230
"name": name,
3331
"title": title,
3432
}
35-
return server_context.make_request(url, json_dumps(payload), headers=headers)
33+
return server_context.make_request(url, json=payload)
3634

3735

3836
def delete(server_context: ServerContext, container_path: str = None) -> any:
@@ -44,7 +42,7 @@ def delete(server_context: ServerContext, container_path: str = None) -> any:
4442
"""
4543
headers = {"Content-Type": "application/json"}
4644
url = server_context.build_url("core", "deleteContainer.api", container_path)
47-
return server_context.make_request(url, None, headers=headers)
45+
return server_context.make_request(url, headers=headers)
4846

4947

5048
class ContainerWrapper:

‎labkey/domain.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
from typing import Union, List
1818

1919
from .server_context import ServerContext
20-
from labkey.utils import json_dumps
2120
from labkey.query import QueryFilter
2221

2322

@@ -390,13 +389,12 @@ def create(
390389
:return: Domain
391390
"""
392391
url = server_context.build_url("property", "createDomain.api", container_path=container_path)
393-
headers = {"Content-Type": "application/json"}
394392
domain = None
395393
domain_fields = domain_definition["domainDesign"]["fields"]
396394
domain_definition["domainDesign"]["fields"] = list(
397395
map(__format_conditional_filters, domain_fields)
398396
)
399-
raw_domain = server_context.make_request(url, json_dumps(domain_definition), headers=headers)
397+
raw_domain = server_context.make_request(url, json=domain_definition)
400398

401399
if raw_domain is not None:
402400
domain = Domain(**raw_domain)
@@ -416,10 +414,9 @@ def drop(
416414
:return:
417415
"""
418416
url = server_context.build_url("property", "deleteDomain.api", container_path=container_path)
419-
headers = {"Content-Type": "application/json"}
420417
payload = {"schemaName": schema_name, "queryName": query_name}
421418

422-
return server_context.make_request(url, json_dumps(payload), headers=headers)
419+
return server_context.make_request(url, json=payload)
423420

424421

425422
def get(
@@ -455,7 +452,7 @@ def infer_fields(
455452
:return:
456453
"""
457454
url = server_context.build_url("property", "inferDomain.api", container_path=container_path)
458-
raw_infer = server_context.make_request(url, None, file_payload={"inferfile": data_file})
455+
raw_infer = server_context.make_request(url, file_payload={"inferfile": data_file})
459456

460457
fields = None
461458
if "fields" in raw_infer:
@@ -483,14 +480,13 @@ def save(
483480
:return:
484481
"""
485482
url = server_context.build_url("property", "saveDomain.api", container_path=container_path)
486-
headers = {"Content-Type": "application/json"}
487483
payload = {
488484
"domainDesign": domain.to_json(),
489485
"queryName": query_name,
490486
"schemaName": schema_name,
491487
}
492488

493-
return server_context.make_request(url, json_dumps(payload), headers=headers)
489+
return server_context.make_request(url, json=payload)
494490

495491

496492
class DomainWrapper:

‎labkey/experiment.py

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
from typing import List, Optional
1818

1919
from .server_context import ServerContext
20-
from labkey.utils import json_dumps
2120

2221

2322
class ExpObject:
@@ -103,7 +102,7 @@ def to_json(self):
103102
data["materialOutputs"] = self.material_outputs
104103
data["plateMetadata"] = self.plate_metadata
105104

106-
# Issue 2489: Drop empty values. Server supplies default values for missing keys,
105+
# Issue 2489: Drop empty values. Server supplies default values for missing keys,
107106
# and will throw exception if a null value is supplied
108107
data = {k: v for k, v in data.items() if v}
109108
return data
@@ -163,14 +162,9 @@ def load_batch(server_context: ServerContext, assay_id: int, batch_id: int) -> O
163162
"""
164163
load_batch_url = server_context.build_url("assay", "getAssayBatch.api")
165164
loaded_batch = None
166-
167165
payload = {"assayId": assay_id, "batchId": batch_id}
166+
json_body = server_context.make_request(load_batch_url, json=payload)
168167

169-
headers = {"Content-type": "application/json", "Accept": "text/plain"}
170-
171-
json_body = server_context.make_request(
172-
load_batch_url, json_dumps(payload, sort_keys=True), headers=headers
173-
)
174168
if json_body is not None:
175169
loaded_batch = Batch(**json_body["batch"])
176170

@@ -215,11 +209,8 @@ def save_batches(
215209
raise Exception('save_batch() "batches" expected to be a set Batch instances')
216210

217211
payload = {"assayId": assay_id, "batches": json_batches}
218-
headers = {"Content-type": "application/json", "Accept": "text/plain"}
212+
json_body = server_context.make_request(save_batch_url, json=payload)
219213

220-
json_body = server_context.make_request(
221-
save_batch_url, json_dumps(payload, sort_keys=True), headers=headers
222-
)
223214
if json_body is not None:
224215
resp_batches = json_body["batches"]
225216
return [Batch(**resp_batch) for resp_batch in resp_batches]

‎labkey/query.py

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,6 @@
4444
from typing import List
4545

4646
from .server_context import ServerContext
47-
from labkey.utils import json_dumps
48-
49-
_query_headers = {"Content-Type": "application/json"}
5047

5148
_default_timeout = 60 * 5 # 5 minutes
5249

@@ -176,8 +173,7 @@ def delete_rows(
176173

177174
return server_context.make_request(
178175
url,
179-
json_dumps(payload, sort_keys=True),
180-
headers=_query_headers,
176+
json=payload,
181177
timeout=timeout,
182178
)
183179

@@ -203,8 +199,7 @@ def truncate_table(
203199

204200
return server_context.make_request(
205201
url,
206-
json_dumps(payload, sort_keys=True),
207-
headers=_query_headers,
202+
json=payload,
208203
timeout=timeout,
209204
)
210205

@@ -294,8 +289,7 @@ def insert_rows(
294289

295290
return server_context.make_request(
296291
url,
297-
json_dumps(payload, sort_keys=True),
298-
headers=_query_headers,
292+
json=payload,
299293
timeout=timeout,
300294
)
301295

@@ -430,8 +424,7 @@ def update_rows(
430424

431425
return server_context.make_request(
432426
url,
433-
json_dumps(payload, sort_keys=True),
434-
headers=_query_headers,
427+
json=payload,
435428
timeout=timeout,
436429
)
437430

‎labkey/security.py

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,15 @@
1414
# limitations under the License.
1515
#
1616
import functools
17+
from dataclasses import dataclass
18+
1719
from typing import Union, List
1820

1921
from labkey.server_context import ServerContext
2022

2123
SECURITY_CONTROLLER = "security"
2224
USER_CONTROLLER = "user"
25+
LOGIN_CONTROLLER = "login"
2326

2427

2528
def activate_users(
@@ -156,7 +159,7 @@ def get_roles(server_context: ServerContext, container_path: str = None):
156159
url = server_context.build_url(
157160
SECURITY_CONTROLLER, "getRoles.api", container_path=container_path
158161
)
159-
return server_context.make_request(url, None)
162+
return server_context.make_request(url)
160163

161164

162165
def get_user_by_email(server_context: ServerContext, email: str):
@@ -167,7 +170,7 @@ def get_user_by_email(server_context: ServerContext, email: str):
167170
:return:
168171
"""
169172
url = server_context.build_url(USER_CONTROLLER, "getUsers.api")
170-
payload = dict(includeDeactivatedAccounts=True)
173+
payload = {"includeDeactivatedAccounts": True}
171174
result = server_context.make_request(url, payload)
172175

173176
if result is None or result["users"] is None:
@@ -184,7 +187,6 @@ def list_groups(
184187
server_context: ServerContext, include_site_groups: bool = False, container_path: str = None
185188
):
186189
url = server_context.build_url(SECURITY_CONTROLLER, "listProjectGroups.api", container_path)
187-
188190
return server_context.make_request(url, {"includeSiteGroups": include_site_groups})
189191

190192

@@ -242,10 +244,68 @@ def reset_password(server_context: ServerContext, email: str, container_path: st
242244
:return:
243245
"""
244246
url = server_context.build_url(SECURITY_CONTROLLER, "adminRotatePassword.api", container_path)
245-
246247
return server_context.make_request(url, {"email": email})
247248

248249

250+
def impersonate_user(
251+
server_context: ServerContext,
252+
user_id: int = None,
253+
email: str = None,
254+
container_path: str = None,
255+
):
256+
"""
257+
For site-admins or project-admins only, start impersonating a user.
258+
259+
Admins may impersonate other users to perform actions on their behalf.
260+
Site users may impersonate any user in any project. Project admins must
261+
execute this command in a project in which they have admin permission
262+
and may impersonate any user that has access to the project.
263+
264+
To finish an impersonation session use `stop_impersonating`.
265+
266+
:param user_id: to impersonate (must supply this or email)
267+
:param email: to impersonate (must supply this or user_id)
268+
:param container_path: in which to impersonate the user
269+
"""
270+
if email is None and user_id is None:
271+
raise ValueError("Must supply either [email] or [user_id]")
272+
273+
url = server_context.build_url(USER_CONTROLLER, "impersonateUser.api", container_path)
274+
return server_context.make_request(url, {"userId": user_id, "email": email})
275+
276+
277+
def stop_impersonating(server_context: ServerContext):
278+
"""
279+
Stop impersonating a user while keeping the original user logged in.
280+
"""
281+
url = server_context.build_url(LOGIN_CONTROLLER, "stopImpersonating.api")
282+
return server_context.make_request(url)
283+
284+
285+
@dataclass
286+
class WhoAmI:
287+
id: int
288+
email: str
289+
display_name: str
290+
impersonated: str
291+
CSRF: str
292+
293+
294+
def who_am_i(server_context: ServerContext) -> WhoAmI:
295+
"""
296+
Calls the whoami API and returns a WhoAmI object.
297+
"""
298+
url = server_context.build_url("login", "whoami.api")
299+
response = server_context.make_request(url)
300+
return WhoAmI(
301+
response["id"],
302+
response["email"],
303+
response["displayName"],
304+
response["impersonated"],
305+
response["CSRF"],
306+
)
307+
308+
249309
def __make_security_group_api_request(
250310
server_context: ServerContext,
251311
api: str,
@@ -378,3 +438,15 @@ def remove_from_role(
378438
@functools.wraps(reset_password)
379439
def reset_password(self, email: str, container_path: str = None):
380440
return reset_password(self.server_context, email, container_path)
441+
442+
@functools.wraps(impersonate_user)
443+
def impersonate_user(self, user_id: int = None, email: str = None, container_path: str = None):
444+
return impersonate_user(self.server_context, user_id, email, container_path)
445+
446+
@functools.wraps(stop_impersonating)
447+
def stop_impersonating(self):
448+
return stop_impersonating(self.server_context)
449+
450+
@functools.wraps(who_am_i)
451+
def who_am_i(self):
452+
return who_am_i(self.server_context)

0 commit comments

Comments
 (0)