Skip to content

Commit 0157776

Browse files
authored
Merge pull request #2945 from murgatroid99/grpc-js-xds_rbac_filter
grpc-js-xds: Implement RBAC HTTP filter
2 parents 537b32f + 9843648 commit 0157776

File tree

15 files changed

+529
-19
lines changed

15 files changed

+529
-19
lines changed

packages/grpc-js-xds/gulpfile.ts

+1
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ const copyTestFixtures = checkTask(() =>
7171
const runTests = checkTask(() => {
7272
process.env.GRPC_EXPERIMENTAL_XDS_FEDERATION = 'true';
7373
process.env.GRPC_EXPERIMENTAL_PICKFIRST_LB_CONFIG = 'true';
74+
process.env.GRPC_XDS_EXPERIMENTAL_RBAC = 'true';
7475
if (Number(process.versions.node.split('.')[0]) <= 14) {
7576
process.env.GRPC_XDS_EXPERIMENTAL_ENABLE_RING_HASH = 'false';
7677
}

packages/grpc-js-xds/interop/test-server.Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ COPY --from=build /node/src/grpc-node/packages/grpc-js ./packages/grpc-js/
4646
COPY --from=build /node/src/grpc-node/packages/grpc-js-xds ./packages/grpc-js-xds/
4747

4848
ENV GRPC_VERBOSITY="DEBUG"
49-
ENV GRPC_TRACE=xds_client,server,xds_server,http_filter,certificate_provider
49+
ENV GRPC_TRACE=xds_client,server,xds_server,http_filter,certificate_provider,rbac_filter
5050

5151
# tini serves as PID 1 and enables the server to properly respond to signals.
5252
COPY --from=build /tini /tini

packages/grpc-js-xds/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"prepare": "npm run generate-types && npm run compile",
1313
"pretest": "npm run compile",
1414
"posttest": "npm run check",
15-
"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",
15+
"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",
1616
"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",
1717
"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"
1818
},

packages/grpc-js-xds/src/environment.ts

+1
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ export const EXPERIMENTAL_RING_HASH = (process.env.GRPC_XDS_EXPERIMENTAL_ENABLE_
2727
export const EXPERIMENTAL_PICK_FIRST = (process.env.GRPC_EXPERIMENTAL_PICKFIRST_LB_CONFIG ?? 'false') === 'true';
2828
export const EXPERIMENTAL_DUALSTACK_ENDPOINTS = (process.env.GRPC_EXPERIMENTAL_XDS_DUALSTACK_ENDPOINTS ?? 'true') === 'true';
2929
export const AGGREGATE_CLUSTER_BACKWARDS_COMPAT = (process.env.GRPC_XDS_AGGREGATE_CLUSTER_BACKWARD_COMPAT ?? 'false') === 'true';
30+
export const EXPERIMENTAL_RBAC = (process.env.GRPC_XDS_EXPERIMENTAL_RBAC ?? 'false') === 'true';

packages/grpc-js-xds/src/generated/envoy/extensions/filters/http/rbac/v3/RBAC.ts

+104
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/grpc-js-xds/src/generated/envoy/extensions/filters/http/rbac/v3/RBACPerRoute.ts

+19
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/grpc-js-xds/src/generated/rbac.ts

+12
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/*
2+
* Copyright 2021 gRPC authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
// This is a non-public, unstable API, but it's very convenient
18+
import { loadProtosWithOptionsSync } from '@grpc/proto-loader/build/src/util';
19+
import { experimental, logVerbosity, ServerInterceptingCall, ServerInterceptor, ServerListener, status } from '@grpc/grpc-js';
20+
import { Any__Output } from '../generated/google/protobuf/Any';
21+
import { HttpFilterConfig, registerHttpFilter } from '../http-filter';
22+
import { RbacPolicyGroup, UnifiedInfo as UnifiedRbacInfo } from '../rbac';
23+
import { RBAC__Output } from '../generated/envoy/extensions/filters/http/rbac/v3/RBAC';
24+
import { RBACPerRoute__Output } from '../generated/envoy/extensions/filters/http/rbac/v3/RBACPerRoute';
25+
import { parseConfig as parseRbacConfig } from '../rbac';
26+
import { EXPERIMENTAL_RBAC } from '../environment';
27+
28+
const TRACER_NAME = 'rbac_filter';
29+
30+
function trace(text: string): void {
31+
experimental.trace(logVerbosity.DEBUG, TRACER_NAME, text);
32+
}
33+
34+
const resourceRoot = loadProtosWithOptionsSync([
35+
'envoy/extensions/filters/http/rbac/v3/rbac.proto'], {
36+
keepCase: true,
37+
includeDirs: [
38+
// Paths are relative to src/build/http-filter
39+
__dirname + '/../../../deps/xds/',
40+
__dirname + '/../../../deps/envoy-api/',
41+
__dirname + '/../../../deps/protoc-gen-validate/',
42+
__dirname + '/../../../deps/googleapis/'
43+
],
44+
}
45+
);
46+
47+
const RBAC_FILTER_URL = 'type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC';
48+
const RBAC_FILTER_OVERRIDE_URL ='type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute';
49+
50+
const toObjectOptions = {
51+
longs: String,
52+
enums: String,
53+
defaults: true,
54+
oneofs: true
55+
}
56+
57+
function parseAnyMessage<MessageType>(message: Any__Output): MessageType | null {
58+
const typeName = message.type_url.substring(message.type_url.lastIndexOf('/') + 1);
59+
const messageType = resourceRoot.lookup(typeName);
60+
if (messageType) {
61+
const decodedMessage = (messageType as any).decode(message.value);
62+
return decodedMessage.$type.toObject(decodedMessage, toObjectOptions) as MessageType;
63+
} else {
64+
return null;
65+
}
66+
}
67+
68+
interface RbacFilterConfig extends HttpFilterConfig {
69+
typeUrl: 'type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC';
70+
config: RbacPolicyGroup;
71+
}
72+
73+
function parseTopLevelRbacConfig(encodedConfig: Any__Output): RbacFilterConfig | null {
74+
if (encodedConfig.type_url !== RBAC_FILTER_URL) {
75+
trace('Config parsing failed: unexpected type URL: ' + encodedConfig.type_url);
76+
return null;
77+
}
78+
const parsedMessage = parseAnyMessage<RBAC__Output>(encodedConfig);
79+
if (parsedMessage === null) {
80+
trace('Config parsing failed: failed to parse RBAC message');
81+
return null;
82+
}
83+
trace('Parsing RBAC message ' + JSON.stringify(parsedMessage, undefined, 2));
84+
if (!parsedMessage.rules) {
85+
trace('Config parsing failed: no rules found');
86+
return null;
87+
}
88+
try {
89+
return {
90+
typeUrl: RBAC_FILTER_URL,
91+
config: parseRbacConfig(parsedMessage.rules)
92+
};
93+
} catch (e) {
94+
trace('Config parsing failed: ' + (e as Error).message);
95+
return null;
96+
}
97+
}
98+
99+
function parseOverrideRbacConfig(encodedConfig: Any__Output): RbacFilterConfig | null {
100+
if (encodedConfig.type_url !== RBAC_FILTER_OVERRIDE_URL) {
101+
trace('Config parsing failed: unexpected type URL: ' + encodedConfig.type_url);
102+
return null;
103+
}
104+
const parsedMessage = parseAnyMessage<RBACPerRoute__Output>(encodedConfig);
105+
if (parsedMessage === null) {
106+
trace('Config parsing failed: failed to parse RBACPerRoute message');
107+
return null;
108+
}
109+
trace('Parsing RBAC message ' + JSON.stringify(parsedMessage, undefined, 2));
110+
if (!parsedMessage.rbac?.rules) {
111+
trace('Config parsing failed: no rules found');
112+
return null;
113+
}
114+
try {
115+
return {
116+
typeUrl: RBAC_FILTER_URL,
117+
config: parseRbacConfig(parsedMessage.rbac.rules)
118+
};
119+
} catch (e) {
120+
trace('Config parsing failed: ' + (e as Error).message);
121+
return null;
122+
}
123+
}
124+
125+
function createRbacServerFilter(config: HttpFilterConfig, overrideConfigMap: Map<string, HttpFilterConfig>): ServerInterceptor {
126+
return function rbacServerFilter(methodDescriptor, call): ServerInterceptingCall {
127+
const listener: ServerListener = {
128+
onReceiveMetadata: (metadata, next) => {
129+
let activeConfig = config;
130+
const routeName = metadata.get('grpc-route')[0];
131+
if (routeName) {
132+
const overrideConfig = overrideConfigMap.get(routeName as string);
133+
if (overrideConfig) {
134+
activeConfig = overrideConfig;
135+
}
136+
}
137+
const rbacMetadata = metadata.clone();
138+
rbacMetadata.set(':method', 'POST');
139+
rbacMetadata.set(':authority', call.getHost());
140+
rbacMetadata.set(':path', methodDescriptor.path);
141+
const connectionInfo = call.getConnectionInfo();
142+
const authContext = call.getAuthContext();
143+
const info: UnifiedRbacInfo = {
144+
destinationIp: connectionInfo.localAddress!,
145+
destinationPort: connectionInfo.localPort!,
146+
sourceIp: connectionInfo.remoteAddress!,
147+
headers: rbacMetadata,
148+
tls: authContext.transportSecurityType !== undefined,
149+
peerCertificate: authContext.sslPeerCertificate ?? null,
150+
urlPath: methodDescriptor.path
151+
};
152+
if ((activeConfig as RbacFilterConfig).config.apply(info)) {
153+
next(metadata);
154+
} else {
155+
call.sendStatus({code: status.PERMISSION_DENIED, details: 'Unauthorized RPC rejected'});
156+
}
157+
}
158+
};
159+
return new ServerInterceptingCall(call, {
160+
start: next => {
161+
next(listener);
162+
}
163+
});
164+
}
165+
}
166+
167+
export function setup() {
168+
if (EXPERIMENTAL_RBAC) {
169+
registerHttpFilter(RBAC_FILTER_URL, {
170+
parseTopLevelFilterConfig: parseTopLevelRbacConfig,
171+
parseOverrideFilterConfig: parseOverrideRbacConfig,
172+
createServerFilter: createRbacServerFilter
173+
});
174+
}
175+
}

packages/grpc-js-xds/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import * as xds_wrr_locality from './load-balancer-xds-wrr-locality';
2525
import * as ring_hash from './load-balancer-ring-hash';
2626
import * as router_filter from './http-filter/router-filter';
2727
import * as fault_injection_filter from './http-filter/fault-injection-filter';
28+
import * as rbac_filter from './http-filter/rbac-filter';
2829
import * as csds from './csds';
2930
import * as round_robin_lb from './lb-policy-registry/round-robin';
3031
import * as typed_struct_lb from './lb-policy-registry/typed-struct';
@@ -53,6 +54,7 @@ export function register() {
5354
ring_hash.setup();
5455
router_filter.setup();
5556
fault_injection_filter.setup();
57+
rbac_filter.setup();
5658
csds.setup();
5759
round_robin_lb.setup();
5860
typed_struct_lb.setup();

0 commit comments

Comments
 (0)