From 10ec6c760cab4249df4dd4894e4a19bd5dd0b015 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Drumi=C5=84ski?= Date: Mon, 19 Aug 2024 14:16:16 +0200 Subject: [PATCH] Introduction of Glob Patterns in extension of API related to URL paths (#417) * Extended API for the 'paths' field. --------- Co-authored-by: radoslaw.chrzanowski Co-authored-by: jan.kozlowski --- CHANGELOG.md | 4 ++ .../envoycontrol/groups/NodeMetadata.kt | 31 +++++++-- .../listeners/filters/JwtFilterFactory.kt | 66 +++++++++++++++---- .../filters/RBACFilterPermissions.kt | 27 +++++++- .../envoycontrol/groups/NodeMetadataTest.kt | 8 +-- .../listeners/filters/JwtFilterFactoryTest.kt | 4 +- .../filters/rbac/RBACFilterFactoryJwtTest.kt | 8 ++- .../filters/rbac/RBACFilterFactoryTest.kt | 55 +++++++++++----- .../IncomingPermissionsPathMatchingTest.kt | 43 ++++++++++++ 9 files changed, 199 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfb409cae..d99b5f825 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). ### Changed - Add JWT failure reason to metadata and use it in jwt-status field on denied requests +## [0.21.0] +### Changed +- Added `paths` field in API to support Glob Patterns + ## [0.20.15] ### Changed - Java-control-plane update to 1.0.45 diff --git a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/groups/NodeMetadata.kt b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/groups/NodeMetadata.kt index cd35301b3..c3e57573d 100644 --- a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/groups/NodeMetadata.kt +++ b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/groups/NodeMetadata.kt @@ -398,9 +398,11 @@ fun Value.toIncomingEndpoint(properties: SnapshotProperties): IncomingEndpoint { val pathPrefix = this.field("pathPrefix")?.stringValue val path = this.field("path")?.stringValue val pathRegex = this.field("pathRegex")?.stringValue + val paths = this.field("paths")?.list().orEmpty().map { it.stringValue }.toSet() - if (isMoreThanOnePropertyDefined(path, pathPrefix, pathRegex)) { - throw NodeMetadataValidationException("Precisely one of 'path', 'pathPrefix' or 'pathRegex' field is allowed") + if (isMoreThanOnePropertyDefined(paths, path, pathPrefix, pathRegex)) { + throw NodeMetadataValidationException( + "Precisely one of 'paths', 'path', 'pathPrefix' or 'pathRegex' field is allowed") } val methods = this.field("methods")?.list().orEmpty().map { it.stringValue }.toSet() @@ -409,16 +411,20 @@ fun Value.toIncomingEndpoint(properties: SnapshotProperties): IncomingEndpoint { val oauth = properties.let { this.field("oauth")?.toOAuth(it) } return when { - path != null -> IncomingEndpoint(path, PathMatchingType.PATH, methods, clients, unlistedClientsPolicy, oauth) + paths.isNotEmpty() -> IncomingEndpoint( + paths, "", PathMatchingType.PATH, methods, clients, unlistedClientsPolicy, oauth) + path != null -> IncomingEndpoint( + paths, path, PathMatchingType.PATH, methods, clients, unlistedClientsPolicy, oauth) pathPrefix != null -> IncomingEndpoint( - pathPrefix, PathMatchingType.PATH_PREFIX, methods, clients, unlistedClientsPolicy, oauth + paths, pathPrefix, PathMatchingType.PATH_PREFIX, methods, clients, unlistedClientsPolicy, oauth ) pathRegex != null -> IncomingEndpoint( - pathRegex, PathMatchingType.PATH_REGEX, methods, clients, unlistedClientsPolicy, oauth + paths, pathRegex, PathMatchingType.PATH_REGEX, methods, clients, unlistedClientsPolicy, oauth ) - else -> throw NodeMetadataValidationException("One of 'path', 'pathPrefix' or 'pathRegex' field is required") + else -> throw NodeMetadataValidationException( + "One of 'paths', 'path', 'pathPrefix' or 'pathRegex' field is required") } } @@ -449,7 +455,16 @@ fun Value.toIncomingRateLimitEndpoint(): IncomingRateLimitEndpoint { } } -fun isMoreThanOnePropertyDefined(vararg properties: String?): Boolean = properties.filterNotNull().count() > 1 +fun isMoreThanOnePropertyDefined(vararg properties: Any?): Boolean = + countNonNullAndNotEmptyProperties(properties.toList()) > 1 + +private fun countNonNullAndNotEmptyProperties(props: List): Int = props.filterNotNull().count { + if (it is Set<*>) { + it.isNotEmpty() + } else { + true + } +} private fun Value?.toIncomingTimeoutPolicy(): Incoming.TimeoutPolicy { val idleTimeout: Duration? = this?.field("idleTimeout")?.toDuration() @@ -786,6 +801,7 @@ data class ClientWithSelector private constructor( } data class IncomingEndpoint( + override val paths: Set = emptySet(), override val path: String = "", override val pathMatchingType: PathMatchingType = PathMatchingType.PATH, override val methods: Set = emptySet(), @@ -826,6 +842,7 @@ data class OAuth( } interface EndpointBase { + val paths: Set val path: String val pathMatchingType: PathMatchingType val methods: Set diff --git a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/JwtFilterFactory.kt b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/JwtFilterFactory.kt index d2cd273ce..b5ee3bce7 100644 --- a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/JwtFilterFactory.kt +++ b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/JwtFilterFactory.kt @@ -4,6 +4,7 @@ import com.google.protobuf.Any import com.google.protobuf.Empty import com.google.protobuf.util.Durations import io.envoyproxy.envoy.config.core.v3.HttpUri +import io.envoyproxy.envoy.config.core.v3.TypedExtensionConfig import io.envoyproxy.envoy.config.route.v3.HeaderMatcher import io.envoyproxy.envoy.config.route.v3.RouteMatch import io.envoyproxy.envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication @@ -13,6 +14,7 @@ import io.envoyproxy.envoy.extensions.filters.http.jwt_authn.v3.JwtRequirementOr import io.envoyproxy.envoy.extensions.filters.http.jwt_authn.v3.RemoteJwks import io.envoyproxy.envoy.extensions.filters.http.jwt_authn.v3.RequirementRule import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpFilter +import io.envoyproxy.envoy.extensions.path.match.uri_template.v3.UriTemplateMatchConfig import io.envoyproxy.envoy.type.matcher.v3.RegexMatcher import pl.allegro.tech.servicemesh.envoycontrol.groups.Group import pl.allegro.tech.servicemesh.envoycontrol.groups.IncomingEndpoint @@ -98,10 +100,10 @@ class JwtFilterFactory( } private fun createRules(endpoints: List): Set { - return endpoints.mapNotNull(this::createRuleForEndpoint).toSet() + return endpoints.flatMap(this::createRulesForEndpoint).toSet() } - private fun createRuleForEndpoint(endpoint: IncomingEndpoint): RequirementRule? { + private fun createRulesForEndpoint(endpoint: IncomingEndpoint): Set { val providers = mutableSetOf() if (endpoint.oauth != null) { @@ -111,11 +113,44 @@ class JwtFilterFactory( providers.addAll(endpoint.clients.filter { it.name in clientToOAuthProviderName.keys } .mapNotNull { clientToOAuthProviderName[it.name] }) - return if (providers.isNotEmpty()) { - requirementRuleWithPathMatching(endpoint.path, endpoint.pathMatchingType, endpoint.methods, providers) + if (providers.isEmpty()) { + return emptySet() + } + + return if (endpoint.paths.isNotEmpty()) { + endpoint.paths.map { + requirementRuleWithURITemplateMatching(it, endpoint.methods, providers) + }.toSet() } else { - null + setOf(requirementRuleWithPathMatching( + endpoint.path, endpoint.pathMatchingType, endpoint.methods, providers)) + } + } + + private fun requirementRuleWithURITemplateMatching( + pathGlobPattern: String, + methods: Set, + providers: MutableSet + ): RequirementRule { + val pathMatching = RouteMatch.newBuilder().setPathMatchPolicy( + TypedExtensionConfig.newBuilder() + .setName("envoy.path.match.uri_template.uri_template_matcher") + .setTypedConfig( + Any.pack( + UriTemplateMatchConfig.newBuilder() + .setPathTemplate(pathGlobPattern) + .build() + ) + ).build() + ) + if (methods.isNotEmpty()) { + pathMatching.addHeaders(createHeaderMatcherBuilder(methods)) } + + return RequirementRule.newBuilder() + .setMatch(pathMatching) + .setRequires(createJwtRequirement(providers)) + .build() } private fun requirementRuleWithPathMatching( @@ -135,22 +170,25 @@ class JwtFilterFactory( ) } if (methods.isNotEmpty()) { - val methodsRegexp = methods.joinToString("|") - val headerMatcher = HeaderMatcher.newBuilder() - .setName(":method") - .setSafeRegexMatch( - RegexMatcher.newBuilder().setRegex(methodsRegexp).setGoogleRe2( - RegexMatcher.GoogleRE2.getDefaultInstance() - ).build() - ) - pathMatching.addHeaders(headerMatcher) + pathMatching.addHeaders(createHeaderMatcherBuilder(methods)) } + return RequirementRule.newBuilder() .setMatch(pathMatching) .setRequires(createJwtRequirement(providers)) .build() } + private fun createHeaderMatcherBuilder(methods: Set): HeaderMatcher.Builder { + return HeaderMatcher.newBuilder() + .setName(":method") + .setSafeRegexMatch( + RegexMatcher.newBuilder().setRegex(methods.joinToString("|")).setGoogleRe2( + RegexMatcher.GoogleRE2.getDefaultInstance() + ).build() + ) + } + private val requirementsForProviders: Map = jwtProviders.keys.associateWith { JwtRequirement.newBuilder().setProviderName(it).build() } diff --git a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/RBACFilterPermissions.kt b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/RBACFilterPermissions.kt index fb5ef436d..64e54c0bd 100644 --- a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/RBACFilterPermissions.kt +++ b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/RBACFilterPermissions.kt @@ -1,7 +1,10 @@ package pl.allegro.tech.servicemesh.envoycontrol.snapshot.resource.listeners.filters +import com.google.protobuf.Any +import io.envoyproxy.envoy.config.core.v3.TypedExtensionConfig import io.envoyproxy.envoy.config.rbac.v3.Permission import io.envoyproxy.envoy.config.route.v3.HeaderMatcher +import io.envoyproxy.envoy.extensions.path.match.uri_template.v3.UriTemplateMatchConfig import io.envoyproxy.envoy.type.matcher.v3.PathMatcher import io.envoyproxy.envoy.type.matcher.v3.RegexMatcher import io.envoyproxy.envoy.type.matcher.v3.StringMatcher @@ -11,8 +14,12 @@ import pl.allegro.tech.servicemesh.envoycontrol.groups.PathMatchingType class RBACFilterPermissions { fun createCombinedPermissions(incomingEndpoint: IncomingEndpoint): Permission.Builder { val permissions = listOfNotNull( - createPathPermissionForEndpoint(incomingEndpoint), - createMethodPermissions(incomingEndpoint) + if (incomingEndpoint.paths.isNotEmpty()) { + createPathTemplatesPermissionForEndpoint(incomingEndpoint) + } else { + createPathPermissionForEndpoint(incomingEndpoint) + }, + createMethodPermissions(incomingEndpoint), ) .map { it.build() } @@ -21,6 +28,22 @@ class RBACFilterPermissions { ) } + private fun createPathTemplatesPermissionForEndpoint(incomingEndpoint: IncomingEndpoint): Permission.Builder { + return permission() + .setOrRules(Permission.Set.newBuilder().addAllRules( + incomingEndpoint.paths.map(this::createPathTemplate))) + } + + private fun createPathTemplate(path: String): Permission { + return permission().setUriTemplate(TypedExtensionConfig.newBuilder() + .setName("envoy.path.match.uri_template.uri_template_matcher") + .setTypedConfig(Any.pack( + UriTemplateMatchConfig.newBuilder() + .setPathTemplate(path) + .build() + ))).build() + } + fun createPathPermission(path: String, matchingType: PathMatchingType): Permission.Builder { return permission().setUrlPath(createPathMatcher(path, matchingType)) } diff --git a/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/groups/NodeMetadataTest.kt b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/groups/NodeMetadataTest.kt index bd361f9b4..82c7a3ba5 100644 --- a/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/groups/NodeMetadataTest.kt +++ b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/groups/NodeMetadataTest.kt @@ -119,7 +119,7 @@ class NodeMetadataTest { // expects val exception = assertThrows { proto.toIncomingEndpoint(snapshotProperties()) } - assertThat(exception.status.description).isEqualTo("Precisely one of 'path', 'pathPrefix' or 'pathRegex' field is allowed") + assertThat(exception.status.description).isEqualTo("Precisely one of 'paths', 'path', 'pathPrefix' or 'pathRegex' field is allowed") assertThat(exception.status.code).isEqualTo(Status.Code.INVALID_ARGUMENT) } @@ -135,18 +135,18 @@ class NodeMetadataTest { // expects val exception = assertThrows { proto.toIncomingEndpoint(snapshotProperties()) } - assertThat(exception.status.description).isEqualTo("Precisely one of 'path', 'pathPrefix' or 'pathRegex' field is allowed") + assertThat(exception.status.description).isEqualTo("Precisely one of 'paths', 'path', 'pathPrefix' or 'pathRegex' field is allowed") assertThat(exception.status.code).isEqualTo(Status.Code.INVALID_ARGUMENT) } @Test - fun `should reject endpoint with no path or pathPrefix or pathRegex defined`() { + fun `should reject endpoint with empty paths or without path or pathPrefix or pathRegex defined`() { // given val proto = incomingEndpointProto(path = null, pathPrefix = null, pathRegex = null) // expects val exception = assertThrows { proto.toIncomingEndpoint(snapshotProperties()) } - assertThat(exception.status.description).isEqualTo("One of 'path', 'pathPrefix' or 'pathRegex' field is required") + assertThat(exception.status.description).isEqualTo("One of 'paths', 'path', 'pathPrefix' or 'pathRegex' field is required") assertThat(exception.status.code).isEqualTo(Status.Code.INVALID_ARGUMENT) } diff --git a/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/JwtFilterFactoryTest.kt b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/JwtFilterFactoryTest.kt index 590d671ae..16f4df375 100644 --- a/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/JwtFilterFactoryTest.kt +++ b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/JwtFilterFactoryTest.kt @@ -164,7 +164,7 @@ internal class JwtFilterFactoryTest { Incoming( pathToProvider.map { (path, provider) -> IncomingEndpoint( - path, + path = path, oauth = OAuth(provider, policy = policy) ) } @@ -182,7 +182,7 @@ internal class JwtFilterFactoryTest { Incoming( pathToProvider.map { (path, _) -> IncomingEndpoint( - path, + path = path, clients = setOf(ClientWithSelector.create("oauth", "client")), oauth = null ) diff --git a/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/rbac/RBACFilterFactoryJwtTest.kt b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/rbac/RBACFilterFactoryJwtTest.kt index aebc9238c..30fb45451 100644 --- a/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/rbac/RBACFilterFactoryJwtTest.kt +++ b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/rbac/RBACFilterFactoryJwtTest.kt @@ -71,6 +71,7 @@ internal class RBACFilterFactoryJwtTest : RBACFilterFactoryTestUtils { permissionsEnabled = true, endpoints = listOf( IncomingEndpoint( + emptySet(), "/oauth-protected", PathMatchingType.PATH, setOf("GET"), @@ -110,6 +111,7 @@ internal class RBACFilterFactoryJwtTest : RBACFilterFactoryTestUtils { permissionsEnabled = true, endpoints = listOf( IncomingEndpoint( + emptySet(), "/oauth-protected", PathMatchingType.PATH, setOf("GET"), @@ -152,6 +154,7 @@ internal class RBACFilterFactoryJwtTest : RBACFilterFactoryTestUtils { permissionsEnabled = true, endpoints = listOf( IncomingEndpoint( + emptySet(), "/oauth-protected", PathMatchingType.PATH, setOf("GET"), @@ -178,6 +181,7 @@ internal class RBACFilterFactoryJwtTest : RBACFilterFactoryTestUtils { permissionsEnabled = true, endpoints = listOf( IncomingEndpoint( + emptySet(), "/oauth-protected", PathMatchingType.PATH, setOf("GET"), @@ -219,6 +223,7 @@ internal class RBACFilterFactoryJwtTest : RBACFilterFactoryTestUtils { permissionsEnabled = true, endpoints = listOf( IncomingEndpoint( + emptySet(), "/oauth-protected", PathMatchingType.PATH, setOf("GET"), @@ -264,6 +269,7 @@ internal class RBACFilterFactoryJwtTest : RBACFilterFactoryTestUtils { permissionsEnabled = true, endpoints = listOf( IncomingEndpoint( + emptySet(), "/oauth-protected", PathMatchingType.PATH, setOf("GET"), @@ -336,7 +342,7 @@ internal class RBACFilterFactoryJwtTest : RBACFilterFactoryTestUtils { ) = """ { "policies": { - "IncomingEndpoint(path=/oauth-protected, pathMatchingType=PATH, methods=[GET], clients=[$clientsWithSelector], unlistedClientsPolicy=$unlistedClientsPolicy, oauth=$oauth)": { + "IncomingEndpoint(paths=[], path=/oauth-protected, pathMatchingType=PATH, methods=[GET], clients=[$clientsWithSelector], unlistedClientsPolicy=$unlistedClientsPolicy, oauth=$oauth)": { "permissions": [ { "and_rules": { diff --git a/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/rbac/RBACFilterFactoryTest.kt b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/rbac/RBACFilterFactoryTest.kt index e8ab1d68b..251d271bf 100644 --- a/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/rbac/RBACFilterFactoryTest.kt +++ b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/rbac/RBACFilterFactoryTest.kt @@ -225,7 +225,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { @Test fun `should generate RBAC rules for incoming permissions with log unlisted clients and endpoints`() { // given - val policyName = "IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[GET, POST], " + + val policyName = "IncomingEndpoint(paths=[], path=/example, pathMatchingType=PATH, methods=[GET, POST], " + "clients=[ClientWithSelector(name=client1, selector=null, negated=false), ClientWithSelector(name=client2, selector=null, negated=false)], " + "unlistedClientsPolicy=LOG, oauth=null)" val expectedShadowRules = expectedSimpleEndpointPermissionsJson(policyName) @@ -238,6 +238,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { permissionsEnabled = true, endpoints = listOf( IncomingEndpoint( + emptySet(), "/example", PathMatchingType.PATH, setOf("GET", "POST"), @@ -258,7 +259,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { @Test fun `should generate RBAC rules for incoming permissions with block unlisted endpoints and log clients`() { // given - val policyName = "IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[GET, POST], " + + val policyName = "IncomingEndpoint(paths=[], path=/example, pathMatchingType=PATH, methods=[GET, POST], " + "clients=[ClientWithSelector(name=client1, selector=null, negated=false), ClientWithSelector(name=client2, selector=null, negated=false)], " + "unlistedClientsPolicy=LOG, oauth=null)" val expectedShadowRules = expectedSimpleEndpointPermissionsJson(policyName) @@ -271,6 +272,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { permissionsEnabled = true, endpoints = listOf( IncomingEndpoint( + emptySet(), "/example", PathMatchingType.PATH, setOf("GET", "POST"), @@ -291,7 +293,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { @Test fun `should generate RBAC rules for incoming permissions with log unlisted endpoints and block clients`() { // given - val policyName = "IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[GET, POST], " + + val policyName = "IncomingEndpoint(paths=[], path=/example, pathMatchingType=PATH, methods=[GET, POST], " + "clients=[ClientWithSelector(name=client1, selector=null, negated=false), ClientWithSelector(name=client2, selector=null, negated=false)], " + "unlistedClientsPolicy=BLOCKANDLOG, oauth=null)" val expectedShadowRules = expectedSimpleEndpointPermissionsJson(policyName) @@ -304,6 +306,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { permissionsEnabled = true, endpoints = listOf( IncomingEndpoint( + emptySet(), "/example", PathMatchingType.PATH, setOf("GET", "POST"), @@ -324,12 +327,13 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { @Test fun `should generate RBAC rules for incoming permissions with roles`() { // given - val policyName = "IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[GET, POST], " + + val policyName = "IncomingEndpoint(paths=[], path=/example, pathMatchingType=PATH, methods=[GET, POST], " + "clients=[ClientWithSelector(name=role-1, selector=null, negated=false)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)" val expectedRbacBuilder = getRBACFilter(expectedSimpleEndpointPermissionsJson(policyName)) val incomingPermission = Incoming( permissionsEnabled = true, endpoints = listOf(IncomingEndpoint( + emptySet(), "/example", PathMatchingType.PATH, setOf("GET", "POST"), @@ -351,6 +355,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { val incomingPermission = Incoming( permissionsEnabled = true, endpoints = listOf(IncomingEndpoint( + emptySet(), "/example", PathMatchingType.PATH, setOf("GET", "POST"), @@ -383,11 +388,13 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { val incomingPermission = Incoming( permissionsEnabled = true, endpoints = listOf(IncomingEndpoint( + emptySet(), "/example", PathMatchingType.PATH, setOf("GET"), setOf(ClientWithSelector.create("client1")) ), IncomingEndpoint( + emptySet(), "/example2", PathMatchingType.PATH, setOf("POST"), @@ -408,11 +415,13 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { val incomingPermission = Incoming( permissionsEnabled = true, endpoints = listOf(IncomingEndpoint( + emptySet(), "/example", PathMatchingType.PATH, setOf("GET", "POST"), setOf(ClientWithSelector.create("role-1")) ), IncomingEndpoint( + emptySet(), "/example2", PathMatchingType.PATH, setOf("GET", "POST"), @@ -436,11 +445,13 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { val incomingPermission = Incoming( permissionsEnabled = true, endpoints = listOf(IncomingEndpoint( + emptySet(), "/example", PathMatchingType.PATH, setOf("GET", "POST"), setOf(ClientWithSelector.create("client2"), ClientWithSelector.create("role-1")) ), IncomingEndpoint( + emptySet(), "/example2", PathMatchingType.PATH, setOf("GET", "POST"), @@ -465,6 +476,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { val incomingPermission = Incoming( permissionsEnabled = true, endpoints = listOf(IncomingEndpoint( + emptySet(), "/example", PathMatchingType.PATH, setOf("GET", "POST"), @@ -485,6 +497,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { val incomingPermission = Incoming( permissionsEnabled = true, endpoints = listOf(IncomingEndpoint( + emptySet(), "/example", PathMatchingType.PATH, setOf() @@ -506,7 +519,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { val expectedActual = """ { "policies": { - "IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[], clients=[], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { + "IncomingEndpoint(paths=[], path=/example, pathMatchingType=PATH, methods=[], clients=[], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { "permissions": [{ "and_rules": { "rules": [ ${pathRule("/example")} ] @@ -601,6 +614,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { val incomingPermission = Incoming( permissionsEnabled = true, endpoints = listOf(IncomingEndpoint( + emptySet(), "/example", PathMatchingType.PATH, setOf("GET"), @@ -622,6 +636,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { val incomingPermission = Incoming( permissionsEnabled = true, endpoints = listOf(IncomingEndpoint( + emptySet(), "/example", PathMatchingType.PATH, setOf("GET"), @@ -646,6 +661,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { val incomingPermission = Incoming( permissionsEnabled = true, endpoints = listOf(IncomingEndpoint( + emptySet(), "/example", PathMatchingType.PATH, setOf("GET"), @@ -670,6 +686,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { val incomingPermission = Incoming( permissionsEnabled = true, endpoints = listOf(IncomingEndpoint( + emptySet(), "/example", PathMatchingType.PATH, setOf("GET"), @@ -694,6 +711,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { val incomingPermission = Incoming( permissionsEnabled = true, endpoints = listOf(IncomingEndpoint( + emptySet(), "/example", PathMatchingType.PATH, setOf("GET"), @@ -722,6 +740,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { val incomingPermission = Incoming( permissionsEnabled = true, endpoints = listOf(IncomingEndpoint( + emptySet(), "/example", PathMatchingType.PATH, setOf("GET"), @@ -745,6 +764,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { val incomingPermission = Incoming( permissionsEnabled = true, endpoints = listOf(IncomingEndpoint( + emptySet(), "/default", PathMatchingType.PATH, setOf("GET"), @@ -768,6 +788,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { val incomingPermission = Incoming( permissionsEnabled = true, endpoints = listOf(IncomingEndpoint( + emptySet(), "/custom", PathMatchingType.PATH, setOf("GET"), @@ -788,7 +809,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { private val expectedEndpointPermissionsWithDifferentRulesForDifferentClientsJson = """ { "policies": { - "IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[GET], clients=[ClientWithSelector(name=client1, selector=null, negated=false)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { + "IncomingEndpoint(paths=[], path=/example, pathMatchingType=PATH, methods=[GET], clients=[ClientWithSelector(name=client1, selector=null, negated=false)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { "permissions": [ { "and_rules": { @@ -808,7 +829,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { ${originalAndAuthenticatedPrincipal("client1")} ] }, - "IncomingEndpoint(path=/example2, pathMatchingType=PATH, methods=[POST], clients=[ClientWithSelector(name=client2, selector=null, negated=false)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { + "IncomingEndpoint(paths=[], path=/example2, pathMatchingType=PATH, methods=[POST], clients=[ClientWithSelector(name=client2, selector=null, negated=false)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { "permissions": [ { "and_rules": { @@ -835,7 +856,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { private val expectedSourceIpFromDiscoveryWithSelectorAuthPermissionsJson = """ { "policies": { - "IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[GET], clients=[ClientWithSelector(name=client1, selector=selector, negated=false)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { + "IncomingEndpoint(paths=[], path=/example, pathMatchingType=PATH, methods=[GET], clients=[ClientWithSelector(name=client1, selector=selector, negated=false)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { "permissions": [ { "and_rules": { @@ -870,7 +891,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { private val expectedSourceIpWithSelectorAuthPermissionsJson = """ { "policies": { - "IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[GET], clients=[ClientWithSelector(name=client2, selector=selector, negated=false)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { + "IncomingEndpoint(paths=[], path=/example, pathMatchingType=PATH, methods=[GET], clients=[ClientWithSelector(name=client2, selector=selector, negated=false)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { "permissions": [ { "and_rules": { @@ -905,7 +926,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { private val expectedSourceIpWithStaticRangeAndSelectorAuthPermissionsAndRolesJson = """ { "policies": { - "IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[GET], clients=[ClientWithSelector(name=role1, selector=null, negated=false)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { + "IncomingEndpoint(paths=[], path=/example, pathMatchingType=PATH, methods=[GET], clients=[ClientWithSelector(name=role1, selector=null, negated=false)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { "permissions": [ { "and_rules": { @@ -949,7 +970,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { private val expectedSourceIpAuthPermissionsJson = """ { "policies": { - "IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[GET, POST], clients=[ClientWithSelector(name=client1, selector=null, negated=false), ClientWithSelector(name=client2, selector=null, negated=false)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { + "IncomingEndpoint(paths=[], path=/example, pathMatchingType=PATH, methods=[GET, POST], clients=[ClientWithSelector(name=client1, selector=null, negated=false), ClientWithSelector(name=client2, selector=null, negated=false)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { "permissions": [ { "and_rules": { @@ -1008,7 +1029,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { { "policies": { """ /* notice that duplicated clients occurs only once here */ + """ - "IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[GET, POST], clients=[ClientWithSelector(name=client1, selector=null, negated=false), ClientWithSelector(name=client1, selector=selector, negated=false), ClientWithSelector(name=client1-duplicated, selector=selector, negated=false), ClientWithSelector(name=client1-duplicated, selector=null, negated=false), ClientWithSelector(name=role-1, selector=null, negated=false)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { + "IncomingEndpoint(paths=[], path=/example, pathMatchingType=PATH, methods=[GET, POST], clients=[ClientWithSelector(name=client1, selector=null, negated=false), ClientWithSelector(name=client1, selector=selector, negated=false), ClientWithSelector(name=client1-duplicated, selector=selector, negated=false), ClientWithSelector(name=client1-duplicated, selector=null, negated=false), ClientWithSelector(name=role-1, selector=null, negated=false)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { "permissions": [ { "and_rules": { @@ -1228,7 +1249,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { private val expectedSourceIpAuthWithStaticRangeJson = """ { "policies": { - "IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[GET], clients=[ClientWithSelector(name=client1, selector=null, negated=false)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { + "IncomingEndpoint(paths=[], path=/example, pathMatchingType=PATH, methods=[GET], clients=[ClientWithSelector(name=client1, selector=null, negated=false)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { "permissions": [ { "and_rules": { @@ -1255,7 +1276,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { private val expectedSourceIpAuthWithStaticRangeAndSourceIpJson = """ { "policies": { - "IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[GET], clients=[ClientWithSelector(name=client1, selector=null, negated=false), ClientWithSelector(name=client2, selector=null, negated=false)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { + "IncomingEndpoint(paths=[], path=/example, pathMatchingType=PATH, methods=[GET], clients=[ClientWithSelector(name=client1, selector=null, negated=false), ClientWithSelector(name=client2, selector=null, negated=false)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { "permissions": [ { "and_rules": { @@ -1292,7 +1313,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { private val expectedEndpointPermissionsLogUnlistedEndpointsAndBlockUnlistedClients = """ { "policies": { - "IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[GET, POST], clients=[ClientWithSelector(name=client1, selector=null, negated=false), ClientWithSelector(name=client2, selector=null, negated=false)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { + "IncomingEndpoint(paths=[], path=/example, pathMatchingType=PATH, methods=[GET, POST], clients=[ClientWithSelector(name=client1, selector=null, negated=false), ClientWithSelector(name=client2, selector=null, negated=false)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { "permissions": [ { "and_rules": { @@ -1348,7 +1369,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { private fun expectedPoliciesForAllowedClient(principals: String) = """ { "policies": { - "IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[GET], clients=[ClientWithSelector(name=client1, selector=null, negated=false)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { + "IncomingEndpoint(paths=[], path=/example, pathMatchingType=PATH, methods=[GET], clients=[ClientWithSelector(name=client1, selector=null, negated=false)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { "permissions": [ { "and_rules": { @@ -1373,7 +1394,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { private fun expectedPoliciesForDefaultAndCustomLists(clients: List, principals: List, path: String) = """ { "policies": { - "IncomingEndpoint(path=$path, pathMatchingType=PATH, methods=[GET], clients=[${clients.joinToString(", ") { "ClientWithSelector(name=$it, selector=null, negated=false)" }}], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { + "IncomingEndpoint(paths=[], path=$path, pathMatchingType=PATH, methods=[GET], clients=[${clients.joinToString(", ") { "ClientWithSelector(name=$it, selector=null, negated=false)" }}], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { "permissions": [ { "and_rules": { diff --git a/envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/permissions/IncomingPermissionsPathMatchingTest.kt b/envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/permissions/IncomingPermissionsPathMatchingTest.kt index 221fca022..aa520b42e 100644 --- a/envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/permissions/IncomingPermissionsPathMatchingTest.kt +++ b/envoy-control-tests/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/permissions/IncomingPermissionsPathMatchingTest.kt @@ -26,6 +26,14 @@ class IncomingPermissionsPathMatchingTest { incoming: unlistedEndpointsPolicy: blockAndLog endpoints: + - paths: + - /api/products + - /api/products/*/reviews + - /api/offers/** + - /api/**/description + - /*/login + - /**/health + clients: ["echo2"] - path: "/path" clients: ["echo2"] - pathPrefix: "/prefix" @@ -139,4 +147,39 @@ class IncomingPermissionsPathMatchingTest { assertThat(it).isForbidden() } } + + @Test + fun `echo should allow echo2 to access endpoints for matched Glob patterns in the 'paths' field`() { + // expect + echo2Envoy.egressOperations.callService(service = "echo", pathAndQuery = "/api/products").also { + assertThat(it).isOk() + } + echo2Envoy.egressOperations.callService(service = "echo", pathAndQuery = "/api/products/some/reviews").also { + assertThat(it).isOk() + } + echo2Envoy.egressOperations.callService(service = "echo", pathAndQuery = "/api/offers/electronics/phones").also { + assertThat(it).isOk() + } + echo2Envoy.egressOperations.callService(service = "echo", pathAndQuery = "/some/status/health").also { + assertThat(it).isOk() + } + echo2Envoy.egressOperations.callService(service = "echo", pathAndQuery = "/api/path/with/description").also { + assertThat(it).isOk() + } + echo2Envoy.egressOperations.callService(service = "echo", pathAndQuery = "/api/paths/with/description").also { + assertThat(it).isOk() + } + echo2Envoy.egressOperations.callService(service = "echo", pathAndQuery = "/api/login").also { + assertThat(it).isOk() + } + echo2Envoy.egressOperations.callService(service = "echo", pathAndQuery = "/api/products/too/many/reviews").also { + assertThat(it).isForbidden() + } + echo2Envoy.egressOperations.callService(service = "echo", pathAndQuery = "/api/products/forbidden").also { + assertThat(it).isForbidden() + } + echo2Envoy.egressOperations.callService(service = "echo", pathAndQuery = "/status/health/login").also { + assertThat(it).isForbidden() + } + } }