Skip to content

Commit

Permalink
Feat: support client identity exchange
Browse files Browse the repository at this point in the history
Signed-off-by: Yin Da <[email protected]>
  • Loading branch information
Somefive authored and yue9944882 committed Jan 3, 2023
1 parent 6d70297 commit 42b63b5
Show file tree
Hide file tree
Showing 14 changed files with 503 additions and 15 deletions.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,18 @@ to the hosting cluster which is basically responsible for:

The addon-manager can be installed via simple helm commands, please refer to
the installation guide [here](https://open-cluster-management.io/scenarios/pushing-kube-api-requests/#installation).

### Identity Passing

When feature flag `ClientIdentityPenetration` is enabled, cluster-gateway will
recognize the identity in the incoming requests and use the [impersonation mechanism](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#user-impersonation)
to send requests to managed clusters with identity impersonated. By default,
the impersonated identity is consistent with the identity in the incoming requests.

In the cases that the identity in different clusters are not aligned, the [ClientIdentityExchanger](https://github.com/oam-dev/cluster-gateway/issues/120)
feature would be helpful to make projections. You can use either the global configuration
or the cluster configuration for declaring the identity exchange rules, like the given
[example](https://github.com/oam-dev/cluster-gateway/tree/master/examples/client-identity-exchanger/config.yaml).
For global configuration, you need to set up the `--cluster-gateway-proxy-config=<the configuration file path>`
to enable it. For cluster configuration, you can set the annotation `cluster.core.oam.dev/cluster-gateway-proxy-configuration`
value to enable the configuration for the requests to the attached cluster.
4 changes: 4 additions & 0 deletions cmd/apiserver/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ func main() {
if err := config.ValidateClusterProxy(); err != nil {
klog.Fatal(err)
}
if err := clusterv1alpha1.LoadGlobalClusterGatewayProxyConfig(); err != nil {
klog.Fatal(err)
}
return options
}).
WithPostStartHook("init-master-loopback-client", singleton.InitLoopbackClient).
Expand All @@ -76,6 +79,7 @@ func main() {
config.AddClusterProxyFlags(cmd.Flags())
config.AddProxyAuthorizationFlags(cmd.Flags())
config.AddUserAgentFlags(cmd.Flags())
config.AddClusterGatewayProxyConfig(cmd.Flags())
cmd.Flags().BoolVarP(&options.OCMIntegration, "ocm-integration", "", false,
"Enabling OCM integration, reading cluster CA and api endpoint from managed "+
"cluster.")
Expand Down
1 change: 0 additions & 1 deletion e2e/benchmark/configmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ var _ = Describe("Basic RoundTrip Test",
"shouldn't take too long.")
}, 100)


Measure("get namespace kube-system from managed cluster", func(b Benchmarker) {
runtime := b.Time("runtime", func() {
_, err = multiClusterClient.CoreV1().Namespaces().Get(
Expand Down
16 changes: 16 additions & 0 deletions examples/client-identity-exchanger/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
apiVersion: cluster.core.oam.dev/v1alpha1
kind: ClusterGatewayProxyConfiguration
spec:
clientIdentityExchanger:
rules:
- name: super-user
source:
group: sudoer
type: PrivilegedIdentityExchanger
- name: mapping
source:
user: user-12345
cluster: cluster-34567
target:
user: user-34567
type: StaticMappingIdentityExchanger
23 changes: 21 additions & 2 deletions pkg/apis/cluster/v1alpha1/clustergateway_proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"time"

utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/klog/v2"

"github.com/oam-dev/cluster-gateway/pkg/config"
"github.com/oam-dev/cluster-gateway/pkg/featuregates"
Expand Down Expand Up @@ -224,7 +225,7 @@ func (p *proxyHandler) ServeHTTP(writer http.ResponseWriter, request *http.Reque
return
}
if p.impersonate || utilfeature.DefaultFeatureGate.Enabled(featuregates.ClientIdentityPenetration) {
cfg.Impersonate = getImpersonationConfig(request)
cfg.Impersonate = p.getImpersonationConfig(request)
}
rt, err := restclient.TransportFor(cfg)
if err != nil {
Expand Down Expand Up @@ -305,8 +306,26 @@ func (e ErrorResponderFunc) Error(w http.ResponseWriter, req *http.Request, err
e(w, req, err)
}

func getImpersonationConfig(req *http.Request) restclient.ImpersonationConfig {
func (p *proxyHandler) getImpersonationConfig(req *http.Request) restclient.ImpersonationConfig {
user, _ := request.UserFrom(req.Context())
if p.clusterGateway.Spec.ProxyConfig != nil {
matched, ruleName, projected, err := ExchangeIdentity(&p.clusterGateway.Spec.ProxyConfig.Spec.ClientIdentityExchanger, user, p.parentName)
if err != nil {
klog.Errorf("exchange identity with cluster config error: %w", err)
}
if matched {
klog.Infof("identity exchanged with rule `%s` in the proxy config from cluster `%s`", ruleName, p.clusterGateway.Name)
return *projected
}
}
matched, ruleName, projected, err := ExchangeIdentity(&GlobalClusterGatewayProxyConfiguration.Spec.ClientIdentityExchanger, user, p.parentName)
if err != nil {
klog.Errorf("exchange identity with global config error: %w", err)
}
if matched {
klog.Infof("identity exchanged with rule `%s` in the proxy config from global config", ruleName)
return *projected
}
return restclient.ImpersonationConfig{
UserName: user.GetName(),
Groups: user.GetGroups(),
Expand Down
172 changes: 172 additions & 0 deletions pkg/apis/cluster/v1alpha1/clustergateway_proxy_configuration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/*
Copyright 2022 The KubeVela 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 v1alpha1

import (
"fmt"
"os"
"regexp"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/client-go/rest"
"k8s.io/utils/strings/slices"

"github.com/oam-dev/cluster-gateway/pkg/config"
)

const (
AnnotationClusterGatewayProxyConfiguration = "cluster.core.oam.dev/cluster-gateway-proxy-configuration"
)

type ClusterGatewayProxyConfiguration struct {
metav1.TypeMeta `json:",inline"`
Spec ClusterGatewayProxyConfigurationSpec `json:"spec"`
}

type ClusterGatewayProxyConfigurationSpec struct {
ClientIdentityExchanger `json:"clientIdentityExchanger"`
}

type ClientIdentityExchanger struct {
Rules []ClientIdentityExchangeRule `json:"rules,omitempty"`
}

type ClientIdentityExchangeType string

const (
PrivilegedIdentityExchanger ClientIdentityExchangeType = "PrivilegedIdentityExchanger"
StaticMappingIdentityExchanger ClientIdentityExchangeType = "StaticMappingIdentityExchanger"
ExternalIdentityExchanger ClientIdentityExchangeType = "ExternalIdentityExchanger"
)

type ClientIdentityExchangeRule struct {
Name string `json:"name"`
Type ClientIdentityExchangeType `json:"type"`
Source *IdentityExchangerSource `json:"source"`

Target *IdentityExchangerTarget `json:"target,omitempty"`
URL *string `json:"url,omitempty"`
}

type IdentityExchangerTarget struct {
User string `json:"user,omitempty"`
Groups []string `json:"groups,omitempty"`
UID string `json:"uid,omitempty"`
}

type IdentityExchangerSource struct {
User *string `json:"user,omitempty"`
Group *string `json:"group,omitempty"`
UID *string `json:"uid,omitempty"`
Cluster *string `json:"cluster,omitempty"`

UserPattern *string `json:"userPattern,omitempty"`
GroupPattern *string `json:"groupPattern,omitempty"`
ClusterPattern *string `json:"clusterPattern,omitempty"`
}

var GlobalClusterGatewayProxyConfiguration = &ClusterGatewayProxyConfiguration{}

func LoadGlobalClusterGatewayProxyConfig() error {
if config.ClusterGatewayProxyConfigPath == "" {
return nil
}
bs, err := os.ReadFile(config.ClusterGatewayProxyConfigPath)
if err != nil {
return err
}
return yaml.Unmarshal(bs, GlobalClusterGatewayProxyConfiguration)
}

func ExchangeIdentity(exchanger *ClientIdentityExchanger, userInfo user.Info, cluster string) (matched bool, ruleName string, projected *rest.ImpersonationConfig, err error) {
for _, rule := range exchanger.Rules {
if matched, projected, err = exchangeIdentity(&rule, userInfo, cluster); matched {
return matched, rule.Name, projected, err
}
}
return false, "", nil, nil
}

func exchangeIdentity(rule *ClientIdentityExchangeRule, userInfo user.Info, cluster string) (matched bool, projected *rest.ImpersonationConfig, err error) {
if !matchIdentity(rule.Source, userInfo, cluster) {
return false, nil, nil
}
switch rule.Type {
case PrivilegedIdentityExchanger:
return true, &rest.ImpersonationConfig{}, nil
case StaticMappingIdentityExchanger:
return true, &rest.ImpersonationConfig{
UserName: rule.Target.User,
Groups: rule.Target.Groups,
UID: rule.Target.UID,
}, nil
case ExternalIdentityExchanger:
return true, nil, fmt.Errorf("ExternalIdentityExchanger is not implemented")
}
return true, nil, fmt.Errorf("unknown exchanger type: %s", rule.Type)
}

// denyQuery return true when the pattern is valid and could be used as regular expression,
// and the given query does not match the pattern, otherwise return false
func (in *IdentityExchangerSource) denyQuery(pattern *string, query string) bool {
if pattern == nil {
return false
}
matched, err := regexp.Match(*pattern, []byte(query))
if err != nil {
return false
}
return !matched
}

// denyGroups return true if none of the group matches the given pattern
func (in *IdentityExchangerSource) denyGroups(groupPattern *string, groups []string) bool {
if groupPattern == nil {
return false
}
for _, group := range groups {
if !in.denyQuery(groupPattern, group) {
return false
}
}
return true
}

func matchIdentity(in *IdentityExchangerSource, userInfo user.Info, cluster string) bool {
if in == nil {
return false
}
switch {
case in.User != nil && userInfo.GetName() != *in.User:
return false
case in.Group != nil && !slices.Contains(userInfo.GetGroups(), *in.Group):
return false
case in.UID != nil && userInfo.GetUID() != *in.UID:
return false
case in.Cluster != nil && cluster != *in.Cluster:
return false
case in.denyQuery(in.UserPattern, userInfo.GetName()):
return false
case in.denyGroups(in.GroupPattern, userInfo.GetGroups()):
return false
case in.denyQuery(in.ClusterPattern, cluster):
return false
}
return true
}
Loading

0 comments on commit 42b63b5

Please sign in to comment.