-
Notifications
You must be signed in to change notification settings - Fork 3.8k
/
authorizer.go
233 lines (214 loc) · 8.57 KB
/
authorizer.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
// Copyright 2023 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.
package tenantcapabilitiesauthorizer
import (
"context"
"github.com/cockroachdb/cockroach/pkg/kv/kvpb"
"github.com/cockroachdb/cockroach/pkg/multitenant/tenantcapabilities"
"github.com/cockroachdb/cockroach/pkg/roachpb"
"github.com/cockroachdb/cockroach/pkg/settings"
"github.com/cockroachdb/cockroach/pkg/settings/cluster"
"github.com/cockroachdb/cockroach/pkg/util/log"
"github.com/cockroachdb/errors"
)
// authorizerEnabled dictates whether the Authorizer performs any capability
// checks or not. It is intended as an escape hatch to turn off the tenant
// capabilities infrastructure; as such, it is intended to be a sort of hammer
// of last resort, and isn't expected to be used during normal cluster
// operation.
var authorizerEnabled = settings.RegisterBoolSetting(
settings.SystemOnly,
"tenant_capabilities.authorizer.enabled",
"enables authorization based on capability checks for incoming (secondary tenant) requests",
true,
)
// Authorizer is a concrete implementation of the tenantcapabilities.Authorizer
// interface. It's safe for concurrent use.
type Authorizer struct {
capabilitiesReader tenantcapabilities.Reader
settings *cluster.Settings
knobs tenantcapabilities.TestingKnobs
}
var _ tenantcapabilities.Authorizer = &Authorizer{}
// New constructs a new tenantcapabilities.Authorizer.
func New(settings *cluster.Settings, knobs *tenantcapabilities.TestingKnobs) *Authorizer {
var testingKnobs tenantcapabilities.TestingKnobs
if knobs != nil {
testingKnobs = *knobs
}
a := &Authorizer{
settings: settings,
knobs: testingKnobs,
// capabilitiesReader is set post construction, using BindReader.
}
return a
}
// HasCapabilityForBatch implements the tenantcapabilities.Authorizer interface.
func (a *Authorizer) HasCapabilityForBatch(
ctx context.Context, tenID roachpb.TenantID, ba *kvpb.BatchRequest,
) error {
if a.elideCapabilityChecks(ctx, tenID) {
return nil
}
if a.capabilitiesReader == nil {
return errors.AssertionFailedf("programming error: trying to perform capability check when no reader exists")
}
cp, found := a.capabilitiesReader.GetCapabilities(tenID)
if !found {
log.VInfof(ctx, 2,
"no capability information for tenant %s; requests that require capabilities may be denied",
tenID)
}
for _, ru := range ba.Requests {
request := ru.GetInner()
capability, hasCap := reqMethodToCap[request.Method()]
if capability == noCapCheckNeeded {
continue
}
switch request.Method() {
case kvpb.AdminSplit, kvpb.AdminScatter:
// AdminSplit and AdminScatter requests must are codified as
// "disable_admin_{split,scatter}" in the capabilities proto, as tenants
// have the ability to perform splits and scatters by default. This is
// because things like IMPORT/RESTORE rely on these operations for
// performance. As such, they must be explicitly revoked by the operator.
// This is in contrast to other capabilities that require authorization,
// which are explicitly granted.
if !found {
return nil // allowed by default
}
if disabled := cp.GetBool(capability); disabled {
return errors.Newf(
"client tenant capability %q prevents operation (%T)", capability, request,
)
}
default:
// All other requests that require capabilities are expressed in their
// "enabled" form.
if !hasCap || capability == onlySystemTenant || !found || !cp.GetBool(capability) {
// All allowable request types must be explicitly opted into the
// reqMethodToCap map. If a request type is missing from the map
// (!hasCap), we must be conservative and assume it is
// disallowed. This prevents accidents where someone adds a new
// sensitive request type in KV and forgets to add an explicit
// authorization rule for it here.
return errors.Newf("client tenant does not have capability %q (%T)", capability, request)
}
}
}
return nil
}
var reqMethodToCap = map[kvpb.Method]tenantcapabilities.CapabilityID{
// The following requests are authorized for all workloads.
kvpb.AddSSTable: noCapCheckNeeded,
kvpb.Barrier: noCapCheckNeeded,
kvpb.ClearRange: noCapCheckNeeded,
kvpb.ConditionalPut: noCapCheckNeeded,
kvpb.DeleteRange: noCapCheckNeeded,
kvpb.Delete: noCapCheckNeeded,
kvpb.EndTxn: noCapCheckNeeded,
kvpb.Export: noCapCheckNeeded,
kvpb.Get: noCapCheckNeeded,
kvpb.HeartbeatTxn: noCapCheckNeeded,
kvpb.Increment: noCapCheckNeeded,
kvpb.InitPut: noCapCheckNeeded,
kvpb.IsSpanEmpty: noCapCheckNeeded,
kvpb.LeaseInfo: noCapCheckNeeded,
kvpb.PushTxn: noCapCheckNeeded,
kvpb.Put: noCapCheckNeeded,
kvpb.QueryIntent: noCapCheckNeeded,
kvpb.QueryLocks: noCapCheckNeeded,
kvpb.QueryTxn: noCapCheckNeeded,
kvpb.RangeStats: noCapCheckNeeded,
kvpb.RecoverTxn: noCapCheckNeeded,
kvpb.Refresh: noCapCheckNeeded,
kvpb.RefreshRange: noCapCheckNeeded,
kvpb.ResolveIntentRange: noCapCheckNeeded,
kvpb.ResolveIntent: noCapCheckNeeded,
kvpb.ReverseScan: noCapCheckNeeded,
kvpb.RevertRange: noCapCheckNeeded,
kvpb.Scan: noCapCheckNeeded,
// The following are authorized via specific capabilities.
kvpb.AdminScatter: tenantcapabilities.DisableAdminScatter,
kvpb.AdminSplit: tenantcapabilities.DisableAdminSplit,
kvpb.AdminUnsplit: tenantcapabilities.CanAdminUnsplit,
// TODO(ecwall): The following should also be authorized via specific capabilities.
kvpb.AdminChangeReplicas: noCapCheckNeeded,
kvpb.AdminMerge: noCapCheckNeeded,
kvpb.AdminRelocateRange: noCapCheckNeeded,
kvpb.AdminTransferLease: noCapCheckNeeded,
// TODO(knz,arul): Verify with the relevant teams whether secondary
// tenants have legitimate access to any of those.
kvpb.TruncateLog: onlySystemTenant,
kvpb.Merge: onlySystemTenant,
kvpb.RequestLease: onlySystemTenant,
kvpb.TransferLease: onlySystemTenant,
kvpb.Probe: onlySystemTenant,
kvpb.RecomputeStats: onlySystemTenant,
kvpb.ComputeChecksum: onlySystemTenant,
kvpb.CheckConsistency: onlySystemTenant,
kvpb.AdminVerifyProtectedTimestamp: onlySystemTenant,
kvpb.Migrate: onlySystemTenant,
kvpb.Subsume: onlySystemTenant,
kvpb.QueryResolvedTimestamp: onlySystemTenant,
kvpb.GC: onlySystemTenant,
}
const (
noCapCheckNeeded = iota + tenantcapabilities.MaxCapabilityID + 1
onlySystemTenant
)
// BindReader implements the tenantcapabilities.Authorizer interface.
func (a *Authorizer) BindReader(reader tenantcapabilities.Reader) {
a.capabilitiesReader = reader
}
func (a *Authorizer) HasNodeStatusCapability(ctx context.Context, tenID roachpb.TenantID) error {
if a.elideCapabilityChecks(ctx, tenID) {
return nil
}
cp, found := a.capabilitiesReader.GetCapabilities(tenID)
if !found {
log.Infof(ctx,
"no capability information for tenant %s; requests that require capabilities may be denied",
tenID,
)
}
if !found || !cp.GetBool(tenantcapabilities.CanViewNodeInfo) {
return errors.Newf("client tenant does not have capability to query cluster node metadata")
}
return nil
}
func (a *Authorizer) HasTSDBQueryCapability(ctx context.Context, tenID roachpb.TenantID) error {
if a.elideCapabilityChecks(ctx, tenID) {
return nil
}
cp, found := a.capabilitiesReader.GetCapabilities(tenID)
if !found {
log.Infof(ctx,
"no capability information for tenant %s; requests that require capabilities may be denied",
tenID,
)
}
if !found || !cp.GetBool(tenantcapabilities.CanViewTSDBMetrics) {
return errors.Newf("client tenant does not have capability to query timeseries data")
}
return nil
}
// elideCapabilityChecks returns true if capability checks should be skipped for
// the supplied tenant.
func (a *Authorizer) elideCapabilityChecks(ctx context.Context, tenID roachpb.TenantID) bool {
if tenID.IsSystem() {
return true // the system tenant is allowed to do as it pleases
}
if !authorizerEnabled.Get(&a.settings.SV) {
log.VInfof(ctx, 3, "authorizer turned off; eliding capability checks")
return true
}
return false
}