Files
kubesphere/pkg/apigateway/caddy-plugin/authentication/authentication.go
hongming 2d14a0f589 improve path exclusion rule
Signed-off-by: hongming <talonwan@yunify.com>
2019-10-25 18:44:07 +08:00

307 lines
7.7 KiB
Go

/*
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"
"kubesphere.io/kubesphere/pkg/apigateway/caddy-plugin/internal"
"kubesphere.io/kubesphere/pkg/utils/k8sutil"
"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/informers"
"kubesphere.io/kubesphere/pkg/utils/sliceutil"
)
type Authentication struct {
Rule *Rule
Next httpserver.Handler
}
type Rule struct {
Path string
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 := 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 permissionValidate(attrs authorizer.Attributes) (bool, error) {
if attrs.GetResource() == "users" && attrs.GetUser().GetName() == attrs.GetName() {
return true, nil
}
permitted, err := clusterRoleValidate(attrs)
if err != nil {
log.Println("lister error", err)
return false, err
}
if permitted {
return true, nil
}
if attrs.GetNamespace() != "" {
permitted, err = roleValidate(attrs)
if err != nil {
log.Println("lister error", err)
return false, err
}
if permitted {
return true, nil
}
}
return false, nil
}
func roleValidate(attrs authorizer.Attributes) (bool, error) {
roleBindingLister := informers.SharedInformerFactory().Rbac().V1().RoleBindings().Lister()
roleLister := informers.SharedInformerFactory().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 k8sutil.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 clusterRoleValidate(attrs authorizer.Attributes) (bool, error) {
clusterRoleBindingLister := informers.SharedInformerFactory().Rbac().V1().ClusterRoleBindings().Lister()
clusterRoleBindings, err := clusterRoleBindingLister.List(labels.Everything())
clusterRoleLister := informers.SharedInformerFactory().Rbac().V1().ClusterRoles().Lister()
if err != nil {
return false, err
}
for _, clusterRoleBinding := range clusterRoleBindings {
if k8sutil.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
}