blob: f39e1acda55b10cccdb686ae3d772c2aa5975c06 [file] [log] [blame]
# Copyright 2021 gRPC authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import datetime
import time
from typing import Optional
from absl import flags
from absl.testing import absltest
import grpc
from framework import xds_k8s_testcase
from framework.helpers import skips
flags.adopt_module_key_flags(xds_k8s_testcase)
# Type aliases
_XdsTestServer = xds_k8s_testcase.XdsTestServer
_XdsTestClient = xds_k8s_testcase.XdsTestClient
_SecurityMode = xds_k8s_testcase.SecurityXdsKubernetesTestCase.SecurityMode
_Lang = skips.Lang
# The client generates QPS even when it is still loading information from xDS.
# Once it finally connects there will be an outpouring of the bufferred RPCs and
# the server needs time to chew through the backlog, especially since it is
# still a new process and so probably interpreted. The server on one run
# processed 225 RPCs a second, so with the client configured for 25 qps this is
# 40 seconds worth of buffering before starting to drain the backlog.
_SETTLE_DURATION = datetime.timedelta(seconds=5)
_SAMPLE_DURATION = datetime.timedelta(seconds=0.5)
class AuthzTest(xds_k8s_testcase.SecurityXdsKubernetesTestCase):
RPC_TYPE_CYCLE = {
'UNARY_CALL': 'EMPTY_CALL',
'EMPTY_CALL': 'UNARY_CALL',
}
@staticmethod
def is_supported(config: skips.TestConfig) -> bool:
# Per "Authorization (RBAC)" in
# https://github.com/grpc/grpc/blob/master/doc/grpc_xds_features.md
if config.client_lang in _Lang.CPP | _Lang.PYTHON:
return config.version_gte('v1.47.x')
elif config.client_lang in _Lang.GO | _Lang.JAVA:
return config.version_gte('v1.42.x')
elif config.client_lang == _Lang.NODE:
return False
return True
def setUp(self):
super().setUp()
self.next_rpc_type: Optional[int] = None
def authz_rules(self):
return [
{
"destinations": {
"hosts": [f"*:{self.server_xds_port}"],
"ports": [self.server_port],
"httpHeaderMatch": {
"headerName": "test",
"regexMatch": "host-wildcard",
},
},
},
{
"destinations": {
"hosts": [f"*:{self.server_xds_port}"],
"ports": [self.server_port],
"httpHeaderMatch": {
"headerName": "test",
"regexMatch": "header-regex-a+",
},
},
},
{
"destinations": [{
"hosts": [f"{self.server_xds_host}:{self.server_xds_port}"],
"ports": [self.server_port],
"httpHeaderMatch": {
"headerName": "test",
"regexMatch": "host-match1",
},
}, {
"hosts": [
f"a-not-it.com:{self.server_xds_port}",
f"{self.server_xds_host}:{self.server_xds_port}",
"z-not-it.com:1",
],
"ports": [1, self.server_port, 65535],
"httpHeaderMatch": {
"headerName": "test",
"regexMatch": "host-match2",
},
}],
},
{
"destinations": {
"hosts": [
f"not-the-host:{self.server_xds_port}",
"not-the-host",
],
"ports": [self.server_port],
"httpHeaderMatch": {
"headerName": "test",
"regexMatch": "never-match-host",
},
},
},
{
"destinations": {
"hosts": [f"*:{self.server_xds_port}"],
"ports": [1],
"httpHeaderMatch": {
"headerName": "test",
"regexMatch": "never-match-port",
},
},
},
# b/202058316. The wildcard principal is generating invalid config
# {
# "sources": {
# "principals": ["*"],
# },
# "destinations": {
# "hosts": [f"*:{self.server_xds_port}"],
# "ports": [self.server_port],
# "httpHeaderMatch": {
# "headerName": "test",
# "regexMatch": "principal-present",
# },
# },
# },
{
"sources": [{
"principals": [
f"spiffe://{self.project}.svc.id.goog/not/the/client",
],
}, {
"principals": [
f"spiffe://{self.project}.svc.id.goog/not/the/client",
f"spiffe://{self.project}.svc.id.goog/ns/"
f"{self.client_namespace}/sa/{self.client_name}",
],
}],
"destinations": {
"hosts": [f"*:{self.server_xds_port}"],
"ports": [self.server_port],
"httpHeaderMatch": {
"headerName": "test",
"regexMatch": "match-principal",
},
},
},
{
"sources": {
"principals": [
f"spiffe://{self.project}.svc.id.goog/not/the/client",
],
},
"destinations": {
"hosts": [f"*:{self.server_xds_port}"],
"ports": [self.server_port],
"httpHeaderMatch": {
"headerName": "test",
"regexMatch": "never-match-principal",
},
},
},
]
def configure_and_assert(self, test_client: _XdsTestClient,
test_metadata_val: Optional[str],
status_code: grpc.StatusCode) -> None:
# Swap method type every sub-test to avoid mixing results
rpc_type = self.next_rpc_type
if rpc_type is None:
stats = test_client.get_load_balancer_accumulated_stats()
for t in self.RPC_TYPE_CYCLE:
if not stats.stats_per_method[t].rpcs_started:
rpc_type = t
self.assertIsNotNone(rpc_type, "All RPC types already used")
self.next_rpc_type = self.RPC_TYPE_CYCLE[rpc_type]
metadata = None
if test_metadata_val is not None:
metadata = ((rpc_type, "test", test_metadata_val),)
test_client.update_config.configure(rpc_types=[rpc_type],
metadata=metadata)
# b/228743575 Python has as race. Give us time to fix it.
stray_rpc_limit = 1 if self.lang_spec.client_lang == _Lang.PYTHON else 0
self.assertRpcStatusCodes(test_client,
expected_status=status_code,
duration=_SAMPLE_DURATION,
method=rpc_type,
stray_rpc_limit=stray_rpc_limit)
def test_plaintext_allow(self) -> None:
self.setupTrafficDirectorGrpc()
self.td.create_authz_policy(action='ALLOW', rules=self.authz_rules())
self.setupSecurityPolicies(server_tls=False,
server_mtls=False,
client_tls=False,
client_mtls=False)
test_server: _XdsTestServer = self.startSecureTestServer()
self.setupServerBackends()
test_client: _XdsTestClient = self.startSecureTestClient(test_server)
time.sleep(_SETTLE_DURATION.total_seconds())
with self.subTest('01_host_wildcard'):
self.configure_and_assert(test_client, 'host-wildcard',
grpc.StatusCode.OK)
with self.subTest('02_no_match'):
self.configure_and_assert(test_client, 'no-such-rule',
grpc.StatusCode.PERMISSION_DENIED)
self.configure_and_assert(test_client, None,
grpc.StatusCode.PERMISSION_DENIED)
with self.subTest('03_header_regex'):
self.configure_and_assert(test_client, 'header-regex-a',
grpc.StatusCode.OK)
self.configure_and_assert(test_client, 'header-regex-aa',
grpc.StatusCode.OK)
self.configure_and_assert(test_client, 'header-regex-',
grpc.StatusCode.PERMISSION_DENIED)
self.configure_and_assert(test_client, 'header-regex-ab',
grpc.StatusCode.PERMISSION_DENIED)
self.configure_and_assert(test_client, 'aheader-regex-a',
grpc.StatusCode.PERMISSION_DENIED)
with self.subTest('04_host_match'):
self.configure_and_assert(test_client, 'host-match1',
grpc.StatusCode.OK)
self.configure_and_assert(test_client, 'host-match2',
grpc.StatusCode.OK)
with self.subTest('05_never_match_host'):
self.configure_and_assert(test_client, 'never-match-host',
grpc.StatusCode.PERMISSION_DENIED)
with self.subTest('06_never_match_port'):
self.configure_and_assert(test_client, 'never-match-port',
grpc.StatusCode.PERMISSION_DENIED)
# b/202058316
# with self.subTest('07_principal_present'):
# self.configure_and_assert(test_client, 'principal-present',
# grpc.StatusCode.PERMISSION_DENIED)
def test_tls_allow(self) -> None:
self.setupTrafficDirectorGrpc()
self.td.create_authz_policy(action='ALLOW', rules=self.authz_rules())
self.setupSecurityPolicies(server_tls=True,
server_mtls=False,
client_tls=True,
client_mtls=False)
test_server: _XdsTestServer = self.startSecureTestServer()
self.setupServerBackends()
test_client: _XdsTestClient = self.startSecureTestClient(test_server)
time.sleep(_SETTLE_DURATION.total_seconds())
with self.subTest('01_host_wildcard'):
self.configure_and_assert(test_client, 'host-wildcard',
grpc.StatusCode.OK)
with self.subTest('02_no_match'):
self.configure_and_assert(test_client, None,
grpc.StatusCode.PERMISSION_DENIED)
# b/202058316
# with self.subTest('03_principal_present'):
# self.configure_and_assert(test_client, 'principal-present',
# grpc.StatusCode.PERMISSION_DENIED)
def test_mtls_allow(self) -> None:
self.setupTrafficDirectorGrpc()
self.td.create_authz_policy(action='ALLOW', rules=self.authz_rules())
self.setupSecurityPolicies(server_tls=True,
server_mtls=True,
client_tls=True,
client_mtls=True)
test_server: _XdsTestServer = self.startSecureTestServer()
self.setupServerBackends()
test_client: _XdsTestClient = self.startSecureTestClient(test_server)
time.sleep(_SETTLE_DURATION.total_seconds())
with self.subTest('01_host_wildcard'):
self.configure_and_assert(test_client, 'host-wildcard',
grpc.StatusCode.OK)
with self.subTest('02_no_match'):
self.configure_and_assert(test_client, None,
grpc.StatusCode.PERMISSION_DENIED)
# b/202058316
# with self.subTest('03_principal_present'):
# self.configure_and_assert(test_client, 'principal-present',
# grpc.StatusCode.OK)
with self.subTest('04_match_principal'):
self.configure_and_assert(test_client, 'match-principal',
grpc.StatusCode.OK)
with self.subTest('05_never_match_principal'):
self.configure_and_assert(test_client, 'never-match-principal',
grpc.StatusCode.PERMISSION_DENIED)
def test_plaintext_deny(self) -> None:
self.setupTrafficDirectorGrpc()
self.td.create_authz_policy(action='DENY', rules=self.authz_rules())
self.setupSecurityPolicies(server_tls=False,
server_mtls=False,
client_tls=False,
client_mtls=False)
test_server: _XdsTestServer = self.startSecureTestServer()
self.setupServerBackends()
test_client: _XdsTestClient = self.startSecureTestClient(test_server)
time.sleep(_SETTLE_DURATION.total_seconds())
with self.subTest('01_host_wildcard'):
self.configure_and_assert(test_client, 'host-wildcard',
grpc.StatusCode.PERMISSION_DENIED)
with self.subTest('02_no_match'):
self.configure_and_assert(test_client, None, grpc.StatusCode.OK)
if __name__ == '__main__':
absltest.main()