open-cluster-management.io/governance-policy-propagator@v0.13.0/controllers/complianceeventsapi/auth.go (about)

     1  // Copyright Contributors to the Open Cluster Management project
     2  package complianceeventsapi
     3  
     4  import (
     5  	"encoding/base64"
     6  	"encoding/json"
     7  	"net/http"
     8  	"slices"
     9  	"strings"
    10  
    11  	"github.com/stolostron/rbac-api-utils/pkg/rbac"
    12  	authzv1 "k8s.io/api/authorization/v1"
    13  	k8serrors "k8s.io/apimachinery/pkg/api/errors"
    14  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    15  	"k8s.io/apimachinery/pkg/runtime/schema"
    16  	"k8s.io/client-go/kubernetes"
    17  	"k8s.io/client-go/rest"
    18  )
    19  
    20  func getManagedClusterRules(userChangedConfig *rest.Config, managedClusterNames []string,
    21  ) (map[string][]string, error) {
    22  	kclient, err := kubernetes.NewForConfig(userChangedConfig)
    23  	if err != nil {
    24  		log.Error(err, "Failed to create a Kubernetes client with the user token")
    25  
    26  		return nil, err
    27  	}
    28  
    29  	managedClusterGR := schema.GroupResource{
    30  		Group:    "cluster.open-cluster-management.io",
    31  		Resource: "managedclusters",
    32  	}
    33  
    34  	// key is managedClusterName and value is verbs.
    35  	// ex: {"managed1": ["get"], "managed2": ["list","create"], "managed3": []}
    36  	return rbac.GetResourceAccess(kclient, managedClusterGR, managedClusterNames, "")
    37  }
    38  
    39  func canGetManagedCluster(userChangedConfig *rest.Config, managedClusterName string,
    40  ) (bool, error) {
    41  	allRules, err := getManagedClusterRules(userChangedConfig, []string{managedClusterName})
    42  	if err != nil {
    43  		return false, err
    44  	}
    45  
    46  	return getAccessByClusterName(allRules, managedClusterName), nil
    47  }
    48  
    49  func getAccessByClusterName(allManagedClusterRules map[string][]string, clusterName string) bool {
    50  	starRules, ok := allManagedClusterRules["*"]
    51  	if ok && slices.Contains(starRules, "get") || slices.Contains(starRules, "*") {
    52  		return true
    53  	}
    54  
    55  	rules, ok := allManagedClusterRules[clusterName]
    56  	if ok && slices.Contains(rules, "get") || slices.Contains(rules, "*") {
    57  		return true
    58  	}
    59  
    60  	return false
    61  }
    62  
    63  // parseToken will return the token string in the Authorization header.
    64  func parseToken(req *http.Request) string {
    65  	return strings.TrimSpace(strings.TrimPrefix(req.Header.Get("Authorization"), "Bearer"))
    66  }
    67  
    68  // canRecordComplianceEvent will perform token authentication and perform a self subject access review to
    69  // ensure the input user has patch access to patch the policy status in the managed cluster namespace. An error is
    70  // returned if the authorization could not be determined.
    71  func canRecordComplianceEvent(cfg *rest.Config, clusterName string, req *http.Request) (bool, error) {
    72  	userConfig, err := getUserKubeConfig(cfg, req)
    73  	if err != nil {
    74  		return false, err
    75  	}
    76  
    77  	userClient, err := kubernetes.NewForConfig(userConfig)
    78  	if err != nil {
    79  		return false, err
    80  	}
    81  
    82  	result, err := userClient.AuthorizationV1().SelfSubjectAccessReviews().Create(
    83  		req.Context(),
    84  		&authzv1.SelfSubjectAccessReview{
    85  			Spec: authzv1.SelfSubjectAccessReviewSpec{
    86  				ResourceAttributes: &authzv1.ResourceAttributes{
    87  					Group:       "policy.open-cluster-management.io",
    88  					Version:     "v1",
    89  					Resource:    "policies",
    90  					Verb:        "patch",
    91  					Namespace:   clusterName,
    92  					Subresource: "status",
    93  				},
    94  			},
    95  		},
    96  		metav1.CreateOptions{},
    97  	)
    98  	if err != nil {
    99  		if k8serrors.IsUnauthorized(err) {
   100  			return false, ErrUnauthorized
   101  		}
   102  
   103  		return false, err
   104  	}
   105  
   106  	if !result.Status.Allowed {
   107  		log.V(0).Info(
   108  			"The user is not authorized to record a compliance event",
   109  			"cluster", clusterName,
   110  			"user", getTokenUsername(userConfig.BearerToken),
   111  		)
   112  	}
   113  
   114  	return result.Status.Allowed, nil
   115  }
   116  
   117  // getTokenUsername will parse the token and return the username. If the token is invalid, an empty string is returned.
   118  func getTokenUsername(token string) string {
   119  	parts := strings.Split(token, ".")
   120  	if len(parts) != 3 {
   121  		log.V(2).Info("The token does not have the expected three parts")
   122  
   123  		return ""
   124  	}
   125  
   126  	userInfoBytes, err := base64.StdEncoding.DecodeString(parts[1])
   127  	if err != nil {
   128  		log.V(2).Info("The token does not have valid base64")
   129  
   130  		return ""
   131  	}
   132  
   133  	userInfo := map[string]interface{}{}
   134  
   135  	err = json.Unmarshal(userInfoBytes, &userInfo)
   136  	if err != nil {
   137  		log.V(2).Info("The token does not have valid JSON")
   138  
   139  		return ""
   140  	}
   141  
   142  	username, ok := userInfo["sub"].(string)
   143  	if !ok {
   144  		return ""
   145  	}
   146  
   147  	return username
   148  }
   149  
   150  func getUserKubeConfig(config *rest.Config, r *http.Request) (*rest.Config, error) {
   151  	userConfig := &rest.Config{
   152  		Host:    config.Host,
   153  		APIPath: config.APIPath,
   154  		TLSClientConfig: rest.TLSClientConfig{
   155  			CAFile:     config.TLSClientConfig.CAFile,
   156  			CAData:     config.TLSClientConfig.CAData,
   157  			ServerName: config.TLSClientConfig.ServerName,
   158  			// For testing
   159  			Insecure: config.TLSClientConfig.Insecure,
   160  		},
   161  	}
   162  
   163  	userConfig.BearerToken = parseToken(r)
   164  
   165  	if userConfig.BearerToken == "" {
   166  		return nil, ErrUnauthorized
   167  	}
   168  
   169  	return userConfig, nil
   170  }