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