Skip to content

Commit 1e7bcbc

Browse files
committed
Implement OpenID Connect Agent (oidc-agent) support
1 parent 78bad7f commit 1e7bcbc

File tree

6 files changed

+204
-14
lines changed

6 files changed

+204
-14
lines changed

doc/source/user/shell.rst

+19-3
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,25 @@
44
Authentication
55
--------------
66

7-
Before using :program:`orpy`, put your a valid OpenID Connnect access token
8-
into the ``ORCHESTRATOR_TOKEN`` environment variable, so that we can use this
9-
token for authentication.
7+
In order to interact with the INDIGO PaaS Orchestrator we need to use an
8+
OpenID Connect access token from a trusted OpenID Connect provider at the
9+
orchestrator.
10+
11+
Please either store your access token in ``ORCHESTRATOR_TOKEN`` or set the
12+
account to use with :program:`oidc-agent` in the ``OIDC_ACCOUNT`` and the
13+
socket path of the oidc-agent in the ``OIDC_SOCK`` environment variable::
14+
15+
export ORCHESTRATOR_TOKEN=<your access token>
16+
OR
17+
export OIDC_SOCK=<path to the oidc-agent socket>
18+
export OIDC_ACCOUNT=<account to use>
19+
20+
Usually, the ``OIDC_SOCK`` environmental variable is already exported if you
21+
are using :program:`oidc-agent`.
22+
23+
As an alternative, you can pass the socket path and the account through the
24+
command line with the ``--oidc-agent-sock`` and ``--oidc-agent-account``
25+
parameters.
1026

1127
Usage
1228
-----

orpy/client/client.py

+37-3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import json
2121
import logging
2222
import uuid
23+
import warnings
2324

2425
import requests
2526
import six
@@ -46,16 +47,42 @@ def default(self, o):
4647
class OrpyClient(object):
4748
"""An INDIGO-DataCloud PaaS orchestrator client class."""
4849

49-
def __init__(self, url, token, debug=False):
50+
def __init__(self, url, oidc_agent=None, token=None, debug=False):
5051
"""Initialization of OrpyClient object.
5152
53+
You MUST pass either a valid orpy.oidc.OpenIDConnectAgent object into
54+
the oidc_agent parameter or a valid access token for authentication.
55+
The former method is the preferred, you can create an object this way
56+
(assuming that the oidc-agent account is named "oidc-agent-account":
57+
58+
from orpy import oidc
59+
from orpy.client import client
60+
oidc_agent = oidc.OpenIDConnectAgent("oidc-agent-account")
61+
62+
cli = client.OrpyClient(url, oidc_agent=oidc_agent)
63+
64+
Note that passing both will result in a warning, with the access token
65+
passed in the token paramter ignored.
66+
5267
:param str url: Orchestrator URL
53-
:param str token: OpenID Connect access token to use for auth
68+
:param orpy.oidc.OpenIDConnectAgent oidc_agent: OpenID Connect agent
69+
object to use for
70+
fetching access tokens
71+
:param str token: OpenID Connect access token to use for auth.
5472
:param bool debug: whether to enable debug logging
5573
"""
5674

5775
self.url = url
58-
self.token = token
76+
self._token = token
77+
self.oidc_agent = oidc_agent
78+
79+
if not any([oidc_agent, token]):
80+
raise exceptions.InvalidUsage("Must pass either an oidc-agent "
81+
"object or an access token.")
82+
if all([oidc_agent, token]):
83+
msg = ("Using both oidc-agent and access token means that the "
84+
"user provider token will be ignored.")
85+
warnings.warn(msg, RuntimeWarning)
5986

6087
self.http_debug = debug
6188

@@ -81,6 +108,13 @@ def __init__(self, url, token, debug=False):
81108
self._json = _JSONEncoder()
82109
self.session = requests.Session()
83110

111+
@property
112+
def token(self):
113+
token = self._token
114+
if self.oidc_agent is not None:
115+
token = self.oidc_agent.get_token()["access_token"]
116+
return token
117+
84118
@property
85119
def deployments(self):
86120
"""Interface to query for deployments.

orpy/exceptions.py

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ def __init__(self, message=None, **kwargs):
3737
print("%s: %s" % (name, value))
3838
six.reraise(exc_info[0], exc_info[1], exc_info[2])
3939

40+
message = "ERROR: " + message
4041
super(ClientException, self).__init__(message)
4142

4243

orpy/oidc.py

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# -*- coding: utf-8 -*-
2+
3+
# Copyright 2019 Spanish National Research Council (CSIC)
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
6+
# not use this file except in compliance with the License. You may obtain
7+
# a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14+
# License for the specific language governing permissions and limitations
15+
# under the License.
16+
17+
import json
18+
import socket
19+
20+
from orpy import exceptions
21+
from orpy import utils
22+
23+
24+
class OpenIDConnectAgent(object):
25+
"""Communicate with an OpenID Connect agent."""
26+
27+
def __init__(self, account, socket_path=None, validity=60):
28+
"""Initialize OpenID Connect Agent connection.
29+
30+
:param str account: Account name to use
31+
:param str socket_path: Path to the oidc-agent UNIX socket
32+
:param int validity: Minimum validity (minutes) for the token
33+
"""
34+
self.account = account
35+
self.validity = validity
36+
37+
if socket_path is None:
38+
socket_path = utils.env("OIDC_SOCK")
39+
40+
self.socket_path = socket_path
41+
42+
def get_token(self):
43+
"""Communicate with the oidc agent and get an access token.
44+
45+
:returns: A dictionary containing the access token
46+
:rtype: dict
47+
"""
48+
message = {
49+
"request": "access_token",
50+
"account": self.account,
51+
"min_valid_period": self.validity,
52+
"application_hint": "orpy",
53+
}
54+
try:
55+
self._sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
56+
self._sock.connect(self.socket_path)
57+
self._sock.sendall(json.dumps(message).encode())
58+
59+
data = ""
60+
while True:
61+
recv = self._sock.recv(16).decode()
62+
if recv:
63+
data += recv
64+
else:
65+
break
66+
except socket.error as err:
67+
raise err
68+
raise exceptions.ClientException("Cannot communicate with the "
69+
"oidc-agent: %s" % err)
70+
finally:
71+
self._sock.close()
72+
73+
return json.loads(data)

orpy/shell.py

+69-8
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from cliff import help
2626

2727
from orpy.client import client
28+
from orpy import oidc
2829
from orpy import utils
2930
from orpy import version
3031

@@ -42,6 +43,7 @@ def __init__(self):
4243

4344
self.client = None
4445
self.token = None
46+
self.oidc_agent = None
4547

4648
# Patch command.Command to add a default auth_required = True
4749
command.Command.auth_required = True
@@ -62,9 +64,17 @@ def initialize_app(self, argv):
6264
for cmd in self.commands:
6365
self.command_manager.add_command(cmd.__name__.lower(), cmd)
6466
self.token = utils.env("ORCHESTRATOR_TOKEN")
67+
68+
if self.options.oidc_agent_sock and self.options.oidc_agent_account:
69+
self.oidc_agent = oidc.OpenIDConnectAgent(
70+
self.options.oidc_agent_account,
71+
socket_path=self.options.oidc_agent_sock
72+
)
73+
6574
if self.client is None:
6675
self.client = client.OrpyClient(self.options.orchestrator_url,
67-
self.token,
76+
oidc_agent=self.oidc_agent,
77+
token=self.token,
6878
debug=self.options.debug)
6979

7080
def prepare_to_run_command(self, cmd):
@@ -76,21 +86,72 @@ def prepare_to_run_command(self, cmd):
7686
"use --url or set the ORCHESTRATOR_URL "
7787
"environment variable.")
7888

79-
if cmd.auth_required and not self.token:
80-
self.parser.error("No token has been provided, please set the "
81-
"ORCHESTRATOR_TOKEN environment variable "
82-
"(see '%s help' for more details on how "
83-
"to set up authentication)" % self.parser.prog)
89+
if cmd.auth_required:
90+
if (not all([self.options.oidc_agent_sock,
91+
self.options.oidc_agent_account])) and not self.token:
92+
93+
self.parser.error("No oidc-agent has been set up or no access "
94+
"token has been provided, please set the "
95+
"ORCHESTRATOR_TOKEN environment variable or "
96+
"set up an oidc-agent "
97+
"(see '%s help' for more details on how "
98+
"to set up authentication)" %
99+
self.parser.prog)
84100

85101
def build_option_parser(self, description, version):
102+
auth_help = """Authentication:
103+
104+
In order to interact with the INDIGO PaaS Orchestrator we need to use an
105+
OpenID Connect access token from a trusted OpenID Connect provider at the
106+
orchestrator.
107+
108+
Please either store your access token in 'ORCHESTRATOR_TOKEN' or set the
109+
account to use with oidc-agent in the 'OIDC_ACCOUNT' and the socket path
110+
of the oidc-agent in the 'OIDC_SOCK' environment variable:
111+
112+
export ORCHESTRATOR_TOKEN=<your access token>
113+
OR
114+
export OIDC_SOCK=<path to the oidc-agent socket>
115+
export OIDC_ACCOUNT=<account to use>
116+
117+
Usually, the OIDC_SOCK environmental variable is already exported if you
118+
are using oidc-agent.
119+
120+
As an alternative, you can pass the socket path and the account through
121+
the command line with the --oidc-agent-sock and --oidc-agent-account
122+
parameters.
123+
124+
"""
86125
parser = super(OrpyApp, self).build_option_parser(
87126
self.__doc__,
88127
version,
89128
argparse_kwargs={
90-
"formatter_class": argparse.RawDescriptionHelpFormatter
129+
"formatter_class": argparse.RawDescriptionHelpFormatter,
130+
"epilog": auth_help,
91131
})
92132

93-
# service token auth argument
133+
parser.add_argument(
134+
'--oidc-agent-sock',
135+
metavar='<oidc-agent-socket>',
136+
dest='oidc_agent_sock',
137+
default=utils.env('OIDC_SOCK'),
138+
help='The path for the oidc-agent socket to use to get and renew '
139+
'access tokens from the OpenID Connect provider. This '
140+
'defaults to the OIDC_SOCK environment variable, that should '
141+
'be automatically set up if you are using oidc-agent. '
142+
'In order to use the oidc-agent you must also pass the '
143+
'--oidc-agent-account parameter, or set the OIDC_ACCOUNT '
144+
'environment variable.'
145+
)
146+
parser.add_argument(
147+
'--oidc-agent-account',
148+
metavar='<oidc-agent-account>',
149+
dest='oidc_agent_account',
150+
default=utils.env('OIDC_ACCOUNT'),
151+
help='The oidc-agent account that we will use to get tokens from. '
152+
'In order to use the oidc-agent you must pass thos parameter '
153+
'or set the OIDC_ACCOUNT environment variable.'
154+
)
94155
parser.add_argument(
95156
'--url',
96157
metavar='<orchestrator-url>',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
features:
3+
- |
4+
Leverage oidc-agent to get the user's access token when interacting with
5+
the INDIGO PaaS orchestrator.

0 commit comments

Comments
 (0)