From 9a2276edf22a6c29c5d9709a352498af594fe5d9 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 16 Jul 2024 14:34:35 -0500 Subject: [PATCH 1/2] CE-1472 - Refactored to do variants a little more generically per different auth-types; made createOAuth2TokenRequest its own overrideable method --- .../core/model/metadata/QBackendMetaData.java | 64 +++++ .../module/api/actions/BaseAPIActionUtil.java | 266 +++++++++++++----- .../module/api/mocks/MockApiActionUtils.java | 3 +- 3 files changed, 258 insertions(+), 75 deletions(-) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QBackendMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QBackendMetaData.java index 628fa870..52f6c95f 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QBackendMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QBackendMetaData.java @@ -53,6 +53,8 @@ public class QBackendMetaData implements TopLevelMetaDataInterface private String variantOptionsTableUsernameField; private String variantOptionsTablePasswordField; private String variantOptionsTableApiKeyField; + private String variantOptionsTableClientIdField; + private String variantOptionsTableClientSecretField; private String variantOptionsTableName; // todo - at some point, we may want to apply this to secret properties on subclasses? @@ -648,4 +650,66 @@ public class QBackendMetaData implements TopLevelMetaDataInterface { qInstance.addBackend(this); } + + /******************************************************************************* + ** Getter for variantOptionsTableClientIdField + *******************************************************************************/ + public String getVariantOptionsTableClientIdField() + { + return (this.variantOptionsTableClientIdField); + } + + + + /******************************************************************************* + ** Setter for variantOptionsTableClientIdField + *******************************************************************************/ + public void setVariantOptionsTableClientIdField(String variantOptionsTableClientIdField) + { + this.variantOptionsTableClientIdField = variantOptionsTableClientIdField; + } + + + + /******************************************************************************* + ** Fluent setter for variantOptionsTableClientIdField + *******************************************************************************/ + public QBackendMetaData withVariantOptionsTableClientIdField(String variantOptionsTableClientIdField) + { + this.variantOptionsTableClientIdField = variantOptionsTableClientIdField; + return (this); + } + + + + /******************************************************************************* + ** Getter for variantOptionsTableClientSecretField + *******************************************************************************/ + public String getVariantOptionsTableClientSecretField() + { + return (this.variantOptionsTableClientSecretField); + } + + + + /******************************************************************************* + ** Setter for variantOptionsTableClientSecretField + *******************************************************************************/ + public void setVariantOptionsTableClientSecretField(String variantOptionsTableClientSecretField) + { + this.variantOptionsTableClientSecretField = variantOptionsTableClientSecretField; + } + + + + /******************************************************************************* + ** Fluent setter for variantOptionsTableClientSecretField + *******************************************************************************/ + public QBackendMetaData withVariantOptionsTableClientSecretField(String variantOptionsTableClientSecretField) + { + this.variantOptionsTableClientSecretField = variantOptionsTableClientSecretField; + return (this); + } + + } diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java index 57cef42d..bbb2b43d 100644 --- a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java +++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java @@ -35,6 +35,7 @@ import java.util.Base64; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; import com.kingsrook.qqq.backend.core.actions.tables.GetAction; import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; import com.kingsrook.qqq.backend.core.context.QContext; @@ -64,6 +65,7 @@ import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.model.statusmessages.SystemErrorStatusMessage; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.JsonUtils; +import com.kingsrook.qqq.backend.core.utils.Pair; import com.kingsrook.qqq.backend.core.utils.SleepUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils; @@ -87,6 +89,7 @@ import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPatch; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.apache.http.client.methods.HttpRequestBase; @@ -117,8 +120,37 @@ public class BaseAPIActionUtil + /*************************************************************************** + ** enum of which HTTP Method the backend uses for Updates. + ***************************************************************************/ public enum UpdateHttpMethod - {PUT, POST} + { + PUT(HttpPut::new), + POST(HttpPost::new), + PATCH(HttpPatch::new); + + private Supplier httpEntitySupplier; + + + + /*************************************************************************** + ** + ***************************************************************************/ + UpdateHttpMethod(Supplier httpEnttySupplier) + { + this.httpEntitySupplier = httpEnttySupplier; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public HttpEntityEnclosingRequestBase newRequest() + { + return (this.httpEntitySupplier.get()); + } + } @@ -347,7 +379,7 @@ public class BaseAPIActionUtil { String paramString = buildQueryStringForUpdate(table, recordList); String url = buildTableUrl(table) + paramString; - HttpEntityEnclosingRequestBase request = getUpdateMethod().equals(UpdateHttpMethod.PUT) ? new HttpPut(url) : new HttpPost(url); + HttpEntityEnclosingRequestBase request = getUpdateMethod().newRequest(); request.setEntity(recordsToEntity(table, recordList)); QHttpResponse response = makeRequest(table, request); @@ -685,54 +717,19 @@ public class BaseAPIActionUtil *******************************************************************************/ public void setupAuthorizationInRequest(HttpRequestBase request) throws QException { - /////////////////////////////////////////////////////////////////////////////////// - // if backend specifies that it uses variants, look for that data in the session // - /////////////////////////////////////////////////////////////////////////////////// - if(backendMetaData.getUsesVariants()) - { - QSession session = QContext.getQSession(); - if(session.getBackendVariants() == null || !session.getBackendVariants().containsKey(backendMetaData.getVariantOptionsTableTypeValue())) - { - throw (new QException("Could not find Backend Variant information for Backend '" + backendMetaData.getName() + "'")); - } - - Serializable variantId = session.getBackendVariants().get(backendMetaData.getVariantOptionsTableTypeValue()); - GetInput getInput = new GetInput(); - getInput.setShouldMaskPasswords(false); - getInput.setTableName(backendMetaData.getVariantOptionsTableName()); - getInput.setPrimaryKey(variantId); - GetOutput getOutput = new GetAction().execute(getInput); - - QRecord record = getOutput.getRecord(); - if(record == null) - { - throw (new QException("Could not find Backend Variant in table " + backendMetaData.getVariantOptionsTableName() + " with id '" + variantId + "'")); - } - - if(backendMetaData.getAuthorizationType().equals(AuthorizationType.BASIC_AUTH_USERNAME_PASSWORD)) - { - request.setHeader("Authorization", getBasicAuthenticationHeader(record.getValueString(backendMetaData.getVariantOptionsTableUsernameField()), record.getValueString(backendMetaData.getVariantOptionsTablePasswordField()))); - } - else if(backendMetaData.getAuthorizationType().equals(AuthorizationType.API_KEY_HEADER)) - { - request.setHeader("API-Key", record.getValueString(backendMetaData.getVariantOptionsTableApiKeyField())); - } - else - { - throw (new IllegalArgumentException("Unexpected variant authorization type specified: " + backendMetaData.getAuthorizationType())); - } - return; - } - - /////////////////////////////////////////////////////////////////////////////////////////// - // if not using variants, the authorization data will be in the backend meta data object // - /////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////// + // update the request based on the authorization type being used // + /////////////////////////////////////////////////////////////////// switch(backendMetaData.getAuthorizationType()) { - case BASIC_AUTH_API_KEY -> request.setHeader("Authorization", getBasicAuthenticationHeader(backendMetaData.getApiKey())); - case BASIC_AUTH_USERNAME_PASSWORD -> request.setHeader("Authorization", getBasicAuthenticationHeader(backendMetaData.getUsername(), backendMetaData.getPassword())); - case API_KEY_HEADER -> request.setHeader("API-Key", backendMetaData.getApiKey()); - case API_TOKEN -> request.setHeader("Authorization", "Token " + backendMetaData.getApiKey()); + case BASIC_AUTH_API_KEY -> request.setHeader("Authorization", getBasicAuthenticationHeader(getApiKey())); + case BASIC_AUTH_USERNAME_PASSWORD -> + { + Pair usernameAndPassword = getUsernameAndPassword(); + request.setHeader("Authorization", getBasicAuthenticationHeader(usernameAndPassword.getA(), usernameAndPassword.getB())); + } + case API_KEY_HEADER -> request.setHeader("API-Key", getApiKey()); + case API_TOKEN -> request.setHeader("Authorization", "Token " + getApiKey()); case OAUTH2 -> request.setHeader("Authorization", "Bearer " + getOAuth2Token()); case API_KEY_QUERY_PARAM -> { @@ -740,7 +737,7 @@ public class BaseAPIActionUtil { String uri = request.getURI().toString(); uri += (uri.contains("?") ? "&" : "?"); - uri += backendMetaData.getApiKeyQueryParamName() + "=" + backendMetaData.getApiKey(); + uri += backendMetaData.getApiKeyQueryParamName() + "=" + getApiKey(); request.setURI(new URI(uri)); } catch(URISyntaxException e) @@ -748,49 +745,113 @@ public class BaseAPIActionUtil throw (new QException("Error setting authorization query parameter", e)); } } - case CUSTOM -> - { - handleCustomAuthorization(request); - } + case CUSTOM -> handleCustomAuthorization(request); default -> throw new IllegalArgumentException("Unexpected authorization type: " + backendMetaData.getAuthorizationType()); } } + /*************************************************************************** + ** + ***************************************************************************/ + protected String getApiKey() throws QException + { + if(backendMetaData.getUsesVariants()) + { + QRecord record = getVariantRecord(); + return (record.getValueString(backendMetaData.getVariantOptionsTableApiKeyField())); + } + + return (backendMetaData.getApiKey()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + protected Pair getUsernameAndPassword() throws QException + { + if(backendMetaData.getUsesVariants()) + { + QRecord record = getVariantRecord(); + return (Pair.of(record.getValueString(backendMetaData.getVariantOptionsTableUsernameField()), record.getValueString(backendMetaData.getVariantOptionsTablePasswordField()))); + } + + return (Pair.of(backendMetaData.getUsername(), backendMetaData.getPassword())); + } + + + + /******************************************************************************* + ** For backends that use variants, look up the variant record (in theory, based + ** on an id in the session's backend variants map, then fetched from the backend's + ** variant options table. + *******************************************************************************/ + protected QRecord getVariantRecord() throws QException + { + Serializable variantId = getVariantId(); + GetInput getInput = new GetInput(); + getInput.setShouldMaskPasswords(false); + getInput.setTableName(backendMetaData.getVariantOptionsTableName()); + getInput.setPrimaryKey(variantId); + GetOutput getOutput = new GetAction().execute(getInput); + + QRecord record = getOutput.getRecord(); + if(record == null) + { + throw (new QException("Could not find Backend Variant in table " + backendMetaData.getVariantOptionsTableName() + " with id '" + variantId + "'")); + } + return record; + } + + + + /******************************************************************************* + ** Get the variant id from the session for the backend. + *******************************************************************************/ + protected Serializable getVariantId() throws QException + { + QSession session = QContext.getQSession(); + if(session.getBackendVariants() == null || !session.getBackendVariants().containsKey(backendMetaData.getVariantOptionsTableTypeValue())) + { + throw (new QException("Could not find Backend Variant information for Backend '" + backendMetaData.getName() + "'")); + } + Serializable variantId = session.getBackendVariants().get(backendMetaData.getVariantOptionsTableTypeValue()); + return variantId; + } + + + /******************************************************************************* ** *******************************************************************************/ - public String getOAuth2Token() throws OAuthCredentialsException + public String getOAuth2Token() throws OAuthCredentialsException, QException { + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // define the key that will be used in the backend's customValues map, to stash the access token. // + // for non-variant backends, this is just a constant string. But for variant-backends, append the variantId to it. // + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + String accessTokenKey = "accessToken"; + if(backendMetaData.getUsesVariants()) + { + Serializable variantId = getVariantId(); + accessTokenKey = accessTokenKey + ":" + variantId; + } + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// // check for the access token in the backend meta data. if it's not there, then issue a request for a token. // // this is not generally meant to be put in the meta data by the app programmer - rather, we're just using // // it as a "cheap & easy" way to "cache" the token within our process's memory... // //////////////////////////////////////////////////////////////////////////////////////////////////////////////// - String accessToken = ValueUtils.getValueAsString(backendMetaData.getCustomValue("accessToken")); - Boolean setCredentialsInHeader = BooleanUtils.isTrue(ValueUtils.getValueAsBoolean(backendMetaData.getCustomValue("setCredentialsInHeader"))); + String accessToken = ValueUtils.getValueAsString(backendMetaData.getCustomValue(accessTokenKey)); if(!StringUtils.hasContent(accessToken)) { - String fullURL = backendMetaData.getBaseUrl() + "oauth/token"; - String postBody = "grant_type=client_credentials"; - - if(!setCredentialsInHeader) - { - postBody += "&client_id=" + backendMetaData.getClientId() + "&client_secret=" + backendMetaData.getClientSecret(); - } - try(CloseableHttpClient client = HttpClients.custom().setConnectionManager(new PoolingHttpClientConnectionManager()).build()) { - HttpPost request = new HttpPost(fullURL); - request.setEntity(new StringEntity(postBody, getCharsetForEntity())); - - if(setCredentialsInHeader) - { - request.setHeader("Authorization", getBasicAuthenticationHeader(backendMetaData.getClientId(), backendMetaData.getClientSecret())); - } - request.setHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8"); + HttpRequestBase request = createOAuth2TokenRequest(); HttpResponse response = executeOAuthTokenRequest(client, request); int statusCode = response.getStatusLine().getStatusCode(); @@ -808,7 +869,7 @@ public class BaseAPIActionUtil /////////////////////////////////////////////////////////////////////////////////////////////////// // stash the access token in the backendMetaData, from which it will be used for future requests // /////////////////////////////////////////////////////////////////////////////////////////////////// - backendMetaData.withCustomValue("accessToken", accessToken); + backendMetaData.withCustomValue(accessTokenKey, accessToken); } catch(OAuthCredentialsException oce) { @@ -827,6 +888,53 @@ public class BaseAPIActionUtil + /*************************************************************************** + ** For doing OAuth2 authentication, create a request for a token. + ***************************************************************************/ + protected HttpRequestBase createOAuth2TokenRequest() throws QException + { + String fullURL = backendMetaData.getBaseUrl() + "oauth/token"; + String postBody = "grant_type=client_credentials"; + + Pair clientIdAndSecret = getClientIdAndSecret(); + String clientId = clientIdAndSecret.getA(); + String clientSecret = clientIdAndSecret.getB(); + + Boolean setCredentialsInHeader = BooleanUtils.isTrue(ValueUtils.getValueAsBoolean(backendMetaData.getCustomValue("setCredentialsInHeader"))); + if(!setCredentialsInHeader) + { + postBody += "&client_id=" + clientId + "&client_secret=" + clientSecret; + } + + HttpPost request = new HttpPost(fullURL); + request.setEntity(new StringEntity(postBody, getCharsetForEntity())); + + if(setCredentialsInHeader) + { + request.setHeader("Authorization", getBasicAuthenticationHeader(clientId, clientSecret)); + } + request.setHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8"); + return request; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + protected Pair getClientIdAndSecret() throws QException + { + if(backendMetaData.getUsesVariants()) + { + QRecord record = getVariantRecord(); + return (Pair.of(record.getValueString(backendMetaData.getVariantOptionsTableClientIdField()), record.getValueString(backendMetaData.getVariantOptionsTableClientSecretField()))); + } + + return (Pair.of(backendMetaData.getClientId(), backendMetaData.getClientSecret())); + } + + + /******************************************************************************* ** Let a subclass change what charset to use for entities (bodies) being posted/put/etc. *******************************************************************************/ @@ -840,6 +948,18 @@ public class BaseAPIActionUtil /******************************************************************************* ** one-line method, factored out so mock/tests can override *******************************************************************************/ + protected CloseableHttpResponse executeOAuthTokenRequest(CloseableHttpClient client, HttpRequestBase request) throws IOException + { + return client.execute(request); + } + + + + /******************************************************************************* + ** one-line method, factored out so mock/tests can override + ** Deprecated, in favor of more generic overload that takes HttpRequestBase + *******************************************************************************/ + @Deprecated protected CloseableHttpResponse executeOAuthTokenRequest(CloseableHttpClient client, HttpPost request) throws IOException { return client.execute(request); diff --git a/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/mocks/MockApiActionUtils.java b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/mocks/MockApiActionUtils.java index b54f37d7..52367b7a 100644 --- a/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/mocks/MockApiActionUtils.java +++ b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/mocks/MockApiActionUtils.java @@ -28,7 +28,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.module.api.actions.BaseAPIActionUtil; import com.kingsrook.qqq.backend.module.api.actions.QHttpResponse; import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.impl.client.CloseableHttpClient; @@ -89,7 +88,7 @@ public class MockApiActionUtils extends BaseAPIActionUtil ** *******************************************************************************/ @Override - protected CloseableHttpResponse executeOAuthTokenRequest(CloseableHttpClient client, HttpPost request) throws IOException + protected CloseableHttpResponse executeOAuthTokenRequest(CloseableHttpClient client, HttpRequestBase request) throws IOException { runMockAsserter(request); return new MockHttpResponse(mockApiUtilsHelper); From 06a69279a8d80f214ae212b3e9bf483e802db854 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 17 Jul 2024 15:07:30 -0500 Subject: [PATCH 2/2] CE-1472 - Fix doUpdate to set URL --- .../qqq/backend/module/api/actions/BaseAPIActionUtil.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java index bbb2b43d..6ef91f16 100644 --- a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java +++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java @@ -380,6 +380,8 @@ public class BaseAPIActionUtil String paramString = buildQueryStringForUpdate(table, recordList); String url = buildTableUrl(table) + paramString; HttpEntityEnclosingRequestBase request = getUpdateMethod().newRequest(); + + request.setURI(new URI(url)); request.setEntity(recordsToEntity(table, recordList)); QHttpResponse response = makeRequest(table, request);