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.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<GenerateO
.withSecuritySchemes(securitySchemes)
.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()
.withType("http")
.withScheme("basic"));
@ -258,13 +265,14 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
.withScheme("bearer")
.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()
.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<GenerateO
.withDescription("Successfully searched the " + tableLabel + " table (though may have found 0 records).")
.withContent(MapBuilder.of("application/json", new Content()
.withSchema(new Schema().withRef("#/components/schemas/" + tableApiName + "SearchResult")))))
.withSecurity(getSecurity(tableReadPermissionName));
.withSecurity(getSecurity(apiInstanceMetaData, tableReadPermissionName));
for(QFieldMetaData tableApiField : tableApiFields)
{
@ -486,7 +494,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
Method queryPost = new Method()
.withSummary("Search the " + tableLabel + " table by posting a QueryFilter object.")
.withTags(ListBuilder.of(tableLabel))
.withSecurity(getSecurity(tableReadPermissionName));
.withSecurity(getSecurity(apiInstanceMetaData, tableReadPermissionName));
if(queryByQueryStringEnabled)
{
@ -514,7 +522,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
.withDescription("Successfully got the requested " + tableLabel)
.withContent(MapBuilder.of("application/json", new Content()
.withSchema(new Schema().withRef("#/components/schemas/" + tableApiName)))))
.withSecurity(getSecurity(tableReadPermissionName));
.withSecurity(getSecurity(apiInstanceMetaData, tableReadPermissionName));
Method idPatch = new Method()
.withSummary("Update one " + tableLabel)
@ -536,7 +544,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
.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.NO_CONTENT.getCode(), new Response().withDescription("Successfully updated the requested " + tableLabel))
.withSecurity(getSecurity(tableUpdatePermissionName));
.withSecurity(getSecurity(apiInstanceMetaData, tableUpdatePermissionName));
Method idDelete = new Method()
.withSummary("Delete one " + tableLabel)
@ -553,7 +561,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
.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.NO_CONTENT.getCode(), new Response().withDescription("Successfully deleted the requested " + tableLabel))
.withSecurity(getSecurity(tableDeletePermissionName));
.withSecurity(getSecurity(apiInstanceMetaData, tableDeletePermissionName));
if(getEnabled || updateEnabled || deleteEnabled)
{
@ -584,7 +592,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
.withType(getFieldType(primaryKeyField))
.withExample("47")))))))
.withTags(ListBuilder.of(tableLabel))
.withSecurity(getSecurity(tableInsertPermissionName));
.withSecurity(getSecurity(apiInstanceMetaData, tableInsertPermissionName));
if(insertEnabled)
{
@ -609,7 +617,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
.withResponses(buildStandardErrorResponses(apiInstanceMetaData))
.withResponse(HttpStatus.MULTI_STATUS.getCode(), buildMultiStatusResponse(tableLabel, primaryKeyApiName, primaryKeyField, "post"))
.withTags(ListBuilder.of(tableLabel))
.withSecurity(getSecurity(tableInsertPermissionName));
.withSecurity(getSecurity(apiInstanceMetaData, tableInsertPermissionName));
Method bulkPatch = new Method()
.withSummary("Update multiple " + tableLabel + " records")
@ -631,7 +639,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
.withResponses(buildStandardErrorResponses(apiInstanceMetaData))
.withResponse(HttpStatus.MULTI_STATUS.getCode(), buildMultiStatusResponse(tableLabel, primaryKeyApiName, primaryKeyField, "patch"))
.withTags(ListBuilder.of(tableLabel))
.withSecurity(getSecurity(tableUpdatePermissionName));
.withSecurity(getSecurity(apiInstanceMetaData, tableUpdatePermissionName));
Method bulkDelete = new Method()
.withSummary("Delete multiple " + tableLabel + " records")
@ -648,7 +656,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
.withResponses(buildStandardErrorResponses(apiInstanceMetaData))
.withResponse(HttpStatus.MULTI_STATUS.getCode(), buildMultiStatusResponse(tableLabel, primaryKeyApiName, primaryKeyField, "delete"))
.withTags(ListBuilder.of(tableLabel))
.withSecurity(getSecurity(tableDeletePermissionName));
.withSecurity(getSecurity(apiInstanceMetaData, tableDeletePermissionName));
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(
MapBuilder.of("OAuth2", List.of(permissionName)),
MapBuilder.of("bearerAuth", List.of(permissionName)),
MapBuilder.of("basicAuth", List.of(permissionName))
);
List<Map<String, List<String>>> rs = new ArrayList<>();
for(Map.Entry<String, SecurityScheme> 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<GenerateO
private Response buildMultiStatusResponse(String tableLabel, String primaryKeyApiName, QFieldMetaData primaryKeyField, String method)
{
List<Object> 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<String, Schema> properties = new LinkedHashMap<>();
properties.put("statusCode", new Schema().withType("integer"));
@ -1125,12 +1134,12 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
private String getFieldType(QFieldType type)
{
return switch(type)
{
case STRING, DATE, TIME, DATE_TIME, TEXT, HTML, PASSWORD, BLOB -> "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<GenerateO
private String getFieldFormat(QFieldType type)
{
return switch(type)
{
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;
};
{
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;
};
}

View File

@ -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<ApiOperation> enabledOperations = new HashSet<>();
private Set<ApiOperation> disabledOperations = new HashSet<>();
private Map<String, SecurityScheme> securitySchemes = new LinkedHashMap<>();
private boolean includeErrorTooManyRequests = true;
@ -590,4 +595,50 @@ public class ApiInstanceMetaData implements ApiOperation.EnabledOperationsProvid
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()
{
setType("oauth2");
setType(SecuritySchemeType.OAUTH2);
}

View File

@ -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);
}
}

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"
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}
<div slot="overview" id="otherVersions">