mirror of https://github.com/grpc/grpc-node.git
Merge pull request #2945 from murgatroid99/grpc-js-xds_rbac_filter
grpc-js-xds: Implement RBAC HTTP filter
This commit is contained in:
commit
0157776059
|
@ -71,6 +71,7 @@ const copyTestFixtures = checkTask(() =>
|
|||
const runTests = checkTask(() => {
|
||||
process.env.GRPC_EXPERIMENTAL_XDS_FEDERATION = 'true';
|
||||
process.env.GRPC_EXPERIMENTAL_PICKFIRST_LB_CONFIG = 'true';
|
||||
process.env.GRPC_XDS_EXPERIMENTAL_RBAC = 'true';
|
||||
if (Number(process.versions.node.split('.')[0]) <= 14) {
|
||||
process.env.GRPC_XDS_EXPERIMENTAL_ENABLE_RING_HASH = 'false';
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ COPY --from=build /node/src/grpc-node/packages/grpc-js ./packages/grpc-js/
|
|||
COPY --from=build /node/src/grpc-node/packages/grpc-js-xds ./packages/grpc-js-xds/
|
||||
|
||||
ENV GRPC_VERBOSITY="DEBUG"
|
||||
ENV GRPC_TRACE=xds_client,server,xds_server,http_filter,certificate_provider
|
||||
ENV GRPC_TRACE=xds_client,server,xds_server,http_filter,certificate_provider,rbac_filter
|
||||
|
||||
# tini serves as PID 1 and enables the server to properly respond to signals.
|
||||
COPY --from=build /tini /tini
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
"prepare": "npm run generate-types && npm run compile",
|
||||
"pretest": "npm run compile",
|
||||
"posttest": "npm run check",
|
||||
"generate-types": "proto-loader-gen-types --keepCase --longs String --enums String --defaults --oneofs --includeComments --includeDirs deps/envoy-api/ deps/xds/ deps/googleapis/ deps/protoc-gen-validate/ -O src/generated/ --grpcLib @grpc/grpc-js envoy/service/discovery/v3/ads.proto envoy/service/load_stats/v3/lrs.proto envoy/config/listener/v3/listener.proto envoy/config/route/v3/route.proto envoy/config/cluster/v3/cluster.proto envoy/config/endpoint/v3/endpoint.proto envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto udpa/type/v1/typed_struct.proto xds/type/v3/typed_struct.proto envoy/extensions/filters/http/fault/v3/fault.proto envoy/service/status/v3/csds.proto envoy/extensions/load_balancing_policies/wrr_locality/v3/wrr_locality.proto envoy/extensions/load_balancing_policies/ring_hash/v3/ring_hash.proto envoy/extensions/load_balancing_policies/pick_first/v3/pick_first.proto envoy/extensions/clusters/aggregate/v3/cluster.proto envoy/extensions/transport_sockets/tls/v3/tls.proto envoy/config/rbac/v3/rbac.proto",
|
||||
"generate-types": "proto-loader-gen-types --keepCase --longs String --enums String --defaults --oneofs --includeComments --includeDirs deps/envoy-api/ deps/xds/ deps/googleapis/ deps/protoc-gen-validate/ -O src/generated/ --grpcLib @grpc/grpc-js envoy/service/discovery/v3/ads.proto envoy/service/load_stats/v3/lrs.proto envoy/config/listener/v3/listener.proto envoy/config/route/v3/route.proto envoy/config/cluster/v3/cluster.proto envoy/config/endpoint/v3/endpoint.proto envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto udpa/type/v1/typed_struct.proto xds/type/v3/typed_struct.proto envoy/extensions/filters/http/fault/v3/fault.proto envoy/service/status/v3/csds.proto envoy/extensions/load_balancing_policies/wrr_locality/v3/wrr_locality.proto envoy/extensions/load_balancing_policies/ring_hash/v3/ring_hash.proto envoy/extensions/load_balancing_policies/pick_first/v3/pick_first.proto envoy/extensions/clusters/aggregate/v3/cluster.proto envoy/extensions/transport_sockets/tls/v3/tls.proto envoy/config/rbac/v3/rbac.proto envoy/extensions/filters/http/rbac/v3/rbac.proto",
|
||||
"generate-interop-types": "proto-loader-gen-types --keep-case --longs String --enums String --defaults --oneofs --json --includeComments --includeDirs proto/ -O interop/generated --grpcLib @grpc/grpc-js grpc/testing/test.proto",
|
||||
"generate-test-types": "proto-loader-gen-types --keep-case --longs String --enums String --defaults --oneofs --json --includeComments --includeDirs proto/ -O test/generated --grpcLib @grpc/grpc-js grpc/testing/echo.proto"
|
||||
},
|
||||
|
|
|
@ -27,3 +27,4 @@ export const EXPERIMENTAL_RING_HASH = (process.env.GRPC_XDS_EXPERIMENTAL_ENABLE_
|
|||
export const EXPERIMENTAL_PICK_FIRST = (process.env.GRPC_EXPERIMENTAL_PICKFIRST_LB_CONFIG ?? 'false') === 'true';
|
||||
export const EXPERIMENTAL_DUALSTACK_ENDPOINTS = (process.env.GRPC_EXPERIMENTAL_XDS_DUALSTACK_ENDPOINTS ?? 'true') === 'true';
|
||||
export const AGGREGATE_CLUSTER_BACKWARDS_COMPAT = (process.env.GRPC_XDS_AGGREGATE_CLUSTER_BACKWARD_COMPAT ?? 'false') === 'true';
|
||||
export const EXPERIMENTAL_RBAC = (process.env.GRPC_XDS_EXPERIMENTAL_RBAC ?? 'false') === 'true';
|
||||
|
|
104
packages/grpc-js-xds/src/generated/envoy/extensions/filters/http/rbac/v3/RBAC.ts
generated
Normal file
104
packages/grpc-js-xds/src/generated/envoy/extensions/filters/http/rbac/v3/RBAC.ts
generated
Normal file
|
@ -0,0 +1,104 @@
|
|||
// Original file: deps/envoy-api/envoy/extensions/filters/http/rbac/v3/rbac.proto
|
||||
|
||||
import type { RBAC as _envoy_config_rbac_v3_RBAC, RBAC__Output as _envoy_config_rbac_v3_RBAC__Output } from '../../../../../../envoy/config/rbac/v3/RBAC';
|
||||
import type { Matcher as _xds_type_matcher_v3_Matcher, Matcher__Output as _xds_type_matcher_v3_Matcher__Output } from '../../../../../../xds/type/matcher/v3/Matcher';
|
||||
|
||||
/**
|
||||
* RBAC filter config.
|
||||
* [#next-free-field: 8]
|
||||
*/
|
||||
export interface RBAC {
|
||||
/**
|
||||
* Specify the RBAC rules to be applied globally.
|
||||
* If absent, no enforcing RBAC policy will be applied.
|
||||
* If present and empty, DENY.
|
||||
* If both rules and matcher are configured, rules will be ignored.
|
||||
*/
|
||||
'rules'?: (_envoy_config_rbac_v3_RBAC | null);
|
||||
/**
|
||||
* Shadow rules are not enforced by the filter (i.e., returning a 403)
|
||||
* but will emit stats and logs and can be used for rule testing.
|
||||
* If absent, no shadow RBAC policy will be applied.
|
||||
* If both shadow rules and shadow matcher are configured, shadow rules will be ignored.
|
||||
*/
|
||||
'shadow_rules'?: (_envoy_config_rbac_v3_RBAC | null);
|
||||
/**
|
||||
* If specified, shadow rules will emit stats with the given prefix.
|
||||
* This is useful to distinguish the stat when there are more than 1 RBAC filter configured with
|
||||
* shadow rules.
|
||||
*/
|
||||
'shadow_rules_stat_prefix'?: (string);
|
||||
/**
|
||||
* The match tree to use when resolving RBAC action for incoming requests. Requests do not
|
||||
* match any matcher will be denied.
|
||||
* If absent, no enforcing RBAC matcher will be applied.
|
||||
* If present and empty, deny all requests.
|
||||
*/
|
||||
'matcher'?: (_xds_type_matcher_v3_Matcher | null);
|
||||
/**
|
||||
* The match tree to use for emitting stats and logs which can be used for rule testing for
|
||||
* incoming requests.
|
||||
* If absent, no shadow matcher will be applied.
|
||||
*/
|
||||
'shadow_matcher'?: (_xds_type_matcher_v3_Matcher | null);
|
||||
/**
|
||||
* If specified, rules will emit stats with the given prefix.
|
||||
* This is useful to distinguish the stat when there are more than 1 RBAC filter configured with
|
||||
* rules.
|
||||
*/
|
||||
'rules_stat_prefix'?: (string);
|
||||
/**
|
||||
* If track_per_rule_stats is true, counters will be published for each rule and shadow rule.
|
||||
*/
|
||||
'track_per_rule_stats'?: (boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* RBAC filter config.
|
||||
* [#next-free-field: 8]
|
||||
*/
|
||||
export interface RBAC__Output {
|
||||
/**
|
||||
* Specify the RBAC rules to be applied globally.
|
||||
* If absent, no enforcing RBAC policy will be applied.
|
||||
* If present and empty, DENY.
|
||||
* If both rules and matcher are configured, rules will be ignored.
|
||||
*/
|
||||
'rules': (_envoy_config_rbac_v3_RBAC__Output | null);
|
||||
/**
|
||||
* Shadow rules are not enforced by the filter (i.e., returning a 403)
|
||||
* but will emit stats and logs and can be used for rule testing.
|
||||
* If absent, no shadow RBAC policy will be applied.
|
||||
* If both shadow rules and shadow matcher are configured, shadow rules will be ignored.
|
||||
*/
|
||||
'shadow_rules': (_envoy_config_rbac_v3_RBAC__Output | null);
|
||||
/**
|
||||
* If specified, shadow rules will emit stats with the given prefix.
|
||||
* This is useful to distinguish the stat when there are more than 1 RBAC filter configured with
|
||||
* shadow rules.
|
||||
*/
|
||||
'shadow_rules_stat_prefix': (string);
|
||||
/**
|
||||
* The match tree to use when resolving RBAC action for incoming requests. Requests do not
|
||||
* match any matcher will be denied.
|
||||
* If absent, no enforcing RBAC matcher will be applied.
|
||||
* If present and empty, deny all requests.
|
||||
*/
|
||||
'matcher': (_xds_type_matcher_v3_Matcher__Output | null);
|
||||
/**
|
||||
* The match tree to use for emitting stats and logs which can be used for rule testing for
|
||||
* incoming requests.
|
||||
* If absent, no shadow matcher will be applied.
|
||||
*/
|
||||
'shadow_matcher': (_xds_type_matcher_v3_Matcher__Output | null);
|
||||
/**
|
||||
* If specified, rules will emit stats with the given prefix.
|
||||
* This is useful to distinguish the stat when there are more than 1 RBAC filter configured with
|
||||
* rules.
|
||||
*/
|
||||
'rules_stat_prefix': (string);
|
||||
/**
|
||||
* If track_per_rule_stats is true, counters will be published for each rule and shadow rule.
|
||||
*/
|
||||
'track_per_rule_stats': (boolean);
|
||||
}
|
19
packages/grpc-js-xds/src/generated/envoy/extensions/filters/http/rbac/v3/RBACPerRoute.ts
generated
Normal file
19
packages/grpc-js-xds/src/generated/envoy/extensions/filters/http/rbac/v3/RBACPerRoute.ts
generated
Normal file
|
@ -0,0 +1,19 @@
|
|||
// Original file: deps/envoy-api/envoy/extensions/filters/http/rbac/v3/rbac.proto
|
||||
|
||||
import type { RBAC as _envoy_extensions_filters_http_rbac_v3_RBAC, RBAC__Output as _envoy_extensions_filters_http_rbac_v3_RBAC__Output } from '../../../../../../envoy/extensions/filters/http/rbac/v3/RBAC';
|
||||
|
||||
export interface RBACPerRoute {
|
||||
/**
|
||||
* Override the global configuration of the filter with this new config.
|
||||
* If absent, the global RBAC policy will be disabled for this route.
|
||||
*/
|
||||
'rbac'?: (_envoy_extensions_filters_http_rbac_v3_RBAC | null);
|
||||
}
|
||||
|
||||
export interface RBACPerRoute__Output {
|
||||
/**
|
||||
* Override the global configuration of the filter with this new config.
|
||||
* If absent, the global RBAC policy will be disabled for this route.
|
||||
*/
|
||||
'rbac': (_envoy_extensions_filters_http_rbac_v3_RBAC__Output | null);
|
||||
}
|
|
@ -93,6 +93,18 @@ export interface ProtoGrpcType {
|
|||
}
|
||||
}
|
||||
}
|
||||
extensions: {
|
||||
filters: {
|
||||
http: {
|
||||
rbac: {
|
||||
v3: {
|
||||
RBAC: MessageTypeDefinition
|
||||
RBACPerRoute: MessageTypeDefinition
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
type: {
|
||||
matcher: {
|
||||
v3: {
|
||||
|
|
|
@ -0,0 +1,175 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// This is a non-public, unstable API, but it's very convenient
|
||||
import { loadProtosWithOptionsSync } from '@grpc/proto-loader/build/src/util';
|
||||
import { experimental, logVerbosity, ServerInterceptingCall, ServerInterceptor, ServerListener, status } from '@grpc/grpc-js';
|
||||
import { Any__Output } from '../generated/google/protobuf/Any';
|
||||
import { HttpFilterConfig, registerHttpFilter } from '../http-filter';
|
||||
import { RbacPolicyGroup, UnifiedInfo as UnifiedRbacInfo } from '../rbac';
|
||||
import { RBAC__Output } from '../generated/envoy/extensions/filters/http/rbac/v3/RBAC';
|
||||
import { RBACPerRoute__Output } from '../generated/envoy/extensions/filters/http/rbac/v3/RBACPerRoute';
|
||||
import { parseConfig as parseRbacConfig } from '../rbac';
|
||||
import { EXPERIMENTAL_RBAC } from '../environment';
|
||||
|
||||
const TRACER_NAME = 'rbac_filter';
|
||||
|
||||
function trace(text: string): void {
|
||||
experimental.trace(logVerbosity.DEBUG, TRACER_NAME, text);
|
||||
}
|
||||
|
||||
const resourceRoot = loadProtosWithOptionsSync([
|
||||
'envoy/extensions/filters/http/rbac/v3/rbac.proto'], {
|
||||
keepCase: true,
|
||||
includeDirs: [
|
||||
// Paths are relative to src/build/http-filter
|
||||
__dirname + '/../../../deps/xds/',
|
||||
__dirname + '/../../../deps/envoy-api/',
|
||||
__dirname + '/../../../deps/protoc-gen-validate/',
|
||||
__dirname + '/../../../deps/googleapis/'
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
const RBAC_FILTER_URL = 'type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC';
|
||||
const RBAC_FILTER_OVERRIDE_URL ='type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute';
|
||||
|
||||
const toObjectOptions = {
|
||||
longs: String,
|
||||
enums: String,
|
||||
defaults: true,
|
||||
oneofs: true
|
||||
}
|
||||
|
||||
function parseAnyMessage<MessageType>(message: Any__Output): MessageType | null {
|
||||
const typeName = message.type_url.substring(message.type_url.lastIndexOf('/') + 1);
|
||||
const messageType = resourceRoot.lookup(typeName);
|
||||
if (messageType) {
|
||||
const decodedMessage = (messageType as any).decode(message.value);
|
||||
return decodedMessage.$type.toObject(decodedMessage, toObjectOptions) as MessageType;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
interface RbacFilterConfig extends HttpFilterConfig {
|
||||
typeUrl: 'type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC';
|
||||
config: RbacPolicyGroup;
|
||||
}
|
||||
|
||||
function parseTopLevelRbacConfig(encodedConfig: Any__Output): RbacFilterConfig | null {
|
||||
if (encodedConfig.type_url !== RBAC_FILTER_URL) {
|
||||
trace('Config parsing failed: unexpected type URL: ' + encodedConfig.type_url);
|
||||
return null;
|
||||
}
|
||||
const parsedMessage = parseAnyMessage<RBAC__Output>(encodedConfig);
|
||||
if (parsedMessage === null) {
|
||||
trace('Config parsing failed: failed to parse RBAC message');
|
||||
return null;
|
||||
}
|
||||
trace('Parsing RBAC message ' + JSON.stringify(parsedMessage, undefined, 2));
|
||||
if (!parsedMessage.rules) {
|
||||
trace('Config parsing failed: no rules found');
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return {
|
||||
typeUrl: RBAC_FILTER_URL,
|
||||
config: parseRbacConfig(parsedMessage.rules)
|
||||
};
|
||||
} catch (e) {
|
||||
trace('Config parsing failed: ' + (e as Error).message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function parseOverrideRbacConfig(encodedConfig: Any__Output): RbacFilterConfig | null {
|
||||
if (encodedConfig.type_url !== RBAC_FILTER_OVERRIDE_URL) {
|
||||
trace('Config parsing failed: unexpected type URL: ' + encodedConfig.type_url);
|
||||
return null;
|
||||
}
|
||||
const parsedMessage = parseAnyMessage<RBACPerRoute__Output>(encodedConfig);
|
||||
if (parsedMessage === null) {
|
||||
trace('Config parsing failed: failed to parse RBACPerRoute message');
|
||||
return null;
|
||||
}
|
||||
trace('Parsing RBAC message ' + JSON.stringify(parsedMessage, undefined, 2));
|
||||
if (!parsedMessage.rbac?.rules) {
|
||||
trace('Config parsing failed: no rules found');
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return {
|
||||
typeUrl: RBAC_FILTER_URL,
|
||||
config: parseRbacConfig(parsedMessage.rbac.rules)
|
||||
};
|
||||
} catch (e) {
|
||||
trace('Config parsing failed: ' + (e as Error).message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function createRbacServerFilter(config: HttpFilterConfig, overrideConfigMap: Map<string, HttpFilterConfig>): ServerInterceptor {
|
||||
return function rbacServerFilter(methodDescriptor, call): ServerInterceptingCall {
|
||||
const listener: ServerListener = {
|
||||
onReceiveMetadata: (metadata, next) => {
|
||||
let activeConfig = config;
|
||||
const routeName = metadata.get('grpc-route')[0];
|
||||
if (routeName) {
|
||||
const overrideConfig = overrideConfigMap.get(routeName as string);
|
||||
if (overrideConfig) {
|
||||
activeConfig = overrideConfig;
|
||||
}
|
||||
}
|
||||
const rbacMetadata = metadata.clone();
|
||||
rbacMetadata.set(':method', 'POST');
|
||||
rbacMetadata.set(':authority', call.getHost());
|
||||
rbacMetadata.set(':path', methodDescriptor.path);
|
||||
const connectionInfo = call.getConnectionInfo();
|
||||
const authContext = call.getAuthContext();
|
||||
const info: UnifiedRbacInfo = {
|
||||
destinationIp: connectionInfo.localAddress!,
|
||||
destinationPort: connectionInfo.localPort!,
|
||||
sourceIp: connectionInfo.remoteAddress!,
|
||||
headers: rbacMetadata,
|
||||
tls: authContext.transportSecurityType !== undefined,
|
||||
peerCertificate: authContext.sslPeerCertificate ?? null,
|
||||
urlPath: methodDescriptor.path
|
||||
};
|
||||
if ((activeConfig as RbacFilterConfig).config.apply(info)) {
|
||||
next(metadata);
|
||||
} else {
|
||||
call.sendStatus({code: status.PERMISSION_DENIED, details: 'Unauthorized RPC rejected'});
|
||||
}
|
||||
}
|
||||
};
|
||||
return new ServerInterceptingCall(call, {
|
||||
start: next => {
|
||||
next(listener);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function setup() {
|
||||
if (EXPERIMENTAL_RBAC) {
|
||||
registerHttpFilter(RBAC_FILTER_URL, {
|
||||
parseTopLevelFilterConfig: parseTopLevelRbacConfig,
|
||||
parseOverrideFilterConfig: parseOverrideRbacConfig,
|
||||
createServerFilter: createRbacServerFilter
|
||||
});
|
||||
}
|
||||
}
|
|
@ -25,6 +25,7 @@ import * as xds_wrr_locality from './load-balancer-xds-wrr-locality';
|
|||
import * as ring_hash from './load-balancer-ring-hash';
|
||||
import * as router_filter from './http-filter/router-filter';
|
||||
import * as fault_injection_filter from './http-filter/fault-injection-filter';
|
||||
import * as rbac_filter from './http-filter/rbac-filter';
|
||||
import * as csds from './csds';
|
||||
import * as round_robin_lb from './lb-policy-registry/round-robin';
|
||||
import * as typed_struct_lb from './lb-policy-registry/typed-struct';
|
||||
|
@ -53,6 +54,7 @@ export function register() {
|
|||
ring_hash.setup();
|
||||
router_filter.setup();
|
||||
fault_injection_filter.setup();
|
||||
rbac_filter.setup();
|
||||
csds.setup();
|
||||
round_robin_lb.setup();
|
||||
typed_struct_lb.setup();
|
||||
|
|
|
@ -278,8 +278,6 @@ export class MetadataPrincipal implements PrincipalRule {
|
|||
}
|
||||
}
|
||||
|
||||
export type RbacAction = 'ALLOW' | 'DENY';
|
||||
|
||||
export interface UnifiedInfo extends PermissionInfo, PrincipalInfo {}
|
||||
|
||||
export class RbacPolicy {
|
||||
|
@ -301,15 +299,20 @@ export class RbacPolicy {
|
|||
}
|
||||
|
||||
export class RbacPolicyGroup {
|
||||
constructor(private policies: Map<string, RbacPolicy>, private action: RbacAction) {}
|
||||
constructor(private policies: Map<string, RbacPolicy>, private allow: boolean) {}
|
||||
|
||||
apply(info: UnifiedInfo): RbacAction | null {
|
||||
/**
|
||||
*
|
||||
* @param info
|
||||
* @returns True if the call should be accepted, false if it should be rejected
|
||||
*/
|
||||
apply(info: UnifiedInfo): boolean {
|
||||
for (const policy of this.policies.values()) {
|
||||
if (policy.matches(info)) {
|
||||
return this.action;
|
||||
return this.allow;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return !this.allow;
|
||||
}
|
||||
|
||||
toString() {
|
||||
|
@ -318,7 +321,7 @@ export class RbacPolicyGroup {
|
|||
policyStrings.push(`${name}: ${policy.toString()}`);
|
||||
}
|
||||
return `RBAC
|
||||
action=${this.action}
|
||||
action=${this.allow ? 'ALLOW' : 'DENY'}
|
||||
policies:
|
||||
${policyStrings.join('\n')}`;
|
||||
}
|
||||
|
@ -392,5 +395,5 @@ export function parseConfig(rbac: RBAC__Output): RbacPolicyGroup {
|
|||
for (const [name, policyConfig] of Object.entries(rbac.policies)) {
|
||||
policyMap.set(name, parsePolicy(policyConfig));
|
||||
}
|
||||
return new RbacPolicyGroup(policyMap, rbac.action);
|
||||
return new RbacPolicyGroup(policyMap, rbac.action === 'ALLOW');
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ import { ClusterConfig } from "../src/generated/envoy/extensions/clusters/aggreg
|
|||
import { Any } from "../src/generated/google/protobuf/Any";
|
||||
import { ControlPlaneServer } from "./xds-server";
|
||||
import { UpstreamTlsContext } from "../src/generated/envoy/extensions/transport_sockets/tls/v3/UpstreamTlsContext";
|
||||
import { HttpFilter } from "../src/generated/envoy/extensions/filters/network/http_connection_manager/v3/HttpFilter";
|
||||
|
||||
interface Endpoint {
|
||||
locality: Locality;
|
||||
|
@ -400,7 +401,7 @@ const DEFAULT_BASE_SERVER_ROUTE_CONFIG: RouteConfiguration = {
|
|||
export class FakeServerRoute {
|
||||
private listener: Listener;
|
||||
private routeConfiguration: RouteConfiguration;
|
||||
constructor(port: number, routeName: string, baseListener?: Listener | undefined, baseRouteConfiguration?: RouteConfiguration) {
|
||||
constructor(port: number, routeName: string, baseListener?: Listener | undefined, baseRouteConfiguration?: RouteConfiguration | undefined, httpFilters?: HttpFilter[]) {
|
||||
this.listener = baseListener ?? {...DEFAULT_BASE_SERVER_LISTENER};
|
||||
this.listener.name = `[::1]:${port}`;
|
||||
this.listener.address = {
|
||||
|
@ -414,11 +415,9 @@ export class FakeServerRoute {
|
|||
rds: {
|
||||
route_config_name: routeName,
|
||||
config_source: {ads: {}}
|
||||
}
|
||||
},
|
||||
http_filters: httpFilters ?? []
|
||||
};
|
||||
this.listener.api_listener = {
|
||||
api_listener: httpConnectionManager
|
||||
}
|
||||
const filterList = [{
|
||||
typed_config: httpConnectionManager
|
||||
}];
|
||||
|
|
|
@ -0,0 +1,188 @@
|
|||
/*
|
||||
* Copyright 2025 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 * as assert from 'assert';
|
||||
import { createBackends } from "./backend";
|
||||
import { XdsTestClient } from "./client";
|
||||
import { FakeEdsCluster, FakeRouteGroup, FakeServerRoute } from "./framework";
|
||||
import { ControlPlaneServer } from "./xds-server";
|
||||
import { AnyExtension } from '@grpc/proto-loader';
|
||||
import { RBAC } from '../src/generated/envoy/extensions/filters/http/rbac/v3/RBAC';
|
||||
import { status } from '@grpc/grpc-js';
|
||||
|
||||
describe.only('RBAC HTTP filter', () => {
|
||||
let xdsServer: ControlPlaneServer;
|
||||
let client: XdsTestClient;
|
||||
beforeEach(done => {
|
||||
xdsServer = new ControlPlaneServer();
|
||||
xdsServer.startServer(error => {
|
||||
done(error);
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
client?.close();
|
||||
xdsServer?.shutdownServer();
|
||||
});
|
||||
it('Should accept matching requests with ALLOW action', async () => {
|
||||
const [backend] = await createBackends(1);
|
||||
const rbacFilter: AnyExtension & RBAC = {
|
||||
'@type': 'type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC',
|
||||
rules: {
|
||||
action: 'ALLOW',
|
||||
policies: {
|
||||
local: {
|
||||
principals: [{any: true}],
|
||||
permissions: [{any: true}]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const routerFilter: AnyExtension = {
|
||||
'@type': 'type.googleapis.com/envoy.extensions.filters.http.router.v3.Router'
|
||||
};
|
||||
const serverRoute = new FakeServerRoute(backend.getPort(), 'serverRoute', undefined, undefined, [{typed_config: rbacFilter, name: 'rbac'}, {typed_config: routerFilter, name: 'router'}]);
|
||||
xdsServer.setRdsResource(serverRoute.getRouteConfiguration());
|
||||
xdsServer.setLdsResource(serverRoute.getListener());
|
||||
xdsServer.addResponseListener((typeUrl, responseState) => {
|
||||
if (responseState.state === 'NACKED') {
|
||||
client?.stopCalls();
|
||||
assert.fail(`Client NACKED ${typeUrl} resource with message ${responseState.errorMessage}`);
|
||||
}
|
||||
});
|
||||
const cluster = new FakeEdsCluster('cluster1', 'endpoint1', [{backends: [backend], locality:{region: 'region1'}}]);
|
||||
const routeGroup = new FakeRouteGroup('listener1', 'route1', [{cluster: cluster}]);
|
||||
await routeGroup.startAllBackends(xdsServer);
|
||||
xdsServer.setEdsResource(cluster.getEndpointConfig());
|
||||
xdsServer.setCdsResource(cluster.getClusterConfig());
|
||||
xdsServer.setRdsResource(routeGroup.getRouteConfiguration());
|
||||
xdsServer.setLdsResource(routeGroup.getListener());
|
||||
client = XdsTestClient.createFromServer('listener1', xdsServer);
|
||||
const error = await client.sendOneCallAsync();
|
||||
assert.strictEqual(error, null);
|
||||
});
|
||||
it('Should reject matching requests with DENY action', async () => {
|
||||
const [backend] = await createBackends(1);
|
||||
const rbacFilter: AnyExtension & RBAC = {
|
||||
'@type': 'type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC',
|
||||
rules: {
|
||||
action: 'DENY',
|
||||
policies: {
|
||||
local: {
|
||||
principals: [{any: true}],
|
||||
permissions: [{any: true}]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const routerFilter: AnyExtension = {
|
||||
'@type': 'type.googleapis.com/envoy.extensions.filters.http.router.v3.Router'
|
||||
};
|
||||
const serverRoute = new FakeServerRoute(backend.getPort(), 'serverRoute', undefined, undefined, [{typed_config: rbacFilter, name: 'rbac'}, {typed_config: routerFilter, name: 'router'}]);
|
||||
xdsServer.setRdsResource(serverRoute.getRouteConfiguration());
|
||||
xdsServer.setLdsResource(serverRoute.getListener());
|
||||
xdsServer.addResponseListener((typeUrl, responseState) => {
|
||||
if (responseState.state === 'NACKED') {
|
||||
client?.stopCalls();
|
||||
assert.fail(`Client NACKED ${typeUrl} resource with message ${responseState.errorMessage}`);
|
||||
}
|
||||
});
|
||||
const cluster = new FakeEdsCluster('cluster1', 'endpoint1', [{backends: [backend], locality:{region: 'region1'}}]);
|
||||
const routeGroup = new FakeRouteGroup('listener1', 'route1', [{cluster: cluster}]);
|
||||
await routeGroup.startAllBackends(xdsServer);
|
||||
xdsServer.setEdsResource(cluster.getEndpointConfig());
|
||||
xdsServer.setCdsResource(cluster.getClusterConfig());
|
||||
xdsServer.setRdsResource(routeGroup.getRouteConfiguration());
|
||||
xdsServer.setLdsResource(routeGroup.getListener());
|
||||
client = XdsTestClient.createFromServer('listener1', xdsServer);
|
||||
const error = await client.sendOneCallAsync();
|
||||
assert.strictEqual(error?.code, status.PERMISSION_DENIED);
|
||||
});
|
||||
it('Should reject non-matching requests with ALLOW action', async () => {
|
||||
const [backend] = await createBackends(1);
|
||||
const rbacFilter: AnyExtension & RBAC = {
|
||||
'@type': 'type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC',
|
||||
rules: {
|
||||
action: 'ALLOW',
|
||||
policies: {
|
||||
local: {
|
||||
principals: [{any: true}],
|
||||
permissions: [{not_rule: {any: true}}]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const routerFilter: AnyExtension = {
|
||||
'@type': 'type.googleapis.com/envoy.extensions.filters.http.router.v3.Router'
|
||||
};
|
||||
const serverRoute = new FakeServerRoute(backend.getPort(), 'serverRoute', undefined, undefined, [{typed_config: rbacFilter, name: 'rbac'}, {typed_config: routerFilter, name: 'router'}]);
|
||||
xdsServer.setRdsResource(serverRoute.getRouteConfiguration());
|
||||
xdsServer.setLdsResource(serverRoute.getListener());
|
||||
xdsServer.addResponseListener((typeUrl, responseState) => {
|
||||
if (responseState.state === 'NACKED') {
|
||||
client?.stopCalls();
|
||||
assert.fail(`Client NACKED ${typeUrl} resource with message ${responseState.errorMessage}`);
|
||||
}
|
||||
});
|
||||
const cluster = new FakeEdsCluster('cluster1', 'endpoint1', [{backends: [backend], locality:{region: 'region1'}}]);
|
||||
const routeGroup = new FakeRouteGroup('listener1', 'route1', [{cluster: cluster}]);
|
||||
await routeGroup.startAllBackends(xdsServer);
|
||||
xdsServer.setEdsResource(cluster.getEndpointConfig());
|
||||
xdsServer.setCdsResource(cluster.getClusterConfig());
|
||||
xdsServer.setRdsResource(routeGroup.getRouteConfiguration());
|
||||
xdsServer.setLdsResource(routeGroup.getListener());
|
||||
client = XdsTestClient.createFromServer('listener1', xdsServer);
|
||||
const error = await client.sendOneCallAsync();
|
||||
assert.strictEqual(error?.code, status.PERMISSION_DENIED);
|
||||
});
|
||||
it('Should accept non-matching requests with DENY action', async () => {
|
||||
const [backend] = await createBackends(1);
|
||||
const rbacFilter: AnyExtension & RBAC = {
|
||||
'@type': 'type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC',
|
||||
rules: {
|
||||
action: 'DENY',
|
||||
policies: {
|
||||
local: {
|
||||
principals: [{any: true}],
|
||||
permissions: [{not_rule: {any: true}}]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const routerFilter: AnyExtension = {
|
||||
'@type': 'type.googleapis.com/envoy.extensions.filters.http.router.v3.Router'
|
||||
};
|
||||
const serverRoute = new FakeServerRoute(backend.getPort(), 'serverRoute', undefined, undefined, [{typed_config: rbacFilter, name: 'rbac'}, {typed_config: routerFilter, name: 'router'}]);
|
||||
xdsServer.setRdsResource(serverRoute.getRouteConfiguration());
|
||||
xdsServer.setLdsResource(serverRoute.getListener());
|
||||
xdsServer.addResponseListener((typeUrl, responseState) => {
|
||||
if (responseState.state === 'NACKED') {
|
||||
client?.stopCalls();
|
||||
assert.fail(`Client NACKED ${typeUrl} resource with message ${responseState.errorMessage}`);
|
||||
}
|
||||
});
|
||||
const cluster = new FakeEdsCluster('cluster1', 'endpoint1', [{backends: [backend], locality:{region: 'region1'}}]);
|
||||
const routeGroup = new FakeRouteGroup('listener1', 'route1', [{cluster: cluster}]);
|
||||
await routeGroup.startAllBackends(xdsServer);
|
||||
xdsServer.setEdsResource(cluster.getEndpointConfig());
|
||||
xdsServer.setCdsResource(cluster.getClusterConfig());
|
||||
xdsServer.setRdsResource(routeGroup.getRouteConfiguration());
|
||||
xdsServer.setLdsResource(routeGroup.getListener());
|
||||
client = XdsTestClient.createFromServer('listener1', xdsServer);
|
||||
const error = await client.sendOneCallAsync();
|
||||
assert.strictEqual(error, null);
|
||||
});
|
||||
});
|
|
@ -54,7 +54,9 @@ const loadedProtos = loadPackageDefinition(loadSync(
|
|||
'envoy/extensions/load_balancing_policies/ring_hash/v3/ring_hash.proto',
|
||||
'envoy/extensions/load_balancing_policies/pick_first/v3/pick_first.proto',
|
||||
'envoy/extensions/transport_sockets/tls/v3/tls.proto',
|
||||
'xds/type/v3/typed_struct.proto'
|
||||
'xds/type/v3/typed_struct.proto',
|
||||
'envoy/extensions/filters/http/router/v3/router.proto',
|
||||
'envoy/extensions/filters/http/rbac/v3/rbac.proto'
|
||||
],
|
||||
{
|
||||
keepCase: true,
|
||||
|
|
|
@ -19,7 +19,7 @@ import * as http2 from 'http2';
|
|||
import { log } from './logging';
|
||||
import { LogVerbosity } from './constants';
|
||||
import { getErrorMessage } from './error';
|
||||
const LEGAL_KEY_REGEX = /^[0-9a-z_.-]+$/;
|
||||
const LEGAL_KEY_REGEX = /^[:0-9a-z_.-]+$/;
|
||||
const LEGAL_NON_BINARY_VALUE_REGEX = /^[ -~]*$/;
|
||||
|
||||
export type MetadataValue = string | Buffer;
|
||||
|
@ -222,6 +222,9 @@ export class Metadata {
|
|||
const result: http2.OutgoingHttpHeaders = {};
|
||||
|
||||
for (const [key, values] of this.internalRepr) {
|
||||
if (key.startsWith(':')) {
|
||||
continue;
|
||||
}
|
||||
// We assume that the user's interaction with this object is limited to
|
||||
// through its public API (i.e. keys and values are already validated).
|
||||
result[key] = values.map(bufToString);
|
||||
|
|
|
@ -1005,9 +1005,10 @@ export class BaseServerInterceptingCall
|
|||
}
|
||||
getAuthContext(): AuthContext {
|
||||
if (this.stream.session?.socket instanceof TLSSocket) {
|
||||
const peerCertificate = this.stream.session.socket.getPeerCertificate();
|
||||
return {
|
||||
transportSecurityType: 'ssl',
|
||||
sslPeerCertificate: this.stream.session.socket.getPeerCertificate()
|
||||
sslPeerCertificate: peerCertificate.raw ? peerCertificate : undefined
|
||||
}
|
||||
} else {
|
||||
return {};
|
||||
|
|
Loading…
Reference in New Issue