/*

 Copyright 2019 The KubeSphere 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 authentication

import (
	"context"
	"errors"
	"fmt"
	"k8s.io/apiserver/pkg/authorization/authorizer"
	"k8s.io/apiserver/pkg/endpoints/request"
	"k8s.io/client-go/informers"
	"kubesphere.io/kubesphere/pkg/apigateway/caddy-plugin/internal"
	"kubesphere.io/kubesphere/pkg/models/iam"
	"kubesphere.io/kubesphere/pkg/simple/client/k8s"
	"log"
	"net/http"
	"strings"

	"github.com/mholt/caddy/caddyhttp/httpserver"
	"k8s.io/api/rbac/v1"
	k8serr "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"kubesphere.io/kubesphere/pkg/utils/sliceutil"
)

type Authentication struct {
	Rule            *Rule
	Next            httpserver.Handler
	informerFactory informers.SharedInformerFactory
}

type Rule struct {
	Path              string
	KubernetesOptions *k8s.KubernetesOptions
	ExclusionRules    []internal.ExclusionRule
}

func (c *Authentication) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {

	if httpserver.Path(r.URL.Path).Matches(c.Rule.Path) {

		for _, rule := range c.Rule.ExclusionRules {
			if httpserver.Path(r.URL.Path).Matches(rule.Path) && (rule.Method == internal.AllMethod || r.Method == rule.Method) {
				return c.Next.ServeHTTP(w, r)
			}
		}

		attrs, err := getAuthorizerAttributes(r.Context())

		// without authenticate, no requestInfo found in the context
		if err != nil {
			return c.Next.ServeHTTP(w, r)
		}

		permitted, err := c.permissionValidate(attrs)

		if err != nil {
			return http.StatusInternalServerError, err
		}

		if !permitted {
			err = k8serr.NewForbidden(schema.GroupResource{Group: attrs.GetAPIGroup(), Resource: attrs.GetResource()}, attrs.GetName(), fmt.Errorf("permission undefined"))
			return handleForbidden(w, err), nil
		}
	}

	return c.Next.ServeHTTP(w, r)

}

func handleForbidden(w http.ResponseWriter, err error) int {
	message := fmt.Sprintf("Forbidden,%s", err.Error())
	w.Header().Add("WWW-Authenticate", message)
	log.Println(message)
	return http.StatusForbidden
}

func (c *Authentication) permissionValidate(attrs authorizer.Attributes) (bool, error) {

	if attrs.GetResource() == "users" && attrs.GetUser().GetName() == attrs.GetName() {
		return true, nil
	}

	permitted, err := c.clusterRoleValidate(attrs)

	if err != nil {
		log.Println("lister error", err)
		return false, err
	}

	if permitted {
		return true, nil
	}

	if attrs.GetNamespace() != "" {
		permitted, err = c.roleValidate(attrs)

		if err != nil {
			log.Println("lister error", err)
			return false, err
		}

		if permitted {
			return true, nil
		}
	}

	return false, nil
}

func (c *Authentication) roleValidate(attrs authorizer.Attributes) (bool, error) {
	roleBindingLister := c.informerFactory.Rbac().V1().RoleBindings().Lister()
	roleLister := c.informerFactory.Rbac().V1().Roles().Lister()
	roleBindings, err := roleBindingLister.RoleBindings(attrs.GetNamespace()).List(labels.Everything())

	if err != nil {
		return false, err
	}

	fullSource := attrs.GetResource()

	if attrs.GetSubresource() != "" {
		fullSource = fullSource + "/" + attrs.GetSubresource()
	}

	for _, roleBinding := range roleBindings {
		if iam.ContainsUser(roleBinding.Subjects, attrs.GetUser().GetName()) {
			role, err := roleLister.Roles(attrs.GetNamespace()).Get(roleBinding.RoleRef.Name)

			if err != nil {
				if k8serr.IsNotFound(err) {
					continue
				}
				return false, err
			}

			for _, rule := range role.Rules {
				if ruleMatchesRequest(rule, attrs.GetAPIGroup(), "", attrs.GetResource(), attrs.GetSubresource(), attrs.GetName(), attrs.GetVerb()) {
					return true, nil
				}
			}
		}
	}

	return false, nil
}

func (c *Authentication) clusterRoleValidate(attrs authorizer.Attributes) (bool, error) {
	clusterRoleBindingLister := c.informerFactory.Rbac().V1().ClusterRoleBindings().Lister()
	clusterRoleBindings, err := clusterRoleBindingLister.List(labels.Everything())
	clusterRoleLister := c.informerFactory.Rbac().V1().ClusterRoles().Lister()
	if err != nil {
		return false, err
	}

	for _, clusterRoleBinding := range clusterRoleBindings {

		if iam.ContainsUser(clusterRoleBinding.Subjects, attrs.GetUser().GetName()) {
			clusterRole, err := clusterRoleLister.Get(clusterRoleBinding.RoleRef.Name)

			if err != nil {
				if k8serr.IsNotFound(err) {
					continue
				}
				return false, err
			}

			for _, rule := range clusterRole.Rules {
				if attrs.IsResourceRequest() {
					if ruleMatchesRequest(rule, attrs.GetAPIGroup(), "", attrs.GetResource(), attrs.GetSubresource(), attrs.GetName(), attrs.GetVerb()) {
						return true, nil
					}
				} else {
					if ruleMatchesRequest(rule, "", attrs.GetPath(), "", "", "", attrs.GetVerb()) {
						return true, nil
					}
				}
			}

		}
	}

	return false, nil
}

func ruleMatchesResources(rule v1.PolicyRule, apiGroup string, resource string, subresource string, resourceName string) bool {

	if resource == "" {
		return false
	}

	if !sliceutil.HasString(rule.APIGroups, apiGroup) && !sliceutil.HasString(rule.APIGroups, v1.ResourceAll) {
		return false
	}

	if len(rule.ResourceNames) > 0 && !sliceutil.HasString(rule.ResourceNames, resourceName) {
		return false
	}

	combinedResource := resource

	if subresource != "" {
		combinedResource = combinedResource + "/" + subresource
	}

	for _, res := range rule.Resources {

		// match "*"
		if res == v1.ResourceAll || res == combinedResource {
			return true
		}

		// match "*/subresource"
		if len(subresource) > 0 && strings.HasPrefix(res, "*/") && subresource == strings.TrimLeft(res, "*/") {
			return true
		}
		// match "resource/*"
		if strings.HasSuffix(res, "/*") && resource == strings.TrimRight(res, "/*") {
			return true
		}
	}

	return false
}

func ruleMatchesRequest(rule v1.PolicyRule, apiGroup string, nonResourceURL string, resource string, subresource string, resourceName string, verb string) bool {

	if !sliceutil.HasString(rule.Verbs, verb) && !sliceutil.HasString(rule.Verbs, v1.VerbAll) {
		return false
	}

	if nonResourceURL == "" {
		return ruleMatchesResources(rule, apiGroup, resource, subresource, resourceName)
	} else {
		return ruleMatchesNonResource(rule, nonResourceURL)
	}
}

func ruleMatchesNonResource(rule v1.PolicyRule, nonResourceURL string) bool {

	if nonResourceURL == "" {
		return false
	}

	for _, spec := range rule.NonResourceURLs {
		if pathMatches(nonResourceURL, spec) {
			return true
		}
	}

	return false
}

func pathMatches(path, spec string) bool {
	if spec == "*" {
		return true
	}
	if spec == path {
		return true
	}
	if strings.HasSuffix(spec, "*") && strings.HasPrefix(path, strings.TrimRight(spec, "*")) {
		return true
	}
	return false
}

func getAuthorizerAttributes(ctx context.Context) (authorizer.Attributes, error) {
	attribs := authorizer.AttributesRecord{}

	user, ok := request.UserFrom(ctx)
	if ok {
		attribs.User = user
	}

	requestInfo, found := request.RequestInfoFrom(ctx)
	if !found {
		return nil, errors.New("no RequestInfo found in the context")
	}

	// Start with common attributes that apply to resource and non-resource requests
	attribs.ResourceRequest = requestInfo.IsResourceRequest
	attribs.Path = requestInfo.Path
	attribs.Verb = requestInfo.Verb

	attribs.APIGroup = requestInfo.APIGroup
	attribs.APIVersion = requestInfo.APIVersion
	attribs.Resource = requestInfo.Resource
	attribs.Subresource = requestInfo.Subresource
	attribs.Namespace = requestInfo.Namespace
	attribs.Name = requestInfo.Name

	return &attribs, nil
}
