Skip to content

Commit dc5700c

Browse files
authored
feat: feeds operations API function (#838)
1 parent 868662b commit dc5700c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+3111
-61
lines changed

.github/workflows/api-deployer.yml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ on:
5656
description: Validator endpoint
5757
required: true
5858
type: string
59+
OPERATIONS_OAUTH2_CLIENT_ID_1PASSWORD:
60+
description: Oauth client id part of the authorization for the operations API
61+
required: true
62+
type: string
5963

6064
env:
6165
python_version: '3.11'
@@ -255,6 +259,11 @@ jobs:
255259
name: feeds_gen
256260
path: api/src/feeds_gen/
257261

262+
- uses: actions/download-artifact@v4
263+
with:
264+
name: feeds_operations_gen
265+
path: functions-python/operations_api/src/feeds_operations_gen/
266+
258267
- name: Build python functions
259268
run: |
260269
scripts/function-python-build.sh --all
@@ -290,11 +299,12 @@ jobs:
290299
env:
291300
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
292301
TRANSITLAND_API_KEY: "op://rbiv7rvkkrsdlpcrz3bmv7nmcu/TansitLand API Key/credential"
302+
OPERATIONS_OAUTH2_CLIENT_ID: ${{ inputs.OPERATIONS_OAUTH2_CLIENT_ID_1PASSWORD }}
293303

294304
- name: Populate Variables
295305
run: |
296306
scripts/replace-variables.sh -in_file infra/backend.conf.rename_me -out_file infra/backend.conf -variables BUCKET_NAME,OBJECT_PREFIX
297-
scripts/replace-variables.sh -in_file infra/vars.tfvars.rename_me -out_file infra/vars.tfvars -variables PROJECT_ID,REGION,ENVIRONMENT,DEPLOYER_SERVICE_ACCOUNT,FEED_API_IMAGE_VERSION,OAUTH2_CLIENT_ID,OAUTH2_CLIENT_SECRET,GLOBAL_RATE_LIMIT_REQ_PER_MINUTE,ARTIFACT_REPO_NAME,VALIDATOR_ENDPOINT,TRANSITLAND_API_KEY
307+
scripts/replace-variables.sh -in_file infra/vars.tfvars.rename_me -out_file infra/vars.tfvars -variables PROJECT_ID,REGION,ENVIRONMENT,DEPLOYER_SERVICE_ACCOUNT,FEED_API_IMAGE_VERSION,OAUTH2_CLIENT_ID,OAUTH2_CLIENT_SECRET,GLOBAL_RATE_LIMIT_REQ_PER_MINUTE,ARTIFACT_REPO_NAME,VALIDATOR_ENDPOINT,TRANSITLAND_API_KEY,OPERATIONS_OAUTH2_CLIENT_ID
298308
299309
- uses: hashicorp/setup-terraform@v3
300310
with:

.github/workflows/api-dev.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ jobs:
2222
GLOBAL_RATE_LIMIT_REQ_PER_MINUTE: ${{ vars.GLOBAL_RATE_LIMIT_REQ_PER_MINUTE }}
2323
TF_APPLY: true
2424
VALIDATOR_ENDPOINT: https://stg-gtfs-validator-web-mbzoxaljzq-ue.a.run.app
25+
OPERATIONS_OAUTH2_CLIENT_ID_1PASSWORD: "op://rbiv7rvkkrsdlpcrz3bmv7nmcu/GCP_RETOOL_OAUTH2_CREDS/username"
2526
secrets:
2627
GCP_MOBILITY_FEEDS_SA_KEY: ${{ secrets.DEV_GCP_MOBILITY_FEEDS_SA_KEY }}
2728
OAUTH2_CLIENT_ID: ${{ secrets.DEV_MOBILITY_FEEDS_OAUTH2_CLIENT_ID}}

.github/workflows/api-prod.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ jobs:
1818
GLOBAL_RATE_LIMIT_REQ_PER_MINUTE: ${{ vars.GLOBAL_RATE_LIMIT_REQ_PER_MINUTE }}
1919
TF_APPLY: true
2020
VALIDATOR_ENDPOINT: https://gtfs-validator-web-mbzoxaljzq-ue.a.run.app
21+
OPERATIONS_OAUTH2_CLIENT_ID_1PASSWORD: "op://rbiv7rvkkrsdlpcrz3bmv7nmcu/GCP_RETOOL_OAUTH2_CREDS/username"
2122
secrets:
2223
GCP_MOBILITY_FEEDS_SA_KEY: ${{ secrets.PROD_GCP_MOBILITY_FEEDS_SA_KEY }}
2324
OAUTH2_CLIENT_ID: ${{ secrets.PROD_MOBILITY_FEEDS_OAUTH2_CLIENT_ID}}

.github/workflows/api-qa.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ jobs:
1818
TF_APPLY: true
1919
GLOBAL_RATE_LIMIT_REQ_PER_MINUTE: ${{ vars.GLOBAL_RATE_LIMIT_REQ_PER_MINUTE }}
2020
VALIDATOR_ENDPOINT: https://stg-gtfs-validator-web-mbzoxaljzq-ue.a.run.app
21+
OPERATIONS_OAUTH2_CLIENT_ID_1PASSWORD: "op://rbiv7rvkkrsdlpcrz3bmv7nmcu/GCP_RETOOL_OAUTH2_CREDS/username"
2122
secrets:
2223
GCP_MOBILITY_FEEDS_SA_KEY: ${{ secrets.QA_GCP_MOBILITY_FEEDS_SA_KEY }}
2324
OAUTH2_CLIENT_ID: ${{ secrets.DEV_MOBILITY_FEEDS_OAUTH2_CLIENT_ID}}

.github/workflows/build-test.yml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ jobs:
8080
scripts/setup-openapi-generator.sh
8181
scripts/api-gen.sh
8282
83+
- name: Generate Operations API code
84+
run: |
85+
scripts/api-operations-gen.sh
86+
8387
- name: Unit tests - API
8488
shell: bash
8589
run: |
@@ -104,9 +108,16 @@ jobs:
104108
path: api/src/database_gen/
105109
overwrite: true
106110

107-
- name: API generated code
111+
- name: Upload API generated code
108112
uses: actions/upload-artifact@v4
109113
with:
110114
name: feeds_gen
111115
path: api/src/feeds_gen/
116+
overwrite: true
117+
118+
- name: Upload Operations API generated code
119+
uses: actions/upload-artifact@v4
120+
with:
121+
name: feeds_operations_gen
122+
path: functions-python/operations_api/src/feeds_operations_gen/
112123
overwrite: true

api/src/feeds/impl/feeds_api_impl.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
LocationTranslation,
4747
get_feeds_location_translations,
4848
)
49+
from utils.logger import Logger
4950

5051
T = TypeVar("T", bound="BasicFeed")
5152

@@ -59,11 +60,17 @@ class FeedsApiImpl(BaseFeedsApi):
5960

6061
APIFeedType = Union[BasicFeed, GtfsFeed, GtfsRTFeed]
6162

63+
def __init__(self) -> None:
64+
self.logger = Logger("FeedsApiImpl").get_logger()
65+
6266
def get_feed(
6367
self,
6468
id: str,
6569
) -> BasicFeed:
6670
"""Get the specified feed from the Mobility Database."""
71+
is_email_restricted = is_user_email_restricted()
72+
self.logger.info(f"User email is restricted: {is_email_restricted}")
73+
6774
feed = (
6875
FeedFilter(stable_id=id, provider__ilike=None, producer_url__ilike=None, status=None)
6976
.filter(Database().get_query_model(Feed))
@@ -72,7 +79,7 @@ def get_feed(
7279
or_(
7380
Feed.operational_status == None, # noqa: E711
7481
Feed.operational_status != "wip",
75-
not is_user_email_restricted(), # Allow all feeds to be returned if the user is not restricted
82+
not is_email_restricted, # Allow all feeds to be returned if the user is not restricted
7683
)
7784
)
7885
.first()
@@ -91,6 +98,8 @@ def get_feeds(
9198
producer_url: str,
9299
) -> List[BasicFeed]:
93100
"""Get some (or all) feeds from the Mobility Database."""
101+
is_email_restricted = is_user_email_restricted()
102+
self.logger.info(f"User email is restricted: {is_email_restricted}")
94103
feed_filter = FeedFilter(
95104
status=status, provider__ilike=provider, producer_url__ilike=producer_url, stable_id=None
96105
)
@@ -100,7 +109,7 @@ def get_feeds(
100109
or_(
101110
Feed.operational_status == None, # noqa: E711
102111
Feed.operational_status != "wip",
103-
not is_user_email_restricted(), # Allow all feeds to be returned if the user is not restricted
112+
not is_email_restricted, # Allow all feeds to be returned if the user is not restricted
104113
)
105114
)
106115
# Results are sorted by provider
@@ -239,6 +248,8 @@ def get_gtfs_feeds(
239248
subquery, dataset_latitudes, dataset_longitudes, bounding_filter_method
240249
).subquery()
241250

251+
is_email_restricted = is_user_email_restricted()
252+
self.logger.info(f"User email is restricted: {is_email_restricted}")
242253
feed_query = (
243254
Database()
244255
.get_session()
@@ -248,7 +259,7 @@ def get_gtfs_feeds(
248259
or_(
249260
Gtfsfeed.operational_status == None, # noqa: E711
250261
Gtfsfeed.operational_status != "wip",
251-
not is_user_email_restricted(), # Allow all feeds to be returned if the user is not restricted
262+
not is_email_restricted, # Allow all feeds to be returned if the user is not restricted
252263
)
253264
)
254265
.options(

api/src/feeds/impl/search_api_impl.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def add_search_query_filters(query, search_query, data_type, feed_id, status) ->
4242
or_(
4343
t_feedsearch.c.operational_status == None, # noqa: E711
4444
t_feedsearch.c.operational_status != "wip",
45-
is_user_email_restricted(),
45+
not is_user_email_restricted(),
4646
)
4747
)
4848
if feed_id:

api/src/middleware/request_context.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,10 @@ def _extract_from_headers(self, headers: dict, scope: Scope) -> None:
9494
def __repr__(self) -> str:
9595
# Omitting sensitive data like email and jwt assertion
9696
safe_properties = dict(
97-
user_id=self.user_id, client_user_agent=self.client_user_agent, client_host=self.client_host
97+
user_id=self.user_id,
98+
client_user_agent=self.client_user_agent,
99+
client_host=self.client_host,
100+
email=self.user_email,
98101
)
99102
return f"request-context={safe_properties})"
100103

@@ -108,8 +111,8 @@ def is_user_email_restricted() -> bool:
108111
Check if an email's domain is restricted (e.g., for WIP visibility).
109112
"""
110113
request_context = get_request_context()
111-
if not isinstance(request_context, RequestContext):
112-
return True # Default to restricted
113-
email = get_request_context().user_email
114-
unrestricted_domains = ["@mobilitydata.org"]
114+
if not request_context:
115+
return True
116+
email = request_context["user_email"]
117+
unrestricted_domains = ["mobilitydata.org"]
115118
return not email or not any(email.endswith(f"@{domain}") for domain in unrestricted_domains)

api/tests/unittest/middleware/test_request_context.py

Lines changed: 1 addition & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from starlette.datastructures import Headers
55

6-
from middleware.request_context import RequestContext, get_request_context, _request_context, is_user_email_restricted
6+
from middleware.request_context import RequestContext, get_request_context, _request_context
77

88

99
class TestRequestContext(unittest.TestCase):
@@ -54,45 +54,3 @@ def test_get_request_context(self):
5454
request_context = RequestContext(MagicMock())
5555
_request_context.set(request_context)
5656
self.assertEqual(request_context, get_request_context())
57-
58-
def test_is_user_email_restricted(self):
59-
self.assertTrue(is_user_email_restricted())
60-
scope_instance = {
61-
"type": "http",
62-
"asgi": {"version": "3.0"},
63-
"http_version": "1.1",
64-
"method": "GET",
65-
"headers": [
66-
(b"host", b"localhost"),
67-
(b"x-forwarded-proto", b"https"),
68-
(b"x-forwarded-for", b"client, proxy1"),
69-
(b"server", b"server"),
70-
(b"user-agent", b"user-agent"),
71-
(b"x-goog-iap-jwt-assertion", b"jwt"),
72-
(b"x-cloud-trace-context", b"TRACE_ID/SPAN_ID;o=1"),
73-
(b"x-goog-authenticated-user-id", b"user_id"),
74-
(b"x-goog-authenticated-user-email", b"email"),
75-
],
76-
"path": "/",
77-
"raw_path": b"/",
78-
"query_string": b"",
79-
"client": ("127.0.0.1", 32767),
80-
"server": ("127.0.0.1", 80),
81-
}
82-
request_context = RequestContext(scope=scope_instance)
83-
_request_context.set(request_context)
84-
self.assertTrue(is_user_email_restricted())
85-
scope_instance["headers"] = [
86-
(b"host", b"localhost"),
87-
(b"x-forwarded-proto", b"https"),
88-
(b"x-forwarded-for", b"client, proxy1"),
89-
(b"server", b"server"),
90-
(b"user-agent", b"user-agent"),
91-
(b"x-goog-iap-jwt-assertion", b"jwt"),
92-
(b"x-cloud-trace-context", b"TRACE_ID/SPAN_ID;o=1"),
93-
(b"x-goog-authenticated-user-id", b"user_id"),
94-
(b"x-goog-authenticated-user-email", b"[email protected]"),
95-
]
96-
request_context = RequestContext(scope=scope_instance)
97-
_request_context.set(request_context)
98-
self.assertTrue(is_user_email_restricted())

0 commit comments

Comments
 (0)