From 6f4e66a8fc8a9f4aee714219bae4180978671dd6 Mon Sep 17 00:00:00 2001
From: Taylor Grafft <tjgrafft@me.com>
Date: Fri, 8 Dec 2023 16:12:01 -0600
Subject: [PATCH 1/4] task/WG-67: Adding JWT verification (#164)

* task/WG-67: Adding JWT verification

* fixing linting issues

* Cleaning up code for decoding JWT and handling exceptions

* Adding logger error text

---------

Co-authored-by: Taylor Grafft <tgrafft@TACCs-MBP.attlocal.net>
---
 README.md                  | 12 +++---------
 geoapi/utils/decorators.py |  9 +++++----
 2 files changed, 8 insertions(+), 13 deletions(-)

diff --git a/README.md b/README.md
index b7865137..b14395ab 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
 
diff --git a/geoapi/utils/decorators.py b/geoapi/utils/decorators.py
index d51c4b47..65e2d126 100644
--- a/geoapi/utils/decorators.py
+++ b/geoapi/utils/decorators.py
@@ -38,15 +38,16 @@ def wrapper(*args, **kwargs):
             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:

From b51b678124a92216ff1266bb68b63d2eed36fb60 Mon Sep 17 00:00:00 2001
From: Nathan Franklin <nathan.franklin@gmail.com>
Date: Tue, 12 Dec 2023 15:13:51 -0600
Subject: [PATCH 2/4] Update migration part (#165)

---
 README.md | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/README.md b/README.md
index b14395ab..392f4b91 100644
--- a/README.md
+++ b/README.md
@@ -59,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

From 2d612c6de8af941323b365de47af335be5bab06c Mon Sep 17 00:00:00 2001
From: Taylor Grafft <tjgrafft@me.com>
Date: Wed, 13 Dec 2023 13:13:57 -0600
Subject: [PATCH 3/4] task/WG-194: GeoAPI PR Template Updated Links (#166)

Co-authored-by: Taylor Grafft <tgrafft@wireless-10-147-64-67.public.utexas.edu>
---
 .github/pull_request_template.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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: ##
 

From 553106fc818fa7c23d570fbd7ab43e866fb2f0ee Mon Sep 17 00:00:00 2001
From: Nathan Franklin <nathan.franklin@gmail.com>
Date: Fri, 15 Dec 2023 09:17:37 -0600
Subject: [PATCH 4/4] task/WG-138-WG-140: improve logs for analytics to include
 application and related DS system (#161)

* Improve logs for anlytics to include application and related DesignSafe project/systems

* Add unit tests

* Remove unused user1

* Use 'project_uuid' for uuids and 'project' for id

* Move analytics related logging to features endpoint

* Check query params for analytic information (due to WG-191)

Headers can't be used due to https://tacc-main.atlassian.net/browse/WG-191

* Add public view information to log statement

* Fix public-view check

* Update tests

* Fix linting
---
 geoapi/models/project.py                      |  2 ++
 geoapi/routes/projects.py                     | 24 ++++++++++---
 .../tests/api_tests/test_projects_routes.py   | 35 +++++++++++++++++--
 geoapi/tests/conftest.py                      |  6 ++++
 geoapi/utils/decorators.py                    |  3 ++
 5 files changed, 63 insertions(+), 7 deletions(-)

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 5acc390a..b711708b 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 65e2d126..320fca83 100644
--- a/geoapi/utils/decorators.py
+++ b/geoapi/utils/decorators.py
@@ -35,6 +35,9 @@ 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: