From a69e7bb50d79815b11c9c05304a6a91c14d73e09 Mon Sep 17 00:00:00 2001 From: Damian Hanusiak Date: Fri, 15 Oct 2021 16:35:11 +0200 Subject: [PATCH 1/2] Refactor RBACFilterFactory --- .../listeners/filters/EnvoyDefaultFilters.kt | 6 +- .../IncomingEndpointsPoliciesFactory.kt | 365 ++++++++++++ .../listeners/filters/RBACFilterFactory.kt | 505 +++------------- .../filters/rbac/RBACFilterFactoryJwtTest.kt | 37 +- .../filters/rbac/RBACFilterFactoryTest.kt | 541 ++++++++++-------- 5 files changed, 790 insertions(+), 664 deletions(-) create mode 100644 envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/IncomingEndpointsPoliciesFactory.kt diff --git a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/EnvoyDefaultFilters.kt b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/EnvoyDefaultFilters.kt index b7959fbc5..f26b0c1f8 100644 --- a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/EnvoyDefaultFilters.kt +++ b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/EnvoyDefaultFilters.kt @@ -11,10 +11,14 @@ import pl.allegro.tech.servicemesh.envoycontrol.snapshot.SnapshotProperties class EnvoyDefaultFilters( private val snapshotProperties: SnapshotProperties ) { + private val incomingEndpointsPoliciesFactory = IncomingEndpointsPoliciesFactory( + snapshotProperties.incomingPermissions, + jwtProperties = snapshotProperties.jwt + ) private val rbacFilterFactory = RBACFilterFactory( snapshotProperties.incomingPermissions, snapshotProperties.routes.status, - jwtProperties = snapshotProperties.jwt + incomingEndpointsPoliciesFactory = incomingEndpointsPoliciesFactory ) private val luaFilterFactory = LuaFilterFactory( snapshotProperties.incomingPermissions diff --git a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/IncomingEndpointsPoliciesFactory.kt b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/IncomingEndpointsPoliciesFactory.kt new file mode 100644 index 000000000..0bbfafa3b --- /dev/null +++ b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/IncomingEndpointsPoliciesFactory.kt @@ -0,0 +1,365 @@ +package pl.allegro.tech.servicemesh.envoycontrol.snapshot.resource.listeners.filters + +import com.google.protobuf.UInt32Value +import io.envoyproxy.controlplane.cache.SnapshotResources +import io.envoyproxy.envoy.config.core.v3.CidrRange +import io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment +import io.envoyproxy.envoy.config.rbac.v3.Policy +import io.envoyproxy.envoy.config.rbac.v3.Principal +import io.envoyproxy.envoy.config.route.v3.HeaderMatcher +import io.envoyproxy.envoy.type.matcher.v3.ListMatcher +import io.envoyproxy.envoy.type.matcher.v3.MetadataMatcher +import io.envoyproxy.envoy.type.matcher.v3.StringMatcher +import io.envoyproxy.envoy.type.matcher.v3.ValueMatcher +import pl.allegro.tech.servicemesh.envoycontrol.groups.ClientWithSelector +import pl.allegro.tech.servicemesh.envoycontrol.groups.Incoming +import pl.allegro.tech.servicemesh.envoycontrol.groups.IncomingEndpoint +import pl.allegro.tech.servicemesh.envoycontrol.groups.OAuth +import pl.allegro.tech.servicemesh.envoycontrol.groups.Role +import pl.allegro.tech.servicemesh.envoycontrol.logger +import pl.allegro.tech.servicemesh.envoycontrol.snapshot.Client +import pl.allegro.tech.servicemesh.envoycontrol.snapshot.IncomingPermissionsProperties +import pl.allegro.tech.servicemesh.envoycontrol.snapshot.JwtFilterProperties +import pl.allegro.tech.servicemesh.envoycontrol.snapshot.OAuthProvider +import pl.allegro.tech.servicemesh.envoycontrol.snapshot.SelectorMatching + +class IncomingEndpointsPoliciesFactory( + private val incomingPermissionsProperties: IncomingPermissionsProperties, + private val rBACFilterPermissions: RBACFilterPermissions = RBACFilterPermissions(), + private val jwtProperties: JwtFilterProperties = JwtFilterProperties() +) { + private val incomingServicesSourceAuthentication = incomingPermissionsProperties + .sourceIpAuthentication + .ipFromServiceDiscovery + .enabledForIncomingServices + + private val oAuthMatchingsClients: List = jwtProperties.providers.values.flatMap { it.matchings.keys } + private val anyPrincipal = principalBuilder().setAny(true).build() + private val denyForAllPrincipal = principalBuilder().setNotId(anyPrincipal).build() + private val sanUriMatcherFactory = SanUriMatcherFactory(incomingPermissionsProperties.tlsAuthentication) + + private val strictPolicyPrincipal = buildPrincipalSetAndIds( + metadataMatcherBuilder() + .setFilter("envoy.filters.http.header_to_metadata") + .addPath(pathSegmentBuilder().setKey("jwt-status").build()) + .setValue(valueMatcherBuilder().setStringMatch(stringMatcherBuilder().setExact("present"))) + .toPrincipal(), + metadataMatcherBuilder() + .setFilter("envoy.filters.http.jwt_authn") + .addPath(pathSegmentBuilder().setKey(jwtProperties.payloadInMetadata)) + .addPath(pathSegmentBuilder().setKey(jwtProperties.fieldRequiredInToken)) + .setValue(valueMatcherBuilder().setPresentMatch(true)) + .toPrincipal() + ) + private val allowMissingPolicyPrincipal = buildPrincipalSetOrIds( + metadataMatcherBuilder() + .setFilter("envoy.filters.http.header_to_metadata") + .addPath( + pathSegmentBuilder().setKey("jwt-status").build() + ) + .setValue( + valueMatcherBuilder().setStringMatch(stringMatcherBuilder().setExact("missing")) + ).toPrincipal(), + strictPolicyPrincipal + ) + + private val staticIpRanges = createStaticIpRanges() + + companion object { + private val logger by logger() + private val EXACT_IP_MASK = UInt32Value.of(32) + } + + fun createIncomingEndpointPolicies( + incomingPermissions: Incoming, + snapshotEndpoints: SnapshotResources + ): List { + val roles = incomingPermissions.roles + val incomingPermissionsEndpoints = incomingPermissions.endpoints + val principalCache = mutableMapOf>() + + return incomingPermissionsEndpoints.map { incomingEndpoint -> + val clientsWithSelectors = resolveClientsWithSelectors(incomingEndpoint, roles) + + val principals = clientsWithSelectors + .flatMap { client -> + getPrincipals( + principalCache, + client, + snapshotEndpoints, + incomingEndpoint.unlistedClientsPolicy, + incomingEndpoint.oauth + ).map { mergeWithOAuthPolicy(client, it, incomingEndpoint.oauth?.policy) } + } + .toSet() + .ifEmpty { + setOf( + oAuthPolicyForEmptyClients( + incomingEndpoint.oauth?.policy, + incomingEndpoint.unlistedClientsPolicy + ) + ) + } + + val policy = Policy.newBuilder().addAllPrincipals(principals) + val combinedPermissions = rBACFilterPermissions.createCombinedPermissions(incomingEndpoint) + policy.addPermissions(combinedPermissions) + EndpointWithPolicy( + incomingEndpoint, + policy + ) + } + } + + private fun resolveClientsWithSelectors( + incomingEndpoint: IncomingEndpoint, + roles: List + ): Collection { + val clients = incomingEndpoint.clients.flatMap { clientOrRole -> + roles.find { it.name == clientOrRole.name }?.clients ?: setOf(clientOrRole) + } + // sorted order ensures that we do not duplicate rules + return clients.toSortedSet() + } + + private fun getPrincipals( + principalCache: MutableMap>, + client: ClientWithSelector, + endpoints: SnapshotResources, + unlistedClientsPolicy: Incoming.UnlistedPolicy, + oauth: OAuth? + ): List { + val principals = principalCache.computeIfAbsent(client) { + mapClientWithSelectorToPrincipals(it, endpoints) + }.toMutableList() + principals += principalForOAuthAndLogUnlistedClients(principals, unlistedClientsPolicy, oauth) + return principals + } + + private fun mergeWithOAuthPolicy( + client: ClientWithSelector, + principal: Principal, + policy: OAuth.Policy? + ): Principal { + return if (client.name in oAuthMatchingsClients) { + principal // don't merge if client has OAuth selector + } else { + when (policy) { + OAuth.Policy.ALLOW_MISSING -> buildPrincipalSetAndIds(allowMissingPolicyPrincipal, principal) + OAuth.Policy.STRICT -> buildPrincipalSetAndIds(strictPolicyPrincipal, principal) + OAuth.Policy.ALLOW_MISSING_OR_FAILED -> principal + null -> principal + } + } + } + + private fun oAuthPolicyForEmptyClients(policy: OAuth.Policy?, unlistedPolicy: Incoming.UnlistedPolicy): Principal { + return if (unlistedPolicy == Incoming.UnlistedPolicy.LOG) { + when (policy) { + OAuth.Policy.STRICT -> strictPolicyPrincipal + OAuth.Policy.ALLOW_MISSING -> allowMissingPolicyPrincipal + OAuth.Policy.ALLOW_MISSING_OR_FAILED -> anyPrincipal + null -> denyForAllPrincipal + } + } else { + denyForAllPrincipal + } + } + + private fun mapClientWithSelectorToPrincipals( + clientWithSelector: ClientWithSelector, + endpoints: SnapshotResources + ): List { + val providerForSelector = jwtProperties.providers.values.firstOrNull { + it.matchings.containsKey(clientWithSelector.name) + } + val selectorMatching = if (providerForSelector == null) { + getSelectorMatching(clientWithSelector, incomingPermissionsProperties) + } else { + null + } + val staticRangesForClient = staticIpRange(clientWithSelector, selectorMatching) + + return when { + clientWithSelector.name in incomingServicesSourceAuthentication -> + ipFromDiscoveryPrincipals(clientWithSelector, selectorMatching, endpoints) + staticRangesForClient != null -> + listOf(staticRangesForClient) + providerForSelector != null && clientWithSelector.selector != null -> + listOf(jwtClientWithSelectorPrincipal(clientWithSelector, providerForSelector)) + else -> + tlsPrincipals(clientWithSelector.name) + } + } + + private fun principalForOAuthAndLogUnlistedClients( + principals: MutableList, + unlistedClientsPolicy: Incoming.UnlistedPolicy, + oauth: OAuth? + ): List { + return if (principals.isNotEmpty() && unlistedClientsPolicy == Incoming.UnlistedPolicy.LOG && oauth != null) { + listOf(anyPrincipal) + } else { + emptyList() + } + } + + private fun getSelectorMatching( + client: ClientWithSelector, + incomingPermissionsProperties: IncomingPermissionsProperties + ): SelectorMatching? { + val matching = incomingPermissionsProperties.selectorMatching[client.name] + if (matching == null && client.selector != null) { + logger.warn( + "No selector matching found for client '${client.name}' with selector '${client.selector}' " + + "in EC properties. Source IP based authentication will not contain additional matching." + ) + return null + } + + return matching + } + + private fun staticIpRange(client: ClientWithSelector, selectorMatching: SelectorMatching?): Principal? { + val ranges = staticIpRanges[client.name] + return if (client.selector != null && selectorMatching != null && ranges != null) { + addAdditionalMatching(client.selector, selectorMatching, ranges) + } else { + ranges + } + } + + private fun ipFromDiscoveryPrincipals( + client: ClientWithSelector, + selectorMatching: SelectorMatching?, + endpoints: SnapshotResources + ): List { + val clusterLoadAssignment = endpoints.resources()[client.name] + val sourceIpPrincipal = mapEndpointsToExactPrincipals(clusterLoadAssignment) + + return when { + sourceIpPrincipal == null -> + emptyList() + client.selector != null && selectorMatching != null -> + listOf(addAdditionalMatching(client.selector, selectorMatching, sourceIpPrincipal)) + else -> + listOf(sourceIpPrincipal) + } + } + + private fun jwtClientWithSelectorPrincipal(client: ClientWithSelector, oAuthProvider: OAuthProvider): Principal = + metadataMatcherBuilder() + .setFilter("envoy.filters.http.jwt_authn") + .addPath(pathSegmentBuilder().setKey(jwtProperties.payloadInMetadata).build()) + .addPath(pathSegmentBuilder().setKey(oAuthProvider.matchings[client.name]).build()) + .setValue( + valueMatcherBuilder().setListMatch( + ListMatcher.newBuilder().setOneOf( + valueMatcherBuilder().setStringMatch( + stringMatcherBuilder() + .setExact(client.selector) + ) + ) + ) + ).toPrincipal() + + private fun tlsPrincipals(client: String): List { + val stringMatcher = sanUriMatcherFactory.createSanUriMatcher(client) + + return listOf( + principalBuilder().setAuthenticated( + Principal.Authenticated.newBuilder() + .setPrincipalName(stringMatcher) + ).build() + ) + } + + private fun createStaticIpRanges(): Map { + val ranges = incomingPermissionsProperties.sourceIpAuthentication.ipFromRange + + return ranges.mapValues { + val principals = it.value.map { ipWithPrefix -> + val (ip, prefixLength) = ipWithPrefix.split("/") + + principalBuilder().setDirectRemoteIp( + CidrRange.newBuilder() + .setAddressPrefix(ip) + .setPrefixLen(UInt32Value.of(prefixLength.toInt())).build() + ) + .build() + } + + buildPrincipalSetOrIds(principals) + } + } + + private fun addAdditionalMatching( + selector: String, + selectorMatching: SelectorMatching, + sourceIpPrincipal: Principal + ): Principal { + return if (selectorMatching.header.isNotEmpty()) { + val additionalMatchingPrincipal = principalBuilder() + .setHeader(HeaderMatcher.newBuilder().setName(selectorMatching.header).setExactMatch(selector)) + .build() + + principalBuilder().setAndIds( + Principal.Set.newBuilder().addAllIds( + listOf(sourceIpPrincipal, additionalMatchingPrincipal) + ).build() + ).build() + } else { + sourceIpPrincipal + } + } + + private fun mapEndpointsToExactPrincipals(clusterLoadAssignment: ClusterLoadAssignment?): Principal? { + val principals = clusterLoadAssignment?.endpointsList?.flatMap { lbEndpoints -> + lbEndpoints.lbEndpointsList.map { lbEndpoint -> + lbEndpoint.endpoint.address + } + }.orEmpty().map { address -> + principalBuilder() + .setDirectRemoteIp( + CidrRange.newBuilder() + .setAddressPrefix(address.socketAddress.address) + .setPrefixLen(EXACT_IP_MASK).build() + ) + .build() + } + + return if (principals.isNotEmpty()) { + principalBuilder().setOrIds(Principal.Set.newBuilder().addAllIds(principals).build()).build() + } else { + null + } + } + + private fun buildPrincipalSetAndIds(vararg principal: Principal): Principal = + principalBuilder().setAndIds( + Principal.Set.newBuilder().addAllIds(principal.toList()).build() + ).build() + + private fun MetadataMatcher.Builder.toPrincipal() = + principalBuilder().setMetadata(this.build()).build() + + private fun buildPrincipalSetOrIds(principals: Collection): Principal = + buildPrincipalSetOrIds(*principals.toTypedArray()) + + private fun buildPrincipalSetOrIds(vararg principal: Principal): Principal = + principalBuilder().setOrIds( + Principal.Set.newBuilder().addAllIds(principal.toList()).build() + ).build() + + private fun principalBuilder() = Principal.newBuilder() + + private fun stringMatcherBuilder() = StringMatcher.newBuilder() + + private fun valueMatcherBuilder() = ValueMatcher.newBuilder() + + private fun pathSegmentBuilder() = MetadataMatcher.PathSegment.newBuilder() + + private fun metadataMatcherBuilder() = MetadataMatcher.newBuilder() +} + +data class EndpointWithPolicy(val endpoint: IncomingEndpoint, val policy: Policy.Builder) diff --git a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/RBACFilterFactory.kt b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/RBACFilterFactory.kt index c483a2cc9..ad3766eef 100644 --- a/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/RBACFilterFactory.kt +++ b/envoy-control-core/src/main/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/RBACFilterFactory.kt @@ -1,31 +1,17 @@ package pl.allegro.tech.servicemesh.envoycontrol.snapshot.resource.listeners.filters import com.google.protobuf.Any -import com.google.protobuf.UInt32Value -import io.envoyproxy.envoy.config.core.v3.CidrRange +import io.envoyproxy.controlplane.cache.SnapshotResources import io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment import io.envoyproxy.envoy.config.rbac.v3.Policy import io.envoyproxy.envoy.config.rbac.v3.Principal import io.envoyproxy.envoy.config.rbac.v3.RBAC -import io.envoyproxy.envoy.config.route.v3.HeaderMatcher import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpFilter -import io.envoyproxy.envoy.type.matcher.v3.ListMatcher -import io.envoyproxy.envoy.type.matcher.v3.MetadataMatcher -import io.envoyproxy.envoy.type.matcher.v3.StringMatcher -import io.envoyproxy.envoy.type.matcher.v3.ValueMatcher import pl.allegro.tech.servicemesh.envoycontrol.groups.ClientWithSelector import pl.allegro.tech.servicemesh.envoycontrol.groups.Group import pl.allegro.tech.servicemesh.envoycontrol.groups.Incoming -import pl.allegro.tech.servicemesh.envoycontrol.groups.IncomingEndpoint -import pl.allegro.tech.servicemesh.envoycontrol.groups.OAuth -import pl.allegro.tech.servicemesh.envoycontrol.groups.Role -import pl.allegro.tech.servicemesh.envoycontrol.logger -import pl.allegro.tech.servicemesh.envoycontrol.snapshot.Client import pl.allegro.tech.servicemesh.envoycontrol.snapshot.GlobalSnapshot import pl.allegro.tech.servicemesh.envoycontrol.snapshot.IncomingPermissionsProperties -import pl.allegro.tech.servicemesh.envoycontrol.snapshot.JwtFilterProperties -import pl.allegro.tech.servicemesh.envoycontrol.snapshot.OAuthProvider -import pl.allegro.tech.servicemesh.envoycontrol.snapshot.SelectorMatching import pl.allegro.tech.servicemesh.envoycontrol.snapshot.StatusRouteProperties import io.envoyproxy.envoy.extensions.filters.http.rbac.v3.RBAC as RBACFilter @@ -33,7 +19,7 @@ class RBACFilterFactory( private val incomingPermissionsProperties: IncomingPermissionsProperties, statusRouteProperties: StatusRouteProperties, private val rBACFilterPermissions: RBACFilterPermissions = RBACFilterPermissions(), - private val jwtProperties: JwtFilterProperties = JwtFilterProperties() + private val incomingEndpointsPoliciesFactory: IncomingEndpointsPoliciesFactory ) { private val incomingServicesSourceAuthentication = incomingPermissionsProperties .sourceIpAuthentication @@ -46,11 +32,11 @@ class RBACFilterFactory( .keys private val anyPrincipal = Principal.newBuilder().setAny(true).build() - private val denyForAllPrincipal = Principal.newBuilder().setNotId(anyPrincipal).build() private val fullAccessClients = incomingPermissionsProperties.clientsAllowedToAllEndpoints.map { ClientWithSelector(name = it) } private val sanUriMatcherFactory = SanUriMatcherFactory(incomingPermissionsProperties.tlsAuthentication) + private val statusRoutePolicy = createStatusRoutePolicy(statusRouteProperties) init { incomingPermissionsProperties.selectorMatching.forEach { @@ -61,145 +47,65 @@ class RBACFilterFactory( } companion object { - private val logger by logger() private const val ALLOW_UNLISTED_POLICY_NAME = "ALLOW_UNLISTED_POLICY" private const val ALLOW_LOGGED_POLICY_NAME = "ALLOW_LOGGED_POLICY" private const val STATUS_ROUTE_POLICY_NAME = "STATUS_ALLOW_ALL_POLICY" - private val EXACT_IP_MASK = UInt32Value.of(32) + private const val HTTP_FILTER_NAME = "envoy.filters.http.rbac" } - private val statusRoutePolicy = createStatusRoutePolicy(statusRouteProperties) - private val staticIpRanges = createStaticIpRanges() - - data class EndpointWithPolicy(val endpoint: IncomingEndpoint, val policy: Policy.Builder) - - private val oAuthMatchingsClients: List = jwtProperties.providers.values.flatMap { it.matchings.keys } - - private fun getIncomingEndpointPolicies( - incomingPermissions: Incoming, - snapshot: GlobalSnapshot, - roles: List - ): List { - val principalCache = mutableMapOf>() - return incomingPermissions.endpoints.map { incomingEndpoint -> - val clientsWithSelectors = resolveClientsWithSelectors(incomingEndpoint, roles) - - val principals = clientsWithSelectors - .flatMap { client -> - getPrincipals( - principalCache, - client, - snapshot, - incomingEndpoint.unlistedClientsPolicy, - incomingEndpoint.oauth - ).map { mergeWithOAuthPolicy(client, it, incomingEndpoint.oauth?.policy) } - } - .toSet() - .ifEmpty { - setOf( - oAuthPolicyForEmptyClients( - incomingEndpoint.oauth?.policy, - incomingEndpoint.unlistedClientsPolicy - ) - ) - } - - val policy = Policy.newBuilder().addAllPrincipals(principals) - val combinedPermissions = rBACFilterPermissions.createCombinedPermissions(incomingEndpoint) - policy.addPermissions(combinedPermissions) - EndpointWithPolicy(incomingEndpoint, policy) - } - } + // TODO why not only pass proxySettings and endpoints from global snapshot? + fun createHttpFilter(group: Group, snapshot: GlobalSnapshot): HttpFilter? = + createHttpFilter(group.proxySettings.incoming, snapshot.endpoints) - private fun getPrincipals( - principalCache: MutableMap>, - client: ClientWithSelector, - snapshot: GlobalSnapshot, - unlistedClientsPolicy: Incoming.UnlistedPolicy, - oauth: OAuth? - ): List { - val principals = principalCache.computeIfAbsent(client) { - mapClientWithSelectorToPrincipals( - it, - snapshot - ) - }.toMutableList() - principals += principalForOAuthAndLogUnlistedClients(principals, unlistedClientsPolicy, oauth) - return principals - } + fun createHttpFilter(incoming: Incoming, endpoints: SnapshotResources): HttpFilter? = + if (incomingPermissionsProperties.enabled && incoming.permissionsEnabled) { + val rules = getRules(incoming, endpoints) + val rbacFilter = RBACFilter.newBuilder() + .setRules(rules.actualRules) + .setShadowRules(rules.shadowRules) + .build() - private fun principalForOAuthAndLogUnlistedClients( - principals: MutableList, - unlistedClientsPolicy: Incoming.UnlistedPolicy, - oauth: OAuth? - ): List { - return if (principals.isNotEmpty() && unlistedClientsPolicy == Incoming.UnlistedPolicy.LOG && oauth != null) { - listOf(anyPrincipal) + HttpFilter.newBuilder() + .setName(HTTP_FILTER_NAME) + .setTypedConfig(Any.pack(rbacFilter)).build() } else { - emptyList() + null } - } - - data class Rules(val shadowRules: RBAC, val actualRules: RBAC) private fun getRules( incomingPermissions: Incoming, - snapshot: GlobalSnapshot, - roles: List + snapshotEndpoints: SnapshotResources ): Rules { - - val incomingEndpointsPolicies = getIncomingEndpointPolicies( - incomingPermissions, snapshot, roles + val incomingEndpointsPolicies = incomingEndpointsPoliciesFactory.createIncomingEndpointPolicies( + incomingPermissions, snapshotEndpoints ) - - val restrictedEndpointsPolicies = incomingEndpointsPolicies.asSequence() - .filter { - it.endpoint.unlistedClientsPolicy == Incoming.UnlistedPolicy.BLOCKANDLOG || - it.endpoint.oauth?.policy != null - } - .map { (endpoint, policy) -> "$endpoint" to policy }.toMap() - - val loggedEndpointsPolicies = incomingEndpointsPolicies.asSequence() - .filter { - it.endpoint.unlistedClientsPolicy == Incoming.UnlistedPolicy.LOG && it.endpoint.oauth?.policy == null - } - .map { (endpoint, policy) -> "$endpoint" to policy }.toMap() - + val restrictedEndpointsPolicies = getRestrictedEndpointsPolicies(incomingEndpointsPolicies) + val loggedEndpointsPolicies = getLoggedEndpointsPolicies(incomingEndpointsPolicies) val allowUnlistedPolicies = unlistedAndLoggedEndpointsPolicies( incomingPermissions, (statusRoutePolicy + restrictedEndpointsPolicies).values, loggedEndpointsPolicies.values ) - val shadowPolicies = (statusRoutePolicy + restrictedEndpointsPolicies + loggedEndpointsPolicies) - .map { (endpoint, policy) -> endpoint to policy.build() }.toMap() - - val shadowRules = RBAC.newBuilder() - .setAction(RBAC.Action.ALLOW) - .putAllPolicies(shadowPolicies) - .build() - // build needs to be called before any modifications happen so we do not have to clone it - - val restrictedEndpointsPoliciesWithFullAccessClient = restrictedEndpointsPolicies.mapValues { - addFullAccessClients(it.value) - } - val actualPolicies = - (statusRoutePolicy + restrictedEndpointsPoliciesWithFullAccessClient + allowUnlistedPolicies) - .map { (endpoint, policy) -> endpoint to policy.build() }.toMap() - - val actualRules = RBAC.newBuilder() - .setAction(RBAC.Action.ALLOW) - .putAllPolicies(actualPolicies) - .build() - + val shadowRules = buildShadowRules(restrictedEndpointsPolicies, loggedEndpointsPolicies) + val actualRules = buildActualRules(restrictedEndpointsPolicies, allowUnlistedPolicies) return Rules(shadowRules = shadowRules, actualRules = actualRules) } - private fun addFullAccessClients(policyBuilder: Policy.Builder): Policy.Builder { - return policyBuilder.addAllPrincipals(fullAccessClients.flatMap { clientWithSelector -> - tlsPrincipals(clientWithSelector.name) - }) - } + private fun getRestrictedEndpointsPolicies( + incomingEndpointsPolicies: List + ): Map = + incomingEndpointsPolicies.getFilteredEndpointPolicyMapping { + it.endpoint.unlistedClientsPolicy == Incoming.UnlistedPolicy.BLOCKANDLOG || + it.endpoint.oauth?.policy != null + } + + private fun getLoggedEndpointsPolicies( + incomingEndpointsPolicies: List + ): Map = + incomingEndpointsPolicies.getFilteredEndpointPolicyMapping { + it.endpoint.unlistedClientsPolicy == Incoming.UnlistedPolicy.LOG && it.endpoint.oauth?.policy == null + } private fun unlistedAndLoggedEndpointsPolicies( incomingPermissions: Incoming, @@ -218,6 +124,52 @@ class RBACFilterFactory( } } + private fun buildShadowRules( + restrictedEndpointsPolicies: Map, + loggedEndpointsPolicies: Map + ): RBAC { + val shadowPolicies = buildPolicies(restrictedEndpointsPolicies, loggedEndpointsPolicies) + return RBAC.newBuilder() + .setAction(RBAC.Action.ALLOW) + .putAllPolicies(shadowPolicies) + .build() + // build needs to be called before any modifications happen so we do not have to clone it + } + + private fun buildActualRules( + restrictedEndpointsPolicies: Map, + allowUnlistedPolicies: Map + ): RBAC { + val restrictedEndpointsPoliciesWithFullAccessClient = restrictedEndpointsPolicies.mapValues { + addFullAccessClients(it.value) + } + val actualPolicies = + buildPolicies(restrictedEndpointsPoliciesWithFullAccessClient, allowUnlistedPolicies) + return RBAC.newBuilder() + .setAction(RBAC.Action.ALLOW) + .putAllPolicies(actualPolicies) + .build() + } + + private fun buildPolicies( + restrictedEndpointsPolicies: Map, + loggedEndpointsPolicies: Map + ) = + (statusRoutePolicy + restrictedEndpointsPolicies + loggedEndpointsPolicies) + .map { (endpoint, policy) -> endpoint to policy.build() }.toMap() + + private fun Iterable.getFilteredEndpointPolicyMapping( + predicate: (EndpointWithPolicy) -> Boolean + ) = + asSequence() + .filter(predicate) + .map { (endpoint, policy) -> "$endpoint" to policy }.toMap() + + private fun addFullAccessClients(policyBuilder: Policy.Builder): Policy.Builder = + policyBuilder.addAllPrincipals(fullAccessClients.flatMap { clientWithSelector -> + tlsPrincipals(clientWithSelector.name) + }) + private fun allowUnlistedEndpointsPolicy( allowedEndpointsPolicies: Collection ): Map { @@ -268,193 +220,6 @@ class RBACFilterFactory( } } - private fun resolveClientsWithSelectors( - incomingEndpoint: IncomingEndpoint, - roles: List - ): Collection { - val clients = incomingEndpoint.clients.flatMap { clientOrRole -> - roles.find { it.name == clientOrRole.name }?.clients ?: setOf(clientOrRole) - } - // sorted order ensures that we do not duplicate rules - return clients.toSortedSet() - } - - private fun mapClientWithSelectorToPrincipals( - clientWithSelector: ClientWithSelector, - snapshot: GlobalSnapshot - ): List { - val providerForSelector = jwtProperties.providers.values.firstOrNull { - it.matchings.containsKey(clientWithSelector.name) - } - val selectorMatching = if (providerForSelector == null) { - getSelectorMatching(clientWithSelector, incomingPermissionsProperties) - } else { - null - } - val staticRangesForClient = staticIpRange(clientWithSelector, selectorMatching) - - return if (clientWithSelector.name in incomingServicesSourceAuthentication) { - ipFromDiscoveryPrincipals(clientWithSelector, selectorMatching, snapshot) - } else if (staticRangesForClient != null) { - listOf(staticRangesForClient) - } else if (providerForSelector != null && clientWithSelector.selector != null) { - listOf(jwtClientWithSelectorPrincipal(clientWithSelector, providerForSelector)) - } else { - tlsPrincipals(clientWithSelector.name) - } - } - - private fun oAuthPolicyForEmptyClients(policy: OAuth.Policy?, unlistedPolicy: Incoming.UnlistedPolicy): Principal { - return if (unlistedPolicy == Incoming.UnlistedPolicy.LOG) { - when (policy) { - OAuth.Policy.STRICT -> strictPolicyPrincipal - OAuth.Policy.ALLOW_MISSING -> allowMissingPolicyPrincipal - OAuth.Policy.ALLOW_MISSING_OR_FAILED -> anyPrincipal - null -> denyForAllPrincipal - } - } else { - denyForAllPrincipal - } - } - - private fun mergeWithOAuthPolicy( - client: ClientWithSelector, - principal: Principal, - policy: OAuth.Policy? - ): Principal { - if (client.name in oAuthMatchingsClients) { - return principal // don't merge if client has OAuth selector - } else { - return when (policy) { - OAuth.Policy.ALLOW_MISSING -> { - Principal.newBuilder().setAndIds( - Principal.Set.newBuilder().addAllIds( - listOf( - allowMissingPolicyPrincipal, - principal - ) - ) - ).build() - } - OAuth.Policy.STRICT -> { - Principal.newBuilder().setAndIds( - Principal.Set.newBuilder().addAllIds( - listOf( - strictPolicyPrincipal, - principal - ) - ) - ).build() - } - OAuth.Policy.ALLOW_MISSING_OR_FAILED -> { - principal - } - null -> { - principal - } - } - } - } - - private val strictPolicyPrincipal = Principal.newBuilder().setAndIds( - Principal.Set.newBuilder().addAllIds( - listOf( - Principal.newBuilder().setMetadata( - MetadataMatcher.newBuilder() - .setFilter("envoy.filters.http.header_to_metadata") - .addPath( - MetadataMatcher.PathSegment.newBuilder().setKey("jwt-status").build() - ) - .setValue( - ValueMatcher.newBuilder().setStringMatch(StringMatcher.newBuilder().setExact("present")) - ) - ).build(), - Principal.newBuilder().setMetadata( - MetadataMatcher.newBuilder() - .setFilter("envoy.filters.http.jwt_authn") - .addPath( - MetadataMatcher.PathSegment.newBuilder() - .setKey(jwtProperties.payloadInMetadata) - ).addPath( - MetadataMatcher.PathSegment.newBuilder() - .setKey(jwtProperties.fieldRequiredInToken) - ) - .setValue(ValueMatcher.newBuilder().setPresentMatch(true)).build() - ).build() - ) - ).build() - ).build() - - private val allowMissingPolicyPrincipal = Principal.newBuilder().setOrIds( - Principal.Set.newBuilder().addAllIds( - listOf( - Principal.newBuilder() - .setMetadata( - MetadataMatcher.newBuilder() - .setFilter("envoy.filters.http.header_to_metadata") - .addPath( - MetadataMatcher.PathSegment.newBuilder().setKey("jwt-status").build() - ) - .setValue( - ValueMatcher.newBuilder().setStringMatch(StringMatcher.newBuilder().setExact("missing")) - ) - ) - .build(), - strictPolicyPrincipal - ) - ).build() - ).build() - - private fun jwtClientWithSelectorPrincipal(client: ClientWithSelector, oAuthProvider: OAuthProvider): Principal { - return Principal.newBuilder().setMetadata( - MetadataMatcher.newBuilder() - .setFilter("envoy.filters.http.jwt_authn") - .addPath( - MetadataMatcher.PathSegment.newBuilder() - .setKey(jwtProperties.payloadInMetadata).build() - ) - .addPath( - MetadataMatcher.PathSegment.newBuilder() - .setKey(oAuthProvider.matchings[client.name]).build() - ) - .setValue( - ValueMatcher.newBuilder().setListMatch( - ListMatcher.newBuilder().setOneOf( - ValueMatcher.newBuilder().setStringMatch( - StringMatcher.newBuilder() - .setExact(client.selector) - ) - ) - ) - ).build() - ).build() - } - - private fun getSelectorMatching( - client: ClientWithSelector, - incomingPermissionsProperties: IncomingPermissionsProperties - ): SelectorMatching? { - val matching = incomingPermissionsProperties.selectorMatching[client.name] - if (matching == null && client.selector != null) { - logger.warn( - "No selector matching found for client '${client.name}' with selector '${client.selector}' " + - "in EC properties. Source IP based authentication will not contain additional matching." - ) - return null - } - - return matching - } - - private fun staticIpRange(client: ClientWithSelector, selectorMatching: SelectorMatching?): Principal? { - val ranges = staticIpRanges[client.name] - return if (client.selector != null && selectorMatching != null && ranges != null) { - addAdditionalMatching(client.selector, selectorMatching, ranges) - } else { - ranges - } - } - private fun tlsPrincipals(client: String): List { val stringMatcher = sanUriMatcherFactory.createSanUriMatcher(client) @@ -465,102 +230,6 @@ class RBACFilterFactory( ).build() ) } - - private fun createStaticIpRanges(): Map { - val ranges = incomingPermissionsProperties.sourceIpAuthentication.ipFromRange - - return ranges.mapValues { - val principals = it.value.map { ipWithPrefix -> - val (ip, prefixLength) = ipWithPrefix.split("/") - - Principal.newBuilder().setDirectRemoteIp( - CidrRange.newBuilder() - .setAddressPrefix(ip) - .setPrefixLen(UInt32Value.of(prefixLength.toInt())).build() - ) - .build() - } - - Principal.newBuilder().setOrIds(Principal.Set.newBuilder().addAllIds(principals).build()).build() - } - } - - private fun ipFromDiscoveryPrincipals( - client: ClientWithSelector, - selectorMatching: SelectorMatching?, - snapshot: GlobalSnapshot - ): List { - val clusterLoadAssignment = snapshot.endpoints.resources()[client.name] - val sourceIpPrincipal = mapEndpointsToExactPrincipals(clusterLoadAssignment) - - return if (sourceIpPrincipal == null) { - listOf() - } else if (client.selector != null && selectorMatching != null) { - listOf(addAdditionalMatching(client.selector, selectorMatching, sourceIpPrincipal)) - } else { - listOf(sourceIpPrincipal) - } - } - - private fun mapEndpointsToExactPrincipals(clusterLoadAssignment: ClusterLoadAssignment?): Principal? { - val principals = clusterLoadAssignment?.endpointsList?.flatMap { lbEndpoints -> - lbEndpoints.lbEndpointsList.map { lbEndpoint -> - lbEndpoint.endpoint.address - } - }.orEmpty().map { address -> - Principal.newBuilder() - .setDirectRemoteIp( - CidrRange.newBuilder() - .setAddressPrefix(address.socketAddress.address) - .setPrefixLen(EXACT_IP_MASK).build() - ) - .build() - } - - return if (principals.isNotEmpty()) { - Principal.newBuilder().setOrIds(Principal.Set.newBuilder().addAllIds(principals).build()).build() - } else { - null - } - } - - private fun addAdditionalMatching( - selector: String, - selectorMatching: SelectorMatching, - sourceIpPrincipal: Principal - ): Principal { - return if (selectorMatching.header.isNotEmpty()) { - val additionalMatchingPrincipal = Principal.newBuilder() - .setHeader(HeaderMatcher.newBuilder().setName(selectorMatching.header).setExactMatch(selector)) - .build() - - Principal.newBuilder().setAndIds( - Principal.Set.newBuilder().addAllIds( - listOf(sourceIpPrincipal, additionalMatchingPrincipal) - ).build() - ).build() - } else { - sourceIpPrincipal - } - } - - fun createHttpFilter(group: Group, snapshot: GlobalSnapshot): HttpFilter? { - return if (incomingPermissionsProperties.enabled && group.proxySettings.incoming.permissionsEnabled) { - val rules = getRules( - group.proxySettings.incoming, - snapshot, - group.proxySettings.incoming.roles - ) - - val rbacFilter = RBACFilter.newBuilder() - .setRules(rules.actualRules) - .setShadowRules(rules.shadowRules) - .build() - - HttpFilter.newBuilder().setName("envoy.filters.http.rbac") - .setTypedConfig(Any.pack(rbacFilter)).build() - } else { - null - } - } } + +private data class Rules(val shadowRules: RBAC, val actualRules: RBAC) 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 672b544fc..26fe1f6e4 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 @@ -15,29 +15,45 @@ import pl.allegro.tech.servicemesh.envoycontrol.snapshot.IncomingPermissionsProp import pl.allegro.tech.servicemesh.envoycontrol.snapshot.JwtFilterProperties import pl.allegro.tech.servicemesh.envoycontrol.snapshot.OAuthProvider import pl.allegro.tech.servicemesh.envoycontrol.snapshot.StatusRouteProperties +import pl.allegro.tech.servicemesh.envoycontrol.snapshot.resource.listeners.filters.IncomingEndpointsPoliciesFactory import pl.allegro.tech.servicemesh.envoycontrol.snapshot.resource.listeners.filters.RBACFilterFactory internal class RBACFilterFactoryJwtTest : RBACFilterFactoryTestUtils { - private val jwtProperties = JwtFilterProperties().also { - it.providers = + private val jwtProperties = JwtFilterProperties().apply { + providers = mapOf( "oauth-provider" to OAuthProvider( matchings = mapOf("oauth-prefix" to "authorities") ) ) - it.fieldRequiredInToken = "exp" + fieldRequiredInToken = "exp" } - private val rbacFilterFactoryWithOAuth = RBACFilterFactory( - IncomingPermissionsProperties().also { - it.enabled = true - it.overlappingPathsFix = true + private val rbacFilterFactoryWithOAuth = createRBACFilterFactory( + incomingProperties = { + enabled = true + overlappingPathsFix = true }, - StatusRouteProperties(), - jwtProperties = jwtProperties + jwtFilterProperties = jwtProperties ) + private fun createRBACFilterFactory( + incomingProperties: IncomingPermissionsProperties.() -> Unit, + jwtFilterProperties: JwtFilterProperties + ): RBACFilterFactory { + val incomingProps = IncomingPermissionsProperties().apply(incomingProperties) + val policiesFactory = IncomingEndpointsPoliciesFactory( + incomingProps, + jwtProperties = jwtFilterProperties + ) + return RBACFilterFactory( + incomingProps, + StatusRouteProperties(), + incomingEndpointsPoliciesFactory = policiesFactory + ) + } + val snapshot = GlobalSnapshot( SnapshotResources.create(listOf(), ""), setOf(), @@ -92,7 +108,8 @@ internal class RBACFilterFactoryJwtTest : RBACFilterFactoryTestUtils { // given val selector = "team1" val client = "oauth-prefix" - val oAuthPrincipal = oAuthClientPrincipal(getTokenFieldForClientWithSelector("oauth-provider", client), selector) + val oAuthPrincipal = + oAuthClientPrincipal(getTokenFieldForClientWithSelector("oauth-provider", client), selector) val expectedRbacBuilder = getRBACFilterWithShadowRules( expectedPoliciesForOAuth( oAuthPrincipal, 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 f9f3a8502..50d1d6f33 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 @@ -24,65 +24,62 @@ import pl.allegro.tech.servicemesh.envoycontrol.snapshot.resource.listeners.filt @Suppress("LargeClass") // TODO: https://github.com/allegro/envoy-control/issues/121 internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { - private val rbacFilterFactory = RBACFilterFactory( - IncomingPermissionsProperties().also { - it.enabled = true - it.overlappingPathsFix = true - }, - StatusRouteProperties() + private val rbacFilterFactory = createRBACFilterFactory( + incomingProperties = { + enabled = true + overlappingPathsFix = true + } ) - private val rbacFilterFactoryWithSourceIpAuth = RBACFilterFactory( - IncomingPermissionsProperties().also { - it.enabled = true - it.sourceIpAuthentication = SourceIpAuthenticationProperties().also { ipProperties -> - ipProperties.ipFromServiceDiscovery.enabledForIncomingServices = listOf("client1") - } - }, - StatusRouteProperties() + + private val rbacFilterFactoryWithSourceIpAuth = createRBACFilterFactory( + incomingProperties = { + enabled = true + sourceIpAuthentication = SourceIpAuthenticationProperties().also { ipProperties -> + ipProperties.ipFromServiceDiscovery.enabledForIncomingServices = listOf("client1") + } + } ) - private val rbacFilterFactoryWithStaticRange = RBACFilterFactory( - IncomingPermissionsProperties().also { - it.enabled = true - it.sourceIpAuthentication = SourceIpAuthenticationProperties().also { ipProperties -> - ipProperties.ipFromRange = mutableMapOf( - "client1" to setOf("192.168.1.0/24", "192.168.2.0/28") - ) - } - }, - StatusRouteProperties() + private val rbacFilterFactoryWithStaticRange = createRBACFilterFactory( + incomingProperties = { + enabled = true + sourceIpAuthentication = SourceIpAuthenticationProperties().also { ipProperties -> + ipProperties.ipFromRange = mutableMapOf( + "client1" to setOf("192.168.1.0/24", "192.168.2.0/28") + ) + } + } ) - private val rbacFilterFactoryWithStaticRangeAndSourceIpAuth = RBACFilterFactory( - IncomingPermissionsProperties().also { - it.enabled = true - it.sourceIpAuthentication = SourceIpAuthenticationProperties().also { ipProperties -> - ipProperties.ipFromServiceDiscovery.enabledForIncomingServices = listOf("client1") - ipProperties.ipFromRange = mutableMapOf("client2" to setOf("192.168.1.0/24", "192.168.2.0/28")) - } - }, - StatusRouteProperties() + + private val rbacFilterFactoryWithStaticRangeAndSourceIpAuth = createRBACFilterFactory( + incomingProperties = { + enabled = true + sourceIpAuthentication = SourceIpAuthenticationProperties().also { ipProperties -> + ipProperties.ipFromServiceDiscovery.enabledForIncomingServices = listOf("client1") + ipProperties.ipFromRange = mutableMapOf("client2" to setOf("192.168.1.0/24", "192.168.2.0/28")) + } + } ) - private val rbacFilterFactoryWithSourceIpWithSelectorAuth = RBACFilterFactory( - IncomingPermissionsProperties().also { - it.enabled = true - it.sourceIpAuthentication = SourceIpAuthenticationProperties().also { ipProperties -> + + private val rbacFilterFactoryWithSourceIpWithSelectorAuth = createRBACFilterFactory( + incomingProperties = { + enabled = true + sourceIpAuthentication = SourceIpAuthenticationProperties().also { ipProperties -> ipProperties.ipFromServiceDiscovery.enabledForIncomingServices = listOf("client1") ipProperties.ipFromRange = mutableMapOf( "client2" to setOf("192.168.1.0/24", "192.168.2.0/28") ) } - it.selectorMatching = mutableMapOf( + selectorMatching = mutableMapOf( "client1" to SelectorMatching().also { it.header = "x-secret-header" }, "client2" to SelectorMatching().also { it.header = "x-secret-header" } ) - }, - StatusRouteProperties() + } ) - private val rbacFilterFactoryWithAllowAllEndpointsForClient = RBACFilterFactory( - IncomingPermissionsProperties().also { - it.enabled = true - it.clientsAllowedToAllEndpoints = mutableListOf("allowed-client") - }, - StatusRouteProperties() + private val rbacFilterFactoryWithAllowAllEndpointsForClient = createRBACFilterFactory( + incomingProperties = { + enabled = true + clientsAllowedToAllEndpoints = mutableListOf("allowed-client") + } ) val snapshot = GlobalSnapshot( @@ -96,18 +93,23 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { ) val clusterLoadAssignment = ClusterLoadAssignment.newBuilder() - .setClusterName("client1") - .addEndpoints(LocalityLbEndpoints.newBuilder() - .addLbEndpoints(LbEndpoint.newBuilder() - .setEndpoint(Endpoint.newBuilder() - .setAddress(Address.newBuilder() - .setSocketAddress(SocketAddress.newBuilder() - .setAddress("127.0.0.1") - ) - ) - ) - ) - ).build() + .setClusterName("client1") + .addEndpoints( + LocalityLbEndpoints.newBuilder() + .addLbEndpoints( + LbEndpoint.newBuilder() + .setEndpoint( + Endpoint.newBuilder() + .setAddress( + Address.newBuilder() + .setSocketAddress( + SocketAddress.newBuilder() + .setAddress("127.0.0.1") + ) + ) + ) + ) + ).build() val snapshotForSourceIpAuth = GlobalSnapshot( SnapshotResources.create(listOf(), ""), @@ -122,9 +124,9 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { @Test fun `should create RBAC filter with status route permissions when no incoming permissions are defined`() { // given - val rbacFilterFactoryWithStatusRoute = RBACFilterFactory( - IncomingPermissionsProperties().also { it.enabled = true }, - StatusRouteProperties().also { it.enabled = true; it.endpoints = mutableListOf(EndpointMatch()) } + val rbacFilterFactoryWithStatusRoute = createRBACFilterFactory( + incomingProperties = { enabled = true }, + statusRouteProperties = { enabled = true; endpoints = mutableListOf(EndpointMatch()) } ) val incomingPermission = Incoming(permissionsEnabled = true) val expectedRbacBuilder = getRBACFilter(expectedStatusRoutePermissionsJson) @@ -139,12 +141,15 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { @Test fun `should create RBAC filter with two status routes permissions when no incoming permissions are defined`() { // given - val rbacFilterFactoryWithStatusRoute = RBACFilterFactory( - IncomingPermissionsProperties().also { it.enabled = true }, - StatusRouteProperties().also { it.enabled = true; it.endpoints = - mutableListOf( + val rbacFilterFactoryWithStatusRoute = createRBACFilterFactory( + incomingProperties = { enabled = true }, + statusRouteProperties = { + enabled = true; + endpoints = mutableListOf( EndpointMatch(), - EndpointMatch().also { endpoint -> endpoint.path = "/example-endpoint/"; endpoint.matchingType = PathMatchingType.PATH } + EndpointMatch().also { endpoint -> + endpoint.path = "/example-endpoint/"; endpoint.matchingType = PathMatchingType.PATH + } ) } ) @@ -199,8 +204,8 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { // given val expectedRbacBuilder = getRBACFilter(expectedEmptyEndpointPermissions) val incomingPermission = Incoming( - permissionsEnabled = true, - endpoints = listOf() + permissionsEnabled = true, + endpoints = listOf() ) // when @@ -226,7 +231,7 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { permissionsEnabled = true, endpoints = listOf( IncomingEndpoint( - "/example", + "/example", PathMatchingType.PATH, setOf("GET", "POST"), setOf(ClientWithSelector("client1"), ClientWithSelector("client2")), @@ -252,21 +257,21 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { val expectedShadowRules = expectedSimpleEndpointPermissionsJson(policyName) val expectedRbacBuilder = getRBACFilterWithShadowRules( - expectedUnlistedClientsPermissions, - expectedShadowRules + expectedUnlistedClientsPermissions, + expectedShadowRules ) val incomingPermission = Incoming( - permissionsEnabled = true, - endpoints = listOf( - IncomingEndpoint( - "/example", - PathMatchingType.PATH, - setOf("GET", "POST"), - setOf(ClientWithSelector("client1"), ClientWithSelector("client2")), - Incoming.UnlistedPolicy.LOG - ) - ), - unlistedEndpointsPolicy = Incoming.UnlistedPolicy.BLOCKANDLOG + permissionsEnabled = true, + endpoints = listOf( + IncomingEndpoint( + "/example", + PathMatchingType.PATH, + setOf("GET", "POST"), + setOf(ClientWithSelector("client1"), ClientWithSelector("client2")), + Incoming.UnlistedPolicy.LOG + ) + ), + unlistedEndpointsPolicy = Incoming.UnlistedPolicy.BLOCKANDLOG ) // when @@ -285,21 +290,21 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { val expectedShadowRules = expectedSimpleEndpointPermissionsJson(policyName) val expectedRbacBuilder = getRBACFilterWithShadowRules( - expectedEndpointPermissionsLogUnlistedEndpointsAndBlockUnlistedClients, - expectedShadowRules + expectedEndpointPermissionsLogUnlistedEndpointsAndBlockUnlistedClients, + expectedShadowRules ) val incomingPermission = Incoming( - permissionsEnabled = true, - endpoints = listOf( - IncomingEndpoint( - "/example", - PathMatchingType.PATH, - setOf("GET", "POST"), - setOf(ClientWithSelector("client1"), ClientWithSelector("client2")), - Incoming.UnlistedPolicy.BLOCKANDLOG - ) - ), - unlistedEndpointsPolicy = Incoming.UnlistedPolicy.LOG + permissionsEnabled = true, + endpoints = listOf( + IncomingEndpoint( + "/example", + PathMatchingType.PATH, + setOf("GET", "POST"), + setOf(ClientWithSelector("client1"), ClientWithSelector("client2")), + Incoming.UnlistedPolicy.BLOCKANDLOG + ) + ), + unlistedEndpointsPolicy = Incoming.UnlistedPolicy.LOG ) // when @@ -316,13 +321,15 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { "clients=[ClientWithSelector(name=role-1, selector=null)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)" val expectedRbacBuilder = getRBACFilter(expectedSimpleEndpointPermissionsJson(policyName)) val incomingPermission = Incoming( - permissionsEnabled = true, - endpoints = listOf(IncomingEndpoint( - "/example", - PathMatchingType.PATH, - setOf("GET", "POST"), - setOf(ClientWithSelector("role-1")) - )), roles = listOf(Role("role-1", setOf(ClientWithSelector("client1"), ClientWithSelector("client2")))) + permissionsEnabled = true, + endpoints = listOf( + IncomingEndpoint( + "/example", + PathMatchingType.PATH, + setOf("GET", "POST"), + setOf(ClientWithSelector("role-1")) + ) + ), roles = listOf(Role("role-1", setOf(ClientWithSelector("client1"), ClientWithSelector("client2")))) ) // when @@ -337,24 +344,30 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { // given val expectedRbacBuilder = getRBACFilter(expectedDuplicatedRole) val incomingPermission = Incoming( - permissionsEnabled = true, - endpoints = listOf(IncomingEndpoint( - "/example", - PathMatchingType.PATH, - setOf("GET", "POST"), - setOf( - ClientWithSelector("client1"), - ClientWithSelector("client1"), - ClientWithSelector("client1", "selector"), - ClientWithSelector("client1-duplicated", "selector"), - ClientWithSelector("client1-duplicated"), - ClientWithSelector("role-1") - ) - )), roles = listOf(Role("role-1", setOf( + permissionsEnabled = true, + endpoints = listOf( + IncomingEndpoint( + "/example", + PathMatchingType.PATH, + setOf("GET", "POST"), + setOf( + ClientWithSelector("client1"), + ClientWithSelector("client1"), + ClientWithSelector("client1", "selector"), + ClientWithSelector("client1-duplicated", "selector"), + ClientWithSelector("client1-duplicated"), + ClientWithSelector("role-1") + ) + ) + ), roles = listOf( + Role( + "role-1", setOf( ClientWithSelector("client1-duplicated"), ClientWithSelector("client1-duplicated"), - ClientWithSelector("client2")) - )) + ClientWithSelector("client2") + ) + ) + ) ) // when @@ -369,18 +382,20 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { // given val expectedRbacBuilder = getRBACFilter(expectedEndpointPermissionsWithDifferentRulesForDifferentClientsJson) val incomingPermission = Incoming( - permissionsEnabled = true, - endpoints = listOf(IncomingEndpoint( - "/example", - PathMatchingType.PATH, - setOf("GET"), - setOf(ClientWithSelector("client1")) + permissionsEnabled = true, + endpoints = listOf( + IncomingEndpoint( + "/example", + PathMatchingType.PATH, + setOf("GET"), + setOf(ClientWithSelector("client1")) ), IncomingEndpoint( - "/example2", - PathMatchingType.PATH, - setOf("POST"), - setOf(ClientWithSelector("client2")) - )) + "/example2", + PathMatchingType.PATH, + setOf("POST"), + setOf(ClientWithSelector("client2")) + ) + ) ) // when @@ -394,22 +409,26 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { fun `should generate minimal RBAC rules for incoming permissions with roles and clients`() { // given val incomingPermission = Incoming( - permissionsEnabled = true, - endpoints = listOf(IncomingEndpoint( - "/example", - PathMatchingType.PATH, - setOf("GET", "POST"), - setOf(ClientWithSelector("role-1")) + permissionsEnabled = true, + endpoints = listOf( + IncomingEndpoint( + "/example", + PathMatchingType.PATH, + setOf("GET", "POST"), + setOf(ClientWithSelector("role-1")) ), IncomingEndpoint( - "/example2", - PathMatchingType.PATH, - setOf("GET", "POST"), - setOf(ClientWithSelector("client2"), ClientWithSelector("client1")) - )), roles = listOf(Role("role-1", setOf(ClientWithSelector("client1"), ClientWithSelector("client2")))) + "/example2", + PathMatchingType.PATH, + setOf("GET", "POST"), + setOf(ClientWithSelector("client2"), ClientWithSelector("client1")) + ) + ), roles = listOf(Role("role-1", setOf(ClientWithSelector("client1"), ClientWithSelector("client2")))) + ) + val expectedRbacBuilder = getRBACFilter( + expectedTwoClientsSimpleEndpointPermissionsJson( + "${incomingPermission.endpoints[0]}", "${incomingPermission.endpoints[1]}" + ) ) - val expectedRbacBuilder = getRBACFilter(expectedTwoClientsSimpleEndpointPermissionsJson( - "${incomingPermission.endpoints[0]}", "${incomingPermission.endpoints[1]}" - )) // when val generated = rbacFilterFactory.createHttpFilter(createGroup(incomingPermission), snapshot) @@ -422,22 +441,30 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { fun `should generate minimal RBAC rules for incoming permissions with roles and single client`() { // given val incomingPermission = Incoming( - permissionsEnabled = true, - endpoints = listOf(IncomingEndpoint( - "/example", - PathMatchingType.PATH, - setOf("GET", "POST"), - setOf(ClientWithSelector("client2"), ClientWithSelector("role-1")) + permissionsEnabled = true, + endpoints = listOf( + IncomingEndpoint( + "/example", + PathMatchingType.PATH, + setOf("GET", "POST"), + setOf(ClientWithSelector("client2"), ClientWithSelector("role-1")) ), IncomingEndpoint( - "/example2", - PathMatchingType.PATH, - setOf("GET", "POST"), - setOf(ClientWithSelector("role-2"), ClientWithSelector("client1")) - )), roles = listOf(Role("role-1", setOf(ClientWithSelector("client1"))), Role("role-2", setOf(ClientWithSelector("client2")))) + "/example2", + PathMatchingType.PATH, + setOf("GET", "POST"), + setOf(ClientWithSelector("role-2"), ClientWithSelector("client1")) + ) + ), + roles = listOf( + Role("role-1", setOf(ClientWithSelector("client1"))), + Role("role-2", setOf(ClientWithSelector("client2"))) + ) + ) + val expectedRbacBuilder = getRBACFilter( + expectedTwoClientsSimpleEndpointPermissionsJson( + "${incomingPermission.endpoints[0]}", "${incomingPermission.endpoints[1]}" + ) ) - val expectedRbacBuilder = getRBACFilter(expectedTwoClientsSimpleEndpointPermissionsJson( - "${incomingPermission.endpoints[0]}", "${incomingPermission.endpoints[1]}" - )) // when val generated = rbacFilterFactory.createHttpFilter(createGroup(incomingPermission), snapshot) @@ -451,17 +478,20 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { // given val expectedRbacBuilder = getRBACFilter(expectedSourceIpAuthPermissionsJson) val incomingPermission = Incoming( - permissionsEnabled = true, - endpoints = listOf(IncomingEndpoint( - "/example", - PathMatchingType.PATH, - setOf("GET", "POST"), - setOf(ClientWithSelector("client1"), ClientWithSelector("client2")) - )) + permissionsEnabled = true, + endpoints = listOf( + IncomingEndpoint( + "/example", + PathMatchingType.PATH, + setOf("GET", "POST"), + setOf(ClientWithSelector("client1"), ClientWithSelector("client2")) + ) + ) ) // when - val generated = rbacFilterFactoryWithSourceIpAuth.createHttpFilter(createGroup(incomingPermission), snapshotForSourceIpAuth) + val generated = + rbacFilterFactoryWithSourceIpAuth.createHttpFilter(createGroup(incomingPermission), snapshotForSourceIpAuth) // then assertThat(generated).isEqualTo(expectedRbacBuilder) @@ -471,12 +501,14 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { fun `should generate RBAC rules for incoming permissions without clients`() { // given val incomingPermission = Incoming( - permissionsEnabled = true, - endpoints = listOf(IncomingEndpoint( - "/example", - PathMatchingType.PATH, - setOf() - )) + permissionsEnabled = true, + endpoints = listOf( + IncomingEndpoint( + "/example", + PathMatchingType.PATH, + setOf() + ) + ) ) val expectedPolicies = expectedDenyForAllEndpointPermissions(policyName = "${incomingPermission.endpoints[0]}") val expectedRbacBuilder = getRBACFilter(expectedPolicies) @@ -523,11 +555,13 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { val incomingPermissions = Incoming( permissionsEnabled = true, unlistedEndpointsPolicy = Incoming.UnlistedPolicy.LOG, - endpoints = listOf(IncomingEndpoint( - path = "/example", - clients = setOf(), - unlistedClientsPolicy = Incoming.UnlistedPolicy.BLOCKANDLOG - )) + endpoints = listOf( + IncomingEndpoint( + path = "/example", + clients = setOf(), + unlistedClientsPolicy = Incoming.UnlistedPolicy.BLOCKANDLOG + ) + ) ) val expectedShadow = expectedDenyForAllEndpointPermissions(policyName = "${incomingPermissions.endpoints[0]}") @@ -565,11 +599,13 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { val incomingPermissions = Incoming( permissionsEnabled = true, unlistedEndpointsPolicy = Incoming.UnlistedPolicy.BLOCKANDLOG, - endpoints = listOf(IncomingEndpoint( - path = "/example", - clients = setOf(), - unlistedClientsPolicy = Incoming.UnlistedPolicy.LOG - )) + endpoints = listOf( + IncomingEndpoint( + path = "/example", + clients = setOf(), + unlistedClientsPolicy = Incoming.UnlistedPolicy.LOG + ) + ) ) val expectedShadow = expectedDenyForAllEndpointPermissions(policyName = "${incomingPermissions.endpoints[0]}") @@ -587,13 +623,15 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { // given val expectedRbacBuilder = getRBACFilter(expectedSourceIpAuthWithStaticRangeJson) val incomingPermission = Incoming( - permissionsEnabled = true, - endpoints = listOf(IncomingEndpoint( - "/example", - PathMatchingType.PATH, - setOf("GET"), - setOf(ClientWithSelector("client1")) - )) + permissionsEnabled = true, + endpoints = listOf( + IncomingEndpoint( + "/example", + PathMatchingType.PATH, + setOf("GET"), + setOf(ClientWithSelector("client1")) + ) + ) ) // when @@ -608,19 +646,21 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { // given val expectedRbacBuilder = getRBACFilter(expectedSourceIpAuthWithStaticRangeAndSourceIpJson) val incomingPermission = Incoming( - permissionsEnabled = true, - endpoints = listOf(IncomingEndpoint( - "/example", - PathMatchingType.PATH, - setOf("GET"), - setOf(ClientWithSelector("client1"), ClientWithSelector("client2")) - )) + permissionsEnabled = true, + endpoints = listOf( + IncomingEndpoint( + "/example", + PathMatchingType.PATH, + setOf("GET"), + setOf(ClientWithSelector("client1"), ClientWithSelector("client2")) + ) + ) ) // when val generated = rbacFilterFactoryWithStaticRangeAndSourceIpAuth.createHttpFilter( - createGroup(incomingPermission), - snapshotForSourceIpAuth + createGroup(incomingPermission), + snapshotForSourceIpAuth ) // then @@ -632,19 +672,21 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { // given val expectedRbacBuilder = getRBACFilter(expectedSourceIpWithSelectorAuthPermissionsJson) val incomingPermission = Incoming( - permissionsEnabled = true, - endpoints = listOf(IncomingEndpoint( - "/example", - PathMatchingType.PATH, - setOf("GET"), - setOf(ClientWithSelector("client2", "selector")) - )) + permissionsEnabled = true, + endpoints = listOf( + IncomingEndpoint( + "/example", + PathMatchingType.PATH, + setOf("GET"), + setOf(ClientWithSelector("client2", "selector")) + ) + ) ) // when val generated = rbacFilterFactoryWithSourceIpWithSelectorAuth.createHttpFilter( - createGroup(incomingPermission), - snapshotForSourceIpAuth + createGroup(incomingPermission), + snapshotForSourceIpAuth ) // then @@ -656,19 +698,21 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { // given val expectedRbacBuilder = getRBACFilter(expectedSourceIpFromDiscoveryWithSelectorAuthPermissionsJson) val incomingPermission = Incoming( - permissionsEnabled = true, - endpoints = listOf(IncomingEndpoint( - "/example", - PathMatchingType.PATH, - setOf("GET"), - setOf(ClientWithSelector("client1", "selector")) - )) + permissionsEnabled = true, + endpoints = listOf( + IncomingEndpoint( + "/example", + PathMatchingType.PATH, + setOf("GET"), + setOf(ClientWithSelector("client1", "selector")) + ) + ) ) // when val generated = rbacFilterFactoryWithSourceIpWithSelectorAuth.createHttpFilter( - createGroup(incomingPermission), - snapshotForSourceIpAuth + createGroup(incomingPermission), + snapshotForSourceIpAuth ) // then @@ -680,23 +724,29 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { // given val expectedRbacBuilder = getRBACFilter(expectedSourceIpWithStaticRangeAndSelectorAuthPermissionsAndRolesJson) val incomingPermission = Incoming( - permissionsEnabled = true, - endpoints = listOf(IncomingEndpoint( - "/example", - PathMatchingType.PATH, - setOf("GET"), - setOf(ClientWithSelector("role1")) - )), - roles = listOf(Role("role1", setOf( + permissionsEnabled = true, + endpoints = listOf( + IncomingEndpoint( + "/example", + PathMatchingType.PATH, + setOf("GET"), + setOf(ClientWithSelector("role1")) + ) + ), + roles = listOf( + Role( + "role1", setOf( ClientWithSelector("client1", "selector1"), - ClientWithSelector("client2", "selector2")) - )) + ClientWithSelector("client2", "selector2") + ) + ) + ) ) // when val generated = rbacFilterFactoryWithSourceIpWithSelectorAuth.createHttpFilter( - createGroup(incomingPermission), - snapshotForSourceIpAuth + createGroup(incomingPermission), + snapshotForSourceIpAuth ) // then @@ -706,15 +756,18 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { @Test fun `should generate RBAC rules for incoming permissions with client allowed to all endpoints`() { // given - val expectedRbacBuilder = getRBACFilterWithShadowRules(expectedRulesForAllowedClient, expectedShadowRulesForAllowedClient) + val expectedRbacBuilder = + getRBACFilterWithShadowRules(expectedRulesForAllowedClient, expectedShadowRulesForAllowedClient) val incomingPermission = Incoming( permissionsEnabled = true, - endpoints = listOf(IncomingEndpoint( - "/example", - PathMatchingType.PATH, - setOf("GET"), - setOf(ClientWithSelector("client1")) - )) + endpoints = listOf( + IncomingEndpoint( + "/example", + PathMatchingType.PATH, + setOf("GET"), + setOf(ClientWithSelector("client1")) + ) + ) ) // when @@ -1187,7 +1240,10 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { } } ], "principals": [ - { "orIds": { "ids": [${principalSourceIp("192.168.1.0", 24)}, ${principalSourceIp("192.168.2.0", 28)}] } } + { "orIds": { "ids": [${principalSourceIp("192.168.1.0", 24)}, ${principalSourceIp( + "192.168.2.0", + 28 + )}] } } ] } } @@ -1319,4 +1375,19 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { private val expectedShadowRulesForAllowedClient = expectedPoliciesForAllowedClient( authenticatedPrincipal("client1") ) + + private fun createRBACFilterFactory( + incomingProperties: IncomingPermissionsProperties.() -> Unit, + statusRouteProperties: StatusRouteProperties.() -> Unit = {} + ): RBACFilterFactory { + val incomingProps = IncomingPermissionsProperties().apply(incomingProperties) + val policiesFactory = IncomingEndpointsPoliciesFactory( + incomingProps + ) + return RBACFilterFactory( + incomingProps, + StatusRouteProperties().apply(statusRouteProperties), + incomingEndpointsPoliciesFactory = policiesFactory + ) + } } From 2b58006e19f316cd2b8fd951229d685103263286 Mon Sep 17 00:00:00 2001 From: Damian Hanusiak Date: Fri, 15 Oct 2021 20:17:50 +0200 Subject: [PATCH 2/2] Create TestPoliciesDsl for better readability in test --- .../filters/rbac/RBACFilterFactoryTest.kt | 54 +---- .../rbac/RBACFilterFactoryTestUtils.kt | 4 + .../listeners/filters/rbac/TestPoliciesDsl.kt | 186 ++++++++++++++++++ 3 files changed, 200 insertions(+), 44 deletions(-) create mode 100644 envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/rbac/TestPoliciesDsl.kt 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 50d1d6f33..122086bfe 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 @@ -780,53 +780,19 @@ internal class RBACFilterFactoryTest : RBACFilterFactoryTestUtils { assertThat(generated).isEqualTo(expectedRbacBuilder) } - private val expectedEndpointPermissionsWithDifferentRulesForDifferentClientsJson = """ - { - "policies": { - "IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[GET], clients=[ClientWithSelector(name=client1, selector=null)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { - "permissions": [ - { - "and_rules": { - "rules": [ - ${pathRule("/example")}, - { - "or_rules": { - "rules": [ - ${methodRule("GET")} - ] - } - } - ] - } - } - ], "principals": [ - ${authenticatedPrincipal("client1")} - ] - }, - "IncomingEndpoint(path=/example2, pathMatchingType=PATH, methods=[POST], clients=[ClientWithSelector(name=client2, selector=null)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)": { - "permissions": [ - { - "and_rules": { - "rules": [ - ${pathRule("/example2")}, - { - "or_rules": { - "rules": [ - ${methodRule("POST")} - ] - } - } - ] - } - } - ], "principals": [ - ${authenticatedPrincipal("client2")} - ] + private val expectedEndpointPermissionsWithDifferentRulesForDifferentClientsJson = + createPolicies { + policy("IncomingEndpoint(path=/example, pathMatchingType=PATH, methods=[GET], clients=[ClientWithSelector(name=client1, selector=null)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)") { + permission { pathRule("/example") and (methodRule("GET") or emptyRule()) } + principal { authenticatedPrincipal("client1") } + } + policy("IncomingEndpoint(path=/example2, pathMatchingType=PATH, methods=[POST], clients=[ClientWithSelector(name=client2, selector=null)], unlistedClientsPolicy=BLOCKANDLOG, oauth=null)") { + permission { pathRule("/example2") and (methodRule("POST") or emptyRule()) } + principal { authenticatedPrincipal("client2") } } - } } - """ + // TODO move jsons to dsl private val expectedSourceIpFromDiscoveryWithSelectorAuthPermissionsJson = """ { "policies": { diff --git a/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/rbac/RBACFilterFactoryTestUtils.kt b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/rbac/RBACFilterFactoryTestUtils.kt index d03db40e6..9d540acdd 100644 --- a/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/rbac/RBACFilterFactoryTestUtils.kt +++ b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/rbac/RBACFilterFactoryTestUtils.kt @@ -109,4 +109,8 @@ interface RBACFilterFactoryTestUtils { incoming = incomingPermission ?: Incoming() ) ) + + fun createPolicies(closure: TestPoliciesBuilderScope.() -> Unit): String { + return TestPoliciesBuilderScope().apply(closure).toString() + } } diff --git a/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/rbac/TestPoliciesDsl.kt b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/rbac/TestPoliciesDsl.kt new file mode 100644 index 000000000..90e911510 --- /dev/null +++ b/envoy-control-core/src/test/kotlin/pl/allegro/tech/servicemesh/envoycontrol/snapshot/resource/listeners/filters/rbac/TestPoliciesDsl.kt @@ -0,0 +1,186 @@ +package pl.allegro.tech.servicemesh.envoycontrol.snapshot.resource.listeners.filters.rbac + +@DslMarker +annotation class TestPoliciesDsl + +@TestPoliciesDsl +class TestPoliciesBuilderScope { + private val policies: MutableList = mutableListOf() + + fun policy(name: String, closure: TestPolicyScope.() -> Unit = {}) { + policies.add(TestPolicyScope(name).apply(closure)) + } + + override fun toString(): String { + val policiesString = policies.joinToString(",") { + it.toString() + } + return """ + { + "policies": { + $policiesString + } + } + """ + } +} + +@TestPoliciesDsl +class TestPolicyScope(private val policyName: String) { + private val permissionRules: MutableList = mutableListOf() + private val principals: MutableList = mutableListOf() + + fun permission(closure: TestPermissionScope.() -> TestPermissionRule) { + permissionRules.add(TestPermissionScope().run(closure)) + } + + fun principal(closure: TestPrincipalScope.() -> Unit) { + principals.add(TestPrincipalScope().apply(closure)) + } + + override fun toString(): String { + val permissionRulesString = permissionRules + .joinToString(",") { it.toString() } + val principalsString = principals.map { it.toString() } + .filter { it.isNotEmpty() }.joinToString(",") + return """ + "$policyName": { + "permissions": [ + $permissionRulesString + ], + "principals": [ + $principalsString + ] + } + """ + } +} + +@TestPoliciesDsl +class TestPrincipalScope { + private var principalValue = "" + + fun anyTrue() { + principalValue = """{ "any": "true"}""" + } + + fun authenticatedPrincipal(value: String) { + principalValue = """{ + "authenticated": { + "principal_name": { + "exact": "spiffe://$value" + } + } + }""" + } + + override fun toString(): String { + return principalValue + } +} + +@TestPoliciesDsl +class TestPermissionScope { + fun pathRule(path: String): TestPermissionRule = + TestPermissionSingleRule( + """{ + "url_path": { + "path": { + "exact": "$path" + } + } + }""" + ) + + fun methodRule(method: String): TestPermissionRule = + TestPermissionSingleRule("""{ + "header": { + "name": ":method", + "exact_match": "$method" + } + }""") + + fun emptyRule(): TestPermissionRule = + TestPermissionEmptyRule() +} + +interface TestPermissionRule { + operator fun not(): TestPermissionRule = + TestPermissionSingleRule( + """{ + "not_rule": $this + }""" + ) + + infix fun and(rule: TestPermissionRule): TestPermissionRule + infix fun or(rule: TestPermissionRule): TestPermissionRule +} + +class TestPermissionSingleRule(private val ruleString: String) : TestPermissionRule { + override infix fun and(rule: TestPermissionRule): TestPermissionRule = + TestPermissionAndRule(listOf(ruleString, rule.toString())) + + override fun or(rule: TestPermissionRule): TestPermissionRule = + TestPermissionOrRule(listOf(ruleString, rule.toString())) + + override fun toString(): String { + return ruleString + } +} + +class TestPermissionEmptyRule: TestPermissionRule { + override fun and(rule: TestPermissionRule): TestPermissionRule = + TestPermissionAndRule(listOf(rule.toString())) + + override fun or(rule: TestPermissionRule): TestPermissionRule = + TestPermissionOrRule(listOf(rule.toString())) + + override fun toString(): String { + return "{}" + } +} + +class TestPermissionAndRule(private val rules: List) : TestPermissionRule { + + override fun and(rule: TestPermissionRule): TestPermissionRule = + TestPermissionAndRule(rules + rule.toString()) + + override fun or(rule: TestPermissionRule): TestPermissionRule = + TestPermissionOrRule(listOf(this.toString(), rule.toString())) + + override fun toString(): String { + val rulesString = rules.filter { it !== "{}" && it.isNotEmpty() }.joinToString(",") + return """ + { + "and_rules": { + "rules": [ + $rulesString + ] + } + } + """ + } +} + +class TestPermissionOrRule(private val rules: List) : TestPermissionRule { + + override fun and(rule: TestPermissionRule): TestPermissionRule = + TestPermissionAndRule(listOf(this.toString(), rule.toString())) + + override fun or(rule: TestPermissionRule): TestPermissionRule = + TestPermissionOrRule(rules + rule.toString()) + + override fun toString(): String { + val rulesString = rules.filter { it !== "{}" && it.isNotEmpty() }.joinToString(",") + return """ + { + "or_rules": { + "rules": [ + $rulesString + ] + } + } + """ + } +} +