argo-client-python
argo-client-python copied to clipboard
Auth with Argo API Token
~~edit: pretty sure this is a PEBCAK error, it seems this token auth method does work, but my permissions configurations are not ideal~~
edit2: ok yeah I think I'm definitely missing something, I get
User "system:serviceaccount:argo:argo-server" cannot list resource "workflows" in API group "argoproj.io" in the namespace "dry-run"
The user is system:serviceaccount:argo:argo-server irrespective of what serivecaccount I use to generate the token.
ubuntu18.04
kubectl 1.20.4
python 3.8.0
argo-workflows 5.0.0
argo:2746/api/v1/version
{"version":"v2.12.9","buildDate":"2021-02-16T22:54:36Z","gitCommit":"737905345d70ba1ebd566ce1230e4f971993dfd0","gitTag":"v2.12.9","gitTreeState":"clean","goVersion":"go1.13.4","compiler":"gc","platform":"linux/amd64"}
I'm developing a bot which can issue Argo workflows. I'm struggling to work out how to provide authorization through the Configuration interface. I've generated an ARGO_TOKEN as per these instructions. Looks like: "eyJhbGciOiJSUzI1NiI...", 880 characters of what looks like b64 encoded RS256 token. I'm actually using the argo serviceaccount so I should have full perms. Works with curl http://localhost:2746/api/v1/workflows/argo -H "Authorization: $ARGO_TOKEN".
bearer = "eyJhbGciOiJSUzI1NiI..."
config = Configuration(host="http://localhost:2746", api_key={'authorization': bearer}, api_key_prefix={'authorization': 'Bearer'})
client = ApiClient(configuration=config)
service = WorkflowServiceApi(api_client=client)
workflows = service.list_workflows(argo_ns)
ApiException: (403)
Reason: Forbidden
HTTP response headers: HTTPHeaderDict({'Content-Type': 'application/json', 'Trailer': 'Grpc-Trailer-Content-Type', 'Date': 'Wed, 03 Mar 2021 23:06:47 GMT', 'Transfer-Encoding': 'chunked'})
HTTP response body: {"code":7,"message":"workflows.argoproj.io is forbidden: User \"system:serviceaccount:argo:argo-server\" cannot list resource \"workflows\" in API group \"argoproj.io\" in the namespace \"dry-run\""}
I feel like I'm configuring api_key and api_key_prefix wrong. I've tried variations of 'authorization', 'bearer', 'Bearer', etc, and can't get it to stick. The documentation isn't super clear how to use various auth schemes (other than cookieAuth) and I'm wondering if the protocol has drifted.
Thanks
Full trace:
ApiException Traceback (most recent call last)
<ipython-input-91-730ead41966d> in <module>
----> 1 workflows = service.list_workflows(argo_ns)
2 workflows
~/.virtualenvs/semafor38/lib/python3.8/site-packages/argo/workflows/client/api/workflow_service_api.py in list_workflows(self, namespace, **kwargs)
601 """
602 kwargs['_return_http_data_only'] = True
--> 603 return self.list_workflows_with_http_info(namespace, **kwargs) # noqa: E501
604
605 def list_workflows_with_http_info(self, namespace, **kwargs): # noqa: E501
~/.virtualenvs/semafor38/lib/python3.8/site-packages/argo/workflows/client/api/workflow_service_api.py in list_workflows_with_http_info(self, namespace, **kwargs)
711 auth_settings = [] # noqa: E501
712
--> 713 return self.api_client.call_api(
714 '/api/v1/workflows/{namespace}', 'GET',
715 path_params,
~/.virtualenvs/semafor38/lib/python3.8/site-packages/argo/workflows/client/api_client.py in call_api(self, resource_path, method, path_params, query_params, header_params, body, post_params, files, response_type, auth_settings, async_req, _return_http_data_only, collection_formats, _preload_content, _request_timeout, _host)
362 """
363 if not async_req:
--> 364 return self.__call_api(resource_path, method,
365 path_params, query_params, header_params,
366 body, post_params, files,
~/.virtualenvs/semafor38/lib/python3.8/site-packages/argo/workflows/client/api_client.py in __call_api(self, resource_path, method, path_params, query_params, header_params, body, post_params, files, response_type, auth_settings, _return_http_data_only, collection_formats, _preload_content, _request_timeout, _host)
186 except ApiException as e:
187 e.body = e.body.decode('utf-8') if six.PY3 else e.body
--> 188 raise e
189
190 content_type = response_data.getheader('content-type')
~/.virtualenvs/semafor38/lib/python3.8/site-packages/argo/workflows/client/api_client.py in __call_api(self, resource_path, method, path_params, query_params, header_params, body, post_params, files, response_type, auth_settings, _return_http_data_only, collection_formats, _preload_content, _request_timeout, _host)
179 try:
180 # perform request and return response
--> 181 response_data = self.request(
182 method, url, query_params=query_params, headers=header_params,
183 post_params=post_params, body=body,
~/.virtualenvs/semafor38/lib/python3.8/site-packages/argo/workflows/client/api_client.py in request(self, method, url, query_params, headers, post_params, body, _preload_content, _request_timeout)
387 """Makes the HTTP request using RESTClient."""
388 if method == "GET":
--> 389 return self.rest_client.GET(url,
390 query_params=query_params,
391 _preload_content=_preload_content,
~/.virtualenvs/semafor38/lib/python3.8/site-packages/argo/workflows/client/rest.py in GET(self, url, headers, query_params, _preload_content, _request_timeout)
228 def GET(self, url, headers=None, query_params=None, _preload_content=True,
229 _request_timeout=None):
--> 230 return self.request("GET", url,
231 headers=headers,
232 _preload_content=_preload_content,
~/.virtualenvs/semafor38/lib/python3.8/site-packages/argo/workflows/client/rest.py in request(self, method, url, query_params, headers, body, post_params, _preload_content, _request_timeout)
222
223 if not 200 <= r.status <= 299:
--> 224 raise ApiException(http_resp=r)
225
226 return r
ApiException: (403)
Reason: Forbidden
HTTP response headers: HTTPHeaderDict({'Content-Type': 'application/json', 'Trailer': 'Grpc-Trailer-Content-Type', 'Date': 'Wed, 03 Mar 2021 23:06:47 GMT', 'Transfer-Encoding': 'chunked'})
HTTP response body: {"code":7,"message":"workflows.argoproj.io is forbidden: User \"system:serviceaccount:argo:argo-server\" cannot list resource \"workflows\" in API group \"argoproj.io\" in the namespace \"dry-run\""}
I created this sa/clusterrole:
apiVersion: v1
kind: ServiceAccount
metadata:
name: argo-master-sa
namespace: default
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: argo-master-role
rules:
- apiGroups:
- argoproj.io
resources:
- workflows
- workflows/finalizers
verbs:
- get
- list
- watch
- update
- patch
- delete
- create
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: argo-master-binding-ns-argo
namespace: argo
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: argo-master-role
subjects:
- kind: ServiceAccount
name: argo-master-sa
namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: argo-master-binding-ns-dry-run
namespace: dry-run
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: argo-master-role
subjects:
- kind: ServiceAccount
name: argo-master-sa
namespace: default
Acquired token:
SECRET=$(kubectl get sa argo-master-sa -o=jsonpath='{.secrets[0].name}')
ARGO_TOKEN="Bearer $(kubectl get secret $SECRET -o=jsonpath='{.data.token}' | base64 --decode)"
Access across namespace with curl:
curl http://localhost:2746/api/v1/workflows/argo -H "Authorization: $ARGO_TOKEN"
{...success...}
curl http://localhost:2746/api/v1/workflows/dry-run -H "Authorization: $ARGO_TOKEN"
{...success...}
And correctly scoped:
curl http://localhost:2746/api/v1/workflows/not-a-ns-this-will-fail-successfully -H "Authorization: $ARGO_TOKEN"
{"code":7,"message":"workflows.argoproj.io is forbidden: User \"system:serviceaccount:default:argo-master-sa\" cannot list resource \"workflows\" in API group \"argoproj.io\" in the namespace \"not-a-ns-this-will-fail-successfully\""}
import os, sys, time, re, json
import argo
import requests
import yaml
from argo.workflows.client import (ApiClient,
WorkflowServiceApi, InfoServiceApi,
Configuration,
V1alpha1WorkflowCreateRequest)
WORKFLOW = 'https://raw.githubusercontent.com/argoproj/argo-workflows/master/examples/hello-world.yaml'
resp = requests.get(WORKFLOW)
manifest: dict = yaml.safe_load(resp.text)
# argo-master-sa
bearer = "eyJhbGc..."
config = Configuration(host="http://localhost:2746", api_key={'authorization': bearer}, api_key_prefix={'authorization': 'Bearer'})
config.debug = True
client = ApiClient(configuration=config)
service = WorkflowServiceApi(api_client=client)
# this works
res = requests.get('http://localhost:2746/api/v1/workflows/argo')
res.json()
# this fails as expected with 403
res = requests.get('http://localhost:2746/api/v1/workflows/dry-run')
res.json()
# this works, we can auth this with our bearer token
res = requests.get('http://localhost:2746/api/v1/workflows/dry-run', headers={"Authorization": "Bearer " + bearer})
res.json()
# this works, since it's the default ns
workflows = service.list_workflows('argo')
# this fails despite setting the token in config
workflows = service.list_workflows('dry-run')
workflows
Notably, the service.list_workflows still works in the argo namespace, even when the token is bad, which I think means that my environment (a jupyter notebook) and the library defaults succeed with the argo namespace.
Aha! Limited success by using
client = ApiClient(configuration=config, header_name="Authorization", header_value= "Bearer " + bearer)
but this feels wrong-ish. I feel like there ought to be a way to set the serviceaccount in the config.
Alas, this indeed seems to be incorrect in some fashion, as the workflows are stuck pending in my alternate namespace:
argo list -n dry-run
NAME STATUS AGE DURATION PRIORITY
coinflip-8q944 Pending 19s 0s 0
hello-world-6666d Pending 18h 0s 0
hello-world-s5ls4 Pending 18h 0s 0
argo list -n argo
NAME STATUS AGE DURATION PRIORITY
coinflip-hmf6h Running 4s 4s 0
hello-world-2gsfc Succeeded 18h 10s 0
hello-world-vr5d6 Succeeded 18h 10s 0
This appears to be a duplicate of #26