Newer
Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include "MantidCatalog/ONCat.h"
#include "MantidCatalog/OAuth.h"
#include "MantidCatalog/Exception.h"
#include "MantidKernel/ConfigService.h"
#include "MantidKernel/Exception.h"
#include "MantidKernel/InternetHelper.h"
#include "MantidKernel/Logger.h"
#include "MantidKernel/make_unique.h"
#include <algorithm>
#include <iostream>
#include <sstream>
#include <boost/algorithm/string/join.hpp>
#include <Poco/Net/HTMLForm.h>
#include <Poco/Net/HTTPResponse.h>
namespace Mantid {
namespace Catalog {
namespace ONCat {
using Poco::Net::HTTPResponse;
using Mantid::Catalog::OAuth::ConfigServiceTokenStore;
using Mantid::Catalog::OAuth::OAuthToken;
using Mantid::Catalog::ONCat::ONCatEntity;
using Mantid::Catalog::Exception::CatalogError;
using Mantid::Catalog::Exception::InvalidCredentialsError;
using Mantid::Catalog::Exception::InvalidRefreshTokenError;
using Mantid::Catalog::Exception::TokenRejectedError;
using Mantid::Kernel::Exception::InternetError;
namespace {
Mantid::Kernel::Logger g_log("ONCat");
static const std::string CONFIG_PATH_BASE = "catalog.oncat.";
// It could be argued that this should be read in from Facilities.xml or
// similar, but I will put this off for now as it is unclear how to
// reconcile ONCat's functionality with the current <soapendpoint> /
// <externaldownload> tags in the XML.
static const std::string DEFAULT_ONCAT_URL = "https://oncat.ornl.gov";
static const std::string DEFAULT_CLIENT_ID =
"d16ea847-41ce-4b30-9167-40298588e755";
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
}
/**
* Constructs an ONCat object based on various settings gathered from
* the ConfigService.
*
* The resulting object will work with resources that require authorization
* in one of two possible modes:
*
* User Login Mode (Default)
* -------------------------
*
* Users must log in with their UCAMS / XCAMS credentials before calls
* to the ONCat API can be made. This mode should work "out of the box"
* (requires no changes to config files), and is the default mode of
* operation. User access to API information is governed by the same
* LDAP instance that controls file system access, so users should only
* see the experiment data they are allowed to see.
*
* This mode uses the "Resource Owner Credentials" OAuth flow.
*
* Machine-to-Machine Mode
* -----------------------
*
* No user login is necessary, but for it to be enabled a client ID and
* secret must exist in the ConfigService. Recommended practice would
* be to add the following two entries to the Mantid.local.properties
* file on the machine to be given access, using the credentials issued
* by the ONCat administrator for the values:
*
* catalog.oncat.client_id = "[CLIENT ID]"
* catalog.oncat.client_secret = "[CLIENT SECRET]"
*
* API read access is completely unrestricted in this mode, which is
* intended for autoreduction use cases or similar.
*
* This mode uses the "Client Credentials" OAuth flow.
*
* @return The constructed ONCat object.
*/
ONCat ONCat::fromMantidSettings() {
auto &config = Mantid::Kernel::ConfigService::Instance();
const auto client_id = config.getString(CONFIG_PATH_BASE + "client_id");
const auto client_secret =
config.getString(CONFIG_PATH_BASE + "client_secret");
const bool hasClientCredentials = client_id != "" && client_secret != "";
if (hasClientCredentials) {
g_log.debug() << "Found client credentials in Mantid.local.properties. "
<< "No user login required." << std::endl;
} else {
g_log.debug()
<< "Could not find client credentials in Mantid.local.properties. "
<< "Falling back to default -- user login required." << std::endl;
return ONCat(DEFAULT_ONCAT_URL,
Mantid::Kernel::make_unique<ConfigServiceTokenStore>(),
hasClientCredentials ? OAuthFlow::CLIENT_CREDENTIALS
: OAuthFlow::RESOURCE_OWNER_CREDENTIALS,
hasClientCredentials ? client_id : DEFAULT_CLIENT_ID,
boost::make_optional(hasClientCredentials, client_secret));
ONCat::ONCat(const std::string &url, IOAuthTokenStore_uptr tokenStore,
OAuthFlow flow, const std::string &clientId,
const boost::optional<std::string> &clientSecret)
: m_url(url), m_tokenStore(std::move(tokenStore)), m_clientId(clientId),
m_clientSecret(clientSecret),
m_flow(flow), m_internetHelper(new Mantid::Kernel::InternetHelper()) {}
ONCat::ONCat(const ONCat &other)
: m_url(other.m_url), m_tokenStore(other.m_tokenStore),
m_clientId(other.m_clientId), m_clientSecret(other.m_clientSecret),
m_flow(other.m_flow), m_internetHelper(other.m_internetHelper) {}
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
ONCat::~ONCat() {}
/**
* Whether or not a user is currently logged in. (Not relevant when
* using machine-to-machine authentication as part of the Client
* Credentials flow, and not required when accessing unauthenticated
* parts of the API.)
*
* Something to bear in mind is that the term "logged in" is used quite
* loosely here. In an OAuth context it roughly equates to, "there is a
* token stored locally", which is not quite the same thing. This may
* sound strange, but consider the following:
*
* - Tokens expire after a given amount of time, at which point they can
* be "refreshed". A successful token refresh happens behind the
* scenes without the user even knowing it took place.
*
* - While it is possible to tell when a token needs to be refreshed,
* token refreshes are not always successful. If they fail then the
* client must prompt the user to enter their credentials again.
*
* - There is no way for the client to know whether or not the refresh
* will be successful ahead of time (i.e., whether a token has been
* revoked server-side), since the OAuth spec provides no mechanism to
* check the validity of a refresh token.
*
* - Tokens can be revoked at any time with absolutely no notice as part
* of standard OAuth practice. Also, only a limited number of tokens
* can exist for each unique client / user combination at any one
* time.
*
* Hopefully it is clear that working with OAuth client-side requires
* you to use an almost-Pythonic "ask for forgiveness rather than for
* permission" strategy -- i.e., code as if locally-stored tokens can be
* refreshed, but be ready to prompt the user for their credentials if
* the refresh fails.
*
* Some useful links with related information:
*
* - http://qr.ae/TUTke2 (quora.com)
* - https://stackoverflow.com/a/30826806/778572
*
* @param true if a user is "logged in", else false.
*/
bool ONCat::isUserLoggedIn() const {
if (m_flow == OAuthFlow::CLIENT_CREDENTIALS) {
// No users are ever authenticated as part of this flow.
return false;
}
return m_tokenStore->getToken().is_initialized();
}
void ONCat::logout() {
// Currently, ONCat OAuth does *not* allow clients to revoke tokens
// that are no longer needed (though this is defined in the OAtuh
// spec). A "logout", then, is simply throwing away whatever token we
// previously stored client-side.
m_tokenStore->setToken(boost::none);
g_log.debug() << "Logging out." << std::endl;
;
}
/**
* Log in as part of the Resource Ownder Credentials flow so that
* authenticated resources may be accessed on behalf of a user.
*
* @param username : The XCAMS / UCAMS ID of the user.
* @param password : The XCAMS / UCAMS password of the user.
*
* @exception Mantid::Catalog::Exception::InvalidCredentialsError :
* Thrown when the given credentials are not valid.
*/
void ONCat::login(const std::string &username, const std::string &password) {
if (m_flow != OAuthFlow::RESOURCE_OWNER_CREDENTIALS) {
g_log.warning()
<< "Unexpected usage detected! "
<< "Logging in with user credentials in not required (and is not "
<< "supported) when machine-to-machine credentials are being used."
<< std::endl;
;
return;
}
Poco::Net::HTMLForm form(Poco::Net::HTMLForm::ENCODING_MULTIPART);
form.set("username", username);
form.set("password", password);
form.set("client_id", m_clientId);
if (m_clientSecret) {
form.set("client_secret", m_clientSecret.get());
}
form.set("grant_type", "password");
m_internetHelper->setBody(form);
try {
std::stringstream ss;
const int statusCode =
m_internetHelper->sendRequest(m_url + "/oauth/token", ss);
if (statusCode == HTTPResponse::HTTP_OK) {
m_tokenStore->setToken(OAuthToken::fromJSONStream(ss));
}
g_log.debug() << "Login was successful!" << std::endl;
;
} catch (InternetError &ie) {
if (ie.errorCode() == HTTPResponse::HTTP_UNAUTHORIZED) {
throw InvalidCredentialsError(
"Invalid UCAMS / XCAMS credentials used for ONCat login.");
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
}
throw CatalogError(ie.what());
}
}
/**
* Retrieve a single entity from the given resource (in the given namespace) of
* ONCat's API.
*
* Please see https://oncat.ornl.gov/#/build for more information about the
* currently-available resources, and what query parameters they allow.
*
* @param identifier :
* The ID or name that uniquely identifies the entity.
* @param resourceNamespace :
* The "namespace" of the resource. The most common, "core" resources all
* belong to the "api" namespace.
* @param resource :
* The name of the resource to retrieve the entity from. I.e., "Datafile"
* entities can be retrieved from the "datafiles" resource.
* @param queryParameters :
* The name-value-pair query-string parameters.
*
* @return The response from the API in the form on an ONCatEntity object.
*
* @exception Mantid::Catalog::Exception::CatalogError
*/
ONCatEntity ONCat::retrieve(const std::string &resourceNamespace,
const std::string &resource,
const std::string &identifier,
const QueryParameters &queryParameters) {
const auto uri =
m_url + "/" + resourceNamespace + "/" + resource + "/" + identifier;
std::stringstream ss;
sendAPIRequest(uri, queryParameters, ss);
return ONCatEntity::fromJSONStream(ss);
}
/**
* Retrieve a collection of entities from the given resource (in the given
* namespace) of ONCat's API.
*
* Please see retrieve documentation for more info.
*/
std::vector<ONCatEntity> ONCat::list(const std::string &resourceNamespace,
const std::string &resource,
const QueryParameters &queryParameters) {
const auto uri = m_url + "/" + resourceNamespace + "/" + resource;
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
std::stringstream ss;
sendAPIRequest(uri, queryParameters, ss);
return ONCatEntity::vectorFromJSONStream(ss);
}
/**
* Refresh the current token if it has expired (and if it actually exists).
*
* To be called behind-the-scenes before each API query, so that we know
* our tokens are up-to-date before being used.
*
* @exception Mantid::Catalog::Exception::InvalidCredentialsError :
* Thrown when the provider decides the current token cannot be refreshed.
*/
void ONCat::refreshTokenIfNeeded() {
refreshTokenIfNeeded(DateAndTime::getCurrentTime());
}
/**
* See overloaded method.
*
* @param currentTime : Used in testing to specify a different time.
*
* @exception Mantid::Catalog::Exception::InvalidRefreshTokenError :
* Thrown when the provider decides the current token cannot be refreshed.
*/
void ONCat::refreshTokenIfNeeded(const DateAndTime ¤tTime) {
const auto currentToken = m_tokenStore->getToken();
if (m_flow == OAuthFlow::CLIENT_CREDENTIALS) {
if (currentToken && !currentToken->isExpired(currentTime)) {
return;
}
Poco::Net::HTMLForm form(Poco::Net::HTMLForm::ENCODING_MULTIPART);
form.set("client_id", m_clientId);
if (m_clientSecret) {
form.set("client_secret", m_clientSecret.get());
}
form.set("grant_type", "client_credentials");
m_internetHelper->reset();
m_internetHelper->setBody(form);
try {
std::stringstream ss;
const int statusCode =
m_internetHelper->sendRequest(m_url + "/oauth/token", ss);
if (statusCode == HTTPResponse::HTTP_OK) {
m_tokenStore->setToken(OAuthToken::fromJSONStream(ss));
}
g_log.debug() << "Token successfully refreshed." << std::endl;
throw CatalogError(ie.what());
}
} else if (m_flow == OAuthFlow::RESOURCE_OWNER_CREDENTIALS) {
if (!currentToken.is_initialized()) {
return;
}
if (!currentToken->isExpired(currentTime)) {
return;
}
const auto currentRefreshToken = currentToken->refreshToken();
if (!currentRefreshToken) {
return;
}
Poco::Net::HTMLForm form(Poco::Net::HTMLForm::ENCODING_MULTIPART);
form.set("client_id", m_clientId);
if (m_clientSecret) {
form.set("client_secret", m_clientSecret.get());
}
form.set("grant_type", "refresh_token");
form.set("refresh_token", currentRefreshToken.get());
m_internetHelper->reset();
m_internetHelper->setBody(form);
try {
std::stringstream ss;
const int statusCode =
m_internetHelper->sendRequest(m_url + "/oauth/token", ss);
if (statusCode == HTTPResponse::HTTP_OK) {
m_tokenStore->setToken(OAuthToken::fromJSONStream(ss));
}
g_log.debug() << "Token successfully refreshed." << std::endl;
if (ie.errorCode() == HTTPResponse::HTTP_UNAUTHORIZED) {
// As per OAuth spec, when a refresh token is no longer valid, we
// can consider ourselves logged out.
logout();
throw InvalidRefreshTokenError(
"You have been logged out. Please login again.");
}
throw CatalogError(ie.what());
}
}
}
void ONCat::setInternetHelper(
std::shared_ptr<Mantid::Kernel::InternetHelper> internetHelper) {
m_internetHelper = std::move(internetHelper);
}
void ONCat::sendAPIRequest(const std::string &uri,
const QueryParameters &queryParameters,
std::ostream &response) {
refreshTokenIfNeeded();
const auto tokenType = m_tokenStore->getToken()->tokenType();
const auto accessToken = m_tokenStore->getToken()->accessToken();
m_internetHelper->clearHeaders();
m_internetHelper->setMethod("GET");
m_internetHelper->addHeader("Authorization", tokenType + " " + accessToken);
std::vector<std::string> queryStringParts(queryParameters.size());
std::transform(queryParameters.begin(), queryParameters.end(),
queryStringParts.begin(),
[](const QueryParameter &queryParameter) -> std::string {
return queryParameter.first + "=" + queryParameter.second;
});
const auto queryString = boost::algorithm::join(queryStringParts, "&");
const auto url = queryString.size() == 0 ? uri : uri + "?" + queryString;
g_log.debug() << "About to make a call to the following ONCat URL: " << url;
try {
m_internetHelper->sendRequest(url, response);
} catch (InternetError &ie) {
if (ie.errorCode() == HTTPResponse::HTTP_UNAUTHORIZED) {
std::string errorMessage;
switch (m_flow) {
case OAuthFlow::RESOURCE_OWNER_CREDENTIALS:
errorMessage = "You have been logged out. Please login again.";
break;
case OAuthFlow::CLIENT_CREDENTIALS:
errorMessage =
"The stored OAuth token appears to be invalid. "
"There are a few cases where this might be expected, but in "
"principle this should rarely happen. "
"Please try again and if the problem persists contact the "
"ONCat administrator at oncat-support@ornl.gov.";
}
// The ONCat API does *not* leak information in the case where a
// resource exists but a user is not allowed access -- a 404 would
// always be returned instead. So, if we ever get a 401, it is
// because our locally-stored token is no longer valid and we
// should log out.
logout();
throw TokenRejectedError(errorMessage);
}
throw CatalogError(ie.what());
}
}
} // namespace ONCat
} // namespace Catalog
} // namespace Mantid