package server

import (
	"errors"
	"fmt"
	"net/http"
	"net/http/httptest"
	"reflect"
	"testing"

	"github.com/docker/distribution/registry/auth"

	kapi "k8s.io/kubernetes/pkg/api"
	"k8s.io/kubernetes/pkg/apimachinery/registered"
	"k8s.io/kubernetes/pkg/client/restclient"
	"k8s.io/kubernetes/pkg/runtime"

	"github.com/docker/distribution/context"
	"github.com/openshift/origin/pkg/api/latest"
	"github.com/openshift/origin/pkg/authorization/api"
	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
	userapi "github.com/openshift/origin/pkg/user/api"

	// install all APIs
	_ "github.com/openshift/origin/pkg/api/install"
	"github.com/openshift/origin/pkg/client"
)

// TestVerifyImageStreamAccess mocks openshift http request/response and
// tests invalid/valid/scoped openshift tokens.
func TestVerifyImageStreamAccess(t *testing.T) {
	tests := []struct {
		openshiftResponse response
		expectedError     error
	}{
		{
			// Test invalid openshift bearer token
			openshiftResponse: response{401, "Unauthorized"},
			expectedError:     ErrOpenShiftAccessDenied,
		},
		{
			// Test valid openshift bearer token but token *not* scoped for create operation
			openshiftResponse: response{
				200,
				runtime.EncodeOrDie(kapi.Codecs.LegacyCodec(registered.GroupOrDie(kapi.GroupName).GroupVersions[0]), &api.SubjectAccessReviewResponse{
					Namespace: "foo",
					Allowed:   false,
					Reason:    "not authorized!",
				}),
			},
			expectedError: ErrOpenShiftAccessDenied,
		},
		{
			// Test valid openshift bearer token and token scoped for create operation
			openshiftResponse: response{
				200,
				runtime.EncodeOrDie(kapi.Codecs.LegacyCodec(registered.GroupOrDie(kapi.GroupName).GroupVersions[0]), &api.SubjectAccessReviewResponse{
					Namespace: "foo",
					Allowed:   true,
					Reason:    "authorized!",
				}),
			},
			expectedError: nil,
		},
	}
	for _, test := range tests {
		ctx := context.Background()
		server, _ := simulateOpenShiftMaster([]response{test.openshiftResponse})
		client, err := client.New(&restclient.Config{BearerToken: "magic bearer token", Host: server.URL})
		if err != nil {
			t.Fatal(err)
		}
		err = verifyImageStreamAccess(ctx, "foo", "bar", "create", client)
		if err == nil || test.expectedError == nil {
			if err != test.expectedError {
				t.Fatalf("verifyImageStreamAccess did not get expected error - got %s - expected %s", err, test.expectedError)
			}
		} else if err.Error() != test.expectedError.Error() {
			t.Fatalf("verifyImageStreamAccess did not get expected error - got %s - expected %s", err, test.expectedError)
		}
		server.Close()
	}
}

// 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,
	}

	tests := map[string]struct {
		access             []auth.Access
		basicToken         string
		openshiftResponses []response
		expectedError      error
		expectedChallenge  bool
		expectedRepoErr    string
		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(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/~"},
		},
		"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(kapi.Codecs.LegacyCodec(registered.GroupOrDie(kapi.GroupName).GroupVersions[0]), &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: "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,
			expectedActions: []string{
				"POST /oapi/v1/namespaces/foo/localsubjectaccessreviews",
				"POST /oapi/v1/namespaces/bar/localsubjectaccessreviews",
				"POST /oapi/v1/subjectaccessreviews",
				"POST /oapi/v1/namespaces/baz/localsubjectaccessreviews",
			},
		},
		"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",
				"POST /oapi/v1/namespaces/pushrepo/localsubjectaccessreviews",
				"POST /oapi/v1/namespaces/fromrepo/localsubjectaccessreviews",
			},
		},
		"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"},
		},
		"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",
			},
		},
	}

	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)
		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 {
			_, 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
			}
		}
	}
}

type response struct {
	code int
	body string
}

func simulateOpenShiftMaster(responses []response) (*httptest.Server, *[]string) {
	i := 0
	actions := []string{}
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		response := response{500, "No response registered"}
		if i < len(responses) {
			response = responses[i]
		}
		i++
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(response.code)
		fmt.Fprintln(w, response.body)
		actions = append(actions, r.Method+" "+r.URL.Path)
	}))
	return server, &actions
}