blob: 746d6ce3330382ef7004a1567f04d347ca0b92ce [file] [log] [blame]
/*****************************************************************************\
* rest.c - Library for managing HPE Slingshot networks
*****************************************************************************
* Copyright 2022-2023 Hewlett Packard Enterprise Development LP
* Written by Jim Nordby <james.nordby@hpe.com>
*
* This file is part of Slurm, a resource management program.
* For details, see <https://slurm.schedmd.com/>.
* Please also read the included file: DISCLAIMER.
*
* Slurm is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* In addition, as a special exception, the copyright holders give permission
* to link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two. You must obey the GNU
* General Public License in all respects for all of the code used other than
* OpenSSL. If you modify file(s) with this exception, you may extend this
* exception to your version of the file(s), but you are not obligated to do
* so. If you do not wish to do so, delete this exception statement from your
* version. If you delete this exception statement from all source files in
* the program, then also delete it here.
*
* Slurm is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along
* with Slurm; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
\*****************************************************************************/
#include "config.h"
#define _GNU_SOURCE
#include <curl/curl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#define CURL_TRACE 0 /* Turn on curl debug tracing */
#if CURL_TRACE
#include <ctype.h>
#endif
#include "src/common/slurm_xlator.h"
#include "src/curl/slurm_curl.h"
#include "switch_hpe_slingshot.h"
#include "rest.h"
static char *_read_authfile(const char *auth_dir, const char *base);
static void _clear_auth_header(slingshot_rest_conn_t *conn);
static bool _get_auth_header(slingshot_rest_conn_t *conn,
struct curl_slist **headers, bool cache_use);
/*
* If an error response was received, log it
*/
static void _log_rest_detail(const char *name, const char *method,
const char *url, json_object *respjson,
long status)
{
json_object *detail = NULL;
if (!(detail = json_object_object_get(respjson, "detail"))) {
error("%s %s %s status %ld no error details",
name, method, url, status);
} else {
error("%s %s %s status %ld: %s", name, method, url, status,
json_object_get_string(detail));
}
}
/*
* Internals of REST POST/PATCH/GET/DELETE calls, with retries, etc.
*/
static json_object *_rest_call(slingshot_rest_conn_t *conn,
http_request_method_t request_method,
const char *urlsuffix, json_object *reqjson,
long *status, bool not_found_ok)
{
json_object *respjson = NULL;
struct curl_slist *headers = NULL;
const char *req = NULL;
bool use_cache = true;
char *response_str = NULL, *username = NULL, *password = NULL;
char *url = NULL;
xassert(conn != NULL);
xassert(urlsuffix != NULL);
/* Create full URL */
if (conn->mtls.enabled)
url = xstrdup_printf("%s%s", conn->mtls.url, urlsuffix);
else
url = xstrdup_printf("%s%s", conn->base_url, urlsuffix);
/* If present, dump JSON payload to string */
if (reqjson) {
if (!(req = json_object_to_json_string(reqjson))) {
error("Couldn't dump JSON request: %m");
goto err;
}
}
again:
debug("%s %s url=%s data='%s'", conn->name,
get_http_method_string(request_method), url, req);
/* Create header list */
headers = curl_slist_append(headers, "Content-Type: application/json");
if (!headers) {
error("curl_slist_append failed to append Content-Type: %m");
goto err;
}
if (!_get_auth_header(conn, &headers, use_cache))
goto err;
/* If using basic auth, add the user name and password */
if ((conn->auth.auth_type == SLINGSHOT_AUTH_BASIC) &&
conn->auth.u.basic.user_name && conn->auth.u.basic.password) {
username = conn->auth.u.basic.user_name;
password = conn->auth.u.basic.password;
}
if (slurm_curl_request(req, url, username, password, conn->mtls.ca_path,
conn->mtls.cert_path, conn->mtls.key_path,
headers, conn->timeout, &response_str, status,
request_method, false, conn->mtls.enabled))
goto err;
if (((*status == HTTP_UNAUTHORIZED) || (*status == HTTP_FORBIDDEN)) &&
(conn->auth.auth_type == SLINGSHOT_AUTH_OAUTH) && use_cache) {
debug("%s %s %s unauthorized status %ld, retrying", conn->name,
get_http_method_string(request_method), url, *status);
/*
* On HTTP_UNAUTHORIZED, free auth header and re-cache token
*/
curl_slist_free_all(headers);
headers = NULL;
use_cache = false;
xfree(response_str);
goto again;
}
/* Decode response into JSON */
if (response_str && response_str[0]) {
enum json_tokener_error jerr;
respjson = json_tokener_parse_verbose(response_str, &jerr);
if (respjson == NULL) {
error("Couldn't decode %s response: %s (data '%s')",
conn->name, json_tokener_error_desc(jerr),
response_str);
goto err;
}
} else if (request_method != HTTP_REQUEST_DELETE) {
debug("%s %s %s No response data received %ld",
conn->name, get_http_method_string(request_method), url,
*status);
goto err;
}
if (((*status >= HTTP_OK) && (*status <= HTTP_LAST_OK)) ||
(*status == HTTP_NOT_FOUND && not_found_ok)) {
debug("%s %s %s successful (%ld)", conn->name,
get_http_method_string(request_method), url, *status);
} else {
_log_rest_detail(conn->name,
get_http_method_string(request_method), url,
respjson, *status);
if (json_object_put(respjson))
respjson = NULL;
goto err;
}
err:
curl_slist_free_all(headers);
xfree(url);
xfree(response_str);
return respjson; /* NULL on error */
}
/*
* POST with JSON payload, and return the response (or NULL on error)
*/
extern json_object *slingshot_rest_post(slingshot_rest_conn_t *conn,
const char *urlsuffix,
json_object *reqjson, long *status)
{
return _rest_call(conn, HTTP_REQUEST_POST, urlsuffix, reqjson, status,
false);
}
/*
* PATCH with JSON payload, and return the response (or NULL on error)
*/
extern json_object *slingshot_rest_patch(slingshot_rest_conn_t *conn,
const char *urlsuffix,
json_object *reqjson, long *status)
{
return _rest_call(conn, HTTP_REQUEST_PATCH, urlsuffix, reqjson, status,
true);
}
/*
* Do a GET from the requested URL; return the JSON response,
* or NULL on error
*/
extern json_object *slingshot_rest_get(slingshot_rest_conn_t *conn,
const char *urlsuffix, long *status)
{
return _rest_call(conn, HTTP_REQUEST_GET, urlsuffix, NULL, status,
true);
}
/*
* DELETE the given URL; return true on success
*/
extern bool slingshot_rest_delete(slingshot_rest_conn_t *conn,
const char *urlsuffix, long *status)
{
/* Only delete if we successfully POSTed before */
if (!conn || !conn->base_url)
return false;
/* Don't need the response. Just free it with json_object_put */
json_object_put(_rest_call(conn, HTTP_REQUEST_DELETE, urlsuffix, NULL,
status, false));
if ((*status >= HTTP_OK) && (*status <= HTTP_LAST_OK))
return true;
else
return false;
}
/*
* Generic handle set up function for network connections to use
*/
extern bool slingshot_rest_connection(slingshot_rest_conn_t *conn,
const char *url,
slingshot_rest_auth_t auth_type,
const char *auth_dir,
const char *basic_user,
const char *basic_pwdfile,
bool mtls_enabled,
const char *mtls_ca_path,
const char *mtls_cert_path,
const char *mtls_key_path,
const char *mtls_url, int timeout,
int connect_timeout,
const char *conn_name)
{
memset(conn, 0, sizeof(*conn));
switch (auth_type) {
case SLINGSHOT_AUTH_BASIC:
conn->auth.auth_type = auth_type;
conn->auth.u.basic.user_name = xstrdup(basic_user);
if (!(conn->auth.u.basic.password = _read_authfile(
auth_dir, basic_pwdfile)))
return false;
break;
case SLINGSHOT_AUTH_OAUTH:
case SLINGSHOT_AUTH_NONE:
conn->auth.auth_type = auth_type;
break;
default:
error("Invalid auth_type value %u", auth_type);
return false;
}
conn->name = xstrdup(conn_name);
conn->base_url = xstrdup(url);
conn->auth.auth_dir = xstrdup(auth_dir);
conn->timeout = timeout;
conn->connect_timeout = connect_timeout;
conn->mtls.enabled = mtls_enabled;
if (mtls_enabled) {
conn->mtls.ca_path = xstrdup(mtls_ca_path);
conn->mtls.cert_path = xstrdup(mtls_cert_path);
conn->mtls.key_path = xstrdup(mtls_key_path);
conn->mtls.url = xstrdup(mtls_url);
}
/*
* Attempt to get an OAUTH token for later use
* (returns immediately if not OAUTH)
*/
if (!_get_auth_header(conn, NULL, false))
return false;
return true;
}
/*
* Free data (including auth data) in this connection
*/
extern void slingshot_rest_destroy_connection(slingshot_rest_conn_t *conn)
{
xfree(conn->name);
xfree(conn->base_url);
if (conn->auth.auth_type == SLINGSHOT_AUTH_BASIC) {
xfree(conn->auth.u.basic.user_name);
if (conn->auth.u.basic.password) {
memset((void *) conn->auth.u.basic.password, 0,
strlen(conn->auth.u.basic.password));
xfree(conn->auth.u.basic.password);
}
}
xfree(conn->auth.auth_dir);
xfree(conn->mtls.ca_path);
xfree(conn->mtls.cert_path);
xfree(conn->mtls.key_path);
xfree(conn->mtls.url);
_clear_auth_header(conn);
}
/*
* Return buffer with contents of authentication file with
* pathname <auth_dir>/<base>; strip any trailing newlines
*/
static char *_read_authfile(const char *auth_dir, const char *base)
{
char *fname = NULL;
int fd = -1;
struct stat statbuf;
size_t siz = 0;
char *buf = NULL;
fname = xstrdup_printf("%s/%s", auth_dir, base);
fd = open(fname, O_RDONLY);
if (fd == -1) {
error("Couldn't open %s: %m", fname);
goto rwfail;
}
if (fstat(fd, &statbuf) == -1) {
error("fstat failed on %s: %m", fname);
goto rwfail;
}
siz = statbuf.st_size;
buf = xmalloc(siz + 1);
safe_read(fd, buf, siz);
while (siz > 0 && buf[siz - 1] == '\n')
siz--;
buf[siz] = '\0';
xfree(fname);
close(fd);
return buf;
rwfail:
xfree(fname);
xfree(buf);
close(fd);
return NULL;
}
/*
* Clear OAUTH authentication header
*/
static void _clear_auth_header(slingshot_rest_conn_t *conn)
{
if (conn->auth.u.oauth.auth_cache) {
memset((void *)conn->auth.u.oauth.auth_cache, 0,
strlen(conn->auth.u.oauth.auth_cache));
xfree(conn->auth.u.oauth.auth_cache);
}
}
/*
* If needed, access a token service to get an OAUTH2 auth token;
* on success, cache the authorization header in conn->auth.u.oauth.auth_cache,
* add the header to *headers and return true;
* if 'cache_use' is set, return the cached auth_header if set
*/
static bool _get_auth_header(slingshot_rest_conn_t *conn,
struct curl_slist **headers, bool cache_use)
{
bool retval = false;
char *client_id = NULL;
char *client_secret = NULL;
char *url = NULL;
slingshot_rest_conn_t token_conn = { 0 }; /* Conn to token service */
char *req = NULL, *response_str = NULL;
json_object *respjson = NULL;
json_object *tokjson = NULL;
const char *token = NULL;
long status = 0;
struct curl_slist *newhdrs = NULL;
/* Just return if not OAUTH */
if (conn->auth.auth_type != SLINGSHOT_AUTH_OAUTH)
return true;
/* Use token service to get token unless cache_use set (or 1st call) */
if (!cache_use || !conn->auth.u.oauth.auth_cache) {
/* Get a new token from the token service */
_clear_auth_header(conn);
/* Get the token URL and client_{id,secret}, create request */
url = _read_authfile(conn->auth.auth_dir,
SLINGSHOT_AUTH_OAUTH_ENDPOINT_FILE);
if (!url)
goto err;
client_id = _read_authfile(conn->auth.auth_dir,
SLINGSHOT_AUTH_OAUTH_CLIENT_ID_FILE);
if (!client_id)
goto err;
client_secret =
_read_authfile(conn->auth.auth_dir,
SLINGSHOT_AUTH_OAUTH_CLIENT_SECRET_FILE);
if (!client_secret)
goto err;
req = xstrdup_printf("grant_type=client_credentials"
"&client_id=%s&client_secret=%s"
"&scope=openid",
client_id, client_secret);
if (slurm_curl_request(req, url, NULL, NULL, conn->mtls.ca_path,
conn->mtls.cert_path,
conn->mtls.key_path, NULL,
SLINGSHOT_TOKEN_TIMEOUT, &response_str,
&status, HTTP_REQUEST_POST, false,
conn->mtls.enabled))
goto err;
/* Decode response into JSON */
if (response_str && response_str[0]) {
enum json_tokener_error jerr;
respjson = json_tokener_parse_verbose(response_str,
&jerr);
if (respjson == NULL) {
error("Couldn't decode %s response: %s (data '%s')",
conn->name, json_tokener_error_desc(jerr),
response_str);
goto err;
}
} else {
error("%s No http response received. Status: %ld",
conn->name, status);
goto err;
}
/* On a successful response, get the access_token out of it */
if (status == HTTP_OK) {
debug("%s POST %s successful", token_conn.name, url);
} else {
_log_rest_detail(token_conn.name, "POST", url,
respjson, status);
goto err;
}
/* Create an authentication header from the access_token */
tokjson = json_object_object_get(respjson, "access_token");
if (!tokjson || !(token = json_object_get_string(tokjson))) {
error("Couldn't get auth token from OAUTH service: json='%s'",
json_object_to_json_string(respjson));
goto err;
}
conn->auth.u.oauth.auth_cache = xstrdup_printf(
"Authorization: Bearer %s", token);
}
/* Append new header and return */
if (!headers) {
retval = true;
} else if (!(newhdrs = curl_slist_append(*headers,
conn->auth.u.oauth.auth_cache))) {
error("curl_slist_append couldn't add OAUTH header");
/* retval already false */
} else {
*headers = newhdrs;
retval = true;
}
err:
xfree(client_id);
xfree(client_secret);
xfree(url);
slingshot_rest_destroy_connection(&token_conn);
xfree(req);
xfree(response_str);
json_object_put(respjson);
return retval;
}