Skip to content

Commit 046755c

Browse files
committed
add feed level fields
1 parent 4baefcd commit 046755c

File tree

3 files changed

+152
-3
lines changed

3 files changed

+152
-3
lines changed

docs/OperationsAPI.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -964,7 +964,7 @@ components:
964964
OperationFeed:
965965
x-operation: true
966966
allOf:
967-
- $ref: "#/components/schemas/BasicFeed"
967+
- $ref: "#/components/schemas/Feed"
968968
- type: object
969969
description: Feed response model.
970970
type: object

functions-python/operations_api/src/feeds_operations/impl/models/operation_feed_impl.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
from feeds_gen.models.operation_feed import OperationFeed
22
from shared.database_gen.sqlacodegen_models import Feed
3-
from shared.db_models.basic_feed_impl import BasicFeedImpl
3+
from shared.db_models.basic_feed_impl import BaseFeedImpl
44

55

6-
class OperationFeedImpl(BasicFeedImpl, OperationFeed):
6+
class OperationFeedImpl(BaseFeedImpl, OperationFeed):
77
"""Base implementation of the feeds models."""
88

99
class Config:

scripts/api-operations-token.sh

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Usage:
5+
# api-operations-token.sh /path/to/credentials.json [scopes] [port]
6+
#
7+
# Examples:
8+
# api-operations-token.sh ./client_secret.json \
9+
# "https://www.googleapis.com/auth/cloud-platform openid email profile" 8080
10+
# api-operations-token.sh ./client_secret.json \
11+
# "openid email profile" 8888
12+
#
13+
# Requires: jq, python3, openssl, curl
14+
# Note: Ensure the chosen port's redirect URI (http://localhost:<port>) is listed under
15+
# your OAuth client’s "Authorized redirect URIs" in GCP.
16+
17+
CREDS_JSON="${1:-}"
18+
SCOPES="${2:-https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile openid}"
19+
PORT="${3:-8080}"
20+
21+
if [[ -z "${CREDS_JSON}" || ! -f "${CREDS_JSON}" ]]; then
22+
echo "Error: provide path to your Google OAuth 'web' credentials JSON." >&2
23+
exit 1
24+
fi
25+
26+
need() { command -v "$1" >/dev/null 2>&1 || { echo "Missing dependency: $1" >&2; exit 1; }; }
27+
need jq
28+
need python3
29+
need openssl
30+
need curl
31+
32+
CLIENT_ID="$(jq -r '.web.client_id' "${CREDS_JSON}")"
33+
CLIENT_SECRET="$(jq -r '.web.client_secret // empty' "${CREDS_JSON}")"
34+
AUTH_URI="$(jq -r '.web.auth_uri' "${CREDS_JSON}")"
35+
TOKEN_URI="$(jq -r '.web.token_uri' "${CREDS_JSON}")"
36+
37+
if [[ -z "${CLIENT_ID}" || -z "${AUTH_URI}" || -z "${TOKEN_URI}" ]]; then
38+
echo "Error: client_id/auth_uri/token_uri not found under .web in ${CREDS_JSON}" >&2
39+
exit 1
40+
fi
41+
42+
REDIRECT_URI="http://localhost:${PORT}"
43+
44+
# Helpers
45+
b64url() { openssl base64 -A | tr '+/' '-_' | tr -d '='; }
46+
47+
# PKCE + state/nonce
48+
CODE_VERIFIER="$(openssl rand -base64 64 | tr -d '\n' | tr '+/' '-_' | tr -d '=')"
49+
CODE_CHALLENGE="$(printf '%s' "${CODE_VERIFIER}" | openssl dgst -binary -sha256 | b64url)"
50+
STATE="$(openssl rand -hex 16)"
51+
NONCE="$(openssl rand -hex 16)"
52+
53+
# Tiny one-shot HTTP server to capture ?code=
54+
TMP_DIR="$(mktemp -d)"
55+
CODE_FILE="${TMP_DIR}/auth_code.txt"
56+
57+
cat > "${TMP_DIR}/server.py" <<'PY'
58+
import http.server, socketserver, urllib.parse, sys
59+
PORT = int(sys.argv[1]); OUT = sys.argv[2]
60+
class H(http.server.BaseHTTPRequestHandler):
61+
def do_GET(self):
62+
p = urllib.parse.urlparse(self.path); q = urllib.parse.parse_qs(p.query)
63+
code = q.get("code", [""])[0]; state = q.get("state", [""])[0]
64+
with open(OUT, "w") as f: f.write(code + "\n" + state + "\n")
65+
self.send_response(200); self.send_header("Content-Type","text/html"); self.end_headers()
66+
self.wfile.write(b"<html><body><h2>You can close this window.</h2></body></html>")
67+
with socketserver.TCPServer(("127.0.0.1", PORT), H) as httpd: httpd.handle_request()
68+
PY
69+
70+
python3 "${TMP_DIR}/server.py" "${PORT}" "${CODE_FILE}" &
71+
SERVER_PID=$!
72+
73+
cleanup() {
74+
echo
75+
echo "Cleaning up local server (PID ${SERVER_PID})..."
76+
kill "${SERVER_PID}" >/dev/null 2>&1 || true
77+
rm -rf "${TMP_DIR}" >/dev/null 2>&1 || true
78+
}
79+
# Clean up on normal exit, Ctrl+C (INT), termination (TERM), or error (ERR)
80+
trap cleanup EXIT INT TERM ERR
81+
82+
# Build consent URL
83+
AUTH_URL="$AUTH_URI?response_type=code&client_id=$(printf %s "${CLIENT_ID}" | jq -sRr @uri)\
84+
&redirect_uri=$(printf %s "${REDIRECT_URI}" | jq -sRr @uri)\
85+
&scope=$(printf %s "${SCOPES}" | jq -sRr @uri)\
86+
&state=${STATE}&code_challenge=${CODE_CHALLENGE}&code_challenge_method=S256\
87+
&access_type=offline&prompt=consent&nonce=${NONCE}"
88+
89+
# Open browser
90+
if command -v open >/dev/null 2>&1; then
91+
open "${AUTH_URL}"
92+
elif command -v xdg-open >/dev/null 2>&1; then
93+
xdg-open "${AUTH_URL}"
94+
else
95+
echo "Open this URL in your browser:"
96+
echo "${AUTH_URL}"
97+
fi
98+
99+
echo "Waiting for authorization on ${REDIRECT_URI} ..."
100+
for _ in {1..180}; do [[ -s "${CODE_FILE}" ]] && break; sleep 1; done
101+
if [[ ! -s "${CODE_FILE}" ]]; then
102+
echo "Error: no authorization code received." >&2
103+
exit 1
104+
fi
105+
106+
AUTH_CODE="$(head -n1 "${CODE_FILE}")"
107+
READ_STATE="$(sed -n '2p' "${CODE_FILE}")"
108+
109+
if [[ "${READ_STATE}" != "${STATE}" ]]; then
110+
echo "Error: state mismatch. Aborting." >&2
111+
exit 1
112+
fi
113+
114+
# Exchange code for tokens
115+
POST_DATA=(
116+
-d "grant_type=authorization_code"
117+
-d "code=${AUTH_CODE}"
118+
-d "redirect_uri=${REDIRECT_URI}"
119+
-d "client_id=${CLIENT_ID}"
120+
-d "code_verifier=${CODE_VERIFIER}"
121+
)
122+
[[ -n "${CLIENT_SECRET}" ]] && POST_DATA+=(-d "client_secret=${CLIENT_SECRET}")
123+
124+
TOKENS_JSON="$(curl -sS -X POST "${TOKEN_URI}" \
125+
-H "Content-Type: application/x-www-form-urlencoded" "${POST_DATA[@]}")"
126+
127+
# Parse
128+
ACCESS_TOKEN="$(echo "${TOKENS_JSON}" | jq -r '.access_token // empty')"
129+
EXPIRES_IN="$(echo "${TOKENS_JSON}" | jq -r '.expires_in // empty')"
130+
REFRESH_TOKEN="$(echo "${TOKENS_JSON}" | jq -r '.refresh_token // empty')"
131+
TOKEN_TYPE="$(echo "${TOKENS_JSON}" | jq -r '.token_type // empty')"
132+
133+
134+
echo
135+
echo "=== Token Response (trimmed) ==="
136+
echo "${TOKENS_JSON}" | jq '{access_token, expires_in, token_type, scope, refresh_token: (has("refresh_token"))}'
137+
138+
echo
139+
echo "Saved:"
140+
echo " ${BASE}.json # full token response (keep secure)"
141+
echo " ${BASE}_access.token # access token"
142+
[[ -n "${REFRESH_TOKEN}" ]] && echo " ${BASE}_refresh.token # refresh token (store securely)"
143+
144+
145+
# Exit non-zero if we failed to produce an access token
146+
if [[ -z "${ACCESS_TOKEN}" ]]; then
147+
echo "ERROR: access_token is empty. Inspect ${BASE}.json for details." >&2
148+
exit 2
149+
fi

0 commit comments

Comments
 (0)