github.com/lusis/distribution@v2.0.1+incompatible/registry/auth/token/accesscontroller.go (about) 1 package token 2 3 import ( 4 "crypto" 5 "crypto/x509" 6 "encoding/pem" 7 "errors" 8 "fmt" 9 "io/ioutil" 10 "net/http" 11 "os" 12 "strings" 13 14 ctxu "github.com/docker/distribution/context" 15 "github.com/docker/distribution/registry/auth" 16 "github.com/docker/libtrust" 17 "golang.org/x/net/context" 18 ) 19 20 // accessSet maps a typed, named resource to 21 // a set of actions requested or authorized. 22 type accessSet map[auth.Resource]actionSet 23 24 // newAccessSet constructs an accessSet from 25 // a variable number of auth.Access items. 26 func newAccessSet(accessItems ...auth.Access) accessSet { 27 accessSet := make(accessSet, len(accessItems)) 28 29 for _, access := range accessItems { 30 resource := auth.Resource{ 31 Type: access.Type, 32 Name: access.Name, 33 } 34 35 set, exists := accessSet[resource] 36 if !exists { 37 set = newActionSet() 38 accessSet[resource] = set 39 } 40 41 set.add(access.Action) 42 } 43 44 return accessSet 45 } 46 47 // contains returns whether or not the given access is in this accessSet. 48 func (s accessSet) contains(access auth.Access) bool { 49 actionSet, ok := s[access.Resource] 50 if ok { 51 return actionSet.contains(access.Action) 52 } 53 54 return false 55 } 56 57 // scopeParam returns a collection of scopes which can 58 // be used for a WWW-Authenticate challenge parameter. 59 // See https://tools.ietf.org/html/rfc6750#section-3 60 func (s accessSet) scopeParam() string { 61 scopes := make([]string, 0, len(s)) 62 63 for resource, actionSet := range s { 64 actions := strings.Join(actionSet.keys(), ",") 65 scopes = append(scopes, fmt.Sprintf("%s:%s:%s", resource.Type, resource.Name, actions)) 66 } 67 68 return strings.Join(scopes, " ") 69 } 70 71 // Errors used and exported by this package. 72 var ( 73 ErrInsufficientScope = errors.New("insufficient scope") 74 ErrTokenRequired = errors.New("authorization token required") 75 ) 76 77 // authChallenge implements the auth.Challenge interface. 78 type authChallenge struct { 79 err error 80 realm string 81 service string 82 accessSet accessSet 83 } 84 85 // Error returns the internal error string for this authChallenge. 86 func (ac *authChallenge) Error() string { 87 return ac.err.Error() 88 } 89 90 // Status returns the HTTP Response Status Code for this authChallenge. 91 func (ac *authChallenge) Status() int { 92 return http.StatusUnauthorized 93 } 94 95 // challengeParams constructs the value to be used in 96 // the WWW-Authenticate response challenge header. 97 // See https://tools.ietf.org/html/rfc6750#section-3 98 func (ac *authChallenge) challengeParams() string { 99 str := fmt.Sprintf("Bearer realm=%q,service=%q", ac.realm, ac.service) 100 101 if scope := ac.accessSet.scopeParam(); scope != "" { 102 str = fmt.Sprintf("%s,scope=%q", str, scope) 103 } 104 105 if ac.err == ErrInvalidToken || ac.err == ErrMalformedToken { 106 str = fmt.Sprintf("%s,error=%q", str, "invalid_token") 107 } else if ac.err == ErrInsufficientScope { 108 str = fmt.Sprintf("%s,error=%q", str, "insufficient_scope") 109 } 110 111 return str 112 } 113 114 // SetHeader sets the WWW-Authenticate value for the given header. 115 func (ac *authChallenge) SetHeader(header http.Header) { 116 header.Add("WWW-Authenticate", ac.challengeParams()) 117 } 118 119 // ServeHttp handles writing the challenge response 120 // by setting the challenge header and status code. 121 func (ac *authChallenge) ServeHTTP(w http.ResponseWriter, r *http.Request) { 122 ac.SetHeader(w.Header()) 123 w.WriteHeader(ac.Status()) 124 } 125 126 // accessController implements the auth.AccessController interface. 127 type accessController struct { 128 realm string 129 issuer string 130 service string 131 rootCerts *x509.CertPool 132 trustedKeys map[string]libtrust.PublicKey 133 } 134 135 // tokenAccessOptions is a convenience type for handling 136 // options to the contstructor of an accessController. 137 type tokenAccessOptions struct { 138 realm string 139 issuer string 140 service string 141 rootCertBundle string 142 } 143 144 // checkOptions gathers the necessary options 145 // for an accessController from the given map. 146 func checkOptions(options map[string]interface{}) (tokenAccessOptions, error) { 147 var opts tokenAccessOptions 148 149 keys := []string{"realm", "issuer", "service", "rootcertbundle"} 150 vals := make([]string, 0, len(keys)) 151 for _, key := range keys { 152 val, ok := options[key].(string) 153 if !ok { 154 return opts, fmt.Errorf("token auth requires a valid option string: %q", key) 155 } 156 vals = append(vals, val) 157 } 158 159 opts.realm, opts.issuer, opts.service, opts.rootCertBundle = vals[0], vals[1], vals[2], vals[3] 160 161 return opts, nil 162 } 163 164 // newAccessController creates an accessController using the given options. 165 func newAccessController(options map[string]interface{}) (auth.AccessController, error) { 166 config, err := checkOptions(options) 167 if err != nil { 168 return nil, err 169 } 170 171 fp, err := os.Open(config.rootCertBundle) 172 if err != nil { 173 return nil, fmt.Errorf("unable to open token auth root certificate bundle file %q: %s", config.rootCertBundle, err) 174 } 175 defer fp.Close() 176 177 rawCertBundle, err := ioutil.ReadAll(fp) 178 if err != nil { 179 return nil, fmt.Errorf("unable to read token auth root certificate bundle file %q: %s", config.rootCertBundle, err) 180 } 181 182 var rootCerts []*x509.Certificate 183 pemBlock, rawCertBundle := pem.Decode(rawCertBundle) 184 for pemBlock != nil { 185 cert, err := x509.ParseCertificate(pemBlock.Bytes) 186 if err != nil { 187 return nil, fmt.Errorf("unable to parse token auth root certificate: %s", err) 188 } 189 190 rootCerts = append(rootCerts, cert) 191 192 pemBlock, rawCertBundle = pem.Decode(rawCertBundle) 193 } 194 195 if len(rootCerts) == 0 { 196 return nil, errors.New("token auth requires at least one token signing root certificate") 197 } 198 199 rootPool := x509.NewCertPool() 200 trustedKeys := make(map[string]libtrust.PublicKey, len(rootCerts)) 201 for _, rootCert := range rootCerts { 202 rootPool.AddCert(rootCert) 203 pubKey, err := libtrust.FromCryptoPublicKey(crypto.PublicKey(rootCert.PublicKey)) 204 if err != nil { 205 return nil, fmt.Errorf("unable to get public key from token auth root certificate: %s", err) 206 } 207 trustedKeys[pubKey.KeyID()] = pubKey 208 } 209 210 return &accessController{ 211 realm: config.realm, 212 issuer: config.issuer, 213 service: config.service, 214 rootCerts: rootPool, 215 trustedKeys: trustedKeys, 216 }, nil 217 } 218 219 // Authorized handles checking whether the given request is authorized 220 // for actions on resources described by the given access items. 221 func (ac *accessController) Authorized(ctx context.Context, accessItems ...auth.Access) (context.Context, error) { 222 challenge := &authChallenge{ 223 realm: ac.realm, 224 service: ac.service, 225 accessSet: newAccessSet(accessItems...), 226 } 227 228 req, err := ctxu.GetRequest(ctx) 229 if err != nil { 230 return nil, err 231 } 232 233 parts := strings.Split(req.Header.Get("Authorization"), " ") 234 235 if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" { 236 challenge.err = ErrTokenRequired 237 return nil, challenge 238 } 239 240 rawToken := parts[1] 241 242 token, err := NewToken(rawToken) 243 if err != nil { 244 challenge.err = err 245 return nil, challenge 246 } 247 248 verifyOpts := VerifyOptions{ 249 TrustedIssuers: []string{ac.issuer}, 250 AcceptedAudiences: []string{ac.service}, 251 Roots: ac.rootCerts, 252 TrustedKeys: ac.trustedKeys, 253 } 254 255 if err = token.Verify(verifyOpts); err != nil { 256 challenge.err = err 257 return nil, challenge 258 } 259 260 accessSet := token.accessSet() 261 for _, access := range accessItems { 262 if !accessSet.contains(access) { 263 challenge.err = ErrInsufficientScope 264 return nil, challenge 265 } 266 } 267 268 return auth.WithUser(ctx, auth.UserInfo{Name: token.Claims.Subject}), nil 269 } 270 271 // init handles registering the token auth backend. 272 func init() { 273 auth.Register("token", auth.InitFunc(newAccessController)) 274 }