argoproj-labs / argo-client-python

⚠️⚠️⚠️This repository is no longer maintained, please find your Java SDKs https://github.com/argoproj/argo-workflows/blob/master/docs/client-libraries.md ⚠️⚠️⚠️
https://github.com/argoproj/argo
Apache License 2.0
78 stars 28 forks source link

Auth with Argo API Token #30

Open xkortex opened 3 years ago

xkortex commented 3 years ago

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\""}
xkortex commented 3 years ago

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.

xkortex commented 3 years ago

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
fvdnabee commented 3 years ago

This appears to be a duplicate of #26