diff --git a/src/main/java/org/commonjava/indy/service/security/common/SecurityConstraint.java b/src/main/java/org/commonjava/indy/service/security/common/SecurityConstraint.java index a23a95f..c3cef90 100644 --- a/src/main/java/org/commonjava/indy/service/security/common/SecurityConstraint.java +++ b/src/main/java/org/commonjava/indy/service/security/common/SecurityConstraint.java @@ -15,17 +15,24 @@ */ package org.commonjava.indy.service.security.common; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + import java.util.Arrays; import java.util.List; +@JsonDeserialize( using = SecurityConstraintDeSerializer.class ) @SuppressWarnings( "unused" ) public class SecurityConstraint { + @JsonProperty( "urlPattern" ) private String urlPattern; + @JsonProperty( "roles" ) private List roles; + @JsonProperty( "methods" ) private List methods; public SecurityConstraint( String urlPattern, List roles, List methods ) diff --git a/src/main/java/org/commonjava/indy/service/security/common/SecurityConstraintDeSerializer.java b/src/main/java/org/commonjava/indy/service/security/common/SecurityConstraintDeSerializer.java new file mode 100644 index 0000000..ea09cc7 --- /dev/null +++ b/src/main/java/org/commonjava/indy/service/security/common/SecurityConstraintDeSerializer.java @@ -0,0 +1,113 @@ +/** + * Copyright (C) 2023 Red Hat, Inc. + * + * Licensed 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.commonjava.indy.service.security.common; + +import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.dataformat.yaml.JacksonYAMLParseException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import static com.fasterxml.jackson.databind.node.JsonNodeType.STRING; + +public class SecurityConstraintDeSerializer + extends StdDeserializer +{ + public SecurityConstraintDeSerializer() + { + this( null ); + } + + public SecurityConstraintDeSerializer( Class vc ) + { + super( vc ); + } + + @Override + public SecurityConstraint deserialize( JsonParser p, DeserializationContext ctxt ) + throws IOException, JacksonException + { + JsonNode node = p.getCodec().readTree( p ); + JsonNode rolesNode = node.get( "roles" ); + if ( rolesNode == null ) + { + rolesNode = node.get( "role" ); + if ( rolesNode == null ) + { + throw new JacksonYAMLParseException( p, "\"roles\" should not be empty!", null ); + } + } + List roles = handleVariableNode( p, rolesNode ); + + JsonNode urlPatNode = node.get( "urlPattern" ); + if ( urlPatNode == null ) + { + throw new JacksonYAMLParseException( p, "\"urlPattern\" should not be empty!", null ); + } + final String urlPattern; + if ( urlPatNode.getNodeType() == STRING ) + { + urlPattern = urlPatNode.textValue(); + } + else + { + throw new JacksonYAMLParseException( p, "\"urlPattern\" should be string type!", null ); + } + + JsonNode methodsNode = node.get( "methods" ); + if ( methodsNode == null ) + { + throw new JacksonYAMLParseException( p, "\"methods\" should not be empty!", null ); + } + List methods = handleVariableNode( p, methodsNode ); + + return new SecurityConstraint( urlPattern, roles, methods ); + } + + private List handleVariableNode( JsonParser p, JsonNode node ) + throws JacksonException + { + List result = new ArrayList<>(); + switch ( node.getNodeType() ) + { + case ARRAY: + for ( JsonNode child : node ) + { + if ( child.getNodeType() == STRING ) + { + final String value = child.textValue(); + result.add( value ); + } + } + break; + case STRING: + final String[] resultStr = node.textValue().split( "," ); + for ( String rt : resultStr ) + { + result.add( rt.trim() ); + } + break; + default: + throw new JacksonYAMLParseException( p, "\"roles\" should be string or list type!", null ); + } + return result; + } +} diff --git a/src/main/java/org/commonjava/indy/service/security/common/SecurityConstraintProvider.java b/src/main/java/org/commonjava/indy/service/security/common/SecurityConstraintProvider.java index 05c9deb..b591c3b 100644 --- a/src/main/java/org/commonjava/indy/service/security/common/SecurityConstraintProvider.java +++ b/src/main/java/org/commonjava/indy/service/security/common/SecurityConstraintProvider.java @@ -70,8 +70,7 @@ public void init() final File constraintFile = new File( loc ); if ( !constraintFile.isFile() ) { - logger.warn( "Cannot load security constraints: {}, will try to load from classpath.", - constraintFile ); + logger.warn( "Cannot load security constraints: {}, will try to load from classpath.", constraintFile ); } else { @@ -119,8 +118,7 @@ public void init() private void parseBindings( InputStream input ) throws IOException { - final ObjectMapper mapper = new ObjectMapper( new YAMLFactory() ); - mapper.findAndRegisterModules(); + final ObjectMapper mapper = new ObjectMapper( new YAMLFactory() ).findAndRegisterModules(); constraintSet = mapper.readValue( input, SecurityBindings.class ); if ( constraintSet != null ) { diff --git a/src/test/java/org/commonjava/indy/service/security/common/SecurityConstraintTest.java b/src/test/java/org/commonjava/indy/service/security/common/SecurityConstraintTest.java new file mode 100644 index 0000000..f99e99d --- /dev/null +++ b/src/test/java/org/commonjava/indy/service/security/common/SecurityConstraintTest.java @@ -0,0 +1,114 @@ +/** + * Copyright (C) 2023 Red Hat, Inc. + * + * Licensed 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.commonjava.indy.service.security.common; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class SecurityConstraintTest +{ + private final ObjectMapper mapper = new ObjectMapper( new YAMLFactory() ).findAndRegisterModules(); + + @Test + public void testDeSerilizationArrayType() + throws Exception + { + assertContraints( "security-bindings.yaml" ); + } + + @Test + public void testDeSerilizationStringType() + throws Exception + { + assertContraints( "security-bindings-string.yaml" ); + } + + @Test + public void testDeSerilizationHyrbidType() + throws Exception + { + assertContraints( "security-bindings-hybrid.yaml" ); + } + + @Test + public void testDeSerilizationHyrbidType2() + throws Exception + { + assertContraints( "security-bindings-hybrid2.yaml" ); + } + + @Test + public void testDeSerilizationOld() + throws Exception + { + assertContraints( "security-bindings-old.yaml" ); + } + + private void assertContraints( final String inputFile ) + throws IOException + { + try (InputStream input = this.getClass().getClassLoader().getResourceAsStream( inputFile )) + { + SecurityBindings constraintSet = mapper.readValue( input, SecurityBindings.class ); + if ( constraintSet != null ) + { + List constraints = constraintSet.getConstraints(); + assertThat( constraints.size(), is( 3 ) ); + for ( SecurityConstraint constraint : constraints ) + { + switch ( constraint.getUrlPattern() ) + { + case "/api/admin/.*": + assertContraintValues( constraint.getMethods(), 3, "POST", "PUT", "DELETE" ); + assertContraintValues( constraint.getRoles(), 1, "admin" ); + break; + case "/api/.*": + assertContraintValues( constraint.getMethods(), 3, "POST", "PUT", "DELETE" ); + assertContraintValues( constraint.getRoles(), 1, "user" ); + break; + case "/api/admin/stores/.*": + assertContraintValues( constraint.getMethods(), 3, "POST", "PUT", "DELETE" ); + assertContraintValues( constraint.getRoles(), 2, "admin", "power-user" ); + break; + default: + Assertions.fail( + String.format( "%s pattern should not exist!", constraint.getUrlPattern() ) ); + } + } + } + } + } + + private void assertContraintValues( List list, int expectSize, String... expectedContents ) + { + assertThat( list.size(), is( expectSize ) ); + for ( String content : expectedContents ) + { + assertTrue( list.contains( content ) ); + } + } + +} diff --git a/src/test/resources/security-bindings-hybrid.yaml b/src/test/resources/security-bindings-hybrid.yaml new file mode 100644 index 0000000..54c8eea --- /dev/null +++ b/src/test/resources/security-bindings-hybrid.yaml @@ -0,0 +1,20 @@ +constraints: + - urlPattern: "/api/admin/.*" + roles: admin + methods: + - POST + - PUT + - DELETE + - urlPattern: "/api/.*" + roles: user + methods: + - POST + - PUT + - DELETE + - urlPattern: "/api/admin/stores/.*" + roles: power-user,admin + methods: + - POST + - PUT + - DELETE + diff --git a/src/test/resources/security-bindings-hybrid2.yaml b/src/test/resources/security-bindings-hybrid2.yaml new file mode 100644 index 0000000..8b2db88 --- /dev/null +++ b/src/test/resources/security-bindings-hybrid2.yaml @@ -0,0 +1,22 @@ +constraints: + - urlPattern: "/api/admin/.*" + roles: admin + methods: + - POST + - PUT + - DELETE + - urlPattern: "/api/.*" + roles: user + methods: + - POST + - PUT + - DELETE + - urlPattern: "/api/admin/stores/.*" + roles: + - power-user + - admin + methods: + - POST + - PUT + - DELETE + diff --git a/src/test/resources/security-bindings-old.yaml b/src/test/resources/security-bindings-old.yaml new file mode 100644 index 0000000..289fb4f --- /dev/null +++ b/src/test/resources/security-bindings-old.yaml @@ -0,0 +1,22 @@ +constraints: + - urlPattern: "/api/admin/.*" + role: admin + methods: + - POST + - PUT + - DELETE + - urlPattern: "/api/.*" + role: user + methods: + - POST + - PUT + - DELETE + - urlPattern: "/api/admin/stores/.*" + role: + - power-user + - admin + methods: + - POST + - PUT + - DELETE + diff --git a/src/test/resources/security-bindings-string.yaml b/src/test/resources/security-bindings-string.yaml new file mode 100644 index 0000000..d0a2ede --- /dev/null +++ b/src/test/resources/security-bindings-string.yaml @@ -0,0 +1,11 @@ +constraints: + - urlPattern: "/api/admin/.*" + roles: admin + methods: POST,PUT,DELETE + - urlPattern: "/api/.*" + roles: user + methods: POST,PUT,DELETE + - urlPattern: "/api/admin/stores/.*" + roles: power-user,admin + methods: POST,PUT,DELETE +