| /*****************************************************************************\ |
| * 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; |
| } |