diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index eff1b737..261f0c7c 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -2,7 +2,7 @@ ## Related Jira tickets: ## -* [WG-999](https://jira.tacc.utexas.edu/browse/WG-999) +* [WG-XYZ](https://tacc-main.atlassian.net/browse/WG-XYZ) ## Summary of Changes: ## diff --git a/README.md b/README.md index b7865137..392f4b91 100644 --- a/README.md +++ b/README.md @@ -31,21 +31,15 @@ under gunicorn on port 8000 `docker exec -it geoapi python initdb.py` -###### Create a JWT +###### Obtain a JWT -Copy this string to here: https://jwt.io/ - -``` -eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ3c28yLm9yZy9wcm9kdWN0cy9hbSIsImV4cCI6MjM4NDQ4MTcxMzg0MiwiaHR0cDovL3dzbzIub3JnL2NsYWltcy9zdWJzY3JpYmVyIjoiWU9VUl9VU0VSTkFNRSIsImh0dHA6Ly93c28yLm9yZy9jbGFpbXMvYXBwbGljYXRpb25pZCI6IjQ0IiwiaHR0cDovL3dzbzIub3JnL2NsYWltcy9hcHBsaWNhdGlvbm5hbWUiOiJEZWZhdWx0QXBwbGljYXRpb24iLCJodHRwOi8vd3NvMi5vcmcvY2xhaW1zL2FwcGxpY2F0aW9udGllciI6IlVubGltaXRlZCIsImh0dHA6Ly93c28yLm9yZy9jbGFpbXMvYXBpY29udGV4dCI6Ii9hcHBzIiwiaHR0cDovL3dzbzIub3JnL2NsYWltcy92ZXJzaW9uIjoiMi4wIiwiaHR0cDovL3dzbzIub3JnL2NsYWltcy90aWVyIjoiVW5saW1pdGVkIiwiaHR0cDovL3dzbzIub3JnL2NsYWltcy9rZXl0eXBlIjoiUFJPRFVDVElPTiIsImh0dHA6Ly93c28yLm9yZy9jbGFpbXMvdXNlcnR5cGUiOiJBUFBMSUNBVElPTl9VU0VSIiwiaHR0cDovL3dzbzIub3JnL2NsYWltcy9lbmR1c2VyIjoiWU9VUl9VU0VSTkFNRSIsImh0dHA6Ly93c28yLm9yZy9jbGFpbXMvZW5kdXNlclRlbmFudElkIjoiMTAiLCJodHRwOi8vd3NvMi5vcmcvY2xhaW1zL2VtYWlsYWRkcmVzcyI6InRlc3R1c2VyM0B0ZXN0LmNvbSIsImh0dHA6Ly93c28yLm9yZy9jbGFpbXMvZnVsbG5hbWUiOiJEZXYgVXNlciIsImh0dHA6Ly93c28yLm9yZy9jbGFpbXMvZ2l2ZW5uYW1lIjoiRGV2IiwiaHR0cDovL3dzbzIub3JnL2NsYWltcy9sYXN0bmFtZSI6IlVzZXIiLCJodHRwOi8vd3NvMi5vcmcvY2xhaW1zL3ByaW1hcnlDaGFsbGVuZ2VRdWVzdGlvbiI6Ik4vQSIsImh0dHA6Ly93c28yLm9yZy9jbGFpbXMvcm9sZSI6IkludGVybmFsL2V2ZXJ5b25lIiwiaHR0cDovL3dzbzIub3JnL2NsYWltcy90aXRsZSI6Ik4vQSJ9.0dIfuYvmXJwES1m1NJKKAPclynbGnaxzX3ygSz-3dqA -``` - -Edit the `YOUR_USERNAME`s in there for your TACC username and copy the modified string +Refer to the confluence page or ask a colleague for assistance in obtaining a JWT. ###### Make some requests You need to add the following header for authentication: -`X-JWT-Assertion` to equal the JWT created above +`X-JWT-Assertion-designsafe` to equal the JWT obtained above ###### Create a new map project @@ -65,16 +59,17 @@ send a GET request to `localhost:8000/projects` and you should get that back. See https://github.com/TACC-Cloud/hazmapper for details. -### Migrations +### Creating migrations when updating database models +These are useful steps to follow when there are changes to the database model. -Applying migrations +First, apply migrations: ``` docker exec -it geoapi alembic upgrade head ``` -Creating migrations +Then, create migrations: ``` docker exec -it geoapi /bin/bash diff --git a/geoapi/models/project.py b/geoapi/models/project.py index 7795f75d..6732aac5 100644 --- a/geoapi/models/project.py +++ b/geoapi/models/project.py @@ -29,6 +29,8 @@ class Project(Base): id = Column(Integer, primary_key=True) uuid = Column(UUID(as_uuid=True), default=uuid.uuid4, nullable=False) tenant_id = Column(String, nullable=False) + # Project system_id/system_path really not used except for analytics. + # This could be improved; see https://jira.tacc.utexas.edu/browse/WG-185 system_id = Column(String, nullable=True) system_path = Column(String, nullable=True) system_file = Column(String, nullable=True) diff --git a/geoapi/routes/projects.py b/geoapi/routes/projects.py index 09b68486..7f5fcd2d 100644 --- a/geoapi/routes/projects.py +++ b/geoapi/routes/projects.py @@ -169,14 +169,15 @@ def get(self): uuid_subset = query.get("uuid") if uuid_subset: - logger.info("Get a subset of projects for user:{} projects:{}".format(u.username, uuid_subset)) + logger.info(f"Getting a subset of projects for user:{u.username} project_uuid:{uuid_subset}") + # Check each project and abort if user (authenticated or anonymous) can't access the project subset = [check_access_and_get_project(request.current_user, uuid=uuid, allow_public_use=True) for uuid in uuid_subset] return subset else: if is_anonymous(u): abort(403, "Access denied") - logger.info("Get all projects for user:{}".format(u.username)) + logger.info(f"Get all projects for user:{u.username}") return ProjectsService.list(db_session, u) @api.doc(id="createProject", @@ -290,8 +291,23 @@ class ProjectFeaturesResource(Resource): @api.marshal_with(feature_collection_model, as_list=True) @project_permissions_allow_public def get(self, projectId: int): - logger.info("Get features of project:{} for user:{}".format( - projectId, request.current_user.username)) + # Following log is for analytics, see https://confluence.tacc.utexas.edu/display/DES/Hazmapper+Logging + application = request.headers.get('X-Geoapi-Application') + if application is None: + # Check if in query parameters due to https://tacc-main.atlassian.net/browse/WG-192 and WG-191 */ + application = request.args.get('application') + + if application is None: + application = "Unknown" + + from geoapi.routes.public_projects import PublicProjectFeaturesResource + is_public_view = issubclass(self.__class__, PublicProjectFeaturesResource) + + prj = ProjectsService.get(db_session, project_id=projectId, user=request.current_user) + logger.info(f"Get features of project for user:{request.current_user.username} application:{application}" + f" public_view:{is_public_view} project_uuid:{prj.uuid} project:{prj.id} tapis_system_id:{prj.system_id} " + f"tapis_system_path:{prj.system_path}") + query = self.parser.parse_args() return ProjectsService.getFeatures(db_session, projectId, query) diff --git a/geoapi/tests/api_tests/test_projects_routes.py b/geoapi/tests/api_tests/test_projects_routes.py index deb7b5f8..24314095 100644 --- a/geoapi/tests/api_tests/test_projects_routes.py +++ b/geoapi/tests/api_tests/test_projects_routes.py @@ -63,6 +63,16 @@ def test_get_projects_using_single_uuid(test_client, projects_fixture, projects_ assert data[0]["deletable"] is True +def test_get_projects_using_single_uuid_observable_project(test_client, observable_projects_fixture, user1): + resp = test_client.get('/projects/', + query_string='uuid={}'.format(observable_projects_fixture.project.uuid), + headers={'x-jwt-assertion-test': user1.jwt}) + data = resp.get_json() + assert resp.status_code == 200 + assert len(data) == 1 + assert data[0]["uuid"] == str(observable_projects_fixture.project.uuid) + + def test_get_projects_using_single_uuid_that_is_wrong(test_client, user1): resp = test_client.get('/projects/', query_string='uuid={}'.format(uuid.uuid4()), @@ -262,20 +272,39 @@ def test_get_point_cloud(test_client, projects_fixture, point_cloud_fixture, use assert resp.status_code == 200 -def test_get_project_features_empty(test_client, projects_fixture, user1): +def test_get_project_features_empty(test_client, projects_fixture, user1, caplog): resp = test_client.get(f'/projects/{projects_fixture.id}/features/', headers={'x-jwt-assertion-test': user1.jwt}) assert resp.status_code == 200 data = resp.get_json() assert len(data['features']) == 0 + log_statement_for_analytics = (f"Get features of project for user:{user1.username} application:Unknown public_view:False " + f"project_uuid:{projects_fixture.uuid} project:{projects_fixture.id} " + f"tapis_system_id:None tapis_system_path:None") + assert log_statement_for_analytics in caplog.text -def test_get_project_features_empty_public_access(test_client, public_projects_fixture): - resp = test_client.get('/projects/{}/features/'.format(public_projects_fixture.id)) +def test_get_project_features_empty_public_access(test_client, public_projects_fixture, caplog): + resp = test_client.get('/public-projects/{}/features/'.format(public_projects_fixture.id)) assert resp.status_code == 200 data = resp.get_json() assert len(data['features']) == 0 + log_statement_for_analytics = (f"Get features of project for user:Guest_Unknown application:Unknown public_view:True " + f"project_uuid:{public_projects_fixture.uuid} project:{public_projects_fixture.id} " + f"tapis_system_id:None tapis_system_path:None") + assert log_statement_for_analytics in caplog.text + + +def test_get_project_features_analytics_with_query_params(test_client, public_projects_fixture, caplog): + # send analytics-related params to projects endpoint only (until we use headers again + # in https://tacc-main.atlassian.net/browse/WG-192) + query = {'application': 'hazmapper', 'guest_uuid': "1234"} + test_client.get('/public-projects/{}/features/'.format(public_projects_fixture.id), query_string=query) + log_statement_for_analytics = (f"Get features of project for user:Guest_1234 application:hazmapper public_view:True " + f"project_uuid:{public_projects_fixture.uuid} project:{public_projects_fixture.id} " + f"tapis_system_id:None tapis_system_path:None") + assert log_statement_for_analytics in caplog.text def test_get_project_features_single_feature(test_client, projects_fixture, feature_fixture, user1): diff --git a/geoapi/tests/conftest.py b/geoapi/tests/conftest.py index 19c994ba..2bb420d3 100644 --- a/geoapi/tests/conftest.py +++ b/geoapi/tests/conftest.py @@ -123,6 +123,12 @@ def observable_projects_fixture(): path="/testPath", watch_content=True ) + + # Project system_id/system_path really not used except for analytics. + # This could be improved; see https://jira.tacc.utexas.edu/browse/WG-185 + proj.system_id = obs.system_id + proj.system_path = obs.path + obs.project = proj proj.users.append(u1) db_session.add(obs) diff --git a/geoapi/utils/decorators.py b/geoapi/utils/decorators.py index d51c4b47..320fca83 100644 --- a/geoapi/utils/decorators.py +++ b/geoapi/utils/decorators.py @@ -35,18 +35,22 @@ def wrapper(*args, **kwargs): except ValueError: # if not JWT information is provided in header, then this is a guest user guest_uuid = request.headers.get('X-Guest-UUID') + if guest_uuid is None: + # Check if in query parameters due to https://tacc-main.atlassian.net/browse/WG-192 and WG-191 */ + guest_uuid = request.args.get('guest_uuid') user = AnonymousUser(guest_unique_id=guest_uuid) if user is None: try: - # TODO: validate token - decoded = jwt.decode(token, pub_key, verify=False) + decoded = jwt.decode(token, pub_key, algorithms=["RS256"], verify=not settings.TESTING) username = decoded["http://wso2.org/claims/enduser"] # remove ant @carbon.super or other nonsense, the tenant # we get from the header anyway username = username.split("@")[0] + + # Exceptions except Exception as e: - logger.exception(e) - abort(400, 'could not decode JWT') + logger.error(f'There is an issue decoding the JWT: {e}') + abort(400, f'There is an issue decoding the JWT: {e}') user = UserService.getUser(db_session, username, tenant) if not user: