more flexible (app-defined) security schemes;

This commit is contained in:
2023-05-02 19:58:19 -05:00
parent b0dbede6fe
commit 0159459db2
6 changed files with 315 additions and 112 deletions

View File

@ -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.ExampleWithSingleValue;
import com.kingsrook.qqq.api.model.openapi.Info; import com.kingsrook.qqq.api.model.openapi.Info;
import com.kingsrook.qqq.api.model.openapi.Method; 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.OpenAPI;
import com.kingsrook.qqq.api.model.openapi.Parameter; import com.kingsrook.qqq.api.model.openapi.Parameter;
import com.kingsrook.qqq.api.model.openapi.Path; import com.kingsrook.qqq.api.model.openapi.Path;
@ -249,6 +247,15 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
.withSecuritySchemes(securitySchemes) .withSecuritySchemes(securitySchemes)
.withExamples(getComponentExamples())); .withExamples(getComponentExamples()));
securitySchemes.putAll(CollectionUtils.nonNullMap(apiInstanceMetaData.getSecuritySchemes()));
@SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
LinkedHashMap<String, String> scopes = new LinkedHashMap<>();
/*
////////////////////////////////////////////////////////////////////////////////
// these are moved to the app to define now, but, leaving here for references //
////////////////////////////////////////////////////////////////////////////////
securitySchemes.put("basicAuth", new SecurityScheme() securitySchemes.put("basicAuth", new SecurityScheme()
.withType("http") .withType("http")
.withScheme("basic")); .withScheme("basic"));
@ -258,13 +265,14 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
.withScheme("bearer") .withScheme("bearer")
.withBearerFormat("JWT")); .withBearerFormat("JWT"));
LinkedHashMap<String, String> 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() securitySchemes.put("OAuth2", new OAuth2()
.withFlows(MapBuilder.of("clientCredentials", new OAuth2Flow() .withFlows(MapBuilder.of("clientCredentials", new OAuth2Flow()
.withTokenUrl("/api/oauth/token")))); .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() componentSchemas.put("baseSearchResultFields", new Schema()
.withType("object") .withType("object")
.withProperties(MapBuilder.of( .withProperties(MapBuilder.of(
@ -468,7 +476,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
.withDescription("Successfully searched the " + tableLabel + " table (though may have found 0 records).") .withDescription("Successfully searched the " + tableLabel + " table (though may have found 0 records).")
.withContent(MapBuilder.of("application/json", new Content() .withContent(MapBuilder.of("application/json", new Content()
.withSchema(new Schema().withRef("#/components/schemas/" + tableApiName + "SearchResult"))))) .withSchema(new Schema().withRef("#/components/schemas/" + tableApiName + "SearchResult")))))
.withSecurity(getSecurity(tableReadPermissionName)); .withSecurity(getSecurity(apiInstanceMetaData, tableReadPermissionName));
for(QFieldMetaData tableApiField : tableApiFields) for(QFieldMetaData tableApiField : tableApiFields)
{ {
@ -486,7 +494,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
Method queryPost = new Method() Method queryPost = new Method()
.withSummary("Search the " + tableLabel + " table by posting a QueryFilter object.") .withSummary("Search the " + tableLabel + " table by posting a QueryFilter object.")
.withTags(ListBuilder.of(tableLabel)) .withTags(ListBuilder.of(tableLabel))
.withSecurity(getSecurity(tableReadPermissionName)); .withSecurity(getSecurity(apiInstanceMetaData, tableReadPermissionName));
if(queryByQueryStringEnabled) if(queryByQueryStringEnabled)
{ {
@ -514,7 +522,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
.withDescription("Successfully got the requested " + tableLabel) .withDescription("Successfully got the requested " + tableLabel)
.withContent(MapBuilder.of("application/json", new Content() .withContent(MapBuilder.of("application/json", new Content()
.withSchema(new Schema().withRef("#/components/schemas/" + tableApiName))))) .withSchema(new Schema().withRef("#/components/schemas/" + tableApiName)))))
.withSecurity(getSecurity(tableReadPermissionName)); .withSecurity(getSecurity(apiInstanceMetaData, tableReadPermissionName));
Method idPatch = new Method() Method idPatch = new Method()
.withSummary("Update one " + tableLabel) .withSummary("Update one " + tableLabel)
@ -536,7 +544,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
.withResponses(buildStandardErrorResponses(apiInstanceMetaData)) .withResponses(buildStandardErrorResponses(apiInstanceMetaData))
.withResponse(HttpStatus.NOT_FOUND.getCode(), buildStandardErrorResponse("The requested " + tableLabel + " record was not found.", "Could not find " + tableLabel + " with " + primaryKeyLabel + " of 47.")) .withResponse(HttpStatus.NOT_FOUND.getCode(), buildStandardErrorResponse("The requested " + tableLabel + " record was not found.", "Could not find " + tableLabel + " with " + primaryKeyLabel + " of 47."))
.withResponse(HttpStatus.NO_CONTENT.getCode(), new Response().withDescription("Successfully updated the requested " + tableLabel)) .withResponse(HttpStatus.NO_CONTENT.getCode(), new Response().withDescription("Successfully updated the requested " + tableLabel))
.withSecurity(getSecurity(tableUpdatePermissionName)); .withSecurity(getSecurity(apiInstanceMetaData, tableUpdatePermissionName));
Method idDelete = new Method() Method idDelete = new Method()
.withSummary("Delete one " + tableLabel) .withSummary("Delete one " + tableLabel)
@ -553,7 +561,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
.withResponses(buildStandardErrorResponses(apiInstanceMetaData)) .withResponses(buildStandardErrorResponses(apiInstanceMetaData))
.withResponse(HttpStatus.NOT_FOUND.getCode(), buildStandardErrorResponse("The requested " + tableLabel + " record was not found.", "Could not find " + tableLabel + " with " + primaryKeyLabel + " of 47.")) .withResponse(HttpStatus.NOT_FOUND.getCode(), buildStandardErrorResponse("The requested " + tableLabel + " record was not found.", "Could not find " + tableLabel + " with " + primaryKeyLabel + " of 47."))
.withResponse(HttpStatus.NO_CONTENT.getCode(), new Response().withDescription("Successfully deleted the requested " + tableLabel)) .withResponse(HttpStatus.NO_CONTENT.getCode(), new Response().withDescription("Successfully deleted the requested " + tableLabel))
.withSecurity(getSecurity(tableDeletePermissionName)); .withSecurity(getSecurity(apiInstanceMetaData, tableDeletePermissionName));
if(getEnabled || updateEnabled || deleteEnabled) if(getEnabled || updateEnabled || deleteEnabled)
{ {
@ -584,7 +592,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
.withType(getFieldType(primaryKeyField)) .withType(getFieldType(primaryKeyField))
.withExample("47"))))))) .withExample("47")))))))
.withTags(ListBuilder.of(tableLabel)) .withTags(ListBuilder.of(tableLabel))
.withSecurity(getSecurity(tableInsertPermissionName)); .withSecurity(getSecurity(apiInstanceMetaData, tableInsertPermissionName));
if(insertEnabled) if(insertEnabled)
{ {
@ -609,7 +617,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
.withResponses(buildStandardErrorResponses(apiInstanceMetaData)) .withResponses(buildStandardErrorResponses(apiInstanceMetaData))
.withResponse(HttpStatus.MULTI_STATUS.getCode(), buildMultiStatusResponse(tableLabel, primaryKeyApiName, primaryKeyField, "post")) .withResponse(HttpStatus.MULTI_STATUS.getCode(), buildMultiStatusResponse(tableLabel, primaryKeyApiName, primaryKeyField, "post"))
.withTags(ListBuilder.of(tableLabel)) .withTags(ListBuilder.of(tableLabel))
.withSecurity(getSecurity(tableInsertPermissionName)); .withSecurity(getSecurity(apiInstanceMetaData, tableInsertPermissionName));
Method bulkPatch = new Method() Method bulkPatch = new Method()
.withSummary("Update multiple " + tableLabel + " records") .withSummary("Update multiple " + tableLabel + " records")
@ -631,7 +639,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
.withResponses(buildStandardErrorResponses(apiInstanceMetaData)) .withResponses(buildStandardErrorResponses(apiInstanceMetaData))
.withResponse(HttpStatus.MULTI_STATUS.getCode(), buildMultiStatusResponse(tableLabel, primaryKeyApiName, primaryKeyField, "patch")) .withResponse(HttpStatus.MULTI_STATUS.getCode(), buildMultiStatusResponse(tableLabel, primaryKeyApiName, primaryKeyField, "patch"))
.withTags(ListBuilder.of(tableLabel)) .withTags(ListBuilder.of(tableLabel))
.withSecurity(getSecurity(tableUpdatePermissionName)); .withSecurity(getSecurity(apiInstanceMetaData, tableUpdatePermissionName));
Method bulkDelete = new Method() Method bulkDelete = new Method()
.withSummary("Delete multiple " + tableLabel + " records") .withSummary("Delete multiple " + tableLabel + " records")
@ -648,7 +656,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
.withResponses(buildStandardErrorResponses(apiInstanceMetaData)) .withResponses(buildStandardErrorResponses(apiInstanceMetaData))
.withResponse(HttpStatus.MULTI_STATUS.getCode(), buildMultiStatusResponse(tableLabel, primaryKeyApiName, primaryKeyField, "delete")) .withResponse(HttpStatus.MULTI_STATUS.getCode(), buildMultiStatusResponse(tableLabel, primaryKeyApiName, primaryKeyField, "delete"))
.withTags(ListBuilder.of(tableLabel)) .withTags(ListBuilder.of(tableLabel))
.withSecurity(getSecurity(tableDeletePermissionName)); .withSecurity(getSecurity(apiInstanceMetaData, tableDeletePermissionName));
if(insertBulkEnabled || updateBulkEnabled || deleteBulkEnabled) if(insertBulkEnabled || updateBulkEnabled || deleteBulkEnabled)
{ {
@ -949,13 +957,14 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
private static List<Map<String, List<String>>> getSecurity(String permissionName) private static List<Map<String, List<String>>> getSecurity(ApiInstanceMetaData apiInstanceMetaData, String permissionName)
{ {
return ListBuilder.of( List<Map<String, List<String>>> rs = new ArrayList<>();
MapBuilder.of("OAuth2", List.of(permissionName)), for(Map.Entry<String, SecurityScheme> entry : CollectionUtils.nonNullMap(apiInstanceMetaData.getSecuritySchemes()).entrySet())
MapBuilder.of("bearerAuth", List.of(permissionName)), {
MapBuilder.of("basicAuth", List.of(permissionName)) rs.add(MapBuilder.of(entry.getKey(), List.of(permissionName)));
); }
return (rs);
} }

View File

@ -24,12 +24,15 @@ package com.kingsrook.qqq.api.model.metadata;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import com.kingsrook.qqq.api.model.APIVersion; import com.kingsrook.qqq.api.model.APIVersion;
import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData;
import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaDataContainer; 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.api.model.openapi.Server;
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
@ -60,6 +63,8 @@ public class ApiInstanceMetaData implements ApiOperation.EnabledOperationsProvid
private Set<ApiOperation> enabledOperations = new HashSet<>(); private Set<ApiOperation> enabledOperations = new HashSet<>();
private Set<ApiOperation> disabledOperations = new HashSet<>(); private Set<ApiOperation> disabledOperations = new HashSet<>();
private Map<String, SecurityScheme> securitySchemes = new LinkedHashMap<>();
private boolean includeErrorTooManyRequests = true; private boolean includeErrorTooManyRequests = true;
@ -590,4 +595,50 @@ public class ApiInstanceMetaData implements ApiOperation.EnabledOperationsProvid
return (this); return (this);
} }
/*******************************************************************************
** Getter for securitySchemes
*******************************************************************************/
public Map<String, SecurityScheme> getSecuritySchemes()
{
return (this.securitySchemes);
}
/*******************************************************************************
** Setter for securitySchemes
*******************************************************************************/
public void setSecuritySchemes(Map<String, SecurityScheme> securitySchemes)
{
this.securitySchemes = securitySchemes;
}
/*******************************************************************************
** Fluent setter for securitySchemes
*******************************************************************************/
public ApiInstanceMetaData withSecuritySchemes(Map<String, SecurityScheme> 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);
}
} }

View File

@ -40,7 +40,7 @@ public class OAuth2 extends SecurityScheme
*******************************************************************************/ *******************************************************************************/
public OAuth2() public OAuth2()
{ {
setType("oauth2"); setType(SecuritySchemeType.OAUTH2);
} }

View File

@ -22,48 +22,23 @@
package com.kingsrook.qqq.api.model.openapi; package com.kingsrook.qqq.api.model.openapi;
import com.fasterxml.jackson.annotation.JsonIgnore;
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
public class SecurityScheme public class SecurityScheme
{ {
private String type; private SecuritySchemeType type;
private String name;
private String in;
private String scheme; private String scheme;
private String bearerFormat; 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 ** Getter for scheme
*******************************************************************************/ *******************************************************************************/
@ -124,4 +99,108 @@ public class SecurityScheme
return (this); 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);
}
} }

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@ -39,6 +39,7 @@
allow-spec-file-download="true" allow-spec-file-download="true"
primary-color="{primaryColor}" primary-color="{primaryColor}"
sort-endpoints-by="method" sort-endpoints-by="method"
allow-authentication="true"
persist-auth="true" persist-auth="true"
render-style="focused" render-style="focused"
show-method-in-nav-bar="as-colored-block" show-method-in-nav-bar="as-colored-block"
@ -46,6 +47,7 @@
css-file="qqq-api-styles.css" css-file="qqq-api-styles.css"
css-classes="qqqApi" css-classes="qqqApi"
info-description-headings-in-navbar="true" info-description-headings-in-navbar="true"
show-curl-before-try="true"
> >
{navLogoImg} {navLogoImg}
<div slot="overview" id="otherVersions"> <div slot="overview" id="otherVersions">