Skip to content

Commit 8b3d0f5

Browse files
chore(cloudrun): refactor to sample 'cloudrun_service_to_service_receive' and its test (#13292)
* fix(cloudrun): update dependencies to latest versions * fix(cloudrun): delete noxfile_config.py as it was outdated for Python 3.8 and is not required anymore * fix(cloudrun): refactor sample and test to comply with current Style Guide. - Apply fixes for Style Guide - Add type hints - Rename variables to be consistent with their values - Add HTTP error codes constants to avoid managing 'magic numbers' - Rewrite comments for accuracy * fix(cloudrun): migrate region tag step 1 - add region with prefix auth as this sample is not specific to Cloud Run * fix(cloudrun): add comment suggested by gemini-code-assist * fix(cloudrun): add missing new line * fix(cloudrun): add test for anonymous request, and rename key function and region tag * fix(cloudrun): add sample to detect an invalid token * fix(cloudrun): add missing new line * fix(cloudrun): apply feedback from PR Review by glasnt PR Review #13292 (review) - Replace HTTP status codes with IntEnum from http.HTTPStatus - Replace hard coded token with a fixture for a fake token - Remove duplicated code for the client, and moving it to a test fixture Also - Add HTTP codes to the app.py `/` endpoint - Replace 'UTF-8' with 'utf-8' to follow the official documentation * fix(cloudrun): apply feedback from PR Review #13292 (comment)
1 parent e36edb4 commit 8b3d0f5

File tree

6 files changed

+129
-99
lines changed

6 files changed

+129
-99
lines changed

run/service-auth/app.py

+11-4
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,29 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from http import HTTPStatus
1516
import os
1617

1718
from flask import Flask, request
1819

19-
from receive import receive_authorized_get_request
20+
from receive import receive_request_and_parse_auth_header
2021

2122
app = Flask(__name__)
2223

2324

2425
@app.route("/")
25-
def main():
26+
def main() -> str:
2627
"""Example route for receiving authorized requests."""
2728
try:
28-
return receive_authorized_get_request(request)
29+
response = receive_request_and_parse_auth_header(request)
30+
31+
status = HTTPStatus.UNAUTHORIZED
32+
if "Hello" in response:
33+
status = HTTPStatus.OK
34+
35+
return response, status
2936
except Exception as e:
30-
return f"Error verifying ID token: {e}"
37+
return f"Error verifying ID token: {e}", HTTPStatus.UNAUTHORIZED
3138

3239

3340
if __name__ == "__main__":

run/service-auth/noxfile_config.py

-36
This file was deleted.

run/service-auth/receive.py

+24-14
Original file line numberDiff line numberDiff line change
@@ -12,39 +12,49 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
"""
16-
Demonstrates how to receive authenticated service-to-service requests, eg
17-
for Cloud Run or Cloud Functions
15+
"""Demonstrates how to receive authenticated service-to-service requests.
16+
17+
For example for Cloud Run or Cloud Functions.
1818
"""
1919

20+
# [START auth_validate_and_decode_bearer_token_on_flask]
2021
# [START cloudrun_service_to_service_receive]
22+
from flask import Request
2123

24+
from google.auth.exceptions import GoogleAuthError
2225
from google.auth.transport import requests
2326
from google.oauth2 import id_token
2427

2528

26-
def receive_authorized_get_request(request):
27-
"""Parse the authorization header and decode the information
28-
being sent by the Bearer token.
29+
def receive_request_and_parse_auth_header(request: Request) -> str:
30+
"""Parse the authorization header, validate the Bearer token
31+
and decode the token to get its information.
2932
3033
Args:
31-
request: Flask request object
34+
request: Flask request object.
3235
3336
Returns:
34-
The email from the request's Authorization header.
37+
One of the following:
38+
a) The email from the request's Authorization header.
39+
b) A welcome message for anonymous users.
40+
c) An error description.
3541
"""
3642
auth_header = request.headers.get("Authorization")
3743
if auth_header:
38-
# split the auth type and value from the header.
44+
# Split the auth type and value from the header.
3945
auth_type, creds = auth_header.split(" ", 1)
4046

4147
if auth_type.lower() == "bearer":
42-
claims = id_token.verify_token(creds, requests.Request())
43-
return f"Hello, {claims['email']}!\n"
44-
48+
# Find more information about `verify_token` function here:
49+
# https://google-auth.readthedocs.io/en/master/reference/google.oauth2.id_token.html#google.oauth2.id_token.verify_token
50+
try:
51+
decoded_token = id_token.verify_token(creds, requests.Request())
52+
return f"Hello, {decoded_token['email']}!\n"
53+
except GoogleAuthError as e:
54+
return f"Invalid token: {e}\n"
4555
else:
4656
return f"Unhandled header format ({auth_type}).\n"
47-
return "Hello, anonymous user.\n"
48-
4957

58+
return "Hello, anonymous user.\n"
5059
# [END cloudrun_service_to_service_receive]
60+
# [END auth_validate_and_decode_bearer_token_on_flask]

run/service-auth/receive_test.py

+90-41
Original file line numberDiff line numberDiff line change
@@ -15,44 +15,80 @@
1515
# This test deploys a secure application running on Cloud Run
1616
# to test that the authentication sample works properly.
1717

18+
from http import HTTPStatus
1819
import os
1920
import subprocess
2021
from urllib import error, request
2122
import uuid
2223

2324
import pytest
25+
2426
import requests
2527
from requests.adapters import HTTPAdapter
2628
from requests.packages.urllib3.util.retry import Retry
29+
from requests.sessions import Session
30+
31+
PROJECT_ID = os.environ["GOOGLE_CLOUD_PROJECT"]
32+
33+
STATUS_FORCELIST = [
34+
HTTPStatus.BAD_REQUEST,
35+
HTTPStatus.UNAUTHORIZED,
36+
HTTPStatus.FORBIDDEN,
37+
HTTPStatus.NOT_FOUND,
38+
HTTPStatus.INTERNAL_SERVER_ERROR,
39+
HTTPStatus.BAD_GATEWAY,
40+
HTTPStatus.SERVICE_UNAVAILABLE,
41+
HTTPStatus.GATEWAY_TIMEOUT,
42+
],
2743

2844

29-
@pytest.fixture()
30-
def services():
31-
# Unique suffix to create distinct service names
32-
suffix = uuid.uuid4().hex
33-
service_name = f"receive-{suffix}"
34-
project = os.environ["GOOGLE_CLOUD_PROJECT"]
45+
@pytest.fixture(scope="module")
46+
def service_name() -> str:
47+
# Add a unique suffix to create distinct service names.
48+
service_name_str = f"receive-{uuid.uuid4().hex}"
3549

36-
# Deploy receive Cloud Run Service
50+
# Deploy the Cloud Run Service.
3751
subprocess.run(
3852
[
3953
"gcloud",
4054
"run",
4155
"deploy",
42-
service_name,
56+
service_name_str,
4357
"--project",
44-
project,
58+
PROJECT_ID,
4559
"--source",
4660
".",
4761
"--region=us-central1",
4862
"--allow-unauthenticated",
4963
"--quiet",
5064
],
65+
# Rise a CalledProcessError exception for a non-zero exit code.
5166
check=True,
5267
)
5368

54-
# Get the URL for the service
55-
endpoint_url = (
69+
yield service_name_str
70+
71+
# Clean-up after running the test.
72+
subprocess.run(
73+
[
74+
"gcloud",
75+
"run",
76+
"services",
77+
"delete",
78+
service_name_str,
79+
"--project",
80+
PROJECT_ID,
81+
"--async",
82+
"--region=us-central1",
83+
"--quiet",
84+
],
85+
check=True,
86+
)
87+
88+
89+
@pytest.fixture(scope="module")
90+
def endpoint_url(service_name: str) -> str:
91+
endpoint_url_str = (
5692
subprocess.run(
5793
[
5894
"gcloud",
@@ -61,7 +97,7 @@ def services():
6197
"describe",
6298
service_name,
6399
"--project",
64-
project,
100+
PROJECT_ID,
65101
"--region=us-central1",
66102
"--format=value(status.url)",
67103
],
@@ -72,7 +108,12 @@ def services():
72108
.decode()
73109
)
74110

75-
token = (
111+
return endpoint_url_str
112+
113+
114+
@pytest.fixture(scope="module")
115+
def token() -> str:
116+
token_str = (
76117
subprocess.run(
77118
["gcloud", "auth", "print-identity-token"],
78119
stdout=subprocess.PIPE,
@@ -82,38 +123,20 @@ def services():
82123
.decode()
83124
)
84125

85-
yield endpoint_url, token
86-
87-
subprocess.run(
88-
[
89-
"gcloud",
90-
"run",
91-
"services",
92-
"delete",
93-
service_name,
94-
"--project",
95-
project,
96-
"--async",
97-
"--region=us-central1",
98-
"--quiet",
99-
],
100-
check=True,
101-
)
102-
126+
return token_str
103127

104-
def test_auth(services):
105-
url = services[0]
106-
token = services[1]
107128

108-
req = request.Request(url)
129+
@pytest.fixture(scope="module")
130+
def client(endpoint_url: str) -> Session:
131+
req = request.Request(endpoint_url)
109132
try:
110133
_ = request.urlopen(req)
111134
except error.HTTPError as e:
112-
assert e.code == 403
135+
assert e.code == HTTPStatus.FORBIDDEN
113136

114137
retry_strategy = Retry(
115138
total=3,
116-
status_forcelist=[400, 401, 403, 404, 500, 502, 503, 504],
139+
status_forcelist=STATUS_FORCELIST,
117140
allowed_methods=["GET", "POST"],
118141
backoff_factor=3,
119142
)
@@ -122,8 +145,34 @@ def test_auth(services):
122145
client = requests.session()
123146
client.mount("https://", adapter)
124147

125-
response = client.get(url, headers={"Authorization": f"Bearer {token}"})
148+
return client
149+
150+
151+
def test_authentication_on_cloud_run(
152+
client: Session, endpoint_url: str, token: str
153+
) -> None:
154+
response = client.get(
155+
endpoint_url, headers={"Authorization": f"Bearer {token}"}
156+
)
157+
response_content = response.content.decode("utf-8")
158+
159+
assert response.status_code == HTTPStatus.OK
160+
assert "Hello" in response_content
161+
assert "anonymous" not in response_content
162+
163+
164+
def test_anonymous_request_on_cloud_run(client: Session, endpoint_url: str) -> None:
165+
response = client.get(endpoint_url)
166+
response_content = response.content.decode("utf-8")
167+
168+
assert response.status_code == HTTPStatus.OK
169+
assert "Hello" in response_content
170+
assert "anonymous" in response_content
171+
172+
173+
def test_invalid_token(client: Session, endpoint_url: str) -> None:
174+
response = client.get(
175+
endpoint_url, headers={"Authorization": "Bearer i-am-not-a-real-token"}
176+
)
126177

127-
assert response.status_code == 200
128-
assert "Hello" in response.content.decode("UTF-8")
129-
assert "anonymous" not in response.content.decode("UTF-8")
178+
assert response.status_code == HTTPStatus.UNAUTHORIZED
+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
pytest==8.2.0
1+
pytest==8.3.5

run/service-auth/requirements.txt

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
google-auth==2.38.0
2-
requests==2.31.0
3-
Flask==3.0.3
2+
requests==2.32.3
3+
Flask==3.1.0
44
gunicorn==23.0.0
5-
Werkzeug==3.0.3
5+
Werkzeug==3.1.3

0 commit comments

Comments
 (0)