// configureLogging prepares the context with a logger using the // configuration. func configureLogging(ctx context.Context, config *configuration.Configuration) (context.Context, error) { if config.Log.Level == "" && config.Log.Formatter == "" { // If no config for logging is set, fallback to deprecated "Loglevel". log.SetLevel(logLevel(config.Loglevel)) ctx = context.WithLogger(ctx, context.GetLogger(ctx, "version")) return ctx, nil } log.SetLevel(logLevel(config.Log.Level)) formatter := config.Log.Formatter if formatter == "" { formatter = "text" // default formatter } switch formatter { case "json": log.SetFormatter(&log.JSONFormatter{ TimestampFormat: time.RFC3339Nano, }) case "text": log.SetFormatter(&log.TextFormatter{ TimestampFormat: time.RFC3339Nano, }) case "logstash": log.SetFormatter(&logstash.LogstashFormatter{ TimestampFormat: time.RFC3339Nano, }) default: // just let the library use default on empty string. if config.Log.Formatter != "" { return ctx, fmt.Errorf("unsupported logging formatter: %q", config.Log.Formatter) } } if config.Log.Formatter != "" { log.Debugf("using %q logging formatter", config.Log.Formatter) } // log the application version with messages ctx = context.WithLogger(ctx, context.GetLogger(ctx, "version")) if len(config.Log.Fields) > 0 { // build up the static fields, if present. var fields []interface{} for k := range config.Log.Fields { fields = append(fields, k) } ctx = context.WithValues(ctx, config.Log.Fields) ctx = context.WithLogger(ctx, context.GetLogger(ctx, fields...)) } return ctx, nil }
// initialize a repo with keys, so they can be rotated func setUpRepo(t *testing.T, tempBaseDir, gun string, ret notary.PassRetriever) ( *httptest.Server, map[string]string) { // Set up server ctx := context.WithValue( context.Background(), "metaStore", storage.NewMemStorage()) // Do not pass one of the const KeyAlgorithms here as the value! Passing a // string is in itself good test that we are handling it correctly as we // will be receiving a string from the configuration. ctx = context.WithValue(ctx, "keyAlgorithm", "ecdsa") // Eat the logs instead of spewing them out l := logrus.New() l.Out = bytes.NewBuffer(nil) ctx = ctxu.WithLogger(ctx, logrus.NewEntry(l)) cryptoService := cryptoservice.NewCryptoService(trustmanager.NewKeyMemoryStore(ret)) ts := httptest.NewServer(server.RootHandler(nil, ctx, cryptoService, nil, nil, nil)) repo, err := client.NewNotaryRepository( tempBaseDir, gun, ts.URL, http.DefaultTransport, ret, trustpinning.TrustPinConfig{}) require.NoError(t, err, "error creating repo: %s", err) rootPubKey, err := repo.CryptoService.Create("root", "", data.ECDSAKey) require.NoError(t, err, "error generating root key: %s", err) err = repo.Initialize(rootPubKey.ID()) require.NoError(t, err) return ts, repo.CryptoService.ListAllKeys() }
func (app *App) logError(context context.Context, errors errcode.Errors) { for _, e1 := range errors { var c ctxu.Context switch e1.(type) { case errcode.Error: e, _ := e1.(errcode.Error) c = ctxu.WithValue(context, "err.code", e.Code) c = ctxu.WithValue(c, "err.message", e.Code.Message()) c = ctxu.WithValue(c, "err.detail", e.Detail) case errcode.ErrorCode: e, _ := e1.(errcode.ErrorCode) c = ctxu.WithValue(context, "err.code", e) c = ctxu.WithValue(c, "err.message", e.Message()) default: // just normal go 'error' c = ctxu.WithValue(context, "err.code", errcode.ErrorCodeUnknown) c = ctxu.WithValue(c, "err.message", e1.Error()) } c = ctxu.WithLogger(c, ctxu.GetLogger(c, "err.code", "err.message", "err.detail")) ctxu.GetResponseLogger(c).Errorf("response completed with error") } }
// context constructs the context object for the application. This only be // called once per request. func (app *App) context(w http.ResponseWriter, r *http.Request) *Context { ctx := defaultContextManager.context(app, w, r) ctx = ctxu.WithVars(ctx, r) ctx = ctxu.WithLogger(ctx, ctxu.GetLogger(ctx, "vars.name", "vars.reference", "vars.digest", "vars.uuid")) context := &Context{ App: app, Context: ctx, } if app.httpHost.Scheme != "" && app.httpHost.Host != "" { // A "host" item in the configuration takes precedence over // X-Forwarded-Proto and X-Forwarded-Host headers, and the // hostname in the request. context.urlBuilder = v2.NewURLBuilder(&app.httpHost) } else { context.urlBuilder = v2.NewURLBuilderFromRequest(r) } return context }
// ServeHTTP serves an HTTP request and implements the http.Handler interface. func (root *rootHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) ctx := ctxu.WithRequest(root.context, r) ctx, w = ctxu.WithResponseWriter(ctx, w) ctx = ctxu.WithLogger(ctx, ctxu.GetRequestLogger(ctx)) ctx = context.WithValue(ctx, "repo", vars["imageName"]) ctx = context.WithValue(ctx, "cryptoService", root.trust) defer func() { ctxu.GetResponseLogger(ctx).Info("response completed") }() if root.auth != nil { access := buildAccessRecords(vars["imageName"], root.actions...) var authCtx context.Context var err error if authCtx, err = root.auth.Authorized(ctx, access...); err != nil { if err, ok := err.(auth.Challenge); ok { err.ServeHTTP(w, r) w.WriteHeader(http.StatusUnauthorized) return } errcode.ServeJSON(w, v2.ErrorCodeUnauthorized) return } ctx = authCtx } if err := root.handler(ctx, w, r); err != nil { e := errcode.ServeJSON(w, err) if e != nil { logrus.Error(e) } return } }
// ServeHTTP serves an HTTP request and implements the http.Handler interface. func (root *rootHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { var ( err error ctx = ctxu.WithRequest(root.context, r) log = ctxu.GetRequestLogger(ctx) vars = mux.Vars(r) ) ctx, w = ctxu.WithResponseWriter(ctx, w) ctx = ctxu.WithLogger(ctx, log) ctx = context.WithValue(ctx, notary.CtxKeyCryptoSvc, root.trust) defer func() { ctxu.GetResponseLogger(ctx).Info("response completed") }() if root.auth != nil { ctx = context.WithValue(ctx, notary.CtxKeyRepo, vars["imageName"]) if ctx, err = root.doAuth(ctx, vars["imageName"], w); err != nil { // errors have already been logged/output to w inside doAuth // just return return } } if err := root.handler(ctx, w, r); err != nil { serveError(log, w, err) } }
// handlerWithContext wraps the given context-aware handler by setting up the // request context from a base context. func handlerWithContext(ctx context.Context, handler func(context.Context, http.ResponseWriter, *http.Request)) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := context.WithRequest(ctx, r) logger := context.GetRequestLogger(ctx) ctx = context.WithLogger(ctx, logger) handler(ctx, w, r) }) }
// main is a modified version of the registry main function: // https://github.com/docker/distribution/blob/6ba799b/cmd/registry/main.go func main() { logrus.SetLevel(logrus.InfoLevel) ctx := context.Background() ctx = context.WithValue(ctx, "version", version.String()) ctx = context.WithLogger(ctx, context.GetLogger(ctx, "version")) client, err := controller.NewClient("", os.Getenv("CONTROLLER_KEY")) if err != nil { context.GetLogger(ctx).Fatalln(err) } release, err := client.GetRelease(os.Getenv("FLYNN_RELEASE_ID")) if err != nil { context.GetLogger(ctx).Fatalln(err) } artifact, err := client.GetArtifact(release.ArtifactIDs[0]) if err != nil { context.GetLogger(ctx).Fatalln(err) } authKey := os.Getenv("AUTH_KEY") middleware.Register("flynn", repositoryMiddleware(client, artifact, authKey)) config := configuration.Configuration{ Version: configuration.CurrentVersion, Storage: configuration.Storage{ blobstore.DriverName: configuration.Parameters{}, "delete": configuration.Parameters{"enabled": true}, }, Middleware: map[string][]configuration.Middleware{ "repository": { {Name: "flynn"}, }, }, Auth: configuration.Auth{ "flynn": configuration.Parameters{ "auth_key": authKey, }, }, } config.HTTP.Secret = os.Getenv("REGISTRY_HTTP_SECRET") status.AddHandler(status.HealthyHandler) app := handlers.NewApp(ctx, config) http.Handle("/", app) addr := ":" + os.Getenv("PORT") context.GetLogger(app).Infof("listening on %s", addr) if err := http.ListenAndServe(addr, nil); err != nil { context.GetLogger(app).Fatalln(err) } }
func (app *App) logError(context context.Context, errors v2.Errors) { for _, e := range errors.Errors { c := ctxu.WithValue(context, "err.code", e.Code) c = ctxu.WithValue(c, "err.message", e.Message) c = ctxu.WithValue(c, "err.detail", e.Detail) c = ctxu.WithLogger(c, ctxu.GetLogger(c, "err.code", "err.message", "err.detail")) ctxu.GetLogger(c).Errorf("An error occured") } }
// ServeHTTP serves an HTTP request and implements the http.Handler interface. func (root *rootHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) ctx := ctxu.WithRequest(root.context, r) log := ctxu.GetRequestLogger(ctx) ctx, w = ctxu.WithResponseWriter(ctx, w) ctx = ctxu.WithLogger(ctx, log) ctx = context.WithValue(ctx, "repo", vars["imageName"]) ctx = context.WithValue(ctx, "cryptoService", root.trust) defer func() { ctxu.GetResponseLogger(ctx).Info("response completed") }() if root.auth != nil { access := buildAccessRecords(vars["imageName"], root.actions...) var authCtx context.Context var err error if authCtx, err = root.auth.Authorized(ctx, access...); err != nil { if challenge, ok := err.(auth.Challenge); ok { // Let the challenge write the response. challenge.SetHeaders(w) if err := errcode.ServeJSON(w, errcode.ErrorCodeUnauthorized.WithDetail(access)); err != nil { log.Errorf("failed to serve challenge response: %s", err.Error()) } return } errcode.ServeJSON(w, errcode.ErrorCodeUnauthorized) return } ctx = authCtx } if err := root.handler(ctx, w, r); err != nil { if httpErr, ok := err.(errcode.ErrorCoder); ok { // info level logging for non-5XX http errors httpErrCode := httpErr.ErrorCode().Descriptor().HTTPStatusCode if httpErrCode >= http.StatusInternalServerError { // error level logging for 5XX http errors log.Error(httpErr) } else { log.Info(httpErr) } } e := errcode.ServeJSON(w, err) if e != nil { log.Error(e) } return } }
func setupServerHandler(metaStore storage.MetaStore) http.Handler { ctx := context.WithValue(context.Background(), "metaStore", metaStore) ctx = context.WithValue(ctx, "keyAlgorithm", data.ECDSAKey) // Eat the logs instead of spewing them out var b bytes.Buffer l := logrus.New() l.Out = &b ctx = ctxu.WithLogger(ctx, logrus.NewEntry(l)) cryptoService := cryptoservice.NewCryptoService(trustmanager.NewKeyMemoryStore(passphrase.ConstantRetriever("pass"))) return server.RootHandler(nil, ctx, cryptoService, nil, nil, nil) }
// context constructs the context object for the application. This only be // called once per request. func (app *App) context(w http.ResponseWriter, r *http.Request) *Context { ctx := defaultContextManager.context(app, w, r) ctx = ctxu.WithVars(ctx, r) ctx = ctxu.WithLogger(ctx, ctxu.GetLogger(ctx, "vars.name", "vars.reference", "vars.digest", "vars.uuid")) context := &Context{ App: app, Context: ctx, urlBuilder: v2.NewURLBuilderFromRequest(r), } return context }
// makes a testing notary-server func setupServer() *httptest.Server { // Set up server ctx := context.WithValue( context.Background(), "metaStore", storage.NewMemStorage()) ctx = context.WithValue(ctx, "keyAlgorithm", data.ECDSAKey) // Eat the logs instead of spewing them out var b bytes.Buffer l := logrus.New() l.Out = &b ctx = ctxu.WithLogger(ctx, logrus.NewEntry(l)) cryptoService := cryptoservice.NewCryptoService( "", trustmanager.NewKeyMemoryStore(retriever)) return httptest.NewServer(server.RootHandler(nil, ctx, cryptoService)) }
func fullTestServer(t *testing.T) *httptest.Server { // Set up server ctx := context.WithValue( context.Background(), "metaStore", storage.NewMemStorage()) // Do not pass one of the const KeyAlgorithms here as the value! Passing a // string is in itself good test that we are handling it correctly as we // will be receiving a string from the configuration. ctx = context.WithValue(ctx, "keyAlgorithm", "ecdsa") // Eat the logs instead of spewing them out var b bytes.Buffer l := logrus.New() l.Out = &b ctx = ctxu.WithLogger(ctx, logrus.NewEntry(l)) cryptoService := cryptoservice.NewCryptoService( "", trustmanager.NewKeyMemoryStore(passphraseRetriever)) return httptest.NewServer(server.RootHandler(nil, ctx, cryptoService)) }
// context either returns a new context or looks it up in the manager. func (cm *contextManager) context(parent context.Context, w http.ResponseWriter, r *http.Request) context.Context { cm.mu.Lock() defer cm.mu.Unlock() ctx, ok := cm.contexts[r] if ok { return ctx } if parent == nil { parent = ctxu.Background() } ctx = ctxu.WithRequest(parent, r) ctx, w = ctxu.WithResponseWriter(ctx, w) ctx = ctxu.WithLogger(ctx, ctxu.GetRequestLogger(ctx)) cm.contexts[r] = ctx return ctx }
// configureLogHook prepares logging hook parameters. func (app *App) configureLogHook(configuration *configuration.Configuration) { logger := ctxu.GetLogger(app).(*log.Entry).Logger for _, configHook := range configuration.Log.Hooks { if !configHook.Disabled { switch configHook.Type { case "mail": hook := &logHook{} hook.LevelsParam = configHook.Levels hook.Mail = &mailer{ Addr: configHook.MailOptions.SMTP.Addr, Username: configHook.MailOptions.SMTP.Username, Password: configHook.MailOptions.SMTP.Password, Insecure: configHook.MailOptions.SMTP.Insecure, From: configHook.MailOptions.From, To: configHook.MailOptions.To, } logger.Hooks.Add(hook) default: } } } app.Context = ctxu.WithLogger(app.Context, logger) }
// NewApp takes a configuration and returns a configured app, ready to serve // requests. The app only implements ServeHTTP and can be wrapped in other // handlers accordingly. func NewApp(ctx context.Context, configuration configuration.Configuration) *App { app := &App{ Config: configuration, Context: ctx, router: v2.RouterWithPrefix(configuration.HTTP.Prefix), isCache: configuration.Proxy.RemoteURL != "", } app.Context = ctxu.WithLogger(app.Context, ctxu.GetLogger(app, "instance.id")) // Register the handler dispatchers. app.register(v2.RouteNameBase, func(ctx *Context, r *http.Request) http.Handler { return http.HandlerFunc(apiBase) }) app.register(v2.RouteNameManifest, imageManifestDispatcher) app.register(v2.RouteNameCatalog, catalogDispatcher) app.register(v2.RouteNameTags, tagsDispatcher) app.register(v2.RouteNameBlob, blobDispatcher) app.register(v2.RouteNameBlobUpload, blobUploadDispatcher) app.register(v2.RouteNameBlobUploadChunk, blobUploadDispatcher) var err error app.driver, err = factory.Create(configuration.Storage.Type(), configuration.Storage.Parameters()) if err != nil { // TODO(stevvooe): Move the creation of a service into a protected // method, where this is created lazily. Its status can be queried via // a health check. panic(err) } purgeConfig := uploadPurgeDefaultConfig() if mc, ok := configuration.Storage["maintenance"]; ok { for k, v := range mc { switch k { case "uploadpurging": purgeConfig = v.(map[interface{}]interface{}) } } } startUploadPurger(app, app.driver, ctxu.GetLogger(app), purgeConfig) app.driver, err = applyStorageMiddleware(app.driver, configuration.Middleware["storage"]) if err != nil { panic(err) } app.configureSecret(&configuration) app.configureEvents(&configuration) app.configureRedis(&configuration) app.configureLogHook(&configuration) // configure deletion var deleteEnabled bool if d, ok := configuration.Storage["delete"]; ok { e, ok := d["enabled"] if ok { if deleteEnabled, ok = e.(bool); !ok { deleteEnabled = false } } } // configure redirects var redirectDisabled bool if redirectConfig, ok := configuration.Storage["redirect"]; ok { v := redirectConfig["disable"] switch v := v.(type) { case bool: redirectDisabled = v default: panic(fmt.Sprintf("invalid type for redirect config: %#v", redirectConfig)) } if redirectDisabled { ctxu.GetLogger(app).Infof("backend redirection disabled") } } // configure storage caches if cc, ok := configuration.Storage["cache"]; ok { v, ok := cc["blobdescriptor"] if !ok { // Backwards compatible: "layerinfo" == "blobdescriptor" v = cc["layerinfo"] } switch v { case "redis": if app.redis == nil { panic("redis configuration required to use for layerinfo cache") } app.registry = storage.NewRegistryWithDriver(app, app.driver, rediscache.NewRedisBlobDescriptorCacheProvider(app.redis), deleteEnabled, !redirectDisabled, app.isCache) ctxu.GetLogger(app).Infof("using redis blob descriptor cache") case "inmemory": app.registry = storage.NewRegistryWithDriver(app, app.driver, memorycache.NewInMemoryBlobDescriptorCacheProvider(), deleteEnabled, !redirectDisabled, app.isCache) ctxu.GetLogger(app).Infof("using inmemory blob descriptor cache") default: if v != "" { ctxu.GetLogger(app).Warnf("unknown cache type %q, caching disabled", configuration.Storage["cache"]) } } } if app.registry == nil { // configure the registry if no cache section is available. app.registry = storage.NewRegistryWithDriver(app.Context, app.driver, nil, deleteEnabled, !redirectDisabled, app.isCache) } app.registry, err = applyRegistryMiddleware(app.Context, app.registry, configuration.Middleware["registry"]) if err != nil { panic(err) } authType := configuration.Auth.Type() if authType != "" { accessController, err := auth.GetAccessController(configuration.Auth.Type(), configuration.Auth.Parameters()) if err != nil { panic(fmt.Sprintf("unable to configure authorization (%s): %v", authType, err)) } app.accessController = accessController ctxu.GetLogger(app).Debugf("configured %q access controller", authType) } // configure as a pull through cache if configuration.Proxy.RemoteURL != "" { app.registry, err = proxy.NewRegistryPullThroughCache(ctx, app.registry, app.driver, configuration.Proxy) if err != nil { panic(err.Error()) } app.isCache = true ctxu.GetLogger(app).Info("Registry configured as a proxy cache to ", configuration.Proxy.RemoteURL) } return app }
// getToken handles authenticating the request and authorizing access to the // requested scopes. func (ts *tokenServer) getToken(ctx context.Context, w http.ResponseWriter, r *http.Request) { context.GetLogger(ctx).Info("getToken") params := r.URL.Query() service := params.Get("service") scopeSpecifiers := params["scope"] requestedAccessList := ResolveScopeSpecifiers(ctx, scopeSpecifiers) authorizedCtx, err := ts.accessController.Authorized(ctx, requestedAccessList...) if err != nil { challenge, ok := err.(auth.Challenge) if !ok { handleError(ctx, err, w) return } // Get response context. ctx, w = context.WithResponseWriter(ctx, w) challenge.SetHeaders(w) handleError(ctx, errcode.ErrorCodeUnauthorized.WithDetail(challenge.Error()), w) context.GetResponseLogger(ctx).Info("get token authentication challenge") return } ctx = authorizedCtx username := context.GetStringValue(ctx, "auth.user.name") ctx = context.WithValue(ctx, "acctSubject", username) ctx = context.WithLogger(ctx, context.GetLogger(ctx, "acctSubject")) context.GetLogger(ctx).Info("authenticated client") ctx = context.WithValue(ctx, "requestedAccess", requestedAccessList) ctx = context.WithLogger(ctx, context.GetLogger(ctx, "requestedAccess")) scopePrefix := username + "/" grantedAccessList := make([]auth.Access, 0, len(requestedAccessList)) for _, access := range requestedAccessList { if access.Type != "repository" { context.GetLogger(ctx).Debugf("Skipping unsupported resource type: %s", access.Type) continue } if !strings.HasPrefix(access.Name, scopePrefix) { context.GetLogger(ctx).Debugf("Resource scope not allowed: %s", access.Name) continue } grantedAccessList = append(grantedAccessList, access) } ctx = context.WithValue(ctx, "grantedAccess", grantedAccessList) ctx = context.WithLogger(ctx, context.GetLogger(ctx, "grantedAccess")) token, err := ts.issuer.CreateJWT(username, service, grantedAccessList) if err != nil { handleError(ctx, err, w) return } context.GetLogger(ctx).Info("authorized client") // Get response context. ctx, w = context.WithResponseWriter(ctx, w) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"token": token}) context.GetResponseLogger(ctx).Info("get token complete") }
// dispatcher returns a handler that constructs a request specific context and // handler, using the dispatch factory function. func (app *App) dispatcher(dispatch dispatchFunc, nameRequired nameRequiredFunc, accessRecords customAccessRecordsFunc) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { context := app.context(w, r) if err := app.authorized(w, r, context, nameRequired, accessRecords(r)); err != nil { ctxu.GetLogger(context).Errorf("error authorizing context: %v", err) return } // Add username to request logging context.Context = ctxu.WithLogger(context.Context, ctxu.GetLogger(context.Context, "auth.user.name")) if nameRequired(r) { repository, err := app.registry.Repository(context, getName(context)) if err != nil { ctxu.GetLogger(context).Errorf("error resolving repository: %v", err) switch err := err.(type) { case distribution.ErrRepositoryUnknown: context.Errors.Push(v2.ErrorCodeNameUnknown, err) case distribution.ErrRepositoryNameInvalid: context.Errors.Push(v2.ErrorCodeNameInvalid, err) } w.WriteHeader(http.StatusBadRequest) serveJSON(w, context.Errors) return } // assign and decorate the authorized repository with an event bridge. context.Repository = notifications.Listen( repository, app.eventBridge(context, r)) context.Repository, err = applyRepoMiddleware(context.Repository, app.Config.Middleware["repository"]) if err != nil { ctxu.GetLogger(context).Errorf("error initializing repository middleware: %v", err) context.Errors.Push(v2.ErrorCodeUnknown, err) w.WriteHeader(http.StatusInternalServerError) serveJSON(w, context.Errors) return } } dispatch(context, r).ServeHTTP(w, r) // Automated error response handling here. Handlers may return their // own errors if they need different behavior (such as range errors // for layer upload). if context.Errors.Len() > 0 { if context.Value("http.response.status") == 0 { // TODO(stevvooe): Getting this value from the context is a // bit of a hack. We can further address with some of our // future refactoring. w.WriteHeader(http.StatusBadRequest) } serveJSON(w, context.Errors) } }) }
// NewApp takes a configuration and returns a configured app, ready to serve // requests. The app only implements ServeHTTP and can be wrapped in other // handlers accordingly. func NewApp(ctx context.Context, configuration configuration.Configuration) *App { app := &App{ Config: configuration, Context: ctx, router: v2.RouterWithPrefix(configuration.HTTP.Prefix), } app.Context = ctxu.WithLogger(app.Context, ctxu.GetLogger(app, "instance.id")) // Register the handler dispatchers. app.register(v2.RouteNameBase, func(ctx *Context, r *http.Request) http.Handler { return http.HandlerFunc(apiBase) }) app.register(v2.RouteNameManifest, imageManifestDispatcher) app.register(v2.RouteNameTags, tagsDispatcher) app.register(v2.RouteNameBlob, layerDispatcher) app.register(v2.RouteNameBlobUpload, layerUploadDispatcher) app.register(v2.RouteNameBlobUploadChunk, layerUploadDispatcher) var err error app.driver, err = factory.Create(configuration.Storage.Type(), configuration.Storage.Parameters()) if err != nil { // TODO(stevvooe): Move the creation of a service into a protected // method, where this is created lazily. Its status can be queried via // a health check. panic(err) } startUploadPurger(app.driver, ctxu.GetLogger(app)) app.driver, err = applyStorageMiddleware(app.driver, configuration.Middleware["storage"]) if err != nil { panic(err) } app.configureEvents(&configuration) app.configureRedis(&configuration) // configure storage caches if cc, ok := configuration.Storage["cache"]; ok { switch cc["layerinfo"] { case "redis": if app.redis == nil { panic("redis configuration required to use for layerinfo cache") } app.registry = storage.NewRegistryWithDriver(app.driver, cache.NewRedisLayerInfoCache(app.redis)) ctxu.GetLogger(app).Infof("using redis layerinfo cache") case "inmemory": app.registry = storage.NewRegistryWithDriver(app.driver, cache.NewInMemoryLayerInfoCache()) ctxu.GetLogger(app).Infof("using inmemory layerinfo cache") default: if cc["layerinfo"] != "" { ctxu.GetLogger(app).Warnf("unkown cache type %q, caching disabled", configuration.Storage["cache"]) } } } if app.registry == nil { // configure the registry if no cache section is available. app.registry = storage.NewRegistryWithDriver(app.driver, nil) } app.registry, err = applyRegistryMiddleware(app.registry, configuration.Middleware["registry"]) if err != nil { panic(err) } authType := configuration.Auth.Type() if authType != "" { accessController, err := auth.GetAccessController(configuration.Auth.Type(), configuration.Auth.Parameters()) if err != nil { panic(fmt.Sprintf("unable to configure authorization (%s): %v", authType, err)) } app.accessController = accessController } return app }
// dispatcher returns a handler that constructs a request specific context and // handler, using the dispatch factory function. func (app *App) dispatcher(dispatch dispatchFunc) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { for headerName, headerValues := range app.Config.HTTP.Headers { for _, value := range headerValues { w.Header().Add(headerName, value) } } context := app.context(w, r) if err := app.authorized(w, r, context); err != nil { ctxu.GetLogger(context).Warnf("error authorizing context: %v", err) return } // Add username to request logging context.Context = ctxu.WithLogger(context.Context, ctxu.GetLogger(context.Context, "auth.user.name")) if app.nameRequired(r) { repository, err := app.registry.Repository(context, getName(context)) if err != nil { ctxu.GetLogger(context).Errorf("error resolving repository: %v", err) switch err := err.(type) { case distribution.ErrRepositoryUnknown: context.Errors = append(context.Errors, v2.ErrorCodeNameUnknown.WithDetail(err)) case distribution.ErrRepositoryNameInvalid: context.Errors = append(context.Errors, v2.ErrorCodeNameInvalid.WithDetail(err)) } if err := errcode.ServeJSON(w, context.Errors); err != nil { ctxu.GetLogger(context).Errorf("error serving error json: %v (from %v)", err, context.Errors) } return } // assign and decorate the authorized repository with an event bridge. context.Repository = notifications.Listen( repository, app.eventBridge(context, r)) context.Repository, err = applyRepoMiddleware(context.Context, context.Repository, app.Config.Middleware["repository"]) if err != nil { ctxu.GetLogger(context).Errorf("error initializing repository middleware: %v", err) context.Errors = append(context.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) if err := errcode.ServeJSON(w, context.Errors); err != nil { ctxu.GetLogger(context).Errorf("error serving error json: %v (from %v)", err, context.Errors) } return } } dispatch(context, r).ServeHTTP(w, r) // Automated error response handling here. Handlers may return their // own errors if they need different behavior (such as range errors // for layer upload). if context.Errors.Len() > 0 { if err := errcode.ServeJSON(w, context.Errors); err != nil { ctxu.GetLogger(context).Errorf("error serving error json: %v (from %v)", err, context.Errors) } app.logError(context, context.Errors) } }) }
// postToken handles authenticating the request and authorizing access to the // requested scopes. func (ts *tokenServer) postToken(ctx context.Context, w http.ResponseWriter, r *http.Request) { grantType := r.PostFormValue("grant_type") if grantType == "" { handleError(ctx, ErrorMissingRequiredField.WithDetail("missing grant_type value"), w) return } service := r.PostFormValue("service") if service == "" { handleError(ctx, ErrorMissingRequiredField.WithDetail("missing service value"), w) return } clientID := r.PostFormValue("client_id") if clientID == "" { handleError(ctx, ErrorMissingRequiredField.WithDetail("missing client_id value"), w) return } var offline bool switch r.PostFormValue("access_type") { case "", "online": case "offline": offline = true default: handleError(ctx, ErrorUnsupportedValue.WithDetail("unknown access_type value"), w) return } requestedAccessList := ResolveScopeList(ctx, r.PostFormValue("scope")) var subject string var rToken string switch grantType { case "refresh_token": rToken = r.PostFormValue("refresh_token") if rToken == "" { handleError(ctx, ErrorUnsupportedValue.WithDetail("missing refresh_token value"), w) return } rt, ok := ts.refreshCache[rToken] if !ok || rt.service != service { handleError(ctx, errcode.ErrorCodeUnauthorized.WithDetail("invalid refresh token"), w) return } subject = rt.subject case "password": ca, ok := ts.accessController.(auth.CredentialAuthenticator) if !ok { handleError(ctx, ErrorUnsupportedValue.WithDetail("password grant type not supported"), w) return } subject = r.PostFormValue("username") if subject == "" { handleError(ctx, ErrorUnsupportedValue.WithDetail("missing username value"), w) return } password := r.PostFormValue("password") if password == "" { handleError(ctx, ErrorUnsupportedValue.WithDetail("missing password value"), w) return } if err := ca.AuthenticateUser(subject, password); err != nil { handleError(ctx, errcode.ErrorCodeUnauthorized.WithDetail("invalid credentials"), w) return } default: handleError(ctx, ErrorUnsupportedValue.WithDetail("unknown grant_type value"), w) return } ctx = context.WithValue(ctx, acctSubject{}, subject) ctx = context.WithLogger(ctx, context.GetLogger(ctx, acctSubject{})) context.GetLogger(ctx).Info("authenticated client") ctx = context.WithValue(ctx, requestedAccess{}, requestedAccessList) ctx = context.WithLogger(ctx, context.GetLogger(ctx, requestedAccess{})) grantedAccessList := filterAccessList(ctx, subject, requestedAccessList) ctx = context.WithValue(ctx, grantedAccess{}, grantedAccessList) ctx = context.WithLogger(ctx, context.GetLogger(ctx, grantedAccess{})) token, err := ts.issuer.CreateJWT(subject, service, grantedAccessList) if err != nil { handleError(ctx, err, w) return } context.GetLogger(ctx).Info("authorized client") response := postTokenResponse{ Token: token, ExpiresIn: int(ts.issuer.Expiration.Seconds()), IssuedAt: time.Now().UTC().Format(time.RFC3339), Scope: ToScopeList(grantedAccessList), } if offline { rToken = newRefreshToken() ts.refreshCache[rToken] = refreshToken{ subject: subject, service: service, } } if rToken != "" { response.RefreshToken = rToken } ctx, w = context.WithResponseWriter(ctx, w) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) context.GetResponseLogger(ctx).Info("post token complete") }
// getToken handles authenticating the request and authorizing access to the // requested scopes. func (ts *tokenServer) getToken(ctx context.Context, w http.ResponseWriter, r *http.Request) { context.GetLogger(ctx).Info("getToken") params := r.URL.Query() service := params.Get("service") scopeSpecifiers := params["scope"] var offline bool if offlineStr := params.Get("offline_token"); offlineStr != "" { var err error offline, err = strconv.ParseBool(offlineStr) if err != nil { handleError(ctx, ErrorBadTokenOption.WithDetail(err), w) return } } requestedAccessList := ResolveScopeSpecifiers(ctx, scopeSpecifiers) authorizedCtx, err := ts.accessController.Authorized(ctx, requestedAccessList...) if err != nil { challenge, ok := err.(auth.Challenge) if !ok { handleError(ctx, err, w) return } // Get response context. ctx, w = context.WithResponseWriter(ctx, w) challenge.SetHeaders(w) handleError(ctx, errcode.ErrorCodeUnauthorized.WithDetail(challenge.Error()), w) context.GetResponseLogger(ctx).Info("get token authentication challenge") return } ctx = authorizedCtx username := context.GetStringValue(ctx, "auth.user.name") ctx = context.WithValue(ctx, acctSubject{}, username) ctx = context.WithLogger(ctx, context.GetLogger(ctx, acctSubject{})) context.GetLogger(ctx).Info("authenticated client") ctx = context.WithValue(ctx, requestedAccess{}, requestedAccessList) ctx = context.WithLogger(ctx, context.GetLogger(ctx, requestedAccess{})) grantedAccessList := filterAccessList(ctx, username, requestedAccessList) ctx = context.WithValue(ctx, grantedAccess{}, grantedAccessList) ctx = context.WithLogger(ctx, context.GetLogger(ctx, grantedAccess{})) token, err := ts.issuer.CreateJWT(username, service, grantedAccessList) if err != nil { handleError(ctx, err, w) return } context.GetLogger(ctx).Info("authorized client") response := tokenResponse{ Token: token, ExpiresIn: int(ts.issuer.Expiration.Seconds()), } if offline { response.RefreshToken = newRefreshToken() ts.refreshCache[response.RefreshToken] = refreshToken{ subject: username, service: service, } } ctx, w = context.WithResponseWriter(ctx, w) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) context.GetResponseLogger(ctx).Info("get token complete") }