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") } }
func TestSillyAccessController(t *testing.T) { ac := &accessController{ realm: "test-realm", service: "test-service", } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := context.WithValue(nil, "http.request", r) authCtx, err := ac.Authorized(ctx) if err != nil { switch err := err.(type) { case auth.Challenge: err.SetHeaders(w) w.WriteHeader(http.StatusUnauthorized) return default: t.Fatalf("unexpected error authorizing request: %v", err) } } userInfo, ok := authCtx.Value(auth.UserKey).(auth.UserInfo) if !ok { t.Fatal("silly accessController did not set auth.user context") } if userInfo.Name != "silly" { t.Fatalf("expected user name %q, got %q", "silly", userInfo.Name) } w.WriteHeader(http.StatusNoContent) })) resp, err := http.Get(server.URL) if err != nil { t.Fatalf("unexpected error during GET: %v", err) } defer resp.Body.Close() // Request should not be authorized if resp.StatusCode != http.StatusUnauthorized { t.Fatalf("unexpected response status: %v != %v", resp.StatusCode, http.StatusUnauthorized) } req, err := http.NewRequest("GET", server.URL, nil) if err != nil { t.Fatalf("unexpected error creating new request: %v", err) } req.Header.Set("Authorization", "seriously, anything") resp, err = http.DefaultClient.Do(req) if err != nil { t.Fatalf("unexpected error during GET: %v", err) } defer resp.Body.Close() // Request should not be authorized if resp.StatusCode != http.StatusNoContent { t.Fatalf("unexpected response status: %v != %v", resp.StatusCode, http.StatusNoContent) } }
// NewRegistry creates a new registry from a context and configuration struct. func NewRegistry(ctx context.Context, config *configuration.Configuration) (*Registry, error) { // Note this ctx = ctxu.WithValue(ctx, "version", version.Version) var err error ctx, err = configureLogging(ctx, config) if err != nil { return nil, fmt.Errorf("error configuring logger: %v", err) } // inject a logger into the uuid library. warns us if there is a problem // with uuid generation under low entropy. uuid.Loggerf = ctxu.GetLogger(ctx).Warnf app := handlers.NewApp(ctx, config) // TODO(aaronl): The global scope of the health checks means NewRegistry // can only be called once per process. app.RegisterHealthChecks() handler := configureReporting(app) handler = alive("/", handler) handler = health.Handler(handler) handler = panicHandler(handler) handler = gorhandlers.CombinedLoggingHandler(os.Stdout, handler) server := &http.Server{ Handler: handler, } return &Registry{ app: app, config: config, server: server, }, nil }
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") } }
// 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) } }
// 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") }
func WithAuthPerformed(parent context.Context) context.Context { return context.WithValue(parent, authPerformedKey, true) }
func WithUserClient(parent context.Context, userClient client.Interface) context.Context { return context.WithValue(parent, userClientKey, userClient) }
func WithDeferredErrors(parent context.Context, errs deferredErrors) context.Context { return context.WithValue(parent, deferredErrorsKey, errs) }
// 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") }
// TestAccessController tests complete integration of the v2 registry auth package. func TestAccessController(t *testing.T) { options := map[string]interface{}{ "addr": "https://openshift-example.com/osapi", "apiVersion": latest.Version, RealmKey: "myrealm", TokenRealmKey: "https://tokenrealm.com/token", } tests := map[string]struct { access []auth.Access basicToken string bearerToken string openshiftResponses []response expectedError error expectedChallenge bool expectedHeaders http.Header expectedRepoErr string expectedActions []string }{ "no token": { access: []auth.Access{}, basicToken: "", expectedError: ErrTokenRequired, expectedChallenge: true, expectedHeaders: http.Header{"Www-Authenticate": []string{`Bearer realm="https://tokenrealm.com/token"`}}, }, "invalid registry token": { access: []auth.Access{{ Resource: auth.Resource{Type: "repository"}, }}, basicToken: "ab-cd-ef-gh", expectedError: ErrTokenInvalid, expectedChallenge: true, expectedHeaders: http.Header{"Www-Authenticate": []string{`Basic realm=myrealm,error="failed to decode credentials"`}}, }, "invalid openshift basic password": { access: []auth.Access{{ Resource: auth.Resource{Type: "repository"}, }}, basicToken: "abcdefgh", expectedError: ErrTokenInvalid, expectedChallenge: true, expectedHeaders: http.Header{"Www-Authenticate": []string{`Basic realm=myrealm,error="failed to decode credentials"`}}, }, "valid openshift token but invalid namespace": { access: []auth.Access{{ Resource: auth.Resource{ Type: "repository", Name: "bar", }, Action: "pull", }}, basicToken: "b3BlbnNoaWZ0OmF3ZXNvbWU=", expectedError: ErrNamespaceRequired, expectedChallenge: false, }, "registry token but does not involve any repository operation": { access: []auth.Access{{}}, basicToken: "b3BlbnNoaWZ0OmF3ZXNvbWU=", expectedError: ErrUnsupportedResource, expectedChallenge: false, }, "registry token but does not involve any known action": { access: []auth.Access{{ Resource: auth.Resource{ Type: "repository", Name: "foo/bar", }, Action: "blah", }}, basicToken: "b3BlbnNoaWZ0OmF3ZXNvbWU=", expectedError: ErrUnsupportedAction, expectedChallenge: false, }, "docker login with invalid openshift creds": { basicToken: "b3BlbnNoaWZ0OmF3ZXNvbWU=", openshiftResponses: []response{{403, ""}}, expectedError: ErrOpenShiftAccessDenied, expectedChallenge: true, expectedHeaders: http.Header{"Www-Authenticate": []string{`Basic realm=myrealm,error="access denied"`}}, expectedActions: []string{"GET /oapi/v1/users/~ (Authorization=Bearer awesome)"}, }, "docker login with valid openshift creds": { basicToken: "dXNyMTphd2Vzb21l", openshiftResponses: []response{ {200, runtime.EncodeOrDie(kapi.Codecs.LegacyCodec(registered.GroupOrDie(kapi.GroupName).GroupVersions[0]), &userapi.User{ObjectMeta: kapi.ObjectMeta{Name: "usr1"}})}, }, expectedError: nil, expectedChallenge: false, expectedActions: []string{"GET /oapi/v1/users/~ (Authorization=Bearer awesome)"}, }, "error running subject access review": { access: []auth.Access{{ Resource: auth.Resource{ Type: "repository", Name: "foo/bar", }, Action: "pull", }}, basicToken: "b3BlbnNoaWZ0OmF3ZXNvbWU=", openshiftResponses: []response{ {500, "Uh oh"}, }, expectedError: errors.New("an error on the server has prevented the request from succeeding (post localSubjectAccessReviews)"), expectedChallenge: false, expectedActions: []string{"POST /oapi/v1/namespaces/foo/localsubjectaccessreviews (Authorization=Bearer awesome)"}, }, "valid openshift token but token not scoped for the given repo operation": { access: []auth.Access{{ Resource: auth.Resource{ Type: "repository", Name: "foo/bar", }, Action: "pull", }}, basicToken: "b3BlbnNoaWZ0OmF3ZXNvbWU=", openshiftResponses: []response{ {200, runtime.EncodeOrDie(kapi.Codecs.LegacyCodec(registered.GroupOrDie(kapi.GroupName).GroupVersions[0]), &api.SubjectAccessReviewResponse{Namespace: "foo", Allowed: false, Reason: "unauthorized!"})}, }, expectedError: ErrOpenShiftAccessDenied, expectedChallenge: true, expectedHeaders: http.Header{"Www-Authenticate": []string{`Basic realm=myrealm,error="access denied"`}}, expectedActions: []string{"POST /oapi/v1/namespaces/foo/localsubjectaccessreviews (Authorization=Bearer awesome)"}, }, "partially valid openshift token": { // Check all the different resource-type/verb combinations we allow to make sure they validate and continue to validate remaining Resource requests access: []auth.Access{ {Resource: auth.Resource{Type: "repository", Name: "foo/aaa"}, Action: "pull"}, {Resource: auth.Resource{Type: "repository", Name: "bar/bbb"}, Action: "push"}, {Resource: auth.Resource{Type: "admin"}, Action: "prune"}, {Resource: auth.Resource{Type: "repository", Name: "baz/ccc"}, Action: "push"}, }, basicToken: "b3BlbnNoaWZ0OmF3ZXNvbWU=", openshiftResponses: []response{ {200, runtime.EncodeOrDie(kapi.Codecs.LegacyCodec(registered.GroupOrDie(kapi.GroupName).GroupVersions[0]), &api.SubjectAccessReviewResponse{Namespace: "foo", Allowed: true, Reason: "authorized!"})}, {200, runtime.EncodeOrDie(kapi.Codecs.LegacyCodec(registered.GroupOrDie(kapi.GroupName).GroupVersions[0]), &api.SubjectAccessReviewResponse{Namespace: "bar", Allowed: true, Reason: "authorized!"})}, {200, runtime.EncodeOrDie(kapi.Codecs.LegacyCodec(registered.GroupOrDie(kapi.GroupName).GroupVersions[0]), &api.SubjectAccessReviewResponse{Namespace: "", Allowed: true, Reason: "authorized!"})}, {200, runtime.EncodeOrDie(kapi.Codecs.LegacyCodec(registered.GroupOrDie(kapi.GroupName).GroupVersions[0]), &api.SubjectAccessReviewResponse{Namespace: "baz", Allowed: false, Reason: "no!"})}, }, expectedError: ErrOpenShiftAccessDenied, expectedChallenge: true, expectedHeaders: http.Header{"Www-Authenticate": []string{`Basic realm=myrealm,error="access denied"`}}, expectedActions: []string{ "POST /oapi/v1/namespaces/foo/localsubjectaccessreviews (Authorization=Bearer awesome)", "POST /oapi/v1/namespaces/bar/localsubjectaccessreviews (Authorization=Bearer awesome)", "POST /oapi/v1/subjectaccessreviews (Authorization=Bearer awesome)", "POST /oapi/v1/namespaces/baz/localsubjectaccessreviews (Authorization=Bearer awesome)", }, }, "deferred cross-mount error": { // cross-mount push requests check pull/push access on the target repo and pull access on the source repo. // we expect the access check failure for fromrepo/bbb to be added to the context as a deferred error, // which our blobstore will look for and prevent a cross mount from. access: []auth.Access{ {Resource: auth.Resource{Type: "repository", Name: "pushrepo/aaa"}, Action: "pull"}, {Resource: auth.Resource{Type: "repository", Name: "pushrepo/aaa"}, Action: "push"}, {Resource: auth.Resource{Type: "repository", Name: "fromrepo/bbb"}, Action: "pull"}, }, basicToken: "b3BlbnNoaWZ0OmF3ZXNvbWU=", openshiftResponses: []response{ {200, runtime.EncodeOrDie(kapi.Codecs.LegacyCodec(registered.GroupOrDie(kapi.GroupName).GroupVersions[0]), &api.SubjectAccessReviewResponse{Namespace: "pushrepo", Allowed: true, Reason: "authorized!"})}, {200, runtime.EncodeOrDie(kapi.Codecs.LegacyCodec(registered.GroupOrDie(kapi.GroupName).GroupVersions[0]), &api.SubjectAccessReviewResponse{Namespace: "pushrepo", Allowed: true, Reason: "authorized!"})}, {200, runtime.EncodeOrDie(kapi.Codecs.LegacyCodec(registered.GroupOrDie(kapi.GroupName).GroupVersions[0]), &api.SubjectAccessReviewResponse{Namespace: "fromrepo", Allowed: false, Reason: "no!"})}, }, expectedError: nil, expectedChallenge: false, expectedRepoErr: "fromrepo/bbb", expectedActions: []string{ "POST /oapi/v1/namespaces/pushrepo/localsubjectaccessreviews (Authorization=Bearer awesome)", "POST /oapi/v1/namespaces/pushrepo/localsubjectaccessreviews (Authorization=Bearer awesome)", "POST /oapi/v1/namespaces/fromrepo/localsubjectaccessreviews (Authorization=Bearer awesome)", }, }, "valid openshift token": { access: []auth.Access{{ Resource: auth.Resource{ Type: "repository", Name: "foo/bar", }, Action: "pull", }}, basicToken: "b3BlbnNoaWZ0OmF3ZXNvbWU=", openshiftResponses: []response{ {200, runtime.EncodeOrDie(kapi.Codecs.LegacyCodec(registered.GroupOrDie(kapi.GroupName).GroupVersions[0]), &api.SubjectAccessReviewResponse{Namespace: "foo", Allowed: true, Reason: "authorized!"})}, }, expectedError: nil, expectedChallenge: false, expectedActions: []string{"POST /oapi/v1/namespaces/foo/localsubjectaccessreviews (Authorization=Bearer awesome)"}, }, "valid anonymous token": { access: []auth.Access{{ Resource: auth.Resource{ Type: "repository", Name: "foo/bar", }, Action: "pull", }}, bearerToken: "anonymous", openshiftResponses: []response{ {200, runtime.EncodeOrDie(kapi.Codecs.LegacyCodec(registered.GroupOrDie(kapi.GroupName).GroupVersions[0]), &api.SubjectAccessReviewResponse{Namespace: "foo", Allowed: true, Reason: "authorized!"})}, }, expectedError: nil, expectedChallenge: false, expectedActions: []string{"POST /oapi/v1/namespaces/foo/localsubjectaccessreviews (Authorization=)"}, }, "pruning": { access: []auth.Access{ { Resource: auth.Resource{ Type: "admin", }, Action: "prune", }, { Resource: auth.Resource{ Type: "repository", Name: "foo/bar", }, Action: "*", }, }, basicToken: "b3BlbnNoaWZ0OmF3ZXNvbWU=", openshiftResponses: []response{ {200, runtime.EncodeOrDie(kapi.Codecs.LegacyCodec(registered.GroupOrDie(kapi.GroupName).GroupVersions[0]), &api.SubjectAccessReviewResponse{Allowed: true, Reason: "authorized!"})}, }, expectedError: nil, expectedChallenge: false, expectedActions: []string{ "POST /oapi/v1/subjectaccessreviews (Authorization=Bearer awesome)", }, }, } for k, test := range tests { req, err := http.NewRequest("GET", options["addr"].(string), nil) if err != nil { t.Errorf("%s: %v", k, err) continue } if len(test.basicToken) > 0 { req.Header.Set("Authorization", fmt.Sprintf("Basic %s", test.basicToken)) } if len(test.bearerToken) > 0 { req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", test.bearerToken)) } ctx := context.WithValue(context.Background(), "http.request", req) server, actions := simulateOpenShiftMaster(test.openshiftResponses) DefaultRegistryClient = NewRegistryClient(&clientcmd.Config{ CommonConfig: restclient.Config{ Host: server.URL, Insecure: true, }, SkipEnv: true, }) accessController, err := newAccessController(options) if err != nil { t.Fatal(err) } authCtx, err := accessController.Authorized(ctx, test.access...) server.Close() expectedActions := test.expectedActions if expectedActions == nil { expectedActions = []string{} } if !reflect.DeepEqual(actions, &expectedActions) { t.Errorf("%s: expected\n\t%#v\ngot\n\t%#v", k, &expectedActions, actions) continue } if err == nil || test.expectedError == nil { if err != test.expectedError { t.Errorf("%s: accessController did not get expected error - got %v - expected %v", k, err, test.expectedError) continue } if authCtx == nil { t.Errorf("%s: expected auth context but got nil", k) continue } if !AuthPerformed(authCtx) { t.Errorf("%s: expected AuthPerformed to be true", k) continue } deferredErrors, hasDeferred := DeferredErrorsFrom(authCtx) if len(test.expectedRepoErr) > 0 { if !hasDeferred || deferredErrors[test.expectedRepoErr] == nil { t.Errorf("%s: expected deferred error for repo %s, got none", k, test.expectedRepoErr) continue } } else { if hasDeferred && len(deferredErrors) > 0 { t.Errorf("%s: didn't expect deferred errors, got %#v", k, deferredErrors) continue } } } else { challengeErr, isChallenge := err.(auth.Challenge) if test.expectedChallenge != isChallenge { t.Errorf("%s: expected challenge=%v, accessController returned challenge=%v", k, test.expectedChallenge, isChallenge) continue } if isChallenge { recorder := httptest.NewRecorder() challengeErr.SetHeaders(recorder) if !reflect.DeepEqual(recorder.HeaderMap, test.expectedHeaders) { t.Errorf("%s: expected headers\n%#v\ngot\n%#v", k, test.expectedHeaders, recorder.HeaderMap) continue } } if err.Error() != test.expectedError.Error() { t.Errorf("%s: accessController did not get expected error - got %s - expected %s", k, err, test.expectedError) continue } if authCtx != nil { t.Errorf("%s: expected nil auth context but got %s", k, authCtx) continue } } } }
// TestAccessController tests complete integration of the v2 registry auth package. func TestAccessController(t *testing.T) { options := map[string]interface{}{ "addr": "https://openshift-example.com/osapi", "apiVersion": latest.Version, } accessController, err := newAccessController(options) if err != nil { t.Fatal(err) } tests := map[string]struct { access []auth.Access basicToken string openshiftResponses []response expectedError error expectedChallenge bool expectedActions []string }{ "no token": { access: []auth.Access{}, basicToken: "", expectedError: ErrTokenRequired, expectedChallenge: true, }, "invalid registry token": { access: []auth.Access{{ Resource: auth.Resource{Type: "repository"}, }}, basicToken: "ab-cd-ef-gh", expectedError: ErrTokenInvalid, expectedChallenge: true, }, "invalid openshift bearer token": { access: []auth.Access{{ Resource: auth.Resource{Type: "repository"}, }}, basicToken: "abcdefgh", expectedError: ErrOpenShiftTokenRequired, expectedChallenge: true, }, "valid openshift token but invalid namespace": { access: []auth.Access{{ Resource: auth.Resource{ Type: "repository", Name: "bar", }, Action: "pull", }}, basicToken: "b3BlbnNoaWZ0OmF3ZXNvbWU=", expectedError: ErrNamespaceRequired, expectedChallenge: false, }, "registry token but does not involve any repository operation": { access: []auth.Access{{}}, basicToken: "b3BlbnNoaWZ0OmF3ZXNvbWU=", expectedError: ErrUnsupportedResource, expectedChallenge: false, }, "registry token but does not involve any known action": { access: []auth.Access{{ Resource: auth.Resource{ Type: "repository", Name: "foo/bar", }, Action: "blah", }}, basicToken: "b3BlbnNoaWZ0OmF3ZXNvbWU=", expectedError: ErrUnsupportedAction, expectedChallenge: false, }, "docker login with invalid openshift creds": { basicToken: "b3BlbnNoaWZ0OmF3ZXNvbWU=", openshiftResponses: []response{{403, ""}}, expectedError: ErrOpenShiftAccessDenied, expectedChallenge: true, expectedActions: []string{"GET /oapi/v1/users/~"}, }, "docker login with valid openshift creds": { basicToken: "dXNyMTphd2Vzb21l", openshiftResponses: []response{ {200, runtime.EncodeOrDie(latest.Codec, &userapi.User{ObjectMeta: kapi.ObjectMeta{Name: "usr1"}})}, }, expectedError: nil, expectedChallenge: false, expectedActions: []string{"GET /oapi/v1/users/~"}, }, "error running subject access review": { access: []auth.Access{{ Resource: auth.Resource{ Type: "repository", Name: "foo/bar", }, Action: "pull", }}, basicToken: "b3BlbnNoaWZ0OmF3ZXNvbWU=", openshiftResponses: []response{ {500, "Uh oh"}, }, expectedError: errors.New("an error on the server has prevented the request from succeeding (post localSubjectAccessReviews)"), expectedChallenge: false, expectedActions: []string{"POST /oapi/v1/namespaces/foo/localsubjectaccessreviews"}, }, "valid openshift token but token not scoped for the given repo operation": { access: []auth.Access{{ Resource: auth.Resource{ Type: "repository", Name: "foo/bar", }, Action: "pull", }}, basicToken: "b3BlbnNoaWZ0OmF3ZXNvbWU=", openshiftResponses: []response{ {200, runtime.EncodeOrDie(latest.Codec, &api.SubjectAccessReviewResponse{Namespace: "foo", Allowed: false, Reason: "unauthorized!"})}, }, expectedError: ErrOpenShiftAccessDenied, expectedChallenge: true, expectedActions: []string{"POST /oapi/v1/namespaces/foo/localsubjectaccessreviews"}, }, "partially valid openshift token": { // Check all the different resource-type/verb combinations we allow to make sure they validate and continue to validate remaining Resource requests access: []auth.Access{ {Resource: auth.Resource{Type: "repository", Name: "foo/aaa"}, Action: "pull"}, {Resource: auth.Resource{Type: "repository", Name: "bar/bbb"}, Action: "push"}, {Resource: auth.Resource{Type: "admin"}, Action: "prune"}, {Resource: auth.Resource{Type: "repository", Name: "baz/ccc"}, Action: "pull"}, }, basicToken: "b3BlbnNoaWZ0OmF3ZXNvbWU=", openshiftResponses: []response{ {200, runtime.EncodeOrDie(latest.Codec, &api.SubjectAccessReviewResponse{Namespace: "foo", Allowed: true, Reason: "authorized!"})}, {200, runtime.EncodeOrDie(latest.Codec, &api.SubjectAccessReviewResponse{Namespace: "bar", Allowed: true, Reason: "authorized!"})}, {200, runtime.EncodeOrDie(latest.Codec, &api.SubjectAccessReviewResponse{Namespace: "", Allowed: true, Reason: "authorized!"})}, {200, runtime.EncodeOrDie(latest.Codec, &api.SubjectAccessReviewResponse{Namespace: "baz", Allowed: false, Reason: "no!"})}, }, expectedError: ErrOpenShiftAccessDenied, expectedChallenge: true, expectedActions: []string{ "POST /oapi/v1/namespaces/foo/localsubjectaccessreviews", "POST /oapi/v1/namespaces/bar/localsubjectaccessreviews", "POST /oapi/v1/subjectaccessreviews", "POST /oapi/v1/namespaces/baz/localsubjectaccessreviews", }, }, "valid openshift token": { access: []auth.Access{{ Resource: auth.Resource{ Type: "repository", Name: "foo/bar", }, Action: "pull", }}, basicToken: "b3BlbnNoaWZ0OmF3ZXNvbWU=", openshiftResponses: []response{ {200, runtime.EncodeOrDie(latest.Codec, &api.SubjectAccessReviewResponse{Namespace: "foo", Allowed: true, Reason: "authorized!"})}, }, expectedError: nil, expectedChallenge: false, expectedActions: []string{"POST /oapi/v1/namespaces/foo/localsubjectaccessreviews"}, }, "pruning": { access: []auth.Access{ { Resource: auth.Resource{ Type: "admin", }, Action: "prune", }, { Resource: auth.Resource{ Type: "repository", Name: "foo/bar", }, Action: "*", }, }, basicToken: "b3BlbnNoaWZ0OmF3ZXNvbWU=", openshiftResponses: []response{ {200, runtime.EncodeOrDie(latest.Codec, &api.SubjectAccessReviewResponse{Allowed: true, Reason: "authorized!"})}, }, expectedError: nil, expectedChallenge: false, expectedActions: []string{ "POST /oapi/v1/subjectaccessreviews", }, }, } for k, test := range tests { req, err := http.NewRequest("GET", options["addr"].(string), nil) if err != nil { t.Errorf("%s: %v", k, err) continue } if len(test.basicToken) > 0 { req.Header.Set("Authorization", fmt.Sprintf("Basic %s", test.basicToken)) } ctx := context.WithValue(context.Background(), "http.request", req) server, actions := simulateOpenShiftMaster(test.openshiftResponses) authCtx, err := accessController.Authorized(ctx, test.access...) server.Close() expectedActions := test.expectedActions if expectedActions == nil { expectedActions = []string{} } if !reflect.DeepEqual(actions, &expectedActions) { t.Errorf("%s: expected\n\t%#v\ngot\n\t%#v", k, &expectedActions, actions) continue } if err == nil || test.expectedError == nil { if err != test.expectedError { t.Errorf("%s: accessController did not get expected error - got %v - expected %v", k, err, test.expectedError) continue } if authCtx == nil { t.Errorf("%s: expected auth context but got nil", k) continue } } else { _, isChallenge := err.(auth.Challenge) if test.expectedChallenge != isChallenge { t.Errorf("%s: expected challenge=%v, accessController returned challenge=%v", k, test.expectedChallenge, isChallenge) continue } if err.Error() != test.expectedError.Error() { t.Errorf("%s: accessController did not get expected error - got %s - expected %s", k, err, test.expectedError) continue } if authCtx != nil { t.Errorf("%s: expected nil auth context but got %s", k, authCtx) continue } } } }
func main() { flag.Usage = usage flag.Parse() if showVersion { version.PrintVersion() return } ctx := context.Background() ctx = context.WithValue(ctx, "version", version.Version) config, err := resolveConfiguration() if err != nil { fatalf("configuration error: %v", err) } ctx, err = configureLogging(ctx, config) if err != nil { fatalf("error configuring logger: %v", err) } app := handlers.NewApp(ctx, *config) handler := configureReporting(app) handler = gorhandlers.CombinedLoggingHandler(os.Stdout, handler) if config.HTTP.Debug.Addr != "" { go debugServer(config.HTTP.Debug.Addr) } if config.HTTP.TLS.Certificate == "" { context.GetLogger(app).Infof("listening on %v", config.HTTP.Addr) if err := http.ListenAndServe(config.HTTP.Addr, handler); err != nil { context.GetLogger(app).Fatalln(err) } } else { tlsConf := &tls.Config{ ClientAuth: tls.NoClientCert, } if len(config.HTTP.TLS.ClientCAs) != 0 { pool := x509.NewCertPool() for _, ca := range config.HTTP.TLS.ClientCAs { caPem, err := ioutil.ReadFile(ca) if err != nil { context.GetLogger(app).Fatalln(err) } if ok := pool.AppendCertsFromPEM(caPem); !ok { context.GetLogger(app).Fatalln(fmt.Errorf("Could not add CA to pool")) } } for _, subj := range pool.Subjects() { context.GetLogger(app).Debugf("CA Subject: %s", string(subj)) } tlsConf.ClientAuth = tls.RequireAndVerifyClientCert tlsConf.ClientCAs = pool } context.GetLogger(app).Infof("listening on %v, tls", config.HTTP.Addr) server := &http.Server{ Addr: config.HTTP.Addr, Handler: handler, TLSConfig: tlsConf, } if err := server.ListenAndServeTLS(config.HTTP.TLS.Certificate, config.HTTP.TLS.Key); err != nil { context.GetLogger(app).Fatalln(err) } } }
func WithRepository(parent context.Context, repo *repository) context.Context { return context.WithValue(parent, repositoryKey, repo) }
func main() { flag.Usage = usage flag.Parse() if showVersion { version.PrintVersion() return } ctx := context.Background() ctx = context.WithValue(ctx, "version", version.Version) config, err := resolveConfiguration() if err != nil { fatalf("configuration error: %v", err) } ctx, err = configureLogging(ctx, config) if err != nil { fatalf("error configuring logger: %v", err) } // inject a logger into the uuid library. warns us if there is a problem // with uuid generation under low entropy. uuid.Loggerf = context.GetLogger(ctx).Warnf app := handlers.NewApp(ctx, *config) app.RegisterHealthChecks() handler := configureReporting(app) handler = panicHandler(handler) handler = health.Handler(handler) handler = gorhandlers.CombinedLoggingHandler(os.Stdout, handler) if config.HTTP.Debug.Addr != "" { go debugServer(config.HTTP.Debug.Addr) } server := &http.Server{ Handler: handler, } ln, err := listener.NewListener(config.HTTP.Net, config.HTTP.Addr) if err != nil { context.GetLogger(app).Fatalln(err) } defer ln.Close() if config.HTTP.TLS.Certificate != "" { tlsConf := &tls.Config{ ClientAuth: tls.NoClientCert, NextProtos: []string{"http/1.1"}, Certificates: make([]tls.Certificate, 1), MinVersion: tls.VersionTLS10, PreferServerCipherSuites: true, CipherSuites: []uint16{ tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, tls.TLS_RSA_WITH_AES_128_CBC_SHA, tls.TLS_RSA_WITH_AES_256_CBC_SHA, }, } tlsConf.Certificates[0], err = tls.LoadX509KeyPair(config.HTTP.TLS.Certificate, config.HTTP.TLS.Key) if err != nil { context.GetLogger(app).Fatalln(err) } if len(config.HTTP.TLS.ClientCAs) != 0 { pool := x509.NewCertPool() for _, ca := range config.HTTP.TLS.ClientCAs { caPem, err := ioutil.ReadFile(ca) if err != nil { context.GetLogger(app).Fatalln(err) } if ok := pool.AppendCertsFromPEM(caPem); !ok { context.GetLogger(app).Fatalln(fmt.Errorf("Could not add CA to pool")) } } for _, subj := range pool.Subjects() { context.GetLogger(app).Debugf("CA Subject: %s", string(subj)) } tlsConf.ClientAuth = tls.RequireAndVerifyClientCert tlsConf.ClientCAs = pool } ln = tls.NewListener(ln, tlsConf) context.GetLogger(app).Infof("listening on %v, tls", ln.Addr()) } else { context.GetLogger(app).Infof("listening on %v", ln.Addr()) } if err := server.Serve(ln); err != nil { context.GetLogger(app).Fatalln(err) } }
func main() { flag.Usage = usage flag.Parse() if showVersion { version.PrintVersion() return } ctx := context.Background() ctx = context.WithValue(ctx, "version", version.Version) config, err := resolveConfiguration() if err != nil { fatalf("configuration error: %v", err) } ctx, err = configureLogging(ctx, config) if err != nil { fatalf("error configuring logger: %v", err) } app := handlers.NewApp(ctx, *config) handler := configureReporting(app) handler = gorhandlers.CombinedLoggingHandler(os.Stdout, handler) if config.HTTP.Debug.Addr != "" { go debugServer(config.HTTP.Debug.Addr) } server := &http.Server{ Handler: handler, } ln, err := listener.NewListener(config.HTTP.Net, config.HTTP.Addr) if err != nil { context.GetLogger(app).Fatalln(err) } defer ln.Close() if config.HTTP.TLS.Certificate != "" { tlsConf := &tls.Config{ ClientAuth: tls.NoClientCert, NextProtos: []string{"http/1.1"}, Certificates: make([]tls.Certificate, 1), } tlsConf.Certificates[0], err = tls.LoadX509KeyPair(config.HTTP.TLS.Certificate, config.HTTP.TLS.Key) if err != nil { context.GetLogger(app).Fatalln(err) } if len(config.HTTP.TLS.ClientCAs) != 0 { pool := x509.NewCertPool() for _, ca := range config.HTTP.TLS.ClientCAs { caPem, err := ioutil.ReadFile(ca) if err != nil { context.GetLogger(app).Fatalln(err) } if ok := pool.AppendCertsFromPEM(caPem); !ok { context.GetLogger(app).Fatalln(fmt.Errorf("Could not add CA to pool")) } } for _, subj := range pool.Subjects() { context.GetLogger(app).Debugf("CA Subject: %s", string(subj)) } tlsConf.ClientAuth = tls.RequireAndVerifyClientCert tlsConf.ClientCAs = pool } ln = tls.NewListener(ln, tlsConf) context.GetLogger(app).Infof("listening on %v, tls", ln.Addr()) } else { context.GetLogger(app).Infof("listening on %v", ln.Addr()) } if err := server.Serve(ln); err != nil { context.GetLogger(app).Fatalln(err) } }
// TestAccessController tests complete integration of the token auth package. // It starts by mocking the options for a token auth accessController which // it creates. It then tries a few mock requests: // - don't supply a token; should error with challenge // - supply an invalid token; should error with challenge // - supply a token with insufficient access; should error with challenge // - supply a valid token; should not error func TestAccessController(t *testing.T) { // Make 2 keys; only the first is to be a trusted root key. rootKeys, err := makeRootKeys(2) if err != nil { t.Fatal(err) } rootCertBundleFilename, err := writeTempRootCerts(rootKeys[:1]) if err != nil { t.Fatal(err) } defer os.Remove(rootCertBundleFilename) realm := "https://auth.example.com/token/" issuer := "test-issuer.example.com" service := "test-service.example.com" options := map[string]interface{}{ "realm": realm, "issuer": issuer, "service": service, "rootcertbundle": rootCertBundleFilename, } accessController, err := newAccessController(options) if err != nil { t.Fatal(err) } // 1. Make a mock http.Request with no token. req, err := http.NewRequest("GET", "http://example.com/foo", nil) if err != nil { t.Fatal(err) } testAccess := auth.Access{ Resource: auth.Resource{ Type: "foo", Name: "bar", }, Action: "baz", } ctx := context.WithValue(nil, "http.request", req) authCtx, err := accessController.Authorized(ctx, testAccess) challenge, ok := err.(auth.Challenge) if !ok { t.Fatal("accessController did not return a challenge") } if challenge.Error() != ErrTokenRequired.Error() { t.Fatalf("accessControler did not get expected error - got %s - expected %s", challenge, ErrTokenRequired) } if authCtx != nil { t.Fatalf("expected nil auth context but got %s", authCtx) } // 2. Supply an invalid token. token, err := makeTestToken( issuer, service, []*ResourceActions{{ Type: testAccess.Type, Name: testAccess.Name, Actions: []string{testAccess.Action}, }}, rootKeys[1], 1, time.Now(), time.Now().Add(5*time.Minute), // Everything is valid except the key which signed it. ) if err != nil { t.Fatal(err) } req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.compactRaw())) authCtx, err = accessController.Authorized(ctx, testAccess) challenge, ok = err.(auth.Challenge) if !ok { t.Fatal("accessController did not return a challenge") } if challenge.Error() != ErrInvalidToken.Error() { t.Fatalf("accessControler did not get expected error - got %s - expected %s", challenge, ErrTokenRequired) } if authCtx != nil { t.Fatalf("expected nil auth context but got %s", authCtx) } // 3. Supply a token with insufficient access. token, err = makeTestToken( issuer, service, []*ResourceActions{}, // No access specified. rootKeys[0], 1, time.Now(), time.Now().Add(5*time.Minute), ) if err != nil { t.Fatal(err) } req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.compactRaw())) authCtx, err = accessController.Authorized(ctx, testAccess) challenge, ok = err.(auth.Challenge) if !ok { t.Fatal("accessController did not return a challenge") } if challenge.Error() != ErrInsufficientScope.Error() { t.Fatalf("accessControler did not get expected error - got %s - expected %s", challenge, ErrInsufficientScope) } if authCtx != nil { t.Fatalf("expected nil auth context but got %s", authCtx) } // 4. Supply the token we need, or deserve, or whatever. token, err = makeTestToken( issuer, service, []*ResourceActions{{ Type: testAccess.Type, Name: testAccess.Name, Actions: []string{testAccess.Action}, }}, rootKeys[0], 1, time.Now(), time.Now().Add(5*time.Minute), ) if err != nil { t.Fatal(err) } req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.compactRaw())) authCtx, err = accessController.Authorized(ctx, testAccess) if err != nil { t.Fatalf("accessController returned unexpected error: %s", err) } userInfo, ok := authCtx.Value(auth.UserKey).(auth.UserInfo) if !ok { t.Fatal("token accessController did not set auth.user context") } if userInfo.Name != "foo" { t.Fatalf("expected user name %q, got %q", "foo", userInfo.Name) } }