Skip to content

Commit 19db7c1

Browse files
authored
Merge pull request #88 from guzman-raphael/delete_quick
Allow cascade option for delete and other fixes
2 parents e6ae1bf + 8ab57b4 commit 19db7c1

13 files changed

+527
-477
lines changed

CHANGELOG.md

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,32 @@
33
Observes [Semantic Versioning](https://semver.org/spec/v2.0.0.html) standard and [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) convention.
44

55
## [Unreleased]
6+
7+
## Security
8+
- Documentation with detail regarding warning on bearer token. (#83) PR #88
9+
10+
## Fixed
11+
- Incorrect virtual module reference of `schema_virtual_module` in table metadata. (#85) PR #88
12+
613
### Added
7-
- Docker `dev` environment that supports hot reloading.
8-
- Documentation on setting up environments within `docker-compose` header.
14+
- Docker `dev` environment that supports hot reloading. PR #79
15+
- Documentation on setting up environments within `docker-compose` header. PR #79
16+
- `cascade` option for `/delete_tuple` route. (#86) PR #88
17+
- When delete with `cascade=False` fails due to foreign key relations, returns a HTTP error code of `409 Conflict` with a JSON body containing specifics of 1st child. (#86) PR #88
18+
19+
### Changed
20+
- Replaced `DJConnector.snake_to_camel_case` usage with `datajoint.utils.to_camel_case`. PR #88
21+
- Default behavior for `/delete_tuple` now deletes without cascading. (#86) PR #88
22+
- Consolidated `pytest` fixtures into `__init__.py` to facilitate reuse. PR #88
923

1024
### Removed
11-
- Docker `base` environment to simplify dependencies.
25+
- Docker `base` environment to simplify dependencies. PR #79
1226

1327
## [0.1.0a5] - 2021-02-18
1428
### Added
1529
- List schemas method.
1630
- List tables method.
17-
- Create, Read, Update, Delete (CRUD) operations for DataJoint table tiers: `dj.Manual`, `dj.Lookup`.
31+
- Data entry, update, delete, and view operations for DataJoint table tiers: `dj.Manual`, `dj.Lookup`.
1832
- Read table records with proper paging and compounding restrictions (i.e. filters).
1933
- Read table definition method.
2034
- Support for DataJoint attribute types: `varchar`, `int`, `float`, `datetime`, `date`, `time`, `decimal`, `uuid`.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ AS_SCRIPT= # If 'TRUE', will not keep container alive but run tests and exit
4444
```shell
4545
PKG_DIR=/opt/conda/lib/python3.8/site-packages/pharus # path to pharus installation
4646
TEST_DB_SERVER=example.com:3306 # testing db server address
47-
TEST_DB_USER=root # testing db server user (needs CRUD on schemas, tables, users)
47+
TEST_DB_USER=root # testing db server user (needs DDL privilege)
4848
TEST_DB_PASS=unsecure # testing db server password
4949
```
5050
- For syntax tests, run `flake8 ${PKG_DIR} --count --select=E9,F63,F7,F82 --show-source --statistics`

docker-compose-test.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ services:
3636
PKG_DIR=/opt/conda/lib/python3.8/site-packages/pharus
3737
flake8 $${PKG_DIR} --count --select=E9,F63,F7,F82 --show-source --statistics
3838
echo "------ UNIT TESTS ------"
39-
pytest -sv --cov-report term-missing --cov=$${PKG_DIR} /main/tests
39+
pytest -sv --cov-report term-missing --cov=pharus /main/tests
4040
echo "------ STYLE TESTS ------"
4141
flake8 $${PKG_DIR} --count --max-complexity=20 --max-line-length=95 --statistics
4242
else

pharus/interface.py

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Library for interfaces into DataJoint pipelines."""
22
import datajoint as dj
3+
from datajoint.utils import to_camel_case
34
import datetime
45
import numpy as np
56
from functools import reduce
@@ -94,9 +95,9 @@ def list_tables(jwt_payload: dict, schema_name: str):
9495
tables_dict_list['imported_tables'].append(dj.utils.to_camel_case(table_name))
9596
elif table_type == 'Part':
9697
table_name_parts = table_name.split('__')
97-
tables_dict_list['part_tables'].append(DJConnector.snake_to_camel_case(
98-
table_name_parts[-2]) + '.' + DJConnector.snake_to_camel_case(
99-
table_name_parts[-1]))
98+
tables_dict_list['part_tables'].append(
99+
to_camel_case(table_name_parts[-2]) + '.' +
100+
to_camel_case(table_name_parts[-1]))
100101
else:
101102
raise UnsupportedTableType(table_name + ' is of unknown table type')
102103

@@ -262,8 +263,9 @@ def get_table_definition(jwt_payload: dict, schema_name: str, table_name: str):
262263
"""
263264
DJConnector.set_datajoint_config(jwt_payload)
264265

265-
schema_virtual_module = dj.create_virtual_module(schema_name, schema_name)
266-
return getattr(schema_virtual_module, table_name).describe()
266+
local_values = locals()
267+
local_values[schema_name] = dj.VirtualModule(schema_name, schema_name)
268+
return getattr(local_values[schema_name], table_name).describe()
267269

268270
@staticmethod
269271
def insert_tuple(jwt_payload: dict, schema_name: str, table_name: str,
@@ -344,7 +346,7 @@ def update_tuple(jwt_payload: dict, schema_name: str, table_name: str,
344346

345347
@staticmethod
346348
def delete_tuple(jwt_payload: dict, schema_name: str, table_name: str,
347-
tuple_to_restrict_by: dict):
349+
tuple_to_restrict_by: dict, cascade: bool = False):
348350
"""
349351
Delete a specific record based on the restriction given (Can only delete 1 at a time)
350352
:param jwt_payload: Dictionary containing databaseAddress, username and password
@@ -356,6 +358,8 @@ def delete_tuple(jwt_payload: dict, schema_name: str, table_name: str,
356358
:type table_name: str
357359
:param tuple_to_restrict_by: Record to restrict the table by to delete
358360
:type tuple_to_restrict_by: dict
361+
:param cascade: Allow for cascading delete, defaults to False
362+
:type cascade: bool
359363
"""
360364
DJConnector.set_datajoint_config(jwt_payload)
361365

@@ -382,7 +386,7 @@ def delete_tuple(jwt_payload: dict, schema_name: str, table_name: str,
382386
raise InvalidDeleteRequest('Nothing to delete')
383387

384388
# All check pass thus proceed to delete
385-
tuple_to_delete.delete(safemode=False)
389+
tuple_to_delete.delete(safemode=False) if cascade else tuple_to_delete.delete_quick()
386390

387391
@staticmethod
388392
def get_table_object(schema_virtual_module, table_name: str):
@@ -413,14 +417,3 @@ def set_datajoint_config(jwt_payload: dict):
413417
dj.config['database.user'] = jwt_payload['username']
414418
dj.config['database.password'] = jwt_payload['password']
415419
dj.conn(reset=True)
416-
417-
@staticmethod
418-
def snake_to_camel_case(string: str):
419-
"""
420-
Helper method for converting snake to camel case
421-
:param string: String in snake format to convert to camel case
422-
:type string: str
423-
:return: String formated in CamelCase notation
424-
:rtype: str
425-
"""
426-
return ''.join(string_component.title() for string_component in string.split('_'))

pharus/server.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
import jwt
1414
from json import loads
1515
from base64 import b64decode
16+
from datajoint.errors import IntegrityError
17+
from datajoint.table import foregn_key_error_regexp
18+
from datajoint.utils import to_camel_case
1619

1720
app = Flask(__name__)
1821
# Check if PRIVATE_KEY and PUBIC_KEY is set, if not generate them.
@@ -65,6 +68,22 @@ def api_version():
6568
@app.route(f"{environ.get('PHARUS_PREFIX', '')}/login", methods=['POST'])
6669
def login():
6770
"""
71+
*WARNING*: Currently, this implementation exposes user database credentials as plain text
72+
in POST body once and stores it within a bearer token as Base64 encoded for subsequent
73+
requests. That is how the server is able to submit queries on user's behalf. Due to
74+
this, it is required that remote hosts expose the server only under HTTPS to ensure
75+
end-to-end encryption. Sending passwords in plain text over HTTPS in POST request body
76+
is common and utilized by companies such as GitHub (2021) and Chase Bank (2021). On
77+
server side, there is no caching, logging, or storage of received passwords or tokens
78+
and thus available only briefly in memory. This means the primary vulnerable point is
79+
client side. Users should be responsible with their passwords and bearer tokens
80+
treating them as one-in-the-same. Be aware that if your client system happens to be
81+
compromised, a bad actor could monitor your outgoing network requests and capture/log
82+
your credentials. However, in such a terrible scenario, a bad actor would not only
83+
collect credentials for your DataJoint database but also other sites such as
84+
github.com, chase.com, etc. Please be responsible and vigilant with credentials and
85+
tokens on client side systems. Improvements to the above strategy is currently being
86+
tracked in https://github.com/datajoint/pharus/issues/82.
6887
Login route which uses DataJoint database server login. Expects:
6988
(html:POST:body): json with keys
7089
{databaseAddress: string, username: string, password: string}
@@ -301,8 +320,17 @@ def delete_tuple(jwt_payload: dict):
301320
DJConnector.delete_tuple(jwt_payload,
302321
request.json["schemaName"],
303322
request.json["tableName"],
304-
request.json["restrictionTuple"])
323+
request.json["restrictionTuple"],
324+
**{k: v.lower() == 'true'
325+
for k, v in request.args.items() if k == 'cascade'},)
305326
return "Delete Sucessful"
327+
except IntegrityError as e:
328+
match = foregn_key_error_regexp.match(e.args[0])
329+
return dict(error=e.__class__.__name__,
330+
error_msg=str(e),
331+
child_schema=match.group('child').split('.')[0][1:-1],
332+
child_table=to_camel_case(match.group('child').split('.')[1][1:-1]),
333+
), 409
306334
except Exception as e:
307335
return str(e), 500
308336

0 commit comments

Comments
 (0)