Updated code to match new rules
[integration/test.git] / csit / libraries / AuthStandalone.py
1 """Library hiding details of restconf authentication.
2
3 Suitable for performance tests against different
4 restconf authentication methods.
5 Type of authentication is currently determined by "scope" value.
6
7 This library does not rely on RobotFramework libraries,
8 so it is suitable for running from wide range of VMs.
9 Requirements: Basic Python installation,
10 which should include "json" and "requests" Python modules.
11
12 *_Using_Session keywords take the same kwargs as requests.Session.request,
13 but instead of method and URL, they take "session" created by
14 Init_Session keyword and URI (without "/restconf/").
15
16 Due to performance of TCP on some systems, two session strategies are available.
17 reuse=True reuses the same requests.Session, which possibly means
18 the library tries to re-use the same source TCP port.
19 This conserves resources (TCP ports available), but it may lead
20 to a significant performance hit (as in 10 times slower).
21 reuse=False closes every requests.Session object, presumably
22 causing the new session to take another TCP port.
23 This has good performance, but may perhaps lead to port starvation in some cases.
24
25 TODO: Put "RESTCONF" to more places,
26 as URIs not starting with /restconf/ are not supported yet.
27 """
28
29 # Copyright (c) 2015 Cisco Systems, Inc. and others.  All rights reserved.
30 #
31 # This program and the accompanying materials are made available under the
32 # terms of the Eclipse Public License v1.0 which accompanies this distribution,
33 # and is available at http://www.eclipse.org/legal/epl-v10.html
34
35 import json
36 import requests
37
38
39 __author__ = "Vratko Polak"
40 __copyright__ = "Copyright(c) 2015, Cisco Systems, Inc."
41 __license__ = "Eclipse Public License v1.0"
42 __email__ = "vrpolak@cisco.com"
43
44
45 #
46 # Karaf Keyword definitions.
47 #
48 def Init_Session(ip, username, password, scope, reuse=True, port="8181"):
49     """Robot keyword, return opaque session object, which handles authentication automatically."""
50     if scope:
51         if reuse:
52             return _TokenReusingSession(ip, username, password, scope, port=port)
53         else:
54             return _TokenClosingSession(ip, username, password, scope, port=port)
55     else:
56         if reuse:
57             return _BasicReusingSession(ip, username, password, port=port)
58         else:
59             return _BasicClosingSession(ip, username, password, port=port)
60
61
62 def Get_Using_Session(session, uri, **kwargs):
63     """Robot keyword, perform GET operation using given opaque session object."""
64     return session.robust_method("GET", uri, **kwargs)
65
66
67 def Post_Using_Session(session, uri, **kwargs):
68     """Robot keyword, perform POST operation using given opaque session object."""
69     return session.robust_method("POST", uri, **kwargs)
70
71
72 def Put_Using_Session(session, uri, **kwargs):
73     """Robot keyword, perform PUT operation using given opaque session object."""
74     return session.robust_method("PUT", uri, **kwargs)
75
76
77 def Delete_Using_Session(session, uri, **kwargs):
78     """Robot keyword, perform DELETE operation using given opaque session object."""
79     return session.robust_method("DELETE", uri, **kwargs)
80
81
82 #
83 # Private classes.
84 #
85 class _BasicReusingSession(object):
86     """Handling of restconf requests using persistent session and basic http authentication."""
87
88     def __init__(self, ip, username="", password="", port="8181"):
89         """Initialize session using hardcoded text data, remember credentials."""
90         self.rest_prefix = "http://" + ip + ":" + port + "/restconf/"
91         self.session = requests.Session()
92         if username:
93             self.session.auth = (username, password)  # May work with non-string values
94         else:
95             self.session.auth = None  # Supports "no authentication mode" as in odl-restconf-noauth
96
97     def robust_method(self, method, uri, **kwargs):
98         """Try method once using session credentials. Return last response."""
99         return self.session.request(method, self.rest_prefix + uri, **kwargs)
100         # TODO: Do we want to fix URI at __init__ just to avoid string concat?
101
102
103 class _BasicClosingSession(object):
104     """Handling of restconf requests using one-time sessions and basic http authentication."""
105
106     def __init__(self, ip, username="", password="", port="8181"):
107         """Prepare session initialization data using hardcoded text, remember credentials."""
108         self.rest_prefix = "http://" + ip + ":" + port + "/restconf/"
109         if username:
110             self.auth = (username, password)  # May work with non-string values
111         else:
112             self.auth = None  # Supports "no authentication mode" as in odl-restconf-noauth
113         self.session = None
114
115     def robust_method(self, method, uri, **kwargs):
116         """Create new session, send method using remembered credentials. Return response"""
117         if self.session:
118             # The session object may still keep binding a TCP port here.
119             # As caller has finished processing previous response,
120             # we can explicitly close the old session to free resources
121             # before creating new session.
122             self.session.close()
123         self.session = requests.Session()
124         self.session.auth = self.auth
125         return self.session.request(method, self.rest_prefix + uri, **kwargs)
126
127
128 class _TokenReusingSession(object):
129     """Handling of restconf requests using token-based authentication, one session per token."""
130
131     def __init__(self, ip, username, password, scope, port="8181"):
132         """Initialize session using hardcoded text data."""
133         self.auth_url = "http://" + ip + ":" + port + "/oauth2/token"
134         self.rest_prefix = "http://" + ip + ":" + port + "/restconf/"
135         self.auth_data = "grant_type=password&username=" + username
136         self.auth_data += "&password=" + password + "&scope=" + scope
137         self.auth_header = {"Content-Type": "application/x-www-form-urlencoded"}
138         self.session = None
139         self.token = None
140         self.refresh_token()
141
142     def refresh_token(self):
143         """Reset session, invoke call to get token, parse it and remember."""
144         if self.session:
145             self.session.close()
146         self.session = requests.Session()
147         resp = self.session.post(self.auth_url, data=self.auth_data, headers=self.auth_header)
148         resp_obj = json.loads(resp.text)
149         try:
150             token = resp_obj["access_token"]
151         except KeyError:
152             raise RuntimeError("Parse failed: " + resp.text)
153         self.token = token
154         # TODO: Use logging so that callers could see token refreshes.
155         # print "DEBUG: token:", token
156         # We keep self.session to use for the following restconf requests.
157
158     def oneshot_method(self, method, uri, **kwargs):
159         """Return response of request of given method to given uri (without restconf/)."""
160         # Token needs to be merged into headers.
161         authed_headers = kwargs.get("headers", {})
162         authed_headers["Authorization"] = "Bearer " + self.token
163         authed_kwargs = dict(kwargs)  # shallow copy
164         authed_kwargs["headers"] = authed_headers
165         return self.session.request(method, self.rest_prefix + uri, **authed_kwargs)
166
167     def robust_method(self, method, uri, **kwargs):
168         """Try method once; upon 401, refresh token and retry once. Return last response."""
169         resp = self.oneshot_method(method, uri, **kwargs)
170         if resp.status_code != 401:
171             return resp
172         self.refresh_token()
173         return self.oneshot_method(method, uri, **kwargs)
174
175
176 class _TokenClosingSession(object):
177     """Handling of restconf requests using token-based authentication, one session per request."""
178
179     def __init__(self, ip, username, password, scope, port="8181"):
180         """Prepare session initialization data using hardcoded text."""
181         self.auth_url = "http://" + ip + ":" + port + "/oauth2/token"
182         self.rest_prefix = "http://" + ip + ":" + port + "/restconf/"
183         self.auth_data = "grant_type=password&username=" + username
184         self.auth_data += "&password=" + password + "&scope=" + scope
185         self.auth_header = {"Content-Type": "application/x-www-form-urlencoded"}
186         self.session = None
187         self.token = None
188         self.refresh_token()
189
190     def refresh_token(self):
191         """Reset session, invoke call to get token, parse it and remember."""
192         if self.session:
193             self.session.close()
194         self.session = requests.Session()
195         resp = self.session.post(self.auth_url, data=self.auth_data, headers=self.auth_header)
196         resp_obj = json.loads(resp.text)
197         try:
198             token = resp_obj["access_token"]
199         except KeyError:
200             raise RuntimeError("Parse failed: " + resp.text)
201         self.token = token
202         # TODO: Use logging so that callers could see token refreshes.
203         # print "DEBUG: token:", token
204         # We keep self.session to use for the following restconf requests.
205
206     def oneshot_method(self, method, uri, **kwargs):
207         """Reset session, return response of request of given method to given uri (without restconf/)."""
208         # This assumes self.session was already initialized.
209         self.session.close()
210         self.session = requests.Session()
211         # Token needs to be merged into headers.
212         authed_headers = kwargs.get("headers", {})
213         authed_headers["Authorization"] = "Bearer " + self.token
214         authed_kwargs = dict(kwargs)  # shallow copy
215         authed_kwargs["headers"] = authed_headers
216         return self.session.request(method, self.rest_prefix + uri, **authed_kwargs)
217
218     def robust_method(self, method, uri, **kwargs):
219         """Try method once; upon 401, refresh token and retry once. Return last response."""
220         resp = self.oneshot_method(method, uri, **kwargs)
221         if resp.status_code != 401:
222             return resp
223         self.refresh_token()
224         return self.oneshot_method(method, uri, **kwargs)