-
Notifications
You must be signed in to change notification settings - Fork 0
/
rbac_engine.go
317 lines (290 loc) · 10.8 KB
/
rbac_engine.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
/*
* 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.
*/
// Package rbac provides service-level and method-level access control for a
// service. See
// https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/rbac/v3/rbac.proto#role-based-access-control-rbac
// for documentation.
package rbac
import (
"context"
"crypto/x509"
"errors"
"fmt"
"net"
"strconv"
v3rbacpb "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3"
"github.com/qiyouForSql/grpcforunconflict/authz/audit"
"github.com/qiyouForSql/grpcforunconflict/codes"
"github.com/qiyouForSql/grpcforunconflict/credentials"
"github.com/qiyouForSql/grpcforunconflict/grpclog"
"github.com/qiyouForSql/grpcforunconflict/internal/transport"
"github.com/qiyouForSql/grpcforunconflict/metadata"
"github.com/qiyouForSql/grpcforunconflict/peer"
"github.com/qiyouForSql/grpcforunconflict/status"
"google.golang.org/grpc"
)
var logger = grpclog.Component("rbac")
var getConnection = transport.GetConnection
// ChainEngine represents a chain of RBAC Engines, used to make authorization
// decisions on incoming RPCs.
type ChainEngine struct {
chainedEngines []*engine
}
// NewChainEngine returns a chain of RBAC engines, used to make authorization
// decisions on incoming RPCs. Returns a non-nil error for invalid policies.
func NewChainEngine(policies []*v3rbacpb.RBAC, policyName string) (*ChainEngine, error) {
engines := make([]*engine, 0, len(policies))
for _, policy := range policies {
engine, err := newEngine(policy, policyName)
if err != nil {
return nil, err
}
engines = append(engines, engine)
}
return &ChainEngine{chainedEngines: engines}, nil
}
func (cre *ChainEngine) logRequestDetails(rpcData *rpcData) {
if logger.V(2) {
logger.Infof("checking request: url path=%s", rpcData.fullMethod)
if len(rpcData.certs) > 0 {
cert := rpcData.certs[0]
logger.Infof("uri sans=%q, dns sans=%q, subject=%v", cert.URIs, cert.DNSNames, cert.Subject)
}
}
}
// IsAuthorized determines if an incoming RPC is authorized based on the chain of RBAC
// engines and their associated actions.
//
// Errors returned by this function are compatible with the status package.
func (cre *ChainEngine) IsAuthorized(ctx context.Context) error {
// This conversion step (i.e. pulling things out of ctx) can be done once,
// and then be used for the whole chain of RBAC Engines.
rpcData, err := newRPCData(ctx)
if err != nil {
logger.Errorf("newRPCData: %v", err)
return status.Errorf(codes.Internal, "gRPC RBAC: %v", err)
}
for _, engine := range cre.chainedEngines {
matchingPolicyName, ok := engine.findMatchingPolicy(rpcData)
if logger.V(2) && ok {
logger.Infof("incoming RPC matched to policy %v in engine with action %v", matchingPolicyName, engine.action)
}
switch {
case engine.action == v3rbacpb.RBAC_ALLOW && !ok:
cre.logRequestDetails(rpcData)
engine.doAuditLogging(rpcData, matchingPolicyName, false)
return status.Errorf(codes.PermissionDenied, "incoming RPC did not match an allow policy")
case engine.action == v3rbacpb.RBAC_DENY && ok:
cre.logRequestDetails(rpcData)
engine.doAuditLogging(rpcData, matchingPolicyName, false)
return status.Errorf(codes.PermissionDenied, "incoming RPC matched a deny policy %q", matchingPolicyName)
}
// Every policy in the engine list must be queried. Thus, iterate to the
// next policy.
engine.doAuditLogging(rpcData, matchingPolicyName, true)
}
// If the incoming RPC gets through all of the engines successfully (i.e.
// doesn't not match an allow or match a deny engine), the RPC is authorized
// to proceed.
return nil
}
// engine is used for matching incoming RPCs to policies.
type engine struct {
// TODO(gtcooke94) - differentiate between `policyName`, `policies`, and `rules`
policyName string
policies map[string]*policyMatcher
// action must be ALLOW or DENY.
action v3rbacpb.RBAC_Action
auditLoggers []audit.Logger
auditCondition v3rbacpb.RBAC_AuditLoggingOptions_AuditCondition
}
// newEngine creates an RBAC Engine based on the contents of a policy. Returns a
// non-nil error if the policy is invalid.
func newEngine(config *v3rbacpb.RBAC, policyName string) (*engine, error) {
a := config.GetAction()
if a != v3rbacpb.RBAC_ALLOW && a != v3rbacpb.RBAC_DENY {
return nil, fmt.Errorf("unsupported action %s", config.Action)
}
policies := make(map[string]*policyMatcher, len(config.GetPolicies()))
for name, policy := range config.GetPolicies() {
matcher, err := newPolicyMatcher(policy)
if err != nil {
return nil, err
}
policies[name] = matcher
}
auditLoggers, auditCondition, err := parseAuditOptions(config.GetAuditLoggingOptions())
if err != nil {
return nil, err
}
return &engine{
policyName: policyName,
policies: policies,
action: a,
auditLoggers: auditLoggers,
auditCondition: auditCondition,
}, nil
}
func parseAuditOptions(opts *v3rbacpb.RBAC_AuditLoggingOptions) ([]audit.Logger, v3rbacpb.RBAC_AuditLoggingOptions_AuditCondition, error) {
if opts == nil {
return nil, v3rbacpb.RBAC_AuditLoggingOptions_NONE, nil
}
var auditLoggers []audit.Logger
for _, logger := range opts.LoggerConfigs {
auditLogger, err := buildLogger(logger)
if err != nil {
return nil, v3rbacpb.RBAC_AuditLoggingOptions_NONE, err
}
if auditLogger == nil {
// This occurs when the audit logger is not registered but also
// marked optional.
continue
}
auditLoggers = append(auditLoggers, auditLogger)
}
return auditLoggers, opts.GetAuditCondition(), nil
}
// findMatchingPolicy determines if an incoming RPC matches a policy. On a
// successful match, it returns the name of the matching policy and a true bool
// to specify that there was a matching policy found. It returns false in
// the case of not finding a matching policy.
func (e *engine) findMatchingPolicy(rpcData *rpcData) (string, bool) {
for policy, matcher := range e.policies {
if matcher.match(rpcData) {
return policy, true
}
}
return "", false
}
// newRPCData takes an incoming context (should be a context representing state
// needed for server RPC Call with metadata, peer info (used for source ip/port
// and TLS information) and connection (used for destination ip/port) piped into
// it) and the method name of the Service being called server side and populates
// an rpcData struct ready to be passed to the RBAC Engine to find a matching
// policy.
func newRPCData(ctx context.Context) (*rpcData, error) {
// The caller should populate all of these fields (i.e. for empty headers,
// pipe an empty md into context).
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, errors.New("missing metadata in incoming context")
}
// ":method can be hard-coded to POST if unavailable" - A41
md[":method"] = []string{"POST"}
// "If the transport exposes TE in Metadata, then RBAC must special-case the
// header to treat it as not present." - A41
delete(md, "TE")
pi, ok := peer.FromContext(ctx)
if !ok {
return nil, errors.New("missing peer info in incoming context")
}
// The methodName will be available in the passed in ctx from a unary or streaming
// interceptor, as grpc.Server pipes in a transport stream which contains the methodName
// into contexts available in both unary or streaming interceptors.
mn, ok := grpc.Method(ctx)
if !ok {
return nil, errors.New("missing method in incoming context")
}
// The connection is needed in order to find the destination address and
// port of the incoming RPC Call.
conn := getConnection(ctx)
if conn == nil {
return nil, errors.New("missing connection in incoming context")
}
_, dPort, err := net.SplitHostPort(conn.LocalAddr().String())
if err != nil {
return nil, fmt.Errorf("error parsing local address: %v", err)
}
dp, err := strconv.ParseUint(dPort, 10, 32)
if err != nil {
return nil, fmt.Errorf("error parsing local address: %v", err)
}
var authType string
var peerCertificates []*x509.Certificate
if pi.AuthInfo != nil {
tlsInfo, ok := pi.AuthInfo.(credentials.TLSInfo)
if ok {
authType = pi.AuthInfo.AuthType()
peerCertificates = tlsInfo.State.PeerCertificates
}
}
return &rpcData{
md: md,
peerInfo: pi,
fullMethod: mn,
destinationPort: uint32(dp),
localAddr: conn.LocalAddr(),
authType: authType,
certs: peerCertificates,
}, nil
}
// rpcData wraps data pulled from an incoming RPC that the RBAC engine needs to
// find a matching policy.
type rpcData struct {
// md is the HTTP Headers that are present in the incoming RPC.
md metadata.MD
// peerInfo is information about the downstream peer.
peerInfo *peer.Peer
// fullMethod is the method name being called on the upstream service.
fullMethod string
// destinationPort is the port that the RPC is being sent to on the
// server.
destinationPort uint32
// localAddr is the address that the RPC is being sent to.
localAddr net.Addr
// authType is the type of authentication e.g. "tls".
authType string
// certs are the certificates presented by the peer during a TLS
// handshake.
certs []*x509.Certificate
}
func (e *engine) doAuditLogging(rpcData *rpcData, rule string, authorized bool) {
// In the RBAC world, we need to have a SPIFFE ID as the principal for this
// to be meaningful
principal := ""
if rpcData.peerInfo != nil && rpcData.peerInfo.AuthInfo != nil && rpcData.peerInfo.AuthInfo.AuthType() == "tls" {
// If AuthType = tls, then we can cast AuthInfo to TLSInfo.
tlsInfo := rpcData.peerInfo.AuthInfo.(credentials.TLSInfo)
if tlsInfo.SPIFFEID != nil {
principal = tlsInfo.SPIFFEID.String()
}
}
//TODO(gtcooke94) check if we need to log before creating the event
event := &audit.Event{
FullMethodName: rpcData.fullMethod,
Principal: principal,
PolicyName: e.policyName,
MatchedRule: rule,
Authorized: authorized,
}
for _, logger := range e.auditLoggers {
switch e.auditCondition {
case v3rbacpb.RBAC_AuditLoggingOptions_ON_DENY:
if !authorized {
logger.Log(event)
}
case v3rbacpb.RBAC_AuditLoggingOptions_ON_ALLOW:
if authorized {
logger.Log(event)
}
case v3rbacpb.RBAC_AuditLoggingOptions_ON_DENY_AND_ALLOW:
logger.Log(event)
}
}
}
// This is used when converting a custom config from raw JSON to a TypedStruct.
// The TypeURL of the TypeStruct will be "grpc.authz.audit_logging/<name>".
const typeURLPrefix = "grpc.authz.audit_logging/"