Feat: multi-cluster authentication (#3713)

Signed-off-by: Somefive <yd219913@alibaba-inc.com>
This commit is contained in:
Somefive
2022-04-24 14:48:26 +08:00
committed by GitHub
parent d7c6f6cc73
commit 2d28fb35eb
37 changed files with 803 additions and 129 deletions

View File

@@ -0,0 +1,22 @@
/*
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 common
const (
// Group api group name
Group = "core.oam.dev"
)

View File

@@ -19,11 +19,13 @@ package v1alpha1
import (
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/scheme"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
)
// Package type metadata.
const (
Group = "core.oam.dev"
Group = common.Group
Version = "v1alpha1"
)

View File

@@ -21,11 +21,13 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/scheme"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
)
// Package type metadata.
const (
Group = "core.oam.dev"
Group = common.Group
Version = "v1alpha2"
)

View File

@@ -21,11 +21,13 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/scheme"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
)
// Package type metadata.
const (
Group = "core.oam.dev"
Group = common.Group
Version = "v1beta1"
)

View File

@@ -18,6 +18,13 @@ package types
import "github.com/oam-dev/kubevela/pkg/oam"
const (
// KubeVelaName name of kubevela
KubeVelaName = "kubevela"
// VelaCoreName name of vela-core
VelaCoreName = "vela-core"
)
const (
// DefaultKubeVelaReleaseName defines the default name of KubeVela Release
DefaultKubeVelaReleaseName = "kubevela"
@@ -153,3 +160,8 @@ const (
// TerrfaormComponentPrefix is the prefix of component type of terraform-xxx
TerrfaormComponentPrefix = "terraform-"
)
const (
// ClusterGatewayAccessorGroup the group to impersonate which allows the access to the cluster-gateway
ClusterGatewayAccessorGroup = "cluster-gateway-accessor"
)

View File

@@ -122,23 +122,27 @@ helm install --create-namespace -n vela-system kubevela kubevela/vela-core --wai
### Common parameters
| Name | Description | Value |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------- |
| `imagePullSecrets` | Image pull secrets | `[]` |
| `nameOverride` | Override name | `""` |
| `fullnameOverride` | Fullname override | `""` |
| `serviceAccount.create` | Specifies whether a service account should be created | `true` |
| `serviceAccount.annotations` | Annotations to add to the service account | `{}` |
| `serviceAccount.name` | The name of the service account to use. If not set and create is true, a name is generated using the fullname template | `nil` |
| `nodeSelector` | Node selector | `{}` |
| `tolerations` | Tolerations | `[]` |
| `affinity` | Affinity | `{}` |
| `rbac.create` | Specifies whether a RBAC role should be created | `true` |
| `logDebug` | Enable debug logs for development purpose | `false` |
| `logFilePath` | If non-empty, write log files in this path | `""` |
| `logFileMaxSize` | Defines the maximum size a log file can grow to. Unit is megabytes. If the value is 0, the maximum file size is unlimited. | `1024` |
| `kubeClient.qps` | The qps for reconcile clients, default is 50 | `50` |
| `kubeClient.burst` | The burst for reconcile clients, default is 100 | `100` |
| Name | Description | Value |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------- | -------------------- |
| `imagePullSecrets` | Image pull secrets | `[]` |
| `nameOverride` | Override name | `""` |
| `fullnameOverride` | Fullname override | `""` |
| `serviceAccount.create` | Specifies whether a service account should be created | `true` |
| `serviceAccount.annotations` | Annotations to add to the service account | `{}` |
| `serviceAccount.name` | The name of the service account to use. If not set and create is true, a name is generated using the fullname template | `nil` |
| `nodeSelector` | Node selector | `{}` |
| `tolerations` | Tolerations | `[]` |
| `affinity` | Affinity | `{}` |
| `rbac.create` | Specifies whether a RBAC role should be created | `true` |
| `logDebug` | Enable debug logs for development purpose | `false` |
| `logFilePath` | If non-empty, write log files in this path | `""` |
| `logFileMaxSize` | Defines the maximum size a log file can grow to. Unit is megabytes. If the value is 0, the maximum file size is unlimited. | `1024` |
| `kubeClient.qps` | The qps for reconcile clients, default is 50 | `50` |
| `kubeClient.burst` | The burst for reconcile clients, default is 100 | `100` |
| `authentication.enabled` | Enable authentication for application | `false` |
| `authentication.withUser` | Application authentication will impersonate as the request User | `false` |
| `authentication.defaultUser` | Application authentication will impersonate as the User if no user provided in Application | `kubevela:vela-core` |
| `authentication.groupPattern` | Application authentication will impersonate as the request Group that matches the pattern | `kubevela:*` |
## Uninstallation

View File

@@ -120,6 +120,32 @@ webhooks:
- UPDATE
resources:
- podspecworkloads
- clientConfig:
caBundle: Cg==
service:
name: {{ template "kubevela.name" . }}-webhook
namespace: {{ .Release.Namespace }}
path: /mutating-core-oam-dev-v1beta1-applications
{{- if .Values.admissionWebhooks.patch.enabled }}
failurePolicy: Ignore
{{- else }}
failurePolicy: Fail
{{- end }}
name: mutating.core.oam.dev.v1beta1.applications
admissionReviewVersions:
- v1beta1
- v1
sideEffects: None
rules:
- apiGroups:
- core.oam.dev
apiVersions:
- v1beta1
operations:
- CREATE
- UPDATE
resources:
- applications
- clientConfig:
caBundle: Cg==
service:

View File

@@ -274,4 +274,30 @@ spec:
runAsGroup: 2000
runAsNonRoot: true
runAsUser: 2000
{{ end }}
---
{{ if and .Values.multicluster.enabled }}
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "kubevela.fullname" . }}:cluster-gateway-access-role
rules:
- apiGroups: [ "cluster.core.oam.dev" ]
resources: [ "clustergateways/proxy" ]
verbs: [ "get", "list", "watch", "create", "update", "patch", "delete" ]
{{ end }}
---
{{ if and .Values.multicluster.enabled }}
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ include "kubevela.fullname" . }}:cluster-gateway-access-rolebinding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ include "kubevela.fullname" . }}:cluster-gateway-access-role
subjects:
- kind: Group
name: cluster-gateway-accessor
apiGroup: rbac.authorization.k8s.io
{{ end }}

View File

@@ -25,6 +25,9 @@ subjects:
- kind: ServiceAccount
name: {{ include "kubevela.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
- kind: Group
name: core.oam.dev
apiGroup: rbac.authorization.k8s.io
---
# permissions to do leader election.
@@ -169,6 +172,14 @@ spec:
- "--max-workflow-wait-backoff-time={{ .Values.workflow.backoff.maxTime.waitState }}"
- "--max-workflow-failed-backoff-time={{ .Values.workflow.backoff.maxTime.failedState }}"
- "--max-workflow-step-error-retry-times={{ .Values.workflow.step.errorRetryTimes }}"
- "--feature-gates=AuthenticateApplication={{- .Values.authentication.enabled | toString -}}"
{{ if .Values.authentication.enabled }}
{{ if .Values.authentication.withUser }}
- "--authentication-with-user"
{{ end }}
- "--authentication-default-user={{ .Values.authentication.defaultUser }}"
- "--authentication-group-pattern={{ .Values.authentication.groupPattern }}"
{{ end }}
image: {{ .Values.imageRegistry }}{{ .Values.image.repository }}:{{ .Values.image.tag }}
imagePullPolicy: {{ quote .Values.image.pullPolicy }}
resources:

View File

@@ -232,3 +232,13 @@ admissionWebhooks:
kubeClient:
qps: 50
burst: 100
## @param authentication.enabled Enable authentication for application
## @param authentication.withUser Application authentication will impersonate as the request User
## @param authentication.defaultUser Application authentication will impersonate as the User if no user provided in Application
## @param authentication.groupPattern Application authentication will impersonate as the request Group that matches the pattern
authentication:
enabled: false
withUser: false
defaultUser: kubevela:vela-core
groupPattern: kubevela:*

View File

@@ -125,22 +125,26 @@ helm install --create-namespace -n vela-system kubevela kubevela/vela-minimal --
### Common parameters
| Name | Description | Value |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------- |
| `imagePullSecrets` | Image pull secrets | `[]` |
| `nameOverride` | Override name | `""` |
| `fullnameOverride` | Fullname override | `""` |
| `serviceAccount.create` | Specifies whether a service account should be created | `true` |
| `serviceAccount.annotations` | Annotations to add to the service account | `{}` |
| `serviceAccount.name` | The name of the service account to use. If not set and create is true, a name is generated using the fullname template | `nil` |
| `nodeSelector` | Node selector | `{}` |
| `tolerations` | Tolerations | `[]` |
| `affinity` | Affinity | `{}` |
| `rbac.create` | Specifies whether a RBAC role should be created | `true` |
| `logDebug` | Enable debug logs for development purpose | `false` |
| `logFilePath` | If non-empty, write log files in this path | `""` |
| `logFileMaxSize` | Defines the maximum size a log file can grow to. Unit is megabytes. If the value is 0, the maximum file size is unlimited. | `1024` |
| `kubeClient.qps` | The qps for reconcile clients, default is 50 | `50` |
| `kubeClient.burst` | The burst for reconcile clients, default is 100 | `100` |
| Name | Description | Value |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------- | -------------------- |
| `imagePullSecrets` | Image pull secrets | `[]` |
| `nameOverride` | Override name | `""` |
| `fullnameOverride` | Fullname override | `""` |
| `serviceAccount.create` | Specifies whether a service account should be created | `true` |
| `serviceAccount.annotations` | Annotations to add to the service account | `{}` |
| `serviceAccount.name` | The name of the service account to use. If not set and create is true, a name is generated using the fullname template | `nil` |
| `nodeSelector` | Node selector | `{}` |
| `tolerations` | Tolerations | `[]` |
| `affinity` | Affinity | `{}` |
| `rbac.create` | Specifies whether a RBAC role should be created | `true` |
| `logDebug` | Enable debug logs for development purpose | `false` |
| `logFilePath` | If non-empty, write log files in this path | `""` |
| `logFileMaxSize` | Defines the maximum size a log file can grow to. Unit is megabytes. If the value is 0, the maximum file size is unlimited. | `1024` |
| `kubeClient.qps` | The qps for reconcile clients, default is 50 | `50` |
| `kubeClient.burst` | The burst for reconcile clients, default is 100 | `100` |
| `authentication.enabled` | Enable authentication for application | `false` |
| `authentication.withUser` | Application authentication will impersonate as the request User | `false` |
| `authentication.defaultUser` | Application authentication will impersonate as the User if no user provided in Application | `kubevela:vela-core` |
| `authentication.groupPattern` | Application authentication will impersonate as the request Group that matches the pattern | `kubevela:*` |

View File

@@ -92,6 +92,32 @@ webhooks:
- UPDATE
resources:
- podspecworkloads
- clientConfig:
caBundle: Cg==
service:
name: {{ template "kubevela.name" . }}-webhook
namespace: {{ .Release.Namespace }}
path: /mutating-core-oam-dev-v1beta1-applications
{{- if .Values.admissionWebhooks.patch.enabled }}
failurePolicy: Ignore
{{- else }}
failurePolicy: Fail
{{- end }}
name: mutating.core.oam.dev.v1beta1.applications
admissionReviewVersions:
- v1beta1
- v1
sideEffects: None
rules:
- apiGroups:
- core.oam.dev
apiVersions:
- v1beta1
operations:
- CREATE
- UPDATE
resources:
- applications
- clientConfig:
caBundle: Cg==
service:

View File

@@ -188,4 +188,30 @@ spec:
runAsGroup: 2000
runAsNonRoot: true
runAsUser: 2000
{{ end }}
{{ end }}
---
{{ if and .Values.multicluster.enabled }}
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "kubevela.fullname" . }}:cluster-gateway-access-role
rules:
- apiGroups: [ "cluster.core.oam.dev" ]
resources: [ "clustergateways/proxy" ]
verbs: [ "get", "list", "watch", "create", "update", "patch", "delete" ]
{{ end }}
---
{{ if and .Values.multicluster.enabled }}
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ include "kubevela.fullname" . }}:cluster-gateway-access-rolebinding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ include "kubevela.fullname" . }}:cluster-gateway-access-role
subjects:
- kind: Group
name: cluster-gateway-accessor
apiGroup: rbac.authorization.k8s.io
{{ end }}

View File

@@ -27,6 +27,9 @@ subjects:
- kind: ServiceAccount
name: {{ include "kubevela.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
- kind: Group
name: core.oam.dev
apiGroup: rbac.authorization.k8s.io
---
# permissions to do leader election.
@@ -142,6 +145,14 @@ spec:
- "--max-workflow-wait-backoff-time={{ .Values.workflow.backoff.maxTime.waitState }}"
- "--max-workflow-failed-backoff-time={{ .Values.workflow.backoff.maxTime.failedState }}"
- "--max-workflow-step-error-retry-times={{ .Values.workflow.step.errorRetryTimes }}"
- "--feature-gates=AuthenticateApplication={{- .Values.authentication.enabled | toString -}}"
{{ if .Values.authentication.enabled }}
{{ if .Values.authentication.withUser }}
- "--authentication-with-user"
{{ end }}
- "--authentication-default-user={{ .Values.authentication.defaultUser }}"
- "--authentication-group-pattern={{ .Values.authentication.groupPattern }}"
{{ end }}
image: {{ .Values.imageRegistry }}{{ .Values.image.repository }}:{{ .Values.image.tag }}
imagePullPolicy: {{ quote .Values.image.pullPolicy }}
resources:

View File

@@ -215,3 +215,13 @@ admissionWebhooks:
kubeClient:
qps: 50
burst: 100
## @param authentication.enabled Enable authentication for application
## @param authentication.withUser Application authentication will impersonate as the request User
## @param authentication.defaultUser Application authentication will impersonate as the User if no user provided in Application
## @param authentication.groupPattern Application authentication will impersonate as the request Group that matches the pattern
authentication:
enabled: false
withUser: false
defaultUser: kubevela:vela-core
groupPattern: kubevela:*

View File

@@ -36,6 +36,8 @@ import (
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/healthz"
apicommon "github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/auth"
ctrlClient "github.com/oam-dev/kubevela/pkg/client"
standardcontroller "github.com/oam-dev/kubevela/pkg/controller"
@@ -50,6 +52,7 @@ import (
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
"github.com/oam-dev/kubevela/pkg/resourcekeeper"
pkgutils "github.com/oam-dev/kubevela/pkg/utils"
"github.com/oam-dev/kubevela/pkg/utils/common"
"github.com/oam-dev/kubevela/pkg/utils/system"
"github.com/oam-dev/kubevela/pkg/utils/util"
@@ -59,10 +62,6 @@ import (
"github.com/oam-dev/kubevela/version"
)
const (
kubevelaName = "kubevela"
)
var (
scheme = common.Scheme
waitSecretTimeout = 90 * time.Second
@@ -202,10 +201,22 @@ func main() {
klog.InfoS("Vela-Core init", "definition namespace", oam.SystemDefinitonNamespace)
restConfig := ctrl.GetConfigOrDie()
restConfig.UserAgent = kubevelaName + "/" + version.GitRevision
restConfig.UserAgent = types.KubeVelaName + "/" + version.GitRevision
restConfig.QPS = float32(qps)
restConfig.Burst = burst
restConfig.Wrap(auth.NewImpersonatingRoundTripper)
restConfig.Impersonate.UserName = types.VelaCoreName
if sub := pkgutils.GetServiceAccountSubjectFromConfig(restConfig); sub != "" {
restConfig.Impersonate.UserName = sub
}
restConfig.Impersonate.Groups = []string{apicommon.Group}
klog.InfoS("Kubernetes Config Loaded",
"UserAgent", restConfig.UserAgent,
"QPS", restConfig.QPS,
"Burst", restConfig.Burst,
"Impersonate-User", restConfig.Impersonate.UserName,
"Impersonate-Group", strings.Join(restConfig.Impersonate.Groups, ","),
)
// wrapper the round tripper by multi cluster rewriter
if enableClusterGateway {
@@ -225,7 +236,7 @@ func main() {
}
ctrl.SetLogger(klogr.New())
leaderElectionID := util.GenerateLeaderElectionID(kubevelaName, controllerArgs.IgnoreAppWithoutControllerRequirement)
leaderElectionID := util.GenerateLeaderElectionID(types.KubeVelaName, controllerArgs.IgnoreAppWithoutControllerRequirement)
mgr, err := ctrl.NewManager(restConfig, ctrl.Options{
Scheme: scheme,
MetricsBindAddress: metricsAddr,

2
go.mod
View File

@@ -264,7 +264,7 @@ require (
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.2.0
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c // indirect
google.golang.org/grpc v1.38.0 // indirect

37
pkg/auth/flags.go Normal file
View File

@@ -0,0 +1,37 @@
/*
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 auth
import (
"k8s.io/apiserver/pkg/authentication/user"
"github.com/oam-dev/kubevela/apis/types"
)
const (
// DefaultAuthenticateGroupPattern default value of groups patterns for authentication
DefaultAuthenticateGroupPattern = types.KubeVelaName + ":*"
)
var (
// AuthenticationWithUser flag for enable the authentication of User in requests
AuthenticationWithUser = false
// AuthenticationDefaultUser the default user to use while no User is set in application
AuthenticationDefaultUser = user.Anonymous
// AuthenticationGroupPattern pattern for the authentication of Group in requests
AuthenticationGroupPattern = DefaultAuthenticateGroupPattern
)

View File

@@ -22,13 +22,17 @@ import (
"net/http"
utilnet "k8s.io/apimachinery/pkg/util/net"
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/client-go/transport"
"github.com/oam-dev/kubevela/pkg/multicluster"
oamutil "github.com/oam-dev/kubevela/pkg/oam/util"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/utils"
)
const (
impersonateKey = "impersonate"
)
var _ utilnet.RoundTripperWrapper = &impersonatingRoundTripper{}
type impersonatingRoundTripper struct {
@@ -45,18 +49,22 @@ func NewImpersonatingRoundTripper(rt http.RoundTripper) http.RoundTripper {
func (rt *impersonatingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
ctx := req.Context()
// Skip impersonation on non-local cluster requests
if !multicluster.IsInLocalCluster(ctx) {
return rt.rt.RoundTrip(req)
req = req.Clone(ctx)
userInfo, exists := request.UserFrom(ctx)
if exists && userInfo != nil {
if name := userInfo.GetName(); name != "" {
req.Header.Set(transport.ImpersonateUserHeader, name)
req.Header.Set(transport.ImpersonateGroupHeader, types.ClusterGatewayAccessorGroup)
for _, group := range userInfo.GetGroups() {
if group != types.ClusterGatewayAccessorGroup {
req.Header.Add(transport.ImpersonateGroupHeader, group)
}
}
q := req.URL.Query()
q.Add(impersonateKey, "true")
req.URL.RawQuery = q.Encode()
}
}
sa := oamutil.GetServiceAccountInContext(ctx)
if sa == "" {
return rt.rt.RoundTrip(req)
}
req = req.Clone(req.Context())
req.Header.Set(transport.ImpersonateUserHeader, sa)
return rt.rt.RoundTrip(req)
}

View File

@@ -24,10 +24,16 @@ import (
"testing"
"github.com/stretchr/testify/require"
authv1 "k8s.io/api/authentication/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/transport"
featuregatetesting "k8s.io/component-base/featuregate/testing"
"github.com/oam-dev/kubevela/pkg/multicluster"
oamutil "github.com/oam-dev/kubevela/pkg/oam/util"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/features"
"github.com/oam-dev/kubevela/pkg/oam"
)
type testRoundTripper struct {
@@ -42,30 +48,51 @@ func (rt *testRoundTripper) RoundTrip(req *http.Request) (*http.Response, error)
}
func TestImpersonatingRoundTripper(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.AuthenticateApplication, true)()
AuthenticationWithUser = true
defer func() {
AuthenticationWithUser = false
}()
testSets := map[string]struct {
ctxFn func(context.Context) context.Context
expected string
ctxFn func(context.Context) context.Context
expectedUser string
expectedGroup []string
}{
"with service account": {
ctxFn: func(ctx context.Context) context.Context {
ctx = oamutil.SetServiceAccountInContext(ctx, "vela-system", "default")
return ctx
app := &v1beta1.Application{}
app.SetNamespace("vela-system")
v1.SetMetaDataAnnotation(&app.ObjectMeta, oam.AnnotationApplicationServiceAccountName, "default")
return ContextWithUserInfo(ctx, app)
},
expected: "system:serviceaccount:vela-system:default",
expectedUser: "system:serviceaccount:vela-system:default",
expectedGroup: []string{types.ClusterGatewayAccessorGroup},
},
"without service account and app": {
ctxFn: func(ctx context.Context) context.Context {
return ContextWithUserInfo(ctx, nil)
},
expectedUser: "",
expectedGroup: []string{types.ClusterGatewayAccessorGroup},
},
"without service account": {
ctxFn: func(ctx context.Context) context.Context {
return ctx
return ContextWithUserInfo(ctx, &v1beta1.Application{})
},
expected: "",
expectedUser: AuthenticationDefaultUser,
expectedGroup: []string{types.ClusterGatewayAccessorGroup},
},
"ignore if non-local cluster request": {
"with user and groups": {
ctxFn: func(ctx context.Context) context.Context {
ctx = multicluster.ContextWithClusterName(ctx, "test-cluster")
ctx = oamutil.SetServiceAccountInContext(ctx, "vela-system", "default")
return ctx
app := &v1beta1.Application{}
SetUserInfoInAnnotation(&app.ObjectMeta, authv1.UserInfo{
Username: "username",
Groups: []string{"kubevela:group1", "kubevela:group2"},
})
return ContextWithUserInfo(ctx, app)
},
expected: "",
expectedUser: "username",
expectedGroup: []string{types.ClusterGatewayAccessorGroup, "kubevela:group1", "kubevela:group2"},
},
}
for name, ts := range testSets {
@@ -76,12 +103,13 @@ func TestImpersonatingRoundTripper(t *testing.T) {
rt := &testRoundTripper{}
_, err := NewImpersonatingRoundTripper(rt).RoundTrip(req)
require.NoError(t, err)
if ts.expected == "" {
if ts.expectedUser == "" {
_, ok := rt.Request.Header[transport.ImpersonateUserHeader]
require.False(t, ok)
return
}
require.Equal(t, ts.expected, rt.Request.Header.Get(transport.ImpersonateUserHeader))
require.Equal(t, ts.expectedUser, rt.Request.Header.Get(transport.ImpersonateUserHeader))
require.Equal(t, ts.expectedGroup, rt.Request.Header.Values(transport.ImpersonateGroupHeader))
})
}
}

92
pkg/auth/userinfo.go Normal file
View File

@@ -0,0 +1,92 @@
/*
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 auth
import (
"context"
"fmt"
"regexp"
"strings"
authv1 "k8s.io/api/authentication/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/endpoints/request"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/utils/strings/slices"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/features"
"github.com/oam-dev/kubevela/pkg/oam"
)
const (
groupSeparator = ","
)
// ContextWithUserInfo inject username & group from app annotations into context
// If serviceAccount is set and username is empty, identity will user the serviceAccount
func ContextWithUserInfo(ctx context.Context, app *v1beta1.Application) context.Context {
if app == nil {
return ctx
}
return request.WithUser(ctx, GetUserInfoInAnnotation(&app.ObjectMeta))
}
// SetUserInfoInAnnotation set username and group from userInfo into annotations
// it will clear the existing service account annotation in avoid of permission leak
func SetUserInfoInAnnotation(obj *metav1.ObjectMeta, userInfo authv1.UserInfo) {
if AuthenticationWithUser {
metav1.SetMetaDataAnnotation(obj, oam.AnnotationApplicationUsername, userInfo.Username)
}
re := regexp.MustCompile(strings.ReplaceAll(AuthenticationGroupPattern, "*", ".*"))
var groups []string
for _, group := range userInfo.Groups {
if re.MatchString(group) {
groups = append(groups, group)
}
}
metav1.SetMetaDataAnnotation(obj, oam.AnnotationApplicationGroup, strings.Join(groups, groupSeparator))
}
// GetUserInfoInAnnotation extract user info from annotations
// support compatibility for serviceAccount when name is empty
func GetUserInfoInAnnotation(obj *metav1.ObjectMeta) user.Info {
annotations := obj.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
name := annotations[oam.AnnotationApplicationUsername]
if serviceAccountName := annotations[oam.AnnotationApplicationServiceAccountName]; serviceAccountName != "" && name == "" {
name = fmt.Sprintf("system:serviceaccount:%s:%s", obj.GetNamespace(), serviceAccountName)
}
if name == "" && utilfeature.DefaultMutableFeatureGate.Enabled(features.AuthenticateApplication) {
name = AuthenticationDefaultUser
}
return &user.DefaultInfo{
Name: name,
Groups: slices.Filter(
[]string{},
strings.Split(annotations[oam.AnnotationApplicationGroup], groupSeparator),
func(s string) bool {
return len(strings.TrimSpace(s)) > 0
}),
}
}

84
pkg/auth/userinfo_test.go Normal file
View File

@@ -0,0 +1,84 @@
/*
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 auth
import (
"testing"
"github.com/stretchr/testify/require"
authv1 "k8s.io/api/authentication/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apiserver/pkg/authentication/user"
utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/features"
"github.com/oam-dev/kubevela/pkg/oam"
)
func TestContextWithUserInfo(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.AuthenticateApplication, true)()
AuthenticationWithUser = true
defer func() {
AuthenticationWithUser = false
}()
testCases := map[string]struct {
UserInfo *authv1.UserInfo
ServiceAccount string
ExpectUserInfo user.Info
}{
"empty": {
ExpectUserInfo: &user.DefaultInfo{
Name: user.Anonymous,
Groups: []string{},
},
},
"service-account": {
ServiceAccount: "sa",
ExpectUserInfo: &user.DefaultInfo{
Name: "system:serviceaccount:default:sa",
Groups: []string{},
},
},
"user-with-groups": {
UserInfo: &authv1.UserInfo{
Username: "user",
Groups: []string{"group0", "kubevela:group1", "kubevela:group2"},
},
ServiceAccount: "override",
ExpectUserInfo: &user.DefaultInfo{
Name: "user",
Groups: []string{"kubevela:group1", "kubevela:group2"},
},
},
}
for name, tt := range testCases {
t.Run(name, func(t *testing.T) {
r := require.New(t)
app := &v1beta1.Application{}
app.SetNamespace("default")
if tt.UserInfo != nil {
SetUserInfoInAnnotation(&app.ObjectMeta, *tt.UserInfo)
}
if tt.ServiceAccount != "" {
metav1.SetMetaDataAnnotation(&app.ObjectMeta, oam.AnnotationApplicationServiceAccountName, tt.ServiceAccount)
}
r.Equal(tt.ExpectUserInfo, GetUserInfoInAnnotation(&app.ObjectMeta))
})
}
}

View File

@@ -19,6 +19,8 @@ package controller
import (
flag "github.com/spf13/pflag"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/auth"
ctrlClient "github.com/oam-dev/kubevela/pkg/client"
"github.com/oam-dev/kubevela/pkg/component"
"github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1alpha2/application"
@@ -52,4 +54,9 @@ func AddAdmissionFlags() {
flag.BoolVar(&resourcekeeper.AllowCrossNamespaceResource, "allow-cross-namespace-resource", true, "If set to false, application can only apply resources within its namespace. Default to be true.")
flag.StringVar(&resourcekeeper.AllowResourceTypes, "allow-resource-types", "", "If not empty, application can only apply resources with specified types. For example, --allow-resource-types=whitelist:Deployment.v1.apps,Job.v1.batch")
flag.StringVar(&component.RefObjectsAvailableScope, "ref-objects-available-scope", component.RefObjectsAvailableScopeGlobal, "The available scope for ref-objects component to refer objects. Should be one of `namespace`, `cluster`, `global`")
// auth flags
flag.BoolVar(&auth.AuthenticationWithUser, "authentication-with-user", false, "If set to true, User will be carried on application. Resource requests will be impersonated as the User.")
flag.StringVar(&auth.AuthenticationDefaultUser, "authentication-default-user", types.KubeVelaName+":"+types.VelaCoreName, "The User to impersonate when the User of application is not set.")
flag.StringVar(&auth.AuthenticationGroupPattern, "authentication-group-pattern", auth.DefaultAuthenticateGroupPattern, "During authentication, only groups with specified pattern will be carried on application. Resource requests will be impersonated as these selected groups.")
}

View File

@@ -23,6 +23,8 @@ import (
)
const (
// Compatibility Features
// DeprecatedPolicySpec enable the use of deprecated policy spec
DeprecatedPolicySpec featuregate.Feature = "DeprecatedPolicySpec"
// LegacyObjectTypeIdentifier enable the use of legacy object type identifier for selecting ref-object
@@ -31,6 +33,11 @@ const (
DeprecatedObjectLabelSelector featuregate.Feature = "DeprecatedObjectLabelSelector"
// LegacyResourceTrackerGC enable the gc of legacy resource tracker in managed clusters
LegacyResourceTrackerGC featuregate.Feature = "LegacyResourceTrackerGC"
// Edge Features
// AuthenticateApplication enable the authentication for application
AuthenticateApplication featuregate.Feature = "AuthenticateApplication"
)
var defaultFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{
@@ -38,6 +45,7 @@ var defaultFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{
LegacyObjectTypeIdentifier: {Default: false, PreRelease: featuregate.Alpha},
DeprecatedObjectLabelSelector: {Default: false, PreRelease: featuregate.Alpha},
LegacyResourceTrackerGC: {Default: true, PreRelease: featuregate.Alpha},
AuthenticateApplication: {Default: false, PreRelease: featuregate.Alpha},
}
func init() {

View File

@@ -36,14 +36,6 @@ func GetCluster(o client.Object) string {
return ""
}
// GetServiceAccountNameFromAnnotations extracts the service account name from the given object's annotations.
func GetServiceAccountNameFromAnnotations(o client.Object) string {
if annotations := o.GetAnnotations(); annotations != nil {
return annotations[AnnotationServiceAccountName]
}
return ""
}
// GetPublishVersion get PublishVersion from object
func GetPublishVersion(o client.Object) string {
if annotations := o.GetAnnotations(); annotations != nil {

View File

@@ -199,7 +199,13 @@ const (
// AnnotationControllerRequirement indicates the controller version that can process the application.
AnnotationControllerRequirement = "app.oam.dev/controller-version-require"
// AnnotationServiceAccountName indicates the name of the ServiceAccount to use to apply Components and run Workflow.
// AnnotationApplicationServiceAccountName indicates the name of the ServiceAccount to use to apply Components and run Workflow.
// ServiceAccount will be used in the local cluster only.
AnnotationServiceAccountName = "app.oam.dev/service-account-name"
AnnotationApplicationServiceAccountName = "app.oam.dev/service-account-name"
// AnnotationApplicationUsername indicates the username of the Application to use to apply resources
AnnotationApplicationUsername = "app.oam.dev/username"
// AnnotationApplicationGroup indicates the group of the Application to use to apply resources
AnnotationApplicationGroup = "app.oam.dev/group"
)

View File

@@ -303,26 +303,6 @@ func SetNamespaceInCtx(ctx context.Context, namespace string) context.Context {
return ctx
}
// GetServiceAccountInContext returns the name of the service account which reconciles the app from the context.
func GetServiceAccountInContext(ctx context.Context) string {
if serviceAccount, ok := ctx.Value(ServiceAccountContextKey).(string); ok {
return serviceAccount
}
return ""
}
// SetServiceAccountInContext sets the name of the service account which reconciles the app.
func SetServiceAccountInContext(ctx context.Context, namespace, name string) context.Context {
if name == "" {
// We may set `default` service account when the service account name is omitted.
// However, setting `default` service account will break existing cluster-scoped applications,
// so it would be better to give users a migration term.
// TODO(devholic): Use `default` service account if omitted.
return ctx
}
return context.WithValue(ctx, ServiceAccountContextKey, fmt.Sprintf("system:serviceaccount:%s:%s", namespace, name))
}
// GetDefinition get definition from two level namespace
func GetDefinition(ctx context.Context, cli client.Reader, definition client.Object, definitionName string) error {
appNs := GetDefinitionNamespaceWithCtx(ctx)

View File

@@ -24,9 +24,9 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/auth"
"github.com/oam-dev/kubevela/pkg/multicluster"
"github.com/oam-dev/kubevela/pkg/oam"
oamutil "github.com/oam-dev/kubevela/pkg/oam/util"
"github.com/oam-dev/kubevela/pkg/resourcetracker"
)
@@ -87,7 +87,7 @@ func (h *resourceKeeper) delete(ctx context.Context, manifest *unstructured.Unst
}
// 2. delete manifests
deleteCtx := multicluster.ContextWithClusterName(ctx, oam.GetCluster(manifest))
deleteCtx = oamutil.SetServiceAccountInContext(deleteCtx, h.app.Namespace, oam.GetServiceAccountNameFromAnnotations(h.app))
deleteCtx = auth.ContextWithUserInfo(deleteCtx, h.app)
if err = h.Client.Delete(deleteCtx, manifest); err != nil && !kerrors.IsNotFound(err) {
return errors.Wrapf(err, "cannot delete manifest, name: %s apiVersion: %s kind: %s", manifest.GetName(), manifest.GetAPIVersion(), manifest.GetKind())
}

View File

@@ -23,9 +23,9 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/pkg/auth"
"github.com/oam-dev/kubevela/pkg/multicluster"
"github.com/oam-dev/kubevela/pkg/oam"
oamutil "github.com/oam-dev/kubevela/pkg/oam/util"
"github.com/oam-dev/kubevela/pkg/resourcetracker"
"github.com/oam-dev/kubevela/pkg/utils/apply"
velaerrors "github.com/oam-dev/kubevela/pkg/utils/errors"
@@ -127,7 +127,7 @@ func (h *resourceKeeper) record(ctx context.Context, manifests []*unstructured.U
func (h *resourceKeeper) dispatch(ctx context.Context, manifests []*unstructured.Unstructured, applyOpts []apply.ApplyOption) error {
errs := parallel.Run(func(manifest *unstructured.Unstructured) error {
applyCtx := multicluster.ContextWithClusterName(ctx, oam.GetCluster(manifest))
applyCtx = oamutil.SetServiceAccountInContext(applyCtx, h.app.Namespace, oam.GetServiceAccountNameFromAnnotations(h.app))
applyCtx = auth.ContextWithUserInfo(applyCtx, h.app)
return h.applicator.Apply(applyCtx, manifest, applyOpts...)
}, manifests, MaxDispatchConcurrent)
return velaerrors.AggregateErrors(errs.([]error))

View File

@@ -22,9 +22,8 @@ import (
"github.com/pkg/errors"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/auth"
"github.com/oam-dev/kubevela/pkg/multicluster"
"github.com/oam-dev/kubevela/pkg/oam"
oamutil "github.com/oam-dev/kubevela/pkg/oam/util"
"github.com/oam-dev/kubevela/pkg/utils/apply"
)
@@ -43,7 +42,7 @@ func (h *resourceKeeper) StateKeep(ctx context.Context) error {
if mr.Deleted {
if entry.exists && entry.obj != nil && entry.obj.GetDeletionTimestamp() == nil {
deleteCtx := multicluster.ContextWithClusterName(ctx, mr.Cluster)
deleteCtx = oamutil.SetServiceAccountInContext(deleteCtx, h.app.Namespace, oam.GetServiceAccountNameFromAnnotations(h.app))
deleteCtx = auth.ContextWithUserInfo(deleteCtx, h.app)
if err := h.Client.Delete(deleteCtx, entry.obj); err != nil {
return errors.Wrapf(err, "failed to delete outdated resource %s in resourcetracker %s", mr.ResourceKey(), rt.Name)
}
@@ -58,7 +57,7 @@ func (h *resourceKeeper) StateKeep(ctx context.Context) error {
return errors.Wrapf(err, "failed to decode resource %s from resourcetracker", mr.ResourceKey())
}
applyCtx := multicluster.ContextWithClusterName(ctx, mr.Cluster)
applyCtx = oamutil.SetServiceAccountInContext(applyCtx, h.app.Namespace, oam.GetServiceAccountNameFromAnnotations(h.app))
applyCtx = auth.ContextWithUserInfo(applyCtx, h.app)
if err = h.applicator.Apply(applyCtx, manifest, apply.MustBeControlledByApp(h.app)); err != nil {
return errors.Wrapf(err, "failed to re-apply resource %s from resourcetracker %s", mr.ResourceKey(), rt.Name)
}

29
pkg/utils/jwt.go Normal file
View File

@@ -0,0 +1,29 @@
/*
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 utils
import "github.com/form3tech-oss/jwt-go"
// GetTokenSubject extract the subject field from the jwt token
func GetTokenSubject(token string) (string, error) {
claims := jwt.MapClaims{}
if _, err := jwt.ParseWithClaims(token, claims, nil); err != nil {
return "", err
}
sub, _ := claims["sub"].(string)
return sub, nil
}

View File

@@ -24,6 +24,7 @@ import (
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8stypes "k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/pkg/oam/util"
@@ -106,3 +107,9 @@ func UpdateNamespace(ctx context.Context, kubeClient client.Client, name string,
}
return kubeClient.Update(ctx, &namespace)
}
// GetServiceAccountSubjectFromConfig extract ServiceAccount subject from token
func GetServiceAccountSubjectFromConfig(cfg *rest.Config) string {
sub, _ := GetTokenSubject(cfg.BearerToken)
return sub
}

View File

@@ -47,6 +47,7 @@ func Register(mgr manager.Manager, args controller.Args) {
traitdefinition.RegisterValidatingHandler(mgr, args)
case "v0.3":
application.RegisterValidatingHandler(mgr, args)
application.RegisterMutatingHandler(mgr)
componentdefinition.RegisterMutatingHandler(mgr, args)
componentdefinition.RegisterValidatingHandler(mgr, args)
traitdefinition.RegisterValidatingHandler(mgr, args)

View File

@@ -0,0 +1,85 @@
/*
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 application
import (
"context"
"encoding/json"
"net/http"
"github.com/pkg/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/utils/strings/slices"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/auth"
"github.com/oam-dev/kubevela/pkg/features"
"github.com/oam-dev/kubevela/pkg/oam"
)
// MutatingHandler adding user info to application annotations
type MutatingHandler struct {
Decoder *admission.Decoder
}
var _ admission.Handler = &MutatingHandler{}
// Handle mutate application
func (h *MutatingHandler) Handle(ctx context.Context, req admission.Request) admission.Response {
if !utilfeature.DefaultMutableFeatureGate.Enabled(features.AuthenticateApplication) {
return admission.Patched("")
}
if slices.Contains(req.UserInfo.Groups, common.Group) {
return admission.Patched("")
}
app := &v1beta1.Application{}
if err := h.Decoder.Decode(req, app); err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
if metav1.HasAnnotation(app.ObjectMeta, oam.AnnotationApplicationServiceAccountName) {
return admission.Errored(http.StatusBadRequest, errors.New("service-account annotation is not permitted when authentication enabled"))
}
auth.SetUserInfoInAnnotation(&app.ObjectMeta, req.UserInfo)
bs, err := json.Marshal(app)
if err != nil {
return admission.Errored(http.StatusInternalServerError, err)
}
return admission.PatchResponseFromRaw(req.AdmissionRequest.Object.Raw, bs)
}
var _ admission.DecoderInjector = &MutatingHandler{}
// InjectDecoder .
func (h *MutatingHandler) InjectDecoder(d *admission.Decoder) error {
h.Decoder = d
return nil
}
// RegisterMutatingHandler will register component mutation handler to the webhook
func RegisterMutatingHandler(mgr manager.Manager) {
server := mgr.GetWebhookServer()
server.Register("/mutating-core-oam-dev-v1beta1-applications", &webhook.Admission{Handler: &MutatingHandler{}})
}

View File

@@ -0,0 +1,114 @@
/*
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 application
import (
"fmt"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"gomodules.xyz/jsonpatch/v2"
admissionv1 "k8s.io/api/admission/v1"
authv1 "k8s.io/api/authentication/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/features"
"github.com/oam-dev/kubevela/pkg/oam"
)
var _ = Describe("Test Application Mutator", func() {
var mutatingHandler *MutatingHandler
BeforeEach(func() {
mutatingHandler = &MutatingHandler{}
Expect(mutatingHandler.InjectDecoder(decoder)).Should(BeNil())
})
It("Test Application Mutator [no authentication]", func() {
Expect(utilfeature.DefaultMutableFeatureGate.Set(fmt.Sprintf("%s=false", features.AuthenticateApplication))).Should(Succeed())
resp := mutatingHandler.Handle(ctx, admission.Request{})
Expect(resp.Allowed).Should(BeTrue())
Expect(resp.Patches).Should(BeNil())
})
It("Test Application Mutator [ignore authentication]", func() {
Expect(utilfeature.DefaultMutableFeatureGate.Set(fmt.Sprintf("%s=true", features.AuthenticateApplication))).Should(Succeed())
resp := mutatingHandler.Handle(ctx, admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
UserInfo: authv1.UserInfo{Groups: []string{common.Group}},
}})
Expect(resp.Allowed).Should(BeTrue())
Expect(resp.Patches).Should(BeNil())
})
It("Test Application Mutator [bad request]", func() {
Expect(utilfeature.DefaultMutableFeatureGate.Set(fmt.Sprintf("%s=true", features.AuthenticateApplication))).Should(Succeed())
req := admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Operation: admissionv1.Create,
Resource: metav1.GroupVersionResource{Group: v1beta1.Group, Version: v1beta1.Version, Resource: "applications"},
Object: runtime.RawExtension{Raw: []byte("bad request")},
},
}
resp := mutatingHandler.Handle(ctx, req)
Expect(resp.Allowed).Should(BeFalse())
})
It("Test Application Mutator [bad request with service-account]", func() {
Expect(utilfeature.DefaultMutableFeatureGate.Set(fmt.Sprintf("%s=true", features.AuthenticateApplication))).Should(Succeed())
req := admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Operation: admissionv1.Create,
Resource: metav1.GroupVersionResource{Group: v1beta1.Group, Version: v1beta1.Version, Resource: "applications"},
Object: runtime.RawExtension{Raw: []byte(`{"apiVersion":"core.oam.dev/v1beta1","kind":"Application","metadata":{"name":"example","annotations":{"app.oam.dev/service-account-name":"default"}}}`)},
},
}
resp := mutatingHandler.Handle(ctx, req)
Expect(resp.Allowed).Should(BeFalse())
Expect(resp.Result.Message).Should(ContainSubstring("service-account annotation is not permitted when authentication enabled"))
})
It("Test Application Mutator [with patch]", func() {
Expect(utilfeature.DefaultMutableFeatureGate.Set(fmt.Sprintf("%s=true", features.AuthenticateApplication))).Should(Succeed())
req := admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Operation: admissionv1.Create,
Resource: metav1.GroupVersionResource{Group: v1beta1.Group, Version: v1beta1.Version, Resource: "applications"},
Object: runtime.RawExtension{Raw: []byte(`{"apiVersion":"core.oam.dev/v1beta1","kind":"Application","metadata":{"name":"example"}}`)},
UserInfo: authv1.UserInfo{
Username: "example-user",
Groups: []string{"kubevela:example-group1", "kubevela:example-group2"},
},
},
}
resp := mutatingHandler.Handle(ctx, req)
Expect(resp.Allowed).Should(BeTrue())
Expect(resp.Patches).Should(ContainElement(jsonpatch.JsonPatchOperation{
Operation: "add",
Path: "/metadata/annotations",
Value: map[string]interface{}{
oam.AnnotationApplicationGroup: "kubevela:example-group1,kubevela:example-group2",
},
}))
})
})

View File

@@ -25,11 +25,10 @@ import (
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/auth"
"github.com/oam-dev/kubevela/pkg/cue/model"
"github.com/oam-dev/kubevela/pkg/cue/model/value"
"github.com/oam-dev/kubevela/pkg/multicluster"
"github.com/oam-dev/kubevela/pkg/oam"
oamutil "github.com/oam-dev/kubevela/pkg/oam/util"
wfContext "github.com/oam-dev/kubevela/pkg/workflow/context"
"github.com/oam-dev/kubevela/pkg/workflow/providers"
"github.com/oam-dev/kubevela/pkg/workflow/types"
@@ -89,7 +88,7 @@ func (h *provider) Apply(ctx wfContext.Context, v *value.Value, act types.Action
return err
}
deployCtx := multicluster.ContextWithClusterName(context.Background(), cluster)
deployCtx = h.setServiceAccountInContext(deployCtx)
deployCtx = auth.ContextWithUserInfo(deployCtx, h.app)
if err := h.apply(deployCtx, cluster, common.WorkflowResourceCreator, workload); err != nil {
return err
}
@@ -124,7 +123,7 @@ func (h *provider) ApplyInParallel(ctx wfContext.Context, v *value.Value, act ty
return err
}
deployCtx := multicluster.ContextWithClusterName(context.Background(), cluster)
deployCtx = h.setServiceAccountInContext(deployCtx)
deployCtx = auth.ContextWithUserInfo(deployCtx, h.app)
if err = h.apply(deployCtx, cluster, common.WorkflowResourceCreator, workloads...); err != nil {
return v.FillObject(err, "err")
}
@@ -151,7 +150,7 @@ func (h *provider) Read(ctx wfContext.Context, v *value.Value, act types.Action)
return err
}
readCtx := multicluster.ContextWithClusterName(context.Background(), cluster)
readCtx = h.setServiceAccountInContext(readCtx)
readCtx = auth.ContextWithUserInfo(readCtx, h.app)
if err := h.cli.Get(readCtx, key, obj); err != nil {
return v.FillObject(err.Error(), "err")
}
@@ -194,7 +193,7 @@ func (h *provider) List(ctx wfContext.Context, v *value.Value, act types.Action)
client.MatchingLabels(filter.MatchingLabels),
}
readCtx := multicluster.ContextWithClusterName(context.Background(), cluster)
readCtx = h.setServiceAccountInContext(readCtx)
readCtx = auth.ContextWithUserInfo(readCtx, h.app)
if err := h.cli.List(readCtx, list, listOpts...); err != nil {
return v.FillObject(err.Error(), "err")
}
@@ -216,20 +215,13 @@ func (h *provider) Delete(ctx wfContext.Context, v *value.Value, act types.Actio
return err
}
deleteCtx := multicluster.ContextWithClusterName(context.Background(), cluster)
deleteCtx = h.setServiceAccountInContext(deleteCtx)
deleteCtx = auth.ContextWithUserInfo(deleteCtx, h.app)
if err := h.delete(deleteCtx, cluster, common.WorkflowResourceCreator, obj); err != nil {
return v.FillObject(err.Error(), "err")
}
return nil
}
func (h *provider) setServiceAccountInContext(ctx context.Context) context.Context {
if h.app == nil {
return ctx
}
return oamutil.SetServiceAccountInContext(ctx, h.app.Namespace, oam.GetServiceAccountNameFromAnnotations(h.app))
}
// Install register handlers to provider discover.
func Install(p providers.Providers, app *v1beta1.Application, cli client.Client, apply Dispatcher, deleter Deleter) {
if app != nil {

View File

@@ -395,7 +395,7 @@ var _ = Describe("Application Normal tests", func() {
Expect(common.ReadYamlToObject("testdata/app/app11.yaml", &newApp)).Should(BeNil())
newApp.Namespace = namespaceName
annotations := newApp.GetAnnotations()
annotations[oam.AnnotationServiceAccountName] = saName
annotations[oam.AnnotationApplicationServiceAccountName] = saName
newApp.SetAnnotations(annotations)
Expect(k8sClient.Create(ctx, &newApp)).Should(BeNil())
@@ -414,7 +414,7 @@ var _ = Describe("Application Normal tests", func() {
Expect(common.ReadYamlToObject("testdata/app/app11.yaml", &newApp)).Should(BeNil())
newApp.Namespace = namespaceName
annotations := newApp.GetAnnotations()
annotations[oam.AnnotationServiceAccountName] = saName
annotations[oam.AnnotationApplicationServiceAccountName] = saName
newApp.SetAnnotations(annotations)
Expect(k8sClient.Create(ctx, &newApp)).Should(BeNil())
@@ -442,7 +442,7 @@ var _ = Describe("Application Normal tests", func() {
Expect(common.ReadYamlToObject("testdata/app/app11.yaml", &newApp)).Should(BeNil())
newApp.Namespace = namespaceName
annotations := newApp.GetAnnotations()
annotations[oam.AnnotationServiceAccountName] = saName
annotations[oam.AnnotationApplicationServiceAccountName] = saName
newApp.SetAnnotations(annotations)
Expect(k8sClient.Create(ctx, &newApp)).Should(BeNil())