cehckpoint - adding security to openapi spec

This commit is contained in:
2023-03-21 11:00:10 -05:00
parent 4a29898405
commit 90a7745246
12 changed files with 541 additions and 14 deletions

View File

@ -75,7 +75,20 @@ public class PermissionsHelper
warnAboutPermissionSubTypeForTables(permissionSubType);
QTableMetaData table = QContext.getQInstance().getTable(tableName);
commonCheckPermissionThrowing(getEffectivePermissionRules(table, QContext.getQInstance()), permissionSubType, table.getName(), actionInput);
commonCheckPermissionThrowing(getEffectivePermissionRules(table, QContext.getQInstance()), permissionSubType, table.getName());
}
/*******************************************************************************
**
*******************************************************************************/
public static String getTablePermissionName(String tableName, TablePermissionSubType permissionSubType)
{
QInstance qInstance = QContext.getQInstance();
QPermissionRules rules = getEffectivePermissionRules(qInstance.getTable(tableName), qInstance);
String permissionBaseName = getEffectivePermissionBaseName(rules, tableName);
return (getPermissionName(permissionBaseName, permissionSubType));
}
@ -181,7 +194,7 @@ public class PermissionsHelper
return;
}
commonCheckPermissionThrowing(effectivePermissionRules, PrivatePermissionSubType.HAS_ACCESS, process.getName(), actionInput);
commonCheckPermissionThrowing(effectivePermissionRules, PrivatePermissionSubType.HAS_ACCESS, process.getName());
}
@ -210,7 +223,7 @@ public class PermissionsHelper
public static void checkAppPermissionThrowing(AbstractActionInput actionInput, String appName) throws QPermissionDeniedException
{
QAppMetaData app = QContext.getQInstance().getApp(appName);
commonCheckPermissionThrowing(getEffectivePermissionRules(app, QContext.getQInstance()), PrivatePermissionSubType.HAS_ACCESS, app.getName(), actionInput);
commonCheckPermissionThrowing(getEffectivePermissionRules(app, QContext.getQInstance()), PrivatePermissionSubType.HAS_ACCESS, app.getName());
}
@ -239,7 +252,7 @@ public class PermissionsHelper
public static void checkReportPermissionThrowing(AbstractActionInput actionInput, String reportName) throws QPermissionDeniedException
{
QReportMetaData report = QContext.getQInstance().getReport(reportName);
commonCheckPermissionThrowing(getEffectivePermissionRules(report, QContext.getQInstance()), PrivatePermissionSubType.HAS_ACCESS, report.getName(), actionInput);
commonCheckPermissionThrowing(getEffectivePermissionRules(report, QContext.getQInstance()), PrivatePermissionSubType.HAS_ACCESS, report.getName());
}
@ -268,7 +281,7 @@ public class PermissionsHelper
public static void checkWidgetPermissionThrowing(AbstractActionInput actionInput, String widgetName) throws QPermissionDeniedException
{
QWidgetMetaDataInterface widget = QContext.getQInstance().getWidget(widgetName);
commonCheckPermissionThrowing(getEffectivePermissionRules(widget, QContext.getQInstance()), PrivatePermissionSubType.HAS_ACCESS, widget.getName(), actionInput);
commonCheckPermissionThrowing(getEffectivePermissionRules(widget, QContext.getQInstance()), PrivatePermissionSubType.HAS_ACCESS, widget.getName());
}
@ -524,7 +537,7 @@ public class PermissionsHelper
/*******************************************************************************
**
*******************************************************************************/
private static void commonCheckPermissionThrowing(QPermissionRules rules, PermissionSubType permissionSubType, String name, AbstractActionInput actionInput) throws QPermissionDeniedException
private static void commonCheckPermissionThrowing(QPermissionRules rules, PermissionSubType permissionSubType, String name) throws QPermissionDeniedException
{
PermissionSubType effectivePermissionSubType = getEffectivePermissionSubType(rules, permissionSubType);
String permissionBaseName = getEffectivePermissionBaseName(rules, name);

View File

@ -37,14 +37,19 @@ 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;
import com.kingsrook.qqq.api.model.openapi.Response;
import com.kingsrook.qqq.api.model.openapi.Schema;
import com.kingsrook.qqq.api.model.openapi.SecurityScheme;
import com.kingsrook.qqq.api.model.openapi.Server;
import com.kingsrook.qqq.api.model.openapi.Tag;
import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction;
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
@ -91,11 +96,22 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
openAPI.setTags(new ArrayList<>());
openAPI.setPaths(new LinkedHashMap<>());
LinkedHashMap<Integer, Response> componentResponses = new LinkedHashMap<>();
LinkedHashMap<String, Schema> componentSchemas = new LinkedHashMap<>();
LinkedHashMap<Integer, Response> componentResponses = new LinkedHashMap<>();
LinkedHashMap<String, Schema> componentSchemas = new LinkedHashMap<>();
LinkedHashMap<String, SecurityScheme> securitySchemes = new LinkedHashMap<>();
openAPI.setComponents(new Components()
.withSchemas(componentSchemas)
.withResponses(componentResponses)
.withSecuritySchemes(securitySchemes)
);
LinkedHashMap<String, String> scopes = new LinkedHashMap<>();
securitySchemes.put("OAuth2", new OAuth2()
.withFlows(MapBuilder.of("authorizationCode", new OAuth2Flow()
.withAuthorizationUrl("https://nutrifresh-one-development.us.auth0.com/authorize")
.withTokenUrl("https://nutrifresh-one-development.us.auth0.com/oauth/token")
.withScopes(scopes)
))
);
componentSchemas.put("baseSearchResultFields", new Schema()
@ -131,6 +147,12 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
List<? extends QFieldMetaData> tableApiFields = new GetTableApiFieldsAction().execute(new GetTableApiFieldsInput().withTableName(tableName).withVersion(version)).getFields();
String tableReadPermissionName = PermissionsHelper.getTablePermissionName(tableName, TablePermissionSubType.READ);
if(StringUtils.hasContent(tableReadPermissionName))
{
scopes.put(tableReadPermissionName, "Permission to read the " + tableLabel + " table");
}
////////////////////////
// tag for this table //
////////////////////////
@ -224,7 +246,9 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
.withContent(MapBuilder.of("application/json", new Content()
.withSchema(new Schema().withRef("#/components/schemas/" + tableName + "SearchResult"))
))
);
).withSecurity(ListBuilder.of(MapBuilder.of(
"OAuth2", List.of(tableReadPermissionName)
)));
for(QFieldMetaData tableApiField : tableApiFields)
{

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.api.model.metadata;
import java.util.List;
import com.kingsrook.qqq.api.ApiMiddlewareType;
import com.kingsrook.qqq.api.model.APIVersion;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.QMiddlewareInstanceMetaData;
@ -40,6 +41,17 @@ public class ApiInstanceMetaData extends QMiddlewareInstanceMetaData
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ApiInstanceMetaData()
{
setType(ApiMiddlewareType.NAME);
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -30,9 +30,10 @@ import java.util.Map;
*******************************************************************************/
public class Components
{
private Map<String, Example> examples;
private Map<String, Schema> schemas;
private Map<Integer, Response> responses;
private Map<String, Example> examples;
private Map<String, Schema> schemas;
private Map<Integer, Response> responses;
private Map<String, SecurityScheme> securitySchemes;
@ -127,4 +128,35 @@ public class Components
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 Components withSecuritySchemes(Map<String, SecurityScheme> securitySchemes)
{
this.securitySchemes = securitySchemes;
return (this);
}
}

View File

@ -41,6 +41,8 @@ public class Method
private List<Parameter> parameters;
private Map<Integer, Response> responses;
private List<Map<String, List<String>>> security;
/*******************************************************************************
@ -303,4 +305,36 @@ public class Method
this.responses.put(code, response);
return (this);
}
/*******************************************************************************
** Getter for security
*******************************************************************************/
public List<Map<String, List<String>>> getSecurity()
{
return (this.security);
}
/*******************************************************************************
** Setter for security
*******************************************************************************/
public void setSecurity(List<Map<String, List<String>>> security)
{
this.security = security;
}
/*******************************************************************************
** Fluent setter for security
*******************************************************************************/
public Method withSecurity(List<Map<String, List<String>>> security)
{
this.security = security;
return (this);
}
}

View File

@ -0,0 +1,77 @@
/*
* 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;
import java.util.Map;
/*******************************************************************************
**
*******************************************************************************/
public class OAuth2 extends SecurityScheme
{
private Map<String, OAuth2Flow> flows;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public OAuth2()
{
setType("oauth2");
}
/*******************************************************************************
** Getter for flows
*******************************************************************************/
public Map<String, OAuth2Flow> getFlows()
{
return (this.flows);
}
/*******************************************************************************
** Setter for flows
*******************************************************************************/
public void setFlows(Map<String, OAuth2Flow> flows)
{
this.flows = flows;
}
/*******************************************************************************
** Fluent setter for flows
*******************************************************************************/
public OAuth2 withFlows(Map<String, OAuth2Flow> flows)
{
this.flows = flows;
return (this);
}
}

View File

@ -0,0 +1,130 @@
/*
* 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;
import java.util.Map;
/*******************************************************************************
**
*******************************************************************************/
public class OAuth2Flow
{
private String authorizationUrl;
private String tokenUrl;
private Map<String, String> scopes;
/*******************************************************************************
** Getter for authorizationUrl
*******************************************************************************/
public String getAuthorizationUrl()
{
return (this.authorizationUrl);
}
/*******************************************************************************
** Setter for authorizationUrl
*******************************************************************************/
public void setAuthorizationUrl(String authorizationUrl)
{
this.authorizationUrl = authorizationUrl;
}
/*******************************************************************************
** Fluent setter for authorizationUrl
*******************************************************************************/
public OAuth2Flow withAuthorizationUrl(String authorizationUrl)
{
this.authorizationUrl = authorizationUrl;
return (this);
}
/*******************************************************************************
** Getter for tokenUrl
*******************************************************************************/
public String getTokenUrl()
{
return (this.tokenUrl);
}
/*******************************************************************************
** Setter for tokenUrl
*******************************************************************************/
public void setTokenUrl(String tokenUrl)
{
this.tokenUrl = tokenUrl;
}
/*******************************************************************************
** Fluent setter for tokenUrl
*******************************************************************************/
public OAuth2Flow withTokenUrl(String tokenUrl)
{
this.tokenUrl = tokenUrl;
return (this);
}
/*******************************************************************************
** Getter for scopes
*******************************************************************************/
public Map<String, String> getScopes()
{
return (this.scopes);
}
/*******************************************************************************
** Setter for scopes
*******************************************************************************/
public void setScopes(Map<String, String> scopes)
{
this.scopes = scopes;
}
/*******************************************************************************
** Fluent setter for scopes
*******************************************************************************/
public OAuth2Flow withScopes(Map<String, String> scopes)
{
this.scopes = scopes;
return (this);
}
}

View File

@ -0,0 +1,63 @@
/*
* 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 class SecurityScheme
{
private String type;
/*******************************************************************************
** 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);
}
}

View File

@ -26,6 +26,7 @@ import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@ -39,6 +40,18 @@ public class BaseTest
/*******************************************************************************
**
*******************************************************************************/
@BeforeEach
@AfterEach
void baseBeforeAndAfterEach()
{
MemoryRecordStore.fullReset();
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -22,6 +22,10 @@
package com.kingsrook.qqq.api;
import java.util.List;
import com.kingsrook.qqq.api.model.APIVersion;
import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData;
import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
@ -39,6 +43,8 @@ public class TestUtils
public static final String MEMORY_BACKEND_NAME = "memory";
private static final String TABLE_NAME_PERSON = "person";
private static final String API_VERSION = "2023.Q1";
/*******************************************************************************
@ -51,6 +57,11 @@ public class TestUtils
qInstance.addBackend(defineMemoryBackend());
qInstance.addTable(defineTablePerson());
qInstance.withMiddlewareMetaData(new ApiInstanceMetaData()
.withCurrentVersion(new APIVersion(API_VERSION))
.withSupportedVersions(List.of(new APIVersion(API_VERSION)))
);
return (qInstance);
}
@ -77,6 +88,7 @@ public class TestUtils
.withName(TABLE_NAME_PERSON)
.withLabel("Person")
.withBackendName(MEMORY_BACKEND_NAME)
.withMiddlewareMetaData(new ApiTableMetaData().withInitialVersion(API_VERSION))
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
.withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false))

View File

@ -0,0 +1,107 @@
/*
* 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.javalin;
import com.kingsrook.qqq.api.TestUtils;
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.javalin.QJavalinImplementation;
import kong.unirest.HttpResponse;
import kong.unirest.Unirest;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/*******************************************************************************
** Unit test for QJavalinApiHandler
*******************************************************************************/
class QJavalinApiHandlerTest
{
private static final int PORT = 6263;
protected static final String BASE_URL = "http://localhost:" + PORT;
private static final String VERSION = "2023.Q1";
protected static QJavalinImplementation qJavalinImplementation;
/*******************************************************************************
**
*******************************************************************************/
@BeforeAll
static void beforeAll() throws QInstanceValidationException
{
QInstance qInstance = TestUtils.defineInstance();
qJavalinImplementation = new QJavalinImplementation(qInstance);
qJavalinImplementation.startJavalinServer(PORT);
qJavalinImplementation.getJavalinService().routes(new QJavalinApiHandler(qInstance).getRoutes());
}
/*******************************************************************************
** Before the class (all) runs, start a javalin server.
**
*******************************************************************************/
@AfterAll
public static void afterAll()
{
qJavalinImplementation.stopJavalinServer();
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testSpec()
{
HttpResponse<String> response = Unirest.get(BASE_URL + "/api/" + VERSION + "/openapi.yaml").asString();
System.out.println(response.getBody());
assertThat(response.getBody())
.contains("""
title: "QQQ API"
""")
.contains("""
/person/query:
""")
;
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testQuery()
{
HttpResponse<String> response = Unirest.get(BASE_URL + "/api/" + VERSION + "/person/query").asString();
System.out.println(response.getBody());
}
}

View File

@ -208,7 +208,7 @@ public class QJavalinImplementation
/*******************************************************************************
**
*******************************************************************************/
void startJavalinServer(int port)
public void startJavalinServer(int port)
{
// todo port from arg
// todo base path from arg? - and then potentially multiple instances too (chosen based on the root path??)
@ -221,6 +221,16 @@ public class QJavalinImplementation
/*******************************************************************************
**
*******************************************************************************/
public Javalin getJavalinService()
{
return (service);
}
/*******************************************************************************
**
*******************************************************************************/
@ -277,7 +287,7 @@ public class QJavalinImplementation
/*******************************************************************************
**
*******************************************************************************/
void stopJavalinServer()
public void stopJavalinServer()
{
service.stop();
}