package authorizer

import (
	"errors"

	kapi "k8s.io/kubernetes/pkg/api"
	"k8s.io/kubernetes/pkg/auth/user"
	kerrors "k8s.io/kubernetes/pkg/util/errors"
	"k8s.io/kubernetes/pkg/util/sets"

	"github.com/openshift/origin/pkg/authorization/rulevalidation"
)

type openshiftAuthorizer struct {
	ruleResolver          rulevalidation.AuthorizationRuleResolver
	forbiddenMessageMaker ForbiddenMessageMaker
}

func NewAuthorizer(ruleResolver rulevalidation.AuthorizationRuleResolver, forbiddenMessageMaker ForbiddenMessageMaker) Authorizer {
	return &openshiftAuthorizer{ruleResolver, forbiddenMessageMaker}
}

func (a *openshiftAuthorizer) Authorize(ctx kapi.Context, passedAttributes Action) (bool, string, error) {
	attributes := CoerceToDefaultAuthorizationAttributes(passedAttributes)

	user, ok := kapi.UserFrom(ctx)
	if !ok {
		return false, "", errors.New("no user available on context")
	}
	namespace, _ := kapi.NamespaceFrom(ctx)
	allowed, reason, err := a.authorizeWithNamespaceRules(user, namespace, attributes)
	if allowed {
		return true, reason, nil
	}
	// errors are allowed to occur
	if err != nil {
		return false, "", err
	}

	denyReason, err := a.forbiddenMessageMaker.MakeMessage(MessageContext{user, namespace, attributes})
	if err != nil {
		denyReason = err.Error()
	}

	return false, denyReason, nil
}

// GetAllowedSubjects returns the subjects it knows can perform the action.
// If we got an error, then the list of subjects may not be complete, but it does not contain any incorrect names.
// This is done because policy rules are purely additive and policy determinations
// can be made on the basis of those rules that are found.
func (a *openshiftAuthorizer) GetAllowedSubjects(ctx kapi.Context, attributes Action) (sets.String, sets.String, error) {
	namespace, _ := kapi.NamespaceFrom(ctx)
	return a.getAllowedSubjectsFromNamespaceBindings(namespace, attributes)
}

func (a *openshiftAuthorizer) getAllowedSubjectsFromNamespaceBindings(namespace string, passedAttributes Action) (sets.String, sets.String, error) {
	attributes := CoerceToDefaultAuthorizationAttributes(passedAttributes)

	var errs []error

	roleBindings, err := a.ruleResolver.GetRoleBindings(namespace)
	if err != nil {
		errs = append(errs, err)
	}

	users := sets.String{}
	groups := sets.String{}
	for _, roleBinding := range roleBindings {
		role, err := a.ruleResolver.GetRole(roleBinding)
		if err != nil {
			// If we got an error, then the list of subjects may not be complete, but it does not contain any incorrect names.
			// This is done because policy rules are purely additive and policy determinations
			// can be made on the basis of those rules that are found.
			errs = append(errs, err)
			continue
		}

		for _, rule := range role.Rules() {
			matches, err := attributes.RuleMatches(rule)
			if err != nil {
				errs = append(errs, err)
				continue
			}

			if matches {
				users.Insert(roleBinding.Users().List()...)
				groups.Insert(roleBinding.Groups().List()...)
			}
		}
	}

	return users, groups, kerrors.NewAggregate(errs)
}

// authorizeWithNamespaceRules returns isAllowed, reason, and error.  If an error is returned, isAllowed and reason are still valid.  This seems strange
// but errors are not always fatal to the authorization process.  It is entirely possible to get an error and be able to continue determine authorization
// status in spite of it.  This is most common when a bound role is missing, but enough roles are still present and bound to authorize the request.
func (a *openshiftAuthorizer) authorizeWithNamespaceRules(user user.Info, namespace string, passedAttributes Action) (bool, string, error) {
	attributes := CoerceToDefaultAuthorizationAttributes(passedAttributes)

	allRules, ruleRetrievalError := a.ruleResolver.RulesFor(user, namespace)

	var errs []error
	for _, rule := range allRules {
		matches, err := attributes.RuleMatches(rule)
		if err != nil {
			errs = append(errs, err)
			continue
		}
		if matches {
			if len(namespace) == 0 {
				return true, "allowed by cluster rule", nil
			}
			// not 100% accurate, because the rule may have been provided by a cluster rule. we no longer have
			// this distinction upstream in practice.
			return true, "allowed by rule in " + namespace, nil
		}
	}
	if len(errs) == 0 {
		return false, "", ruleRetrievalError
	}
	if ruleRetrievalError != nil {
		errs = append(errs, ruleRetrievalError)
	}
	return false, "", kerrors.NewAggregate(errs)
}

// TODO this may or may not be the behavior we want for managing rules.  As a for instance, a verb might be specified
// that our attributes builder will never satisfy.  For now, I think gets us close.  Maybe a warning message of some kind?
func CoerceToDefaultAuthorizationAttributes(passedAttributes Action) *DefaultAuthorizationAttributes {
	attributes, ok := passedAttributes.(*DefaultAuthorizationAttributes)
	if !ok {
		attributes = &DefaultAuthorizationAttributes{
			APIGroup:          passedAttributes.GetAPIGroup(),
			Verb:              passedAttributes.GetVerb(),
			RequestAttributes: passedAttributes.GetRequestAttributes(),
			Resource:          passedAttributes.GetResource(),
			ResourceName:      passedAttributes.GetResourceName(),
			NonResourceURL:    passedAttributes.IsNonResourceURL(),
			URL:               passedAttributes.GetURL(),
		}
	}

	return attributes
}

func doesApplyToUser(ruleUsers, ruleGroups sets.String, user user.Info) bool {
	if ruleUsers.Has(user.GetName()) {
		return true
	}

	for _, currGroup := range user.GetGroups() {
		if ruleGroups.Has(currGroup) {
			return true
		}
	}

	return false
}