forked from project-zot/zot
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathauthz.go
387 lines (310 loc) · 10.2 KB
/
authz.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
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
package api
import (
"context"
"net/http"
glob "github.com/bmatcuk/doublestar/v4"
"github.com/gorilla/mux"
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/common"
"zotregistry.io/zot/pkg/log"
localCtx "zotregistry.io/zot/pkg/requestcontext"
)
const (
// method actions.
Create = "create"
Read = "read"
Update = "update"
Delete = "delete"
// behaviour actions.
DetectManifestCollision = "detectManifestCollision"
BASIC = "Basic"
BEARER = "Bearer"
OPENID = "OpenID"
)
// AccessController authorizes users to act on resources.
type AccessController struct {
Config *config.AccessControlConfig
Log log.Logger
}
func NewAccessController(conf *config.Config) *AccessController {
if conf.HTTP.AccessControl == nil {
return &AccessController{
Config: &config.AccessControlConfig{},
Log: log.NewLogger(conf.Log.Level, conf.Log.Output),
}
}
return &AccessController{
Config: conf.HTTP.AccessControl,
Log: log.NewLogger(conf.Log.Level, conf.Log.Output),
}
}
// getGlobPatterns gets glob patterns from authz config on which <username> has <action> perms.
// used to filter /v2/_catalog repositories based on user rights.
func (ac *AccessController) getGlobPatterns(username string, groups []string, action string) map[string]bool {
globPatterns := make(map[string]bool)
for pattern, policyGroup := range ac.Config.Repositories {
if username == "" {
// check anonymous policy
if common.Contains(policyGroup.AnonymousPolicy, action) {
globPatterns[pattern] = true
}
} else {
// check default policy (authenticated user)
if common.Contains(policyGroup.DefaultPolicy, action) {
globPatterns[pattern] = true
}
}
// check user based policy
for _, p := range policyGroup.Policies {
if common.Contains(p.Users, username) && common.Contains(p.Actions, action) {
globPatterns[pattern] = true
}
}
// check group based policy
for _, group := range groups {
for _, p := range policyGroup.Policies {
if common.Contains(p.Groups, group) && common.Contains(p.Actions, action) {
globPatterns[pattern] = true
}
}
}
// if not allowed then mark it
if _, ok := globPatterns[pattern]; !ok {
globPatterns[pattern] = false
}
}
return globPatterns
}
// can verifies if a user can do action on repository.
func (ac *AccessController) can(ctx context.Context, username, action, repository string) bool {
can := false
var longestMatchedPattern string
for pattern := range ac.Config.Repositories {
matched, err := glob.Match(pattern, repository)
if err == nil {
if matched && len(pattern) > len(longestMatchedPattern) {
longestMatchedPattern = pattern
}
}
}
acCtx, err := localCtx.GetAccessControlContext(ctx)
if err != nil {
return false
}
userGroups := acCtx.Groups
// check matched repo based policy
pg, ok := ac.Config.Repositories[longestMatchedPattern]
if ok {
can = ac.isPermitted(userGroups, username, action, pg)
}
// check admins based policy
if !can {
if ac.isAdmin(username) && common.Contains(ac.Config.AdminPolicy.Actions, action) {
can = true
}
if ac.isAnyGroupInAdminPolicy(userGroups) && common.Contains(ac.Config.AdminPolicy.Actions, action) {
can = true
}
}
return can
}
// isAdmin .
func (ac *AccessController) isAdmin(username string) bool {
return common.Contains(ac.Config.AdminPolicy.Users, username)
}
func (ac *AccessController) isAnyGroupInAdminPolicy(userGroups []string) bool {
for _, group := range userGroups {
if common.Contains(ac.Config.AdminPolicy.Groups, group) {
return true
}
}
return false
}
func (ac *AccessController) getUserGroups(username string) []string {
var groupNames []string
for groupName, group := range ac.Config.Groups {
for _, user := range group.Users {
// find if the user is part of any groups
if user == username {
groupNames = append(groupNames, groupName)
}
}
}
return groupNames
}
// getContext updates an AccessControlContext for a user/anonymous and returns a context.Context containing it.
func (ac *AccessController) getContext(acCtx *localCtx.AccessControlContext, request *http.Request) context.Context {
readGlobPatterns := ac.getGlobPatterns(acCtx.Username, acCtx.Groups, Read)
dmcGlobPatterns := ac.getGlobPatterns(acCtx.Username, acCtx.Groups, DetectManifestCollision)
acCtx.ReadGlobPatterns = readGlobPatterns
acCtx.DmcGlobPatterns = dmcGlobPatterns
if ac.isAdmin(acCtx.Username) {
acCtx.IsAdmin = true
} else {
acCtx.IsAdmin = false
}
authzCtxKey := localCtx.GetContextKey()
ctx := context.WithValue(request.Context(), authzCtxKey, *acCtx)
return ctx
}
// getAuthnMiddlewareContext builds ac context(allowed to read repos and if user is admin) and returns it.
func (ac *AccessController) getAuthnMiddlewareContext(authnType string, request *http.Request) context.Context {
amwCtx := localCtx.AuthnMiddlewareContext{
AuthnType: authnType,
}
amwCtxKey := localCtx.GetAuthnMiddlewareCtxKey()
ctx := context.WithValue(request.Context(), amwCtxKey, amwCtx)
return ctx
}
// isPermitted returns true if username can do action on a repository policy.
func (ac *AccessController) isPermitted(userGroups []string, username, action string,
policyGroup config.PolicyGroup,
) bool {
var result bool
// check repo/system based policies
for _, p := range policyGroup.Policies {
if common.Contains(p.Users, username) && common.Contains(p.Actions, action) {
result = true
return result
}
}
if userGroups != nil {
for _, p := range policyGroup.Policies {
if common.Contains(p.Actions, action) {
for _, group := range p.Groups {
if common.Contains(userGroups, group) {
result = true
return result
}
}
}
}
}
// check defaultPolicy
if !result {
if common.Contains(policyGroup.DefaultPolicy, action) && username != "" {
result = true
}
}
// check anonymousPolicy
if !result {
if common.Contains(policyGroup.AnonymousPolicy, action) && username == "" {
result = true
}
}
return result
}
func BaseAuthzHandler(ctlr *Controller) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
/* NOTE:
since we only do READ actions in extensions, this middleware is enough for them because
it populates the context with user relevant data to be processed by each individual extension
*/
if request.Method == http.MethodOptions {
next.ServeHTTP(response, request)
return
}
// request comes from bearer authn, bypass it
authnMwCtx, err := localCtx.GetAuthnMiddlewareContext(request.Context())
if err != nil || (authnMwCtx != nil && authnMwCtx.AuthnType == BEARER) {
next.ServeHTTP(response, request)
return
}
// bypass authz for /v2/ route
if request.RequestURI == "/v2/" {
next.ServeHTTP(response, request)
return
}
acCtrlr := NewAccessController(ctlr.Config)
var identity string
// anonymous context
acCtx := &localCtx.AccessControlContext{}
// get username from context made in authn.go
if ctlr.Config.IsBasicAuthnEnabled() {
// get access control context made in authn.go if authn is enabled
acCtx, err = localCtx.GetAccessControlContext(request.Context())
if err != nil { // should never happen
authFail(response, request, ctlr.Config.HTTP.Realm, ctlr.Config.HTTP.Auth.FailDelay)
return
}
identity = acCtx.Username
}
if request.TLS != nil {
verifiedChains := request.TLS.VerifiedChains
// still no identity, get it from TLS certs
if identity == "" && verifiedChains != nil &&
len(verifiedChains) > 0 && len(verifiedChains[0]) > 0 {
for _, cert := range request.TLS.PeerCertificates {
identity = cert.Subject.CommonName
}
// if we still don't have an identity
if identity == "" {
acCtrlr.Log.Info().Msg("couldn't get identity from TLS certificate")
authFail(response, request, ctlr.Config.HTTP.Realm, ctlr.Config.HTTP.Auth.FailDelay)
return
}
// assign identity to authz context, needed for extensions
acCtx.Username = identity
}
}
ctx := acCtrlr.getContext(acCtx, request)
next.ServeHTTP(response, request.WithContext(ctx)) //nolint:contextcheck
})
}
}
func DistSpecAuthzHandler(ctlr *Controller) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
if request.Method == http.MethodOptions {
next.ServeHTTP(response, request)
return
}
// request comes from bearer authn, bypass it
authnMwCtx, err := localCtx.GetAuthnMiddlewareContext(request.Context())
if err != nil || (authnMwCtx != nil && authnMwCtx.AuthnType == BEARER) {
next.ServeHTTP(response, request)
return
}
vars := mux.Vars(request)
resource := vars["name"]
reference, ok := vars["reference"]
acCtrlr := NewAccessController(ctlr.Config)
var identity string
// get acCtx built in authn and previous authz middlewares
acCtx, err := localCtx.GetAccessControlContext(request.Context())
if err != nil { // should never happen
authFail(response, request, ctlr.Config.HTTP.Realm, ctlr.Config.HTTP.Auth.FailDelay)
return
}
// get username from context made in authn.go
identity = acCtx.Username
var action string
if request.Method == http.MethodGet || request.Method == http.MethodHead {
action = Read
}
if request.Method == http.MethodPut || request.Method == http.MethodPatch || request.Method == http.MethodPost {
// assume user wants to create
action = Create
// if we get a reference (tag)
if ok {
is := ctlr.StoreController.GetImageStore(resource)
tags, err := is.GetImageTags(resource)
// if repo exists and request's tag exists then action is UPDATE
if err == nil && common.Contains(tags, reference) && reference != "latest" {
action = Update
}
}
}
if request.Method == http.MethodDelete {
action = Delete
}
can := acCtrlr.can(request.Context(), identity, action, resource) //nolint:contextcheck
if !can {
common.AuthzFail(response, request, ctlr.Config.HTTP.Realm, ctlr.Config.HTTP.Auth.FailDelay)
} else {
next.ServeHTTP(response, request) //nolint:contextcheck
}
})
}
}