/
auth.go
332 lines (268 loc) · 11.8 KB
/
auth.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
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
package auth
import (
"context"
"crypto/sha256"
"fmt"
"strings"
"github.com/pachyderm/pachyderm/v2/src/constants"
"github.com/pachyderm/pachyderm/v2/src/internal/errors"
oidc "github.com/coreos/go-oidc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
const (
// ContextTokenKey is the key of the auth token in an
// authenticated context
ContextTokenKey = constants.ContextTokenKey
// ClusterRoleBindingKey is a key in etcd, in the roleBindings collection,
// that contains the set of role bindings for the cluster. These are frequently
// accessed so we cache them.
ClusterRoleBindingKey = "CLUSTER:"
// The following constants are Subject prefixes. These are prepended to
// subject names in ACLs, group membership, and any other references to subjects
// to indicate what type of Subject or Principal they are (every Pachyderm
// Subject has a logical Principal with the same name).
// UserPrefix indicates that this Subject is a Pachyderm user synced from an IDP.
UserPrefix = "user:"
// RobotPrefix indicates that this Subject is a Pachyderm robot user. Any
// string (with this prefix) is a logical Pachyderm robot user.
RobotPrefix = "robot:"
// InternalPrefix indicates that this Subject is internal to Pachyderm itself,
// created to run a background task
InternalPrefix = "internal:"
// PipelinePrefix indicates that this Subject is a PPS pipeline. Any string
// (with this prefix) is a logical PPS pipeline (even though the pipeline may
// not exist).
PipelinePrefix = "pipeline:"
// PachPrefix indicates that this Subject is an internal Pachyderm user.
PachPrefix = "pach:"
// GroupPrefix indicates that this Subject is a group.
GroupPrefix = "group:"
// RootUser is the user created when auth is initialized. Only one token
// can be created for this user (during auth activation) and they cannot
// be removed from the set of cluster super-admins.
RootUser = "pach:root"
// ClusterAdminRole is the role for cluster admins, who have full access to all APIs
ClusterAdminRole = "clusterAdmin"
// RepoOwnerRole is a role which grants access to read, write and modify the role bindings for a repo
RepoOwnerRole = "repoOwner"
// RepoWriterRole is a role which grants ability to both read from and write to a repo
RepoWriterRole = "repoWriter"
// RepoReaderRole is a role which grants ability to both read from a repo
RepoReaderRole = "repoReader"
// IDPAdminRole is a role which grants the ability to configure OIDC apps.
OIDCAppAdminRole = "oidcAppAdmin"
// IDPAdminRole is a role which grants the ability to configure identity providers.
IDPAdminRole = "idpAdmin"
// IdentityAdmin is a role which grants the ability to configure the identity service.
IdentityAdminRole = "identityAdmin"
// DebuggerRole is a role which grants the ability to produce debug dumps.
DebuggerRole = "debugger"
// LokiLogReaderRole is a role which grants the ability to read logs from Loki.
LokiLogReaderRole = "lokiLogReader"
// RobotUserRole is a role which grants the ability to generate tokens for robot
// users.
RobotUserRole = "robotUser"
// LicenseAdminRole is a role which grants the ability to register new
// pachds with the license server, manage pachds and update the enterprise license.
LicenseAdminRole = "licenseAdmin"
// AllClusterUsersSubject is a subject which applies a role binding to all authenticated users
AllClusterUsersSubject = "allClusterUsers"
// SecretAdminRole is a role which grants the ability to manage secrets
SecretAdminRole = "secretAdmin"
// PachdLogReaderRole is a role which grants the ability to pull pachd logs
PachdLogReaderRole = "pachdLogReader"
// ProjectViewerRole is a role which grants the ability to view resources under a project, such as repos and pipelines
ProjectViewerRole = "projectViewer"
// ProjectWriterRole is a role which grants the ability to create resources under a project, such as repos and pipelines
ProjectWriterRole = "projectWriter"
// ProjectOwnerRole is a role which grants the ability to manage RoleBindings, as well as delete resources within a project
ProjectOwnerRole = "projectOwner"
// ProjectCreatorRole is a role which grants the ability to create projects
ProjectCreatorRole = "projectCreator"
)
var (
// ErrNotActivated is returned by an Auth API if the Auth service
// has not been activated.
//
// Note: This error message string is matched in the UI. If edited,
// it also needs to be updated in the UI code
ErrNotActivated = status.Error(codes.Unimplemented, "the auth service is not activated")
// ErrAlreadyActivated is returned by Activate if the Auth service
// is already activated.
ErrAlreadyActivated = status.Error(codes.Unimplemented, "the auth service is already activated")
// ErrNotSignedIn indicates that the caller isn't signed in
//
// Note: This error message string is matched in the UI. If edited,
// it also needs to be updated in the UI code
ErrNotSignedIn = status.Error(codes.Unauthenticated, "no authentication token (try logging in)")
// ErrNoMetadata is returned by the Auth API if the caller sent a request
// containing no auth token.
ErrNoMetadata = status.Error(codes.Internal, "no authentication metadata (try logging in)")
// ErrBadToken is returned by the Auth API if the caller's token is corrupted
// or has expired.
ErrBadToken = status.Error(codes.Unauthenticated, "provided auth token is corrupted or has expired (try logging in again)")
// ErrExpiredToken is returned by the Auth API if a restored token expired in
// the past.
ErrExpiredToken = status.Error(codes.Internal, "token expiration is in the past")
)
var DefaultOIDCScopes = []string{"email", "profile", "groups", oidc.ScopeOpenID}
// IsErrAlreadyActivated checks if an error is a ErrAlreadyActivated
func IsErrAlreadyActivated(err error) bool {
if err == nil {
return false
}
return strings.Contains(err.Error(), status.Convert(ErrAlreadyActivated).Message())
}
// IsErrNotActivated checks if an error is a ErrNotActivated
func IsErrNotActivated(err error) bool {
if err == nil {
return false
}
// TODO(msteffen) This is unstructured because we have no way to propagate
// structured errors across GRPC boundaries. Fix
return strings.Contains(err.Error(), status.Convert(ErrNotActivated).Message())
}
// IsErrNotSignedIn returns true if 'err' is a ErrNotSignedIn
func IsErrNotSignedIn(err error) bool {
if err == nil {
return false
}
// TODO(msteffen) This is unstructured because we have no way to propagate
// structured errors across GRPC boundaries. Fix
return strings.Contains(err.Error(), status.Convert(ErrNotSignedIn).Message())
}
// IsErrNoMetadata returns true if 'err' is an ErrNoMetadata (uses string
// comparison to work across RPC boundaries)
func IsErrNoMetadata(err error) bool {
if err == nil {
return false
}
return strings.Contains(err.Error(), status.Convert(ErrNoMetadata).Message())
}
// IsErrBadToken returns true if 'err' is a ErrBadToken
func IsErrBadToken(err error) bool {
if err == nil {
return false
}
return strings.Contains(err.Error(), status.Convert(ErrBadToken).Message())
}
// IsErrExpiredToken returns true if 'err' is a ErrExpiredToken
func IsErrExpiredToken(err error) bool {
if err == nil {
return false
}
return strings.Contains(err.Error(), status.Convert(ErrExpiredToken).Message())
}
const errNoRoleBindingMsg = "no role binding exists for"
// ErrNoRoleBinding is returned if no role binding exists for a resource.
type ErrNoRoleBinding struct {
Resource *Resource
}
func (e *ErrNoRoleBinding) Error() string {
return fmt.Sprintf("%v %v %v", errNoRoleBindingMsg, e.Resource.Type, e.Resource.Name)
}
// IsErrNoRoleBinding checks if an error is a ErrNoRoleBinding
func IsErrNoRoleBinding(err error) bool {
if err == nil {
return false
}
return strings.Contains(err.Error(), errNoRoleBindingMsg)
}
// ErrNotAuthorized is returned if the user is not authorized to perform
// a certain operation.
type ErrNotAuthorized struct {
Subject string // subject trying to perform blocked operation -- always set
Resource *Resource // Resource that the user is attempting to access
Required []Permission // Caller needs 'Required'-level access to 'Resource'
}
// This error message string is matched in the UI. If edited,
// it also needs to be updated in the UI code
const errNotAuthorizedMsg = "not authorized to perform this operation"
func (e *ErrNotAuthorized) Error() string {
return fmt.Sprintf("%v is %v - needs permissions %v on %v %v. Run `pachctl auth roles-for-permission` to find roles that grant a given permission.", e.Subject, errNotAuthorizedMsg, e.Required, e.Resource.Type, e.Resource.Name)
}
// Implement the interface expected by status.FromError. An ErrNotAuthorized is
// a permission-denied status.
func (e *ErrNotAuthorized) GRPCStatus() *status.Status {
return status.New(codes.PermissionDenied, e.Error())
}
// IsErrNotAuthorized checks if an error is a ErrNotAuthorized
func IsErrNotAuthorized(err error) bool {
if err == nil {
return false
}
// TODO(msteffen) This is unstructured because we have no way to propagate
// structured errors across GRPC boundaries. Fix
return strings.Contains(err.Error(), errNotAuthorizedMsg)
}
// ErrInvalidPrincipal indicates that a an argument to e.g. GetScope,
// SetScope, or SetACL is invalid
type ErrInvalidPrincipal struct {
Principal string
}
func (e *ErrInvalidPrincipal) Error() string {
return fmt.Sprintf("invalid principal \"%s\"; must start with one of \"pipeline:\", \"github:\", or \"robot:\", or have no \":\"", e.Principal)
}
// IsErrInvalidPrincipal returns true if 'err' is an ErrInvalidPrincipal
func IsErrInvalidPrincipal(err error) bool {
if err == nil {
return false
}
return strings.Contains(err.Error(), "invalid principal \"") &&
strings.Contains(err.Error(), "\"; must start with one of \"pipeline:\", \"github:\", or \"robot:\", or have no \":\"")
}
// ErrTooShortTTL is returned by the ExtendAuthToken if request.Token already
// has a TTL longer than request.TTL.
type ErrTooShortTTL struct {
RequestTTL, ExistingTTL int64
}
const errTooShortTTLMsg = "provided TTL (%d) is shorter than token's existing TTL (%d)"
func (e ErrTooShortTTL) Error() string {
return fmt.Sprintf(errTooShortTTLMsg, e.RequestTTL, e.ExistingTTL)
}
// IsErrTooShortTTL returns true if 'err' is a ErrTooShortTTL
func IsErrTooShortTTL(err error) bool {
if err == nil {
return false
}
errMsg := err.Error()
return strings.Contains(errMsg, "provided TTL (") &&
strings.Contains(errMsg, ") is shorter than token's existing TTL (") &&
strings.Contains(errMsg, ")")
}
// HashToken converts a token to a cryptographic hash.
// We don't want to store tokens verbatim in the database, as then whoever
// that has access to the database has access to all tokens.
func HashToken(token string) string {
sum := sha256.Sum256([]byte(token))
return fmt.Sprintf("%x", sum)
}
// GetAuthToken extracts the auth token embedded in 'ctx', if there is one
func GetAuthToken(ctx context.Context) (string, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return "", errors.EnsureStack(ErrNoMetadata)
}
if len(md[ContextTokenKey]) > 1 {
return "", errors.Errorf("multiple authentication token keys found in context")
} else if len(md[ContextTokenKey]) == 0 {
return "", ErrNotSignedIn
}
return md[ContextTokenKey][0], nil
}
// GetAuthTokenOutgoing is the same as GetAuthToken, but it checks the outgoing metadata in the context.
// TODO: It may make sense to merge GetAuthToken and GetAuthTokenOutgoing?
func GetAuthTokenOutgoing(ctx context.Context) (string, error) {
md, ok := metadata.FromOutgoingContext(ctx)
if !ok {
return "", errors.EnsureStack(ErrNoMetadata)
}
if len(md[ContextTokenKey]) > 1 {
return "", errors.Errorf("multiple authentication token keys found in context")
} else if len(md[ContextTokenKey]) == 0 {
return "", ErrNotSignedIn
}
return md[ContextTokenKey][0], nil
}