Skip to content

Commit d6afa4c

Browse files
authored
Merge pull request #1502 from PolicyEngine/codex/analytics-variable-requests-endpoint
Add scoped calculate analytics endpoint
2 parents dcad06f + 566e17c commit d6afa4c

22 files changed

Lines changed: 1335 additions & 66 deletions

File tree

.github/scripts/fetch_auth0_test_token.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,16 @@
1010

1111
def main() -> int:
1212
url = f"https://{os.environ['AUTH0_DOMAIN']}/oauth/token"
13-
payload = json.dumps(
14-
{
15-
"client_id": os.environ["AUTH0_CLIENT_ID"],
16-
"client_secret": os.environ["AUTH0_CLIENT_SECRET"],
17-
"audience": os.environ["AUTH0_AUDIENCE"],
18-
"grant_type": "client_credentials",
19-
}
20-
).encode("utf-8")
13+
token_request = {
14+
"client_id": os.environ["AUTH0_CLIENT_ID"],
15+
"client_secret": os.environ["AUTH0_CLIENT_SECRET"],
16+
"audience": os.environ["AUTH0_AUDIENCE"],
17+
"grant_type": "client_credentials",
18+
}
19+
scope = os.environ.get("AUTH0_TEST_TOKEN_SCOPES")
20+
if scope:
21+
token_request["scope"] = scope
22+
payload = json.dumps(token_request).encode("utf-8")
2123

2224
request = urllib.request.Request(
2325
url,

.github/workflows/deploy-staged.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,7 @@ jobs:
264264
AUTH0_AUDIENCE: ${{ secrets.AUTH0_AUDIENCE_NO_DOMAIN }}
265265
AUTH0_CLIENT_ID: ${{ secrets.AUTH0_TEST_TOKEN_CLIENT_ID }}
266266
AUTH0_CLIENT_SECRET: ${{ secrets.AUTH0_TEST_TOKEN_CLIENT_SECRET }}
267+
AUTH0_TEST_TOKEN_SCOPES: ${{ secrets.AUTH0_TEST_TOKEN_SCOPES }}
267268
run: python .github/scripts/fetch_auth0_test_token.py
268269

269270
- name: Run deployed integration tests
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Added an authenticated, scoped calculate analytics endpoint for value-free
2+
request and unique variable-key queries.

config/README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ auth:
168168
address: Auth0 domain (without https:// or trailing slash)
169169
audience: Auth0 audience/API identifier
170170
test_token: JWT token used only for pre-deployment GitHub Actions tests
171+
test_token_scopes: Space-delimited OAuth scopes for the static test token
171172
172173
ai:
173174
enabled: Whether AI features are enabled (true/false) (these features are only used in the alpha-mode AI explainer endpoint)
@@ -308,6 +309,7 @@ AUTH0_AUDIENCE_NO_DOMAIN=https://your-api-identifier
308309
When Auth0 is enabled, the following endpoints require valid JWT tokens:
309310
- `/<country_id>/calculate` - Main calculation endpoint
310311
- `/<country_id>/ai-analysis` - AI analysis endpoint (remains in alpha)
312+
- `/analytics/calculate/requests` - Calculate analytics endpoint; additionally requires the `read:calculate-analytics` scope
311313

312314
The following endpoints remain unprotected:
313315
- `/` - Home endpoint
@@ -341,6 +343,7 @@ AUTH__ENABLED=true # Enable Auth0 authentication
341343
AUTH0_ADDRESS_NO_DOMAIN=${{ secrets.AUTH0_ADDRESS_NO_DOMAIN }}
342344
AUTH0_AUDIENCE_NO_DOMAIN=${{ secrets.AUTH0_AUDIENCE_NO_DOMAIN }}
343345
AUTH0_TEST_TOKEN_NO_DOMAIN=${{ secrets.AUTH0_TEST_TOKEN_NO_DOMAIN }} # Used for local testing purposes
346+
AUTH0_TEST_TOKEN_SCOPES=read:calculate-analytics # Used for scoped local testing
344347
345348
# Analytics configuration (opt-in)
346349
ANALYTICS__ENABLED=true # Enable user analytics
@@ -390,7 +393,8 @@ auth:
390393
auth0:
391394
address: ${AUTH0_DOMAIN}
392395
audience: ${AUTH0_AUDIENCE}
393-
test_bearer_token: ${AUTH0_TOKEN}
396+
test_token: ${AUTH0_TOKEN}
397+
test_token_scopes: ${AUTH0_TOKEN_SCOPES}
394398
395399
ai:
396400
enabled: true

config/default.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ auth:
4747
audience: "" # Override with AUTH0_AUDIENCE_NO_DOMAIN
4848
# Test JWT token used only for GitHub Actions tests pre-deployment
4949
test_token: "" # Override with AUTH0_TEST_TOKEN_NO_DOMAIN
50+
# Space-delimited OAuth scopes for the static test token
51+
test_token_scopes: "" # Override with AUTH0_TEST_TOKEN_SCOPES
5052

5153
# AI services configuration
5254
ai:

config/production.yaml.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ auth:
2323
address: ${AUTH0_ADDRESS_NO_DOMAIN} # From env var
2424
audience: ${AUTH0_AUDIENCE_NO_DOMAIN} # From env var
2525
test_token: "" # Used only for executing pre-deploy integration tests
26+
test_token_scopes: "" # Used only for executing pre-deploy integration tests
2627

2728
ai:
2829
enabled: true

config/test_with_auth.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ auth:
1212
address: ${AUTH0_ADDRESS_NO_DOMAIN} # From env var
1313
audience: ${AUTH0_AUDIENCE_NO_DOMAIN} # From env var
1414
test_token: ${AUTH0_TEST_TOKEN_NO_DOMAIN} # From env var
15+
test_token_scopes: ${AUTH0_TEST_TOKEN_SCOPES} # From env var
1516

1617
ai:
1718
enabled: false

policyengine_household_api/api.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,15 @@
2121
)
2222

2323
# Internal imports
24-
from .decorators.auth import create_auth_decorator
24+
from .decorators.auth import ANALYTICS_READ_SCOPE, create_auth_decorator
2525
from policyengine_household_api.decorators.analytics import (
2626
log_analytics_if_enabled,
2727
)
2828

2929
# Endpoints
3030
from .endpoints import (
3131
get_home,
32+
get_calculate_analytics_requests,
3233
get_calculate,
3334
generate_ai_explainer,
3435
)
@@ -78,6 +79,13 @@ def calculate(country_id):
7879
return get_calculate(country_id)
7980

8081

82+
@app.route("/analytics/calculate/requests", methods=["GET"])
83+
@require_auth_if_enabled([ANALYTICS_READ_SCOPE])
84+
@limiter.limit("60 per minute")
85+
def calculate_analytics_requests():
86+
return get_calculate_analytics_requests()
87+
88+
8189
@app.route("/<country_id>/ai-analysis", methods=["POST"])
8290
@require_auth_if_enabled()
8391
def ai_analysis(country_id: str):

policyengine_household_api/decorators/auth.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
from authlib.integrations.flask_oauth2 import ResourceProtector
1111
from authlib.oauth2.rfc6750 import BearerTokenValidator
1212
from ..auth.validation import Auth0JWTBearerTokenValidator
13-
from ..utils.config_loader import get_config, get_config_value
13+
from ..utils.config_loader import get_config_value
14+
15+
ANALYTICS_READ_SCOPE = "read:calculate-analytics"
1416

1517

1618
class StaticBearerToken:
@@ -33,15 +35,16 @@ def get_scope(self) -> str:
3335
class StaticBearerTokenValidator(BearerTokenValidator):
3436
"""Accept a single configured bearer token for test environments."""
3537

36-
def __init__(self, expected_token: str):
38+
def __init__(self, expected_token: str, scopes: str | None = ""):
3739
super().__init__()
3840
self.expected_token = expected_token
41+
self.scopes = scopes or ""
3942

4043
def authenticate_token(
4144
self, token_string: Optional[str]
4245
) -> Optional[StaticBearerToken]:
4346
if token_string == self.expected_token:
44-
return StaticBearerToken(token_string)
47+
return StaticBearerToken(token_string, scope=self.scopes)
4548
return None
4649

4750

@@ -98,6 +101,9 @@ def _setup_authentication(self) -> None:
98101
self._auth_enabled = get_config_value("auth.enabled", False)
99102
app_environment = get_config_value("app.environment", "")
100103
auth0_test_token = get_config_value("auth.auth0.test_token", "")
104+
auth0_test_token_scopes = get_config_value(
105+
"auth.auth0.test_token_scopes", ""
106+
)
101107

102108
# Get Auth0 configuration values
103109
auth0_address = get_config_value("auth.auth0.address", "")
@@ -108,7 +114,9 @@ def _setup_authentication(self) -> None:
108114
if app_environment == "test_with_auth" and auth0_test_token:
109115
resource_protector = ResourceProtector()
110116
resource_protector.register_token_validator(
111-
StaticBearerTokenValidator(auth0_test_token)
117+
StaticBearerTokenValidator(
118+
auth0_test_token, auth0_test_token_scopes
119+
)
112120
)
113121
self._decorator = resource_protector
114122
elif auth0_address and auth0_audience:
Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1-
from .home import get_home
2-
from .household import get_calculate
3-
from .household_explainer import generate_ai_explainer
1+
from .analytics import (
2+
get_calculate_analytics_requests as get_calculate_analytics_requests,
3+
)
4+
from .home import get_home as get_home
5+
from .household import get_calculate as get_calculate
6+
from .household_explainer import (
7+
generate_ai_explainer as generate_ai_explainer,
8+
)

0 commit comments

Comments
 (0)