Skip to content

Commit

Permalink
CAMEL-16606 Allow specifying security requirement(s) applicable to en…
Browse files Browse the repository at this point in the history
…tire API

Calls to RestDefinition.security(String key [, String scopes]) without a verb adds the security scheme specified by key
as a security requirement for all rest calls.
  • Loading branch information
electrosaur committed May 14, 2021
1 parent 4299e11 commit ad65210
Show file tree
Hide file tree
Showing 16 changed files with 379 additions and 6 deletions.
Expand Up @@ -158,6 +158,7 @@ secureRandomParameters
secureXML
security
securityDefinitions
securityRequirements
serviceCall
serviceCallConfiguration
serviceChooserConfiguration
Expand Down
Expand Up @@ -21,6 +21,7 @@
"enableCORS": { "kind": "attribute", "displayName": "Enable CORS", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "Whether to enable CORS headers in the HTTP response. This option will override what may be configured on a parent level The default value is false." },
"apiDocs": { "kind": "attribute", "displayName": "Api Docs", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "Whether to include or exclude the VerbDefinition in API documentation. This option will override what may be configured on a parent level The default value is true." },
"securityDefinitions": { "kind": "element", "displayName": "Security Definitions", "required": false, "type": "object", "javaType": "org.apache.camel.model.rest.RestSecuritiesDefinition", "deprecated": false, "autowired": false, "secret": false, "description": "Sets the security definitions such as Basic, OAuth2 etc." },
"securityRequirements": { "kind": "element", "displayName": "Security Requirements", "required": false, "type": "object", "javaType": "org.apache.camel.model.rest.RestSecuritiesRequirement", "deprecated": false, "autowired": false, "secret": false, "description": "Sets the security requirement(s) for all endpoints." },
"id": { "kind": "attribute", "displayName": "Id", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "Sets the id of this node" },
"description": { "kind": "element", "displayName": "Description", "required": false, "type": "object", "javaType": "org.apache.camel.model.DescriptionDefinition", "deprecated": false, "autowired": false, "secret": false, "description": "Sets the description of this node" }
}
Expand Down
@@ -0,0 +1,16 @@
{
"model": {
"kind": "model",
"name": "securityRequirements",
"title": "Security Requirements",
"description": "To configure global rest security requirements.",
"deprecated": false,
"label": "rest,security",
"javaType": "org.apache.camel.model.rest.RestSecuritiesRequirement",
"input": false,
"output": false
},
"properties": {
"securityRequirements": { "kind": "element", "displayName": "Security Requirements", "required": true, "type": "array", "javaType": "java.util.List<org.apache.camel.model.rest.SecurityDefinition>", "oneOf": [ "securityRequirement" ], "deprecated": false, "autowired": false, "secret": false }
}
}
Expand Up @@ -1380,6 +1380,14 @@ To configure rest security definitions.
</xs:annotation>
</xs:element>

<xs:element name="securityRequirements" type="tns:restSecuritiesRequirement">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[
To configure global rest security requirements.
]]></xs:documentation>
</xs:annotation>
</xs:element>

<xs:element name="serviceCall" type="tns:serviceCallDefinition">
<xs:annotation>
<xs:documentation xml:lang="en"><![CDATA[
Expand Down Expand Up @@ -14016,6 +14024,7 @@ to refer to an existing data format instance.
<xs:extension base="tns:optionalIdentifiedDefinition">
<xs:sequence>
<xs:element minOccurs="0" ref="tns:securityDefinitions"/>
<xs:element minOccurs="0" ref="tns:securityRequirements"/>
<xs:choice maxOccurs="unbounded" minOccurs="0">
<xs:element ref="tns:verb"/>
<xs:element ref="tns:delete"/>
Expand Down Expand Up @@ -14213,6 +14222,12 @@ password, application or accessCode.
</xs:complexContent>
</xs:complexType>

<xs:complexType name="restSecuritiesRequirement">
<xs:sequence>
<xs:element maxOccurs="unbounded" minOccurs="0" name="securityRequirement" type="tns:securityDefinition"/>
</xs:sequence>
</xs:complexType>

<xs:complexType name="restsDefinition">
<xs:complexContent>
<xs:extension base="tns:optionalIdentifiedDefinition">
Expand Down
Expand Up @@ -20,6 +20,8 @@
import java.lang.invoke.MethodType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
Expand Down Expand Up @@ -48,6 +50,7 @@
import io.apicurio.datamodels.openapi.models.OasParameter;
import io.apicurio.datamodels.openapi.models.OasPathItem;
import io.apicurio.datamodels.openapi.models.OasSchema;
import io.apicurio.datamodels.openapi.models.OasSecurityRequirement;
import io.apicurio.datamodels.openapi.v2.models.Oas20Document;
import io.apicurio.datamodels.openapi.v2.models.Oas20Header;
import io.apicurio.datamodels.openapi.v2.models.Oas20Items;
Expand Down Expand Up @@ -76,6 +79,7 @@
import org.apache.camel.model.rest.RestParamType;
import org.apache.camel.model.rest.RestPropertyDefinition;
import org.apache.camel.model.rest.RestSecuritiesDefinition;
import org.apache.camel.model.rest.RestSecuritiesRequirement;
import org.apache.camel.model.rest.RestSecurityApiKey;
import org.apache.camel.model.rest.RestSecurityBasicAuth;
import org.apache.camel.model.rest.RestSecurityDefinition;
Expand Down Expand Up @@ -249,6 +253,23 @@ private void parse(
}

doParseVerbs(camelContext, openApi, rest, camelContextId, verbs, pathAsTag);

// setup root security node if necessary
RestSecuritiesRequirement securitiesRequirement = rest.getSecurityRequirements();
if (securitiesRequirement != null) {
Collection<SecurityDefinition> securityRequirements = securitiesRequirement.securityRequirements();
securityRequirements.forEach(requirement -> {
OasSecurityRequirement oasRequirement = openApi.createSecurityRequirement();
List<String> scopes;
if (requirement.getScopes() == null || requirement.getScopes().trim().isEmpty()) {
scopes = Collections.emptyList();
} else {
scopes = Arrays.asList(requirement.getScopes().trim().split("\\s*,\\s*"));
}
oasRequirement.addSecurityRequirementItem(requirement.getKey(), scopes);
openApi.addSecurityRequirement(oasRequirement);
});
}
}

private void parseOas30(Oas30Document openApi, RestDefinition rest, String pathAsTag) {
Expand Down
@@ -0,0 +1,123 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.camel.openapi;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import io.apicurio.datamodels.Library;
import io.apicurio.datamodels.openapi.models.OasDocument;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.impl.engine.DefaultClassResolver;
import org.apache.camel.test.junit5.CamelTestSupport;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class RestOpenApiModelApiSecurityRequirementsTest extends CamelTestSupport {

private final Logger log = LoggerFactory.getLogger(getClass());

@Override
protected RouteBuilder createRouteBuilder() throws Exception {
return new RouteBuilder() {
@Override
public void configure() {
rest()
.securityDefinitions()
.oauth2("petstore_auth")
.authorizationUrl("https://petstore.swagger.io/oauth/dialog")
.end()
.apiKey("api_key")
.withHeader("myHeader").end()
.end()
.security("petstore_auth", "read, write")
.security("api_key");
}
};
}

@Test
public void testReaderRead() throws Exception {
BeanConfig config = new BeanConfig();
config.setHost("localhost:8080");
config.setSchemes(new String[] { "http" });
config.setBasePath("/api");
config.setTitle("Camel User store");
config.setLicense("Apache 2.0");
config.setLicenseUrl("https://www.apache.org/licenses/LICENSE-2.0.html");
config.setVersion("2.0");
RestOpenApiReader reader = new RestOpenApiReader();

OasDocument openApi = reader.read(context, context.getRestDefinitions(), null, config, context.getName(),
new DefaultClassResolver());
assertNotNull(openApi);

ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
Object dump = Library.writeNode(openApi);
String json = mapper.writeValueAsString(dump);

log.info(json);

assertTrue(json.contains("\"securityDefinitions\" : {"));
assertTrue(json.contains("\"type\" : \"oauth2\""));
assertTrue(json.contains("\"authorizationUrl\" : \"https://petstore.swagger.io/oauth/dialog\""));
assertTrue(json.contains("\"flow\" : \"implicit\""));
assertTrue(json.contains("\"type\" : \"apiKey\","));
assertTrue(json.contains("\"security\" : [ {"));
assertTrue(json.contains("\"petstore_auth\" : [ \"read\", \"write\" ]"));
assertTrue(json.contains("\"api_key\" : [ ]"));
}

@Test
public void testReaderReadV3() throws Exception {
BeanConfig config = new BeanConfig();
config.setHost("localhost:8080");
config.setSchemes(new String[] { "http" });
config.setBasePath("/api");
config.setTitle("Camel User store");
config.setLicense("Apache 2.0");
config.setLicenseUrl("https://www.apache.org/licenses/LICENSE-2.0.html");
RestOpenApiReader reader = new RestOpenApiReader();

OasDocument openApi = reader.read(context, context.getRestDefinitions(), null, config, context.getName(),
new DefaultClassResolver());
assertNotNull(openApi);

ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
Object dump = Library.writeNode(openApi);
String json = mapper.writeValueAsString(dump);

log.info(json);

assertTrue(json.contains("securitySchemes"));
assertTrue(json.contains("\"type\" : \"oauth2\""));
assertTrue(json.contains("\"authorizationUrl\" : \"https://petstore.swagger.io/oauth/dialog\""));
assertTrue(json.contains("\"flows\" : {"));
assertTrue(json.contains("\"implicit\""));
assertTrue(json.contains("\"security\" : [ {"));
assertTrue(json.contains("\"petstore_auth\" : [ \"read\", \"write\" ]"));
assertTrue(json.contains("\"api_key\" : [ ]"));
}
}
Expand Up @@ -147,6 +147,7 @@ script
secureXML
security
securityDefinitions
securityRequirements
serviceCall
serviceCallConfiguration
serviceChooserConfiguration
Expand Down
Expand Up @@ -17,6 +17,7 @@ RestOperationResponseMsgDefinition
RestParamType
RestPropertyDefinition
RestSecuritiesDefinition
RestSecuritiesRequirement
RestSecurityApiKey
RestSecurityBasicAuth
RestSecurityOAuth2
Expand Down
Expand Up @@ -21,6 +21,7 @@
"enableCORS": { "kind": "attribute", "displayName": "Enable CORS", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "Whether to enable CORS headers in the HTTP response. This option will override what may be configured on a parent level The default value is false." },
"apiDocs": { "kind": "attribute", "displayName": "Api Docs", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "Whether to include or exclude the VerbDefinition in API documentation. This option will override what may be configured on a parent level The default value is true." },
"securityDefinitions": { "kind": "element", "displayName": "Security Definitions", "required": false, "type": "object", "javaType": "org.apache.camel.model.rest.RestSecuritiesDefinition", "deprecated": false, "autowired": false, "secret": false, "description": "Sets the security definitions such as Basic, OAuth2 etc." },
"securityRequirements": { "kind": "element", "displayName": "Security Requirements", "required": false, "type": "object", "javaType": "org.apache.camel.model.rest.RestSecuritiesRequirement", "deprecated": false, "autowired": false, "secret": false, "description": "Sets the security requirement(s) for all endpoints." },
"id": { "kind": "attribute", "displayName": "Id", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "Sets the id of this node" },
"description": { "kind": "element", "displayName": "Description", "required": false, "type": "object", "javaType": "org.apache.camel.model.DescriptionDefinition", "deprecated": false, "autowired": false, "secret": false, "description": "Sets the description of this node" }
}
Expand Down
@@ -0,0 +1,16 @@
{
"model": {
"kind": "model",
"name": "securityRequirements",
"title": "Security Requirements",
"description": "To configure global rest security requirements.",
"deprecated": false,
"label": "rest,security",
"javaType": "org.apache.camel.model.rest.RestSecuritiesRequirement",
"input": false,
"output": false
},
"properties": {
"securityRequirements": { "kind": "element", "displayName": "Security Requirements", "required": true, "type": "array", "javaType": "java.util.List<org.apache.camel.model.rest.SecurityDefinition>", "oneOf": [ "securityRequirement" ], "deprecated": false, "autowired": false, "secret": false }
}
}
Expand Up @@ -87,6 +87,9 @@ public class RestDefinition extends OptionalIdentifiedDefinition<RestDefinition>
@XmlElement(name = "securityDefinitions") // use the name swagger uses
private RestSecuritiesDefinition securityDefinitions;

@XmlElement(name = "securityRequirements") // use the name swagger/OpenAPI uses
private RestSecuritiesRequirement securityRequirements;

@XmlElementRef
private List<VerbDefinition> verbs = new ArrayList<>();

Expand Down Expand Up @@ -174,6 +177,17 @@ public void setSecurityDefinitions(RestSecuritiesDefinition securityDefinitions)
this.securityDefinitions = securityDefinitions;
}

public RestSecuritiesRequirement getSecurityRequirements() {
return securityRequirements;
}

/**
* Sets the security requirement(s) for all endpoints.
*/
public void setSecurityRequirements(RestSecuritiesRequirement securityRequirements) {
this.securityRequirements = securityRequirements;
}

/**
* The HTTP verbs this REST service accepts and uses
*/
Expand Down Expand Up @@ -562,14 +576,18 @@ public RestDefinition security(String key) {
public RestDefinition security(String key, String scopes) {
// add to last verb
if (getVerbs().isEmpty()) {
throw new IllegalArgumentException("Must add verb first, such as get/post/delete");
if (securityRequirements == null) {
securityRequirements = new RestSecuritiesRequirement();
}
securityRequirements.securityRequirement(key, scopes);
} else {
VerbDefinition verb = getVerbs().get(getVerbs().size() - 1);
SecurityDefinition sd = new SecurityDefinition();
sd.setKey(key);
sd.setScopes(scopes);
verb.getSecurity().add(sd);
}

VerbDefinition verb = getVerbs().get(getVerbs().size() - 1);
SecurityDefinition sd = new SecurityDefinition();
sd.setKey(key);
sd.setScopes(scopes);
verb.getSecurity().add(sd);
return this;
}

Expand Down

0 comments on commit ad65210

Please sign in to comment.