Skip to content

Commit f676327

Browse files
- Robot working locally.
1 parent 90f30f0 commit f676327

File tree

11 files changed

+251
-6
lines changed

11 files changed

+251
-6
lines changed

cicd/requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ psycopg2-binary>=2.9.9
22
psycopg[binary]>=3.1.16
33
PyYaml>=6.0.1
44
robotframework>=6.1.1
5-
sqlalchemy==1.4.44
5+
sqlalchemy==1.4.44
6+
Flask==3.0.3

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ require (
2121
github.com/spf13/cobra v1.4.0
2222
github.com/spf13/pflag v1.0.5
2323
github.com/spf13/viper v1.10.1
24-
github.com/stackql/any-sdk v0.0.3-beta20
24+
github.com/stackql/any-sdk v0.0.3-beta21
2525
github.com/stackql/go-suffix-map v0.0.1-alpha01
2626
github.com/stackql/psql-wire v0.1.1-alpha07
2727
github.com/stackql/stackql-parser v0.0.14-alpha04

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -471,8 +471,8 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
471471
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
472472
github.com/spf13/viper v1.10.1 h1:nuJZuYpG7gTj/XqiUwg8bA0cp1+M2mC3J4g5luUYBKk=
473473
github.com/spf13/viper v1.10.1/go.mod h1:IGlFPqhNAPKRxohIzWpI5QEy4kuI7tcl5WvR+8qy1rU=
474-
github.com/stackql/any-sdk v0.0.3-beta20 h1:7zHdJp0gM9G8vr5IDN/e8H74gqWI2MkWXGrfkQp6irA=
475-
github.com/stackql/any-sdk v0.0.3-beta20/go.mod h1:CIMFo3fC2ScpqzkzeCkzUQQuzYA1VuqpG0p1EZXN+wY=
474+
github.com/stackql/any-sdk v0.0.3-beta21 h1:1x76S9scXukHKcBUmzSVYpwWG8TnZXMhlgU0HHcTO2g=
475+
github.com/stackql/any-sdk v0.0.3-beta21/go.mod h1:CIMFo3fC2ScpqzkzeCkzUQQuzYA1VuqpG0p1EZXN+wY=
476476
github.com/stackql/go-suffix-map v0.0.1-alpha01 h1:TDUDS8bySu41Oo9p0eniUeCm43mnRM6zFEd6j6VUaz8=
477477
github.com/stackql/go-suffix-map v0.0.1-alpha01/go.mod h1:QAi+SKukOyf4dBtWy8UMy+hsXXV+yyEE4vmBkji2V7g=
478478
github.com/stackql/psql-wire v0.1.1-alpha07 h1:LQWVUlx4Bougk6dztDNG5tmXxpIVeeTSsInTj801xCs=

test/assets/credentials/dummy/google/docker-functional-test-dummy-sa-key.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"client_email": "[email protected]",
77
"client_id": "11",
88
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
9-
"token_uri": "https://host.docker.internal:1080/token",
9+
"token_uri": "http://host.docker.internal:2091/google/simple/token",
1010
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
1111
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/silly-sa%40silly-project-01.iam.gserviceaccount.com"
1212
}

test/assets/credentials/dummy/google/functional-test-dummy-sa-key.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"client_email": "[email protected]",
77
"client_id": "11",
88
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
9-
"token_uri": "https://localhost:1080/token",
9+
"token_uri": "http://localhost:2091/google/simple/token",
1010
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
1111
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/silly-sa%40silly-project-01.iam.gserviceaccount.com"
1212
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
from flask import Flask, request
2+
3+
import json
4+
5+
import base64
6+
7+
import logging
8+
9+
from typing import List
10+
11+
12+
"""
13+
- Google stuff based upon [the service account key flow doco](https://developers.google.com/identity/protocols/oauth2/service-account#httprest_1).
14+
15+
Example invocation:
16+
17+
```bash
18+
flask --app=test/python/flask/oaut/token_srv run --port=8070
19+
```
20+
21+
22+
Then, in some other shell:
23+
24+
```bash
25+
26+
curl -d 'grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=eyJhIjogImIifQ==.eyJzdWIiOiAianMifQ==.ZXlKaElqb2dJbUlpZlE9PS5leUp6ZFdJaU9pQWlhbk1pZlE9PQ==' http://127.0.0.1:8070/google/simple/token
27+
```
28+
"""
29+
30+
app = Flask(__name__)
31+
32+
app.logger.setLevel(logging.INFO)
33+
34+
class GoogleServiceAccountJWT(object):
35+
36+
def __init__(self, encoded_jwt: str) -> None:
37+
self._encoded_jwt = encoded_jwt
38+
split_jwt = encoded_jwt.split('.')
39+
if len(split_jwt) != 3:
40+
raise ValueError(f'invalid JWT; length {len(split_jwt)} != 3')
41+
self._jwt_header: dict = json.loads(base64.urlsafe_b64decode(self._pad(split_jwt[0])).decode('utf-8'))
42+
self._jwt_claims: dict = json.loads(base64.urlsafe_b64decode(self._pad(split_jwt[1])).decode('utf-8'))
43+
self._jwt_signature: bytes = base64.urlsafe_b64decode(self._pad(split_jwt[2]))
44+
45+
def _pad(self, s: str) -> str:
46+
return s + '=' * (4 - len(s) % 4)
47+
48+
def generate_google_response_dict(self, default_scopes: List[str]=["https://www.googleapis.com/auth/cloud-platform"]) -> dict:
49+
return {
50+
"access_token": "1/8xbJqaOZXSUZbHLl5EOtu1pxz3fmmetKx9W8CV4t79M",
51+
"scope": ' '.join(default_scopes),
52+
"token_type": "Bearer",
53+
"expires_in": 3600
54+
}
55+
56+
_SIMPLE_RESPONSE = {
57+
"access_token": "1/8xbJqaOZXSUZbHLl5EOtu1pxz3fmmetKx9W8CV4t79M",
58+
"scope": 'my-scope',
59+
"token_type": "Bearer",
60+
"expires_in": 3600
61+
}
62+
63+
@app.route("/")
64+
def hello_world():
65+
return "<p>Hello, World!</p>"
66+
67+
@app.route("/google/simple/token", methods=['POST'])
68+
def google_simple_token():
69+
request_data = request.form
70+
proffrered_jwt = request_data['assertion']
71+
app.logger.info(f'proffered_jwt: {proffrered_jwt}')
72+
google_jwt: GoogleServiceAccountJWT = GoogleServiceAccountJWT(proffrered_jwt)
73+
return json.dumps(google_jwt.generate_google_response_dict(), sort_keys=True)
74+
75+
@app.route("/contrived/simple/error/token", methods=['POST'])
76+
def google_simple_error_token():
77+
return json.dumps({"msg": "auth failed"}, sort_keys=True), 401
78+
79+
@app.route("/contrived/simple/token", methods=['POST'])
80+
def contrived_simple_token():
81+
request_data = request.form
82+
return json.dumps(_SIMPLE_RESPONSE, sort_keys=True)

test/python/oauth2_token.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import base64, json
2+
3+
import argparse
4+
5+
6+
def create_token(header: dict, claims: dict) -> str:
7+
"""
8+
Create a token from the claims.
9+
"""
10+
header_b64 = base64.urlsafe_b64encode(json.dumps(header, sort_keys=True).encode('utf-8')).decode('utf-8')
11+
claims_b64 = base64.urlsafe_b64encode(json.dumps(claims, sort_keys=True).encode('utf-8')).decode('utf-8')
12+
signature = f'{header_b64}.{claims_b64}'
13+
14+
return f'{header_b64}.{claims_b64}.{base64.urlsafe_b64encode(signature.encode("utf-8")).decode("utf-8")}'
15+
16+
17+
def parse_args() -> argparse.Namespace:
18+
"""
19+
Parse the arguments.
20+
"""
21+
parser = argparse.ArgumentParser(description='Create a token.')
22+
parser.add_argument('--create-token', help='Opt-in create token', action=argparse.BooleanOptionalAction)
23+
parser.add_argument('--header', type=str, help='The header.')
24+
parser.add_argument('--claims', type=str, help='The claims.')
25+
return parser.parse_args()
26+
27+
28+
def generate_token(ns: argparse.Namespace) -> str:
29+
"""
30+
Create a token.
31+
"""
32+
header = json.loads(ns.header)
33+
claims = json.loads(ns.claims)
34+
return create_token(header, claims)
35+
36+
37+
def main() -> None:
38+
"""
39+
Main entry point.
40+
"""
41+
args = parse_args()
42+
if args.create_token:
43+
print(generate_token(args))
44+
return
45+
exit(1)
46+
47+
48+
if __name__ == '__main__':
49+
main()
50+

test/robot/functional/stackql.resource

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Library OperatingSystem
1616
Library String
1717
Library ${LOCAL_LIB_HOME}/StackQLInterfaces.py ${EXECUTION_PLATFORM} ${SQL_BACKEND} ${CONCURRENCY_LIMIT}
1818
Library ${LOCAL_LIB_HOME}/CloudIntegration.py
19+
Library ${LOCAL_LIB_HOME}/web_service_keywords.py
1920

2021
*** Keywords ***
2122
Start Mock Server
@@ -42,6 +43,7 @@ Start All Mock Servers
4243
Start Mock Server ${JSON_INIT_FILE_PATH_SUMOLOGIC} ${MOCKSERVER_JAR} ${MOCKSERVER_PORT_SUMOLOGIC}
4344
Start Mock Server ${JSON_INIT_FILE_PATH_DIGITALOCEAN} ${MOCKSERVER_JAR} ${MOCKSERVER_PORT_DIGITALOCEAN}
4445
Start Mock Server ${JSON_INIT_FILE_PATH_STACKQL_AUTH_TESTING} ${MOCKSERVER_JAR} ${MOCKSERVER_PORT_STACKQL_AUTH_TESTING}
46+
Create OAuth2 Client Credentials Web Service ${MOCKSERVER_PORT_OAUTH_CLIENT_CREDENTIALS_TOKEN}
4547

4648

4749
Prepare StackQL Environment

test/robot/lib/stackql_context.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,8 @@ def get_registry_cfg(url :str, local_root :str, nop_verify :bool) -> dict:
562562
JSON_INIT_FILE_PATH_DIGITALOCEAN = os.path.join(REPOSITORY_ROOT, 'test', 'mockserver', 'expectations', 'static-digitalocean-expectations.json')
563563
MOCKSERVER_PORT_DIGITALOCEAN = 1097
564564

565+
MOCKSERVER_PORT_OAUTH_CLIENT_CREDENTIALS_TOKEN = 2091
566+
565567
JSON_INIT_FILE_PATH_REGISTRY = os.path.join(REPOSITORY_ROOT, 'test', 'mockserver', 'expectations', 'static-registry-expectations.json')
566568

567569
PG_SRV_PORT_MTLS = 5476
@@ -892,6 +894,7 @@ def get_variables(execution_env :str, sql_backend_str :str, use_stackql_preinsta
892894
'MOCKSERVER_PORT_GOOGLE': MOCKSERVER_PORT_GOOGLE,
893895
'MOCKSERVER_PORT_GOOGLEADMIN': MOCKSERVER_PORT_GOOGLEADMIN,
894896
'MOCKSERVER_PORT_STACKQL_AUTH_TESTING': MOCKSERVER_PORT_STACKQL_AUTH_TESTING,
897+
'MOCKSERVER_PORT_OAUTH_CLIENT_CREDENTIALS_TOKEN': MOCKSERVER_PORT_OAUTH_CLIENT_CREDENTIALS_TOKEN,
895898
'MOCKSERVER_PORT_K8S': MOCKSERVER_PORT_K8S,
896899
'MOCKSERVER_PORT_OKTA': MOCKSERVER_PORT_OKTA,
897900
'MOCKSERVER_PORT_REGISTRY': MOCKSERVER_PORT_REGISTRY,
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
from robot.api.deco import library, keyword
2+
3+
from robot.libraries.Process import Process
4+
5+
import json
6+
7+
from requests import get, post, Response
8+
9+
import os
10+
11+
from typing import Union, Tuple, List
12+
13+
@library
14+
class web_service_keywords(Process):
15+
16+
_DEFAULT_SQLITE_DB_PATH: str = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "tmp", "robot_cli_affirmation_store.db"))
17+
18+
def _get_dsn(self) -> str:
19+
return self._DEFAULT_SQLITE_DB_PATH
20+
21+
def __init__(self):
22+
self._affirmation_store_web_service = None
23+
self._web_server_app: str = 'test/python/flask/oauth2/token_srv'
24+
super().__init__()
25+
26+
@keyword
27+
def create_oauth2_client_credentials_web_service(
28+
self,
29+
port: int
30+
) -> None:
31+
"""
32+
Sign the input.
33+
"""
34+
return self.start_process(
35+
'flask',
36+
f'--app={self._web_server_app}',
37+
'run',
38+
f'--port={port}',
39+
stdout=os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'log', f'token-client-credentials-{port}-stdout.txt')),
40+
stderr=os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'log', f'token-client-credentials-{port}-stderr.txt'))
41+
)
42+
43+
@keyword
44+
def send_get_request(
45+
self,
46+
address: str
47+
) -> Response:
48+
"""
49+
Send a simple get request.
50+
"""
51+
return get(address)
52+
53+
@keyword
54+
def send_json_post_request(
55+
self,
56+
address: str,
57+
input: dict
58+
) -> Response:
59+
"""
60+
Send a canonical json post request.
61+
"""
62+
return post(address, json=input)
63+
64+
@keyword
65+
def send_to_affirmation_store(
66+
self,
67+
frame_key_val: str,
68+
json_input: Union[str, dict],
69+
affirmation_store_url: str = 'http://127.0.0.1:5848',
70+
path_prefix: str = '/data/',
71+
frame_key: str = '_frame_hash'
72+
) -> Response:
73+
"""
74+
Send a canonical json post request.
75+
"""
76+
if isinstance(json_input, str):
77+
input = json.loads(json_input)
78+
return post(f'{affirmation_store_url}{path_prefix}{frame_key_val}', json=input)
79+
80+
@keyword
81+
def extract_frame_key(
82+
self,
83+
json_input: Union[str, dict],
84+
frame_key: str = '_frame_hash'
85+
) -> Response:
86+
"""
87+
Extract frame hash key.
88+
"""
89+
if isinstance(json_input, str):
90+
json_input = json.loads(json_input)
91+
frame_key_val = json_input.get(frame_key)
92+
return frame_key_val
93+
94+
@keyword
95+
def retrieve_from_affirmation_store(
96+
self,
97+
frame_key_val: str,
98+
affirmation_store_url: str = 'http://127.0.0.1:5848',
99+
path_prefix: str = '/data/',
100+
frame_key: str = '_frame_hash'
101+
) -> Response:
102+
"""
103+
Retrieve from affirmation store.
104+
"""
105+
return get(f'{affirmation_store_url}{path_prefix}{frame_key_val}')

0 commit comments

Comments
 (0)