From 0159459db2240676d872ba39015f6bd00cc78235 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 2 May 2023 19:58:19 -0500 Subject: [PATCH] more flexible (app-defined) security schemes; --- .../actions/GenerateOpenApiSpecAction.java | 167 +++++++++--------- .../model/metadata/ApiInstanceMetaData.java | 51 ++++++ .../qqq/api/model/openapi/OAuth2.java | 2 +- .../qqq/api/model/openapi/SecurityScheme.java | 143 +++++++++++---- .../api/model/openapi/SecuritySchemeType.java | 62 +++++++ .../resources/rapidoc/rapidoc-container.html | 2 + 6 files changed, 315 insertions(+), 112 deletions(-) create mode 100644 qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/SecuritySchemeType.java diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java index 1332e018..1f206378 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java @@ -51,8 +51,6 @@ import com.kingsrook.qqq.api.model.openapi.ExampleWithListValue; import com.kingsrook.qqq.api.model.openapi.ExampleWithSingleValue; import com.kingsrook.qqq.api.model.openapi.Info; import com.kingsrook.qqq.api.model.openapi.Method; -import com.kingsrook.qqq.api.model.openapi.OAuth2; -import com.kingsrook.qqq.api.model.openapi.OAuth2Flow; import com.kingsrook.qqq.api.model.openapi.OpenAPI; import com.kingsrook.qqq.api.model.openapi.Parameter; import com.kingsrook.qqq.api.model.openapi.Path; @@ -249,6 +247,15 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction scopes = new LinkedHashMap<>(); + + /* + //////////////////////////////////////////////////////////////////////////////// + // these are moved to the app to define now, but, leaving here for references // + //////////////////////////////////////////////////////////////////////////////// securitySchemes.put("basicAuth", new SecurityScheme() .withType("http") .withScheme("basic")); @@ -258,13 +265,14 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction scopes = new LinkedHashMap<>(); - // todo, or not todo? .withScopes(scopes) - // seems to make a lot of "noise" on the Auth page, and for no obvious benefit... securitySchemes.put("OAuth2", new OAuth2() .withFlows(MapBuilder.of("clientCredentials", new OAuth2Flow() .withTokenUrl("/api/oauth/token")))); + // todo, or not todo? .withScopes(scopes) + // seems to make a lot of "noise" on the Auth page, and for no obvious benefit... + */ + componentSchemas.put("baseSearchResultFields", new Schema() .withType("object") .withProperties(MapBuilder.of( @@ -468,7 +476,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction>> getSecurity(String permissionName) + private static List>> getSecurity(ApiInstanceMetaData apiInstanceMetaData, String permissionName) { - return ListBuilder.of( - MapBuilder.of("OAuth2", List.of(permissionName)), - MapBuilder.of("bearerAuth", List.of(permissionName)), - MapBuilder.of("basicAuth", List.of(permissionName)) - ); + List>> rs = new ArrayList<>(); + for(Map.Entry entry : CollectionUtils.nonNullMap(apiInstanceMetaData.getSecuritySchemes()).entrySet()) + { + rs.add(MapBuilder.of(entry.getKey(), List.of(permissionName))); + } + return (rs); } @@ -967,51 +976,51 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction example = switch(method.toLowerCase()) - { - case "post" -> ListBuilder.of( - MapBuilder.of(LinkedHashMap::new) - .with("statusCode", HttpStatus.CREATED.getCode()) - .with("statusText", HttpStatus.CREATED.getMessage()) - .with(primaryKeyApiName, "47").build(), - MapBuilder.of(LinkedHashMap::new) - .with("statusCode", HttpStatus.BAD_REQUEST.getCode()) - .with("statusText", HttpStatus.BAD_REQUEST.getMessage()) - .with("error", "Could not create " + tableLabel + ": Duplicate value in unique key field.").build() - ); - case "patch" -> ListBuilder.of( - MapBuilder.of(LinkedHashMap::new) - .with("statusCode", HttpStatus.NO_CONTENT.getCode()) - .with("statusText", HttpStatus.NO_CONTENT.getMessage()) - .with(primaryKeyApiName, "47").build(), - MapBuilder.of(LinkedHashMap::new) - .with("statusCode", HttpStatus.BAD_REQUEST.getCode()) - .with("statusText", HttpStatus.BAD_REQUEST.getMessage()) - .with("error", "Could not update " + tableLabel + ": Missing value in required field: My Field.") - .with(primaryKeyApiName, "47").build(), - MapBuilder.of(LinkedHashMap::new) - .with("statusCode", HttpStatus.NOT_FOUND.getCode()) - .with("statusText", HttpStatus.NOT_FOUND.getMessage()) - .with("error", "The requested " + tableLabel + " to update was not found.") - .with(primaryKeyApiName, "47").build() - ); - case "delete" -> ListBuilder.of( - MapBuilder.of(LinkedHashMap::new) - .with("statusCode", HttpStatus.NO_CONTENT.getCode()) - .with("statusText", HttpStatus.NO_CONTENT.getMessage()) - .with(primaryKeyApiName, "47").build(), - MapBuilder.of(LinkedHashMap::new) - .with("statusCode", HttpStatus.BAD_REQUEST.getCode()) - .with("statusText", HttpStatus.BAD_REQUEST.getMessage()) - .with("error", "Could not delete " + tableLabel + ": Foreign key constraint violation.") - .with(primaryKeyApiName, "47").build(), - MapBuilder.of(LinkedHashMap::new) - .with("statusCode", HttpStatus.NOT_FOUND.getCode()) - .with("statusText", HttpStatus.NOT_FOUND.getMessage()) - .with("error", "The requested " + tableLabel + " to delete was not found.") - .with(primaryKeyApiName, "47").build() - ); - default -> throw (new IllegalArgumentException("Unrecognized method: " + method)); - }; + { + case "post" -> ListBuilder.of( + MapBuilder.of(LinkedHashMap::new) + .with("statusCode", HttpStatus.CREATED.getCode()) + .with("statusText", HttpStatus.CREATED.getMessage()) + .with(primaryKeyApiName, "47").build(), + MapBuilder.of(LinkedHashMap::new) + .with("statusCode", HttpStatus.BAD_REQUEST.getCode()) + .with("statusText", HttpStatus.BAD_REQUEST.getMessage()) + .with("error", "Could not create " + tableLabel + ": Duplicate value in unique key field.").build() + ); + case "patch" -> ListBuilder.of( + MapBuilder.of(LinkedHashMap::new) + .with("statusCode", HttpStatus.NO_CONTENT.getCode()) + .with("statusText", HttpStatus.NO_CONTENT.getMessage()) + .with(primaryKeyApiName, "47").build(), + MapBuilder.of(LinkedHashMap::new) + .with("statusCode", HttpStatus.BAD_REQUEST.getCode()) + .with("statusText", HttpStatus.BAD_REQUEST.getMessage()) + .with("error", "Could not update " + tableLabel + ": Missing value in required field: My Field.") + .with(primaryKeyApiName, "47").build(), + MapBuilder.of(LinkedHashMap::new) + .with("statusCode", HttpStatus.NOT_FOUND.getCode()) + .with("statusText", HttpStatus.NOT_FOUND.getMessage()) + .with("error", "The requested " + tableLabel + " to update was not found.") + .with(primaryKeyApiName, "47").build() + ); + case "delete" -> ListBuilder.of( + MapBuilder.of(LinkedHashMap::new) + .with("statusCode", HttpStatus.NO_CONTENT.getCode()) + .with("statusText", HttpStatus.NO_CONTENT.getMessage()) + .with(primaryKeyApiName, "47").build(), + MapBuilder.of(LinkedHashMap::new) + .with("statusCode", HttpStatus.BAD_REQUEST.getCode()) + .with("statusText", HttpStatus.BAD_REQUEST.getMessage()) + .with("error", "Could not delete " + tableLabel + ": Foreign key constraint violation.") + .with(primaryKeyApiName, "47").build(), + MapBuilder.of(LinkedHashMap::new) + .with("statusCode", HttpStatus.NOT_FOUND.getCode()) + .with("statusText", HttpStatus.NOT_FOUND.getMessage()) + .with("error", "The requested " + tableLabel + " to delete was not found.") + .with(primaryKeyApiName, "47").build() + ); + default -> throw (new IllegalArgumentException("Unrecognized method: " + method)); + }; Map properties = new LinkedHashMap<>(); properties.put("statusCode", new Schema().withType("integer")); @@ -1125,12 +1134,12 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction "string"; - case INTEGER -> "integer"; - case DECIMAL -> "number"; - case BOOLEAN -> "boolean"; - }; + { + case STRING, DATE, TIME, DATE_TIME, TEXT, HTML, PASSWORD, BLOB -> "string"; + case INTEGER -> "integer"; + case DECIMAL -> "number"; + case BOOLEAN -> "boolean"; + }; } @@ -1152,14 +1161,14 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction "date"; - case TIME -> "time"; // non-standard format... - case DATE_TIME -> "date-time"; - case PASSWORD -> "password"; - case BLOB -> "byte"; // base-64-encoded, per https://swagger.io/docs/specification/data-models/data-types/#file - default -> null; - }; + { + case DATE -> "date"; + case TIME -> "time"; // non-standard format... + case DATE_TIME -> "date-time"; + case PASSWORD -> "password"; + case BLOB -> "byte"; // base-64-encoded, per https://swagger.io/docs/specification/data-models/data-types/#file + default -> null; + }; } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaData.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaData.java index ec38dac0..ea8a44d6 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaData.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaData.java @@ -24,12 +24,15 @@ package com.kingsrook.qqq.api.model.metadata; import java.util.Collections; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; import com.kingsrook.qqq.api.model.APIVersion; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer; +import com.kingsrook.qqq.api.model.openapi.SecurityScheme; import com.kingsrook.qqq.api.model.openapi.Server; import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; @@ -60,6 +63,8 @@ public class ApiInstanceMetaData implements ApiOperation.EnabledOperationsProvid private Set enabledOperations = new HashSet<>(); private Set disabledOperations = new HashSet<>(); + private Map securitySchemes = new LinkedHashMap<>(); + private boolean includeErrorTooManyRequests = true; @@ -590,4 +595,50 @@ public class ApiInstanceMetaData implements ApiOperation.EnabledOperationsProvid return (this); } + + + /******************************************************************************* + ** Getter for securitySchemes + *******************************************************************************/ + public Map getSecuritySchemes() + { + return (this.securitySchemes); + } + + + + /******************************************************************************* + ** Setter for securitySchemes + *******************************************************************************/ + public void setSecuritySchemes(Map securitySchemes) + { + this.securitySchemes = securitySchemes; + } + + + + /******************************************************************************* + ** Fluent setter for securitySchemes + *******************************************************************************/ + public ApiInstanceMetaData withSecuritySchemes(Map securitySchemes) + { + this.securitySchemes = securitySchemes; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for securitySchemes + *******************************************************************************/ + public ApiInstanceMetaData withSecurityScheme(String label, SecurityScheme securityScheme) + { + if(this.securitySchemes == null) + { + this.securitySchemes = new LinkedHashMap<>(); + } + this.securitySchemes.put(label, securityScheme); + return (this); + } + } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/OAuth2.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/OAuth2.java index 321eba45..d98978e9 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/OAuth2.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/OAuth2.java @@ -40,7 +40,7 @@ public class OAuth2 extends SecurityScheme *******************************************************************************/ public OAuth2() { - setType("oauth2"); + setType(SecuritySchemeType.OAUTH2); } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/SecurityScheme.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/SecurityScheme.java index 3209031e..dfbd56b9 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/SecurityScheme.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/SecurityScheme.java @@ -22,48 +22,23 @@ package com.kingsrook.qqq.api.model.openapi; +import com.fasterxml.jackson.annotation.JsonIgnore; + + /******************************************************************************* ** *******************************************************************************/ public class SecurityScheme { - private String type; + private SecuritySchemeType type; + + private String name; + private String in; private String scheme; private String bearerFormat; - /******************************************************************************* - ** Getter for type - *******************************************************************************/ - public String getType() - { - return (this.type); - } - - - - /******************************************************************************* - ** Setter for type - *******************************************************************************/ - public void setType(String type) - { - this.type = type; - } - - - - /******************************************************************************* - ** Fluent setter for type - *******************************************************************************/ - public SecurityScheme withType(String type) - { - this.type = type; - return (this); - } - - - /******************************************************************************* ** Getter for scheme *******************************************************************************/ @@ -124,4 +99,108 @@ public class SecurityScheme return (this); } + + + /******************************************************************************* + ** Getter for type + *******************************************************************************/ + public String getType() + { + return (this.type.getType()); + } + + + + /******************************************************************************* + ** Getter for type + *******************************************************************************/ + @JsonIgnore + public SecuritySchemeType getTypeEnum() + { + return (this.type); + } + + + + /******************************************************************************* + ** Setter for type + *******************************************************************************/ + public void setType(SecuritySchemeType type) + { + this.type = type; + } + + + + /******************************************************************************* + ** Fluent setter for type + *******************************************************************************/ + public SecurityScheme withType(SecuritySchemeType type) + { + this.type = type; + return (this); + } + + + + /******************************************************************************* + ** Getter for name + *******************************************************************************/ + public String getName() + { + return (this.name); + } + + + + /******************************************************************************* + ** Setter for name + *******************************************************************************/ + public void setName(String name) + { + this.name = name; + } + + + + /******************************************************************************* + ** Fluent setter for name + *******************************************************************************/ + public SecurityScheme withName(String name) + { + this.name = name; + return (this); + } + + + + /******************************************************************************* + ** Getter for in + *******************************************************************************/ + public String getIn() + { + return (this.in); + } + + + + /******************************************************************************* + ** Setter for in + *******************************************************************************/ + public void setIn(String in) + { + this.in = in; + } + + + + /******************************************************************************* + ** Fluent setter for in + *******************************************************************************/ + public SecurityScheme withIn(String in) + { + this.in = in; + return (this); + } + } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/SecuritySchemeType.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/SecuritySchemeType.java new file mode 100644 index 00000000..06eb002a --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/SecuritySchemeType.java @@ -0,0 +1,62 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.api.model.openapi; + + +/******************************************************************************* + ** + *******************************************************************************/ +public enum SecuritySchemeType +{ + API_KEY("apiKey"), + HTTP("http"), + /////////////////////// + // not yet supported // + /////////////////////// + // MUTUAL_TLS("mutualTLS"), + // OPEN_ID_CONNECT("openIdConnect"), + OAUTH2("oauth2"); + + + private final String type; + + + + /******************************************************************************* + ** + *******************************************************************************/ + SecuritySchemeType(String type) + { + this.type = type; + } + + + + /******************************************************************************* + ** Getter for type + ** + *******************************************************************************/ + public String getType() + { + return type; + } +} diff --git a/qqq-middleware-api/src/main/resources/rapidoc/rapidoc-container.html b/qqq-middleware-api/src/main/resources/rapidoc/rapidoc-container.html index 38c0b4c5..ab77ff6f 100644 --- a/qqq-middleware-api/src/main/resources/rapidoc/rapidoc-container.html +++ b/qqq-middleware-api/src/main/resources/rapidoc/rapidoc-container.html @@ -39,6 +39,7 @@ allow-spec-file-download="true" primary-color="{primaryColor}" sort-endpoints-by="method" + allow-authentication="true" persist-auth="true" render-style="focused" show-method-in-nav-bar="as-colored-block" @@ -46,6 +47,7 @@ css-file="qqq-api-styles.css" css-classes="qqqApi" info-description-headings-in-navbar="true" + show-curl-before-try="true" > {navLogoImg}