/*
Copyright 2018 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package policybased

import (
	"context"
	"testing"

	rbacv1 "k8s.io/api/rbac/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apiserver/pkg/authentication/user"
	"k8s.io/apiserver/pkg/authorization/authorizer"
	"k8s.io/apiserver/pkg/endpoints/request"
	"k8s.io/apiserver/pkg/registry/rest"
	"k8s.io/kubernetes/pkg/apis/rbac"
	_ "k8s.io/kubernetes/pkg/apis/rbac/install"
	"k8s.io/kubernetes/pkg/registry/rbac/validation"
)

func TestEscalation(t *testing.T) {
	createContext := request.WithRequestInfo(request.WithNamespace(context.TODO(), ""), &request.RequestInfo{
		IsResourceRequest: true,
		Verb:              "create",
		APIGroup:          "rbac.authorization.k8s.io",
		APIVersion:        "v1",
		Resource:          "clusterroles",
		Name:              "",
	})
	updateContext := request.WithRequestInfo(request.WithNamespace(context.TODO(), ""), &request.RequestInfo{
		IsResourceRequest: true,
		Verb:              "update",
		APIGroup:          "rbac.authorization.k8s.io",
		APIVersion:        "v1",
		Resource:          "clusterroles",
		Name:              "myrole",
	})

	superuser := &user.DefaultInfo{Name: "superuser", Groups: []string{"system:masters"}}
	bob := &user.DefaultInfo{Name: "bob"}
	steve := &user.DefaultInfo{Name: "steve"}
	alice := &user.DefaultInfo{Name: "alice"}

	authzCalled := 0
	fakeStorage := &fakeStorage{}
	fakeAuthorizer := authorizer.AuthorizerFunc(func(attr authorizer.Attributes) (authorizer.Decision, string, error) {
		authzCalled++
		if attr.GetUser().GetName() == "steve" {
			return authorizer.DecisionAllow, "", nil
		}
		return authorizer.DecisionNoOpinion, "", nil
	})
	fakeRuleResolver, _ := validation.NewTestRuleResolver(
		nil,
		nil,
		[]*rbacv1.ClusterRole{{ObjectMeta: metav1.ObjectMeta{Name: "alice-role"}, Rules: []rbacv1.PolicyRule{{APIGroups: []string{"*"}, Resources: []string{"*"}, Verbs: []string{"*"}}}}},
		[]*rbacv1.ClusterRoleBinding{{RoleRef: rbacv1.RoleRef{Name: "alice-role", APIGroup: "rbac.authorization.k8s.io", Kind: "ClusterRole"}, Subjects: []rbacv1.Subject{{Name: "alice", Kind: "User", APIGroup: "rbac.authorization.k8s.io"}}}},
	)

	role := &rbac.ClusterRole{
		ObjectMeta: metav1.ObjectMeta{Name: "myrole", Namespace: ""},
		Rules:      []rbac.PolicyRule{{APIGroups: []string{""}, Verbs: []string{"get"}, Resources: []string{"pods"}}},
	}

	s := NewStorage(fakeStorage, fakeAuthorizer, fakeRuleResolver)

	testcases := []struct {
		name          string
		user          user.Info
		expectAllowed bool
		expectAuthz   bool
	}{
		// superuser doesn't even trigger an authz check, and is allowed
		{
			name:          "superuser",
			user:          superuser,
			expectAuthz:   false,
			expectAllowed: true,
		},
		// bob triggers an authz check, is disallowed by the authorizer, and has no RBAC permissions, so is not allowed
		{
			name:          "bob",
			user:          bob,
			expectAuthz:   true,
			expectAllowed: false,
		},
		// steve triggers an authz check, is allowed by the authorizer, and has no RBAC permissions, but is still allowed
		{
			name:          "steve",
			user:          steve,
			expectAuthz:   true,
			expectAllowed: true,
		},
		// alice triggers an authz check, is denied by the authorizer, but has RBAC permissions in the fakeRuleResolver, so is allowed
		{
			name:          "alice",
			user:          alice,
			expectAuthz:   true,
			expectAllowed: true,
		},
	}

	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
			authzCalled, fakeStorage.created, fakeStorage.updated = 0, 0, 0
			_, err := s.Create(request.WithUser(createContext, tc.user), role, nil, nil)

			if tc.expectAllowed {
				if err != nil {
					t.Error(err)
					return
				}
				if fakeStorage.created != 1 {
					t.Errorf("unexpected calls to underlying storage.Create: %d", fakeStorage.created)
					return
				}
			} else {
				if !errors.IsForbidden(err) {
					t.Errorf("expected forbidden, got %v", err)
					return
				}
				if fakeStorage.created != 0 {
					t.Errorf("unexpected calls to underlying storage.Create: %d", fakeStorage.created)
					return
				}
			}

			if tc.expectAuthz != (authzCalled > 0) {
				t.Fatalf("expected authz=%v, saw %d calls", tc.expectAuthz, authzCalled)
			}

			authzCalled, fakeStorage.created, fakeStorage.updated = 0, 0, 0
			_, _, err = s.Update(request.WithUser(updateContext, tc.user), role.Name, rest.DefaultUpdatedObjectInfo(role), nil, nil, false, nil)

			if tc.expectAllowed {
				if err != nil {
					t.Error(err)
					return
				}
				if fakeStorage.updated != 1 {
					t.Errorf("unexpected calls to underlying storage.Update: %d", fakeStorage.updated)
					return
				}
			} else {
				if !errors.IsForbidden(err) {
					t.Errorf("expected forbidden, got %v", err)
					return
				}
				if fakeStorage.updated != 0 {
					t.Errorf("unexpected calls to underlying storage.Update: %d", fakeStorage.updated)
					return
				}
			}

			if tc.expectAuthz != (authzCalled > 0) {
				t.Fatalf("expected authz=%v, saw %d calls", tc.expectAuthz, authzCalled)
			}
		})
	}
}

type fakeStorage struct {
	updated int
	created int
	rest.StandardStorage
}

func (f *fakeStorage) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
	f.created++
	return nil, nil
}

func (f *fakeStorage) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
	obj, err := objInfo.UpdatedObject(ctx, &rbac.ClusterRole{})
	if err != nil {
		return obj, false, err
	}
	f.updated++
	return nil, false, nil
}
