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 }