feat: kubesphere 4.0 (#6115)
* feat: kubesphere 4.0 Signed-off-by: ci-bot <ci-bot@kubesphere.io> * feat: kubesphere 4.0 Signed-off-by: ci-bot <ci-bot@kubesphere.io> --------- Signed-off-by: ci-bot <ci-bot@kubesphere.io> Co-authored-by: ks-ci-bot <ks-ci-bot@example.com> Co-authored-by: joyceliu <joyceliu@yunify.com>
This commit is contained in:
committed by
GitHub
parent
b5015ec7b9
commit
447a51f08b
@@ -1,34 +1,22 @@
|
||||
/*
|
||||
Copyright 2020 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.
|
||||
*/
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package basic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
iamv1alpha2 "kubesphere.io/api/iam/v1alpha2"
|
||||
iamv1beta1 "kubesphere.io/api/iam/v1beta1"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/request/basictoken"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/request"
|
||||
"kubesphere.io/kubesphere/pkg/models/auth"
|
||||
|
||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
)
|
||||
|
||||
// TokenAuthenticator implements kubernetes token authenticate interface with our custom logic.
|
||||
@@ -49,15 +37,15 @@ func NewBasicAuthenticator(authenticator auth.PasswordAuthenticator, loginRecord
|
||||
}
|
||||
|
||||
func (t *basicAuthenticator) AuthenticatePassword(ctx context.Context, username, password string) (*authenticator.Response, bool, error) {
|
||||
authenticated, provider, err := t.authenticator.Authenticate(ctx, "", username, password)
|
||||
authenticated, err := t.authenticator.Authenticate(ctx, "", username, password)
|
||||
if err != nil {
|
||||
if t.loginRecorder != nil && err == auth.IncorrectPasswordError {
|
||||
if t.loginRecorder != nil && errors.Is(err, auth.IncorrectPasswordError) {
|
||||
var sourceIP, userAgent string
|
||||
if requestInfo, ok := request.RequestInfoFrom(ctx); ok {
|
||||
sourceIP = requestInfo.SourceIP
|
||||
userAgent = requestInfo.UserAgent
|
||||
}
|
||||
if err := t.loginRecorder.RecordLogin(username, iamv1alpha2.Password, provider, sourceIP, userAgent, err); err != nil {
|
||||
if err := t.loginRecorder.RecordLogin(ctx, username, iamv1beta1.Password, "", sourceIP, userAgent, err); err != nil {
|
||||
klog.Errorf("Failed to record unsuccessful login attempt for user %s, error: %v", username, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,25 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/klog/v2"
|
||||
clusterv1alpha1 "kubesphere.io/api/cluster/v1alpha1"
|
||||
corev1alpha1 "kubesphere.io/api/core/v1alpha1"
|
||||
iamv1beta1 "kubesphere.io/api/iam/v1beta1"
|
||||
runtimecache "sigs.k8s.io/controller-runtime/pkg/cache"
|
||||
|
||||
iamv1alpha2 "kubesphere.io/api/iam/v1alpha2"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/token"
|
||||
"kubesphere.io/kubesphere/pkg/models/auth"
|
||||
|
||||
iamv1alpha2listers "kubesphere.io/kubesphere/pkg/client/listers/iam/v1alpha2"
|
||||
"kubesphere.io/kubesphere/pkg/utils/serviceaccount"
|
||||
)
|
||||
|
||||
// TokenAuthenticator implements kubernetes token authenticate interface with our custom logic.
|
||||
@@ -37,13 +29,15 @@ import (
|
||||
// because some resources are public accessible.
|
||||
type tokenAuthenticator struct {
|
||||
tokenOperator auth.TokenManagementInterface
|
||||
userLister iamv1alpha2listers.UserLister
|
||||
cache runtimecache.Cache
|
||||
clusterRole string
|
||||
}
|
||||
|
||||
func NewTokenAuthenticator(tokenOperator auth.TokenManagementInterface, userLister iamv1alpha2listers.UserLister) authenticator.Token {
|
||||
func NewTokenAuthenticator(cache runtimecache.Cache, tokenOperator auth.TokenManagementInterface, clusterRole string) authenticator.Token {
|
||||
return &tokenAuthenticator{
|
||||
tokenOperator: tokenOperator,
|
||||
userLister: userLister,
|
||||
cache: cache,
|
||||
clusterRole: clusterRole,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,25 +48,51 @@ func (t *tokenAuthenticator) AuthenticateToken(ctx context.Context, token string
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if verified.User.GetName() == iamv1alpha2.PreRegistrationUser {
|
||||
if serviceaccount.IsServiceAccountToken(verified.Subject) {
|
||||
if t.clusterRole == string(clusterv1alpha1.ClusterRoleHost) {
|
||||
_, err = t.validateServiceAccount(ctx, verified)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
return &authenticator.Response{
|
||||
User: verified.User,
|
||||
}, true, nil
|
||||
}
|
||||
|
||||
userInfo, err := t.userLister.Get(verified.User.GetName())
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
if verified.User.GetName() == iamv1beta1.PreRegistrationUser {
|
||||
return &authenticator.Response{
|
||||
User: verified.User,
|
||||
}, true, nil
|
||||
}
|
||||
|
||||
// AuthLimitExceeded state should be ignored
|
||||
if userInfo.Status.State == iamv1alpha2.UserDisabled {
|
||||
return nil, false, auth.AccountIsNotActiveError
|
||||
if t.clusterRole == string(clusterv1alpha1.ClusterRoleHost) {
|
||||
userInfo := &iamv1beta1.User{}
|
||||
if err := t.cache.Get(ctx, types.NamespacedName{Name: verified.User.GetName()}, userInfo); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
// AuthLimitExceeded state should be ignored
|
||||
if userInfo.Status.State == iamv1beta1.UserDisabled {
|
||||
return nil, false, auth.AccountIsNotActiveError
|
||||
}
|
||||
}
|
||||
|
||||
return &authenticator.Response{
|
||||
User: &user.DefaultInfo{
|
||||
Name: userInfo.GetName(),
|
||||
Groups: append(userInfo.Spec.Groups, user.AllAuthenticated),
|
||||
Name: verified.User.GetName(),
|
||||
// TODO(wenhaozhou) Add user`s groups(can be searched by GroupBinding)
|
||||
Groups: []string{user.AllAuthenticated},
|
||||
},
|
||||
}, true, nil
|
||||
}
|
||||
|
||||
func (t *tokenAuthenticator) validateServiceAccount(ctx context.Context, verify *token.VerifiedResponse) (*corev1alpha1.ServiceAccount, error) {
|
||||
// Ensure the relative service account exist
|
||||
name, namespace := serviceaccount.SplitUsername(verify.Username)
|
||||
sa := &corev1alpha1.ServiceAccount{}
|
||||
if err := t.cache.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, sa); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sa, nil
|
||||
}
|
||||
|
||||
@@ -1,18 +1,7 @@
|
||||
/*
|
||||
Copyright 2020 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.
|
||||
*/
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package aliyunidaas
|
||||
|
||||
@@ -23,7 +12,6 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider"
|
||||
@@ -31,7 +19,7 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
identityprovider.RegisterOAuthProvider(&idaasProviderFactory{})
|
||||
identityprovider.RegisterOAuthProviderFactory(&idaasProviderFactory{})
|
||||
}
|
||||
|
||||
type aliyunIDaaS struct {
|
||||
|
||||
@@ -1,18 +1,7 @@
|
||||
/*
|
||||
Copyright 2020 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.
|
||||
*/
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package aliyunidaas
|
||||
|
||||
|
||||
@@ -1,18 +1,7 @@
|
||||
/*
|
||||
Copyright 2020 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.
|
||||
*/
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package cas
|
||||
|
||||
@@ -30,7 +19,7 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
identityprovider.RegisterOAuthProvider(&casProviderFactory{})
|
||||
identityprovider.RegisterOAuthProviderFactory(&casProviderFactory{})
|
||||
}
|
||||
|
||||
type cas struct {
|
||||
|
||||
120
pkg/apiserver/authentication/identityprovider/configuration.go
Normal file
120
pkg/apiserver/authentication/identityprovider/configuration.go
Normal file
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package identityprovider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/klog/v2"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/constants"
|
||||
"kubesphere.io/kubesphere/pkg/server/options"
|
||||
)
|
||||
|
||||
const (
|
||||
MappingMethodManual MappingMethod = "manual"
|
||||
|
||||
MappingMethodAuto MappingMethod = "auto"
|
||||
|
||||
// MappingMethodLookup Looks up an existing identity, user identity mapping, and user, but does not automatically
|
||||
// provision users or identities. Using this method requires you to manually provision users.
|
||||
MappingMethodLookup MappingMethod = "lookup"
|
||||
|
||||
ConfigTypeIdentityProvider = "identityprovider"
|
||||
SecretTypeIdentityProvider = "config.kubesphere.io/" + ConfigTypeIdentityProvider
|
||||
|
||||
SecretDataKey = "configuration.yaml"
|
||||
)
|
||||
|
||||
var ErrorIdentityProviderNotFound = errors.New("the Identity provider was not found")
|
||||
|
||||
type MappingMethod string
|
||||
|
||||
type Configuration struct {
|
||||
// The provider name.
|
||||
Name string `json:"name" yaml:"name"`
|
||||
|
||||
// Defines how new identities are mapped to users when they login. Allowed values are:
|
||||
// - manual: The user needs to confirm the mapped username on the onboarding page.
|
||||
// - auto: Skip the onboarding screen, so the user cannot change its username.
|
||||
// Fails if a user with that username is already mapped to another identity.
|
||||
// - lookup: Looks up an existing identity, user identity mapping, and user, but does not automatically
|
||||
// provision users or identities. Using this method requires you to manually provision users.
|
||||
MappingMethod MappingMethod `json:"mappingMethod" yaml:"mappingMethod"`
|
||||
|
||||
// The type of identity provider
|
||||
Type string `json:"type" yaml:"type"`
|
||||
|
||||
// The options of identify provider
|
||||
ProviderOptions options.DynamicOptions `json:"provider" yaml:"provider"`
|
||||
}
|
||||
|
||||
type ConfigurationGetter interface {
|
||||
GetConfiguration(ctx context.Context, name string) (*Configuration, error)
|
||||
ListConfigurations(ctx context.Context) ([]*Configuration, error)
|
||||
}
|
||||
|
||||
func NewConfigurationGetter(client client.Client) ConfigurationGetter {
|
||||
return &configurationGetter{client}
|
||||
}
|
||||
|
||||
type configurationGetter struct {
|
||||
client.Client
|
||||
}
|
||||
|
||||
func (o *configurationGetter) ListConfigurations(ctx context.Context) ([]*Configuration, error) {
|
||||
configurations := make([]*Configuration, 0)
|
||||
secrets := &v1.SecretList{}
|
||||
if err := o.List(ctx, secrets, client.InNamespace(constants.KubeSphereNamespace), client.MatchingLabels{constants.GenericConfigTypeLabel: ConfigTypeIdentityProvider}); err != nil {
|
||||
klog.Errorf("failed to list secrets: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
for _, secret := range secrets.Items {
|
||||
if secret.Type != SecretTypeIdentityProvider {
|
||||
continue
|
||||
}
|
||||
if c, err := UnmarshalFrom(&secret); err != nil {
|
||||
klog.Errorf("failed to unmarshal secret data: %s", err)
|
||||
continue
|
||||
} else {
|
||||
configurations = append(configurations, c)
|
||||
}
|
||||
}
|
||||
return configurations, nil
|
||||
}
|
||||
|
||||
func (o *configurationGetter) GetConfiguration(ctx context.Context, name string) (*Configuration, error) {
|
||||
configurations, err := o.ListConfigurations(ctx)
|
||||
if err != nil {
|
||||
klog.Errorf("failed to list identity providers: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
for _, c := range configurations {
|
||||
if c.Name == name {
|
||||
return c, nil
|
||||
}
|
||||
}
|
||||
return nil, ErrorIdentityProviderNotFound
|
||||
}
|
||||
|
||||
func UnmarshalFrom(secret *v1.Secret) (*Configuration, error) {
|
||||
c := &Configuration{}
|
||||
if err := yaml.Unmarshal(secret.Data[SecretDataKey], c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func IsIdentityProviderConfiguration(secret *v1.Secret) bool {
|
||||
if secret.Namespace != constants.KubeSphereNamespace {
|
||||
return false
|
||||
}
|
||||
return secret.Type == SecretTypeIdentityProvider
|
||||
}
|
||||
@@ -1,20 +1,7 @@
|
||||
/*
|
||||
|
||||
Copyright 2020 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.
|
||||
|
||||
*/
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package identityprovider
|
||||
|
||||
@@ -30,6 +17,6 @@ type GenericProvider interface {
|
||||
type GenericProviderFactory interface {
|
||||
// Type unique type of the provider
|
||||
Type() string
|
||||
// Apply the dynamic options from kubesphere-config
|
||||
// Create generic identity provider
|
||||
Create(options options.DynamicOptions) (GenericProvider, error)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,7 @@
|
||||
/*
|
||||
Copyright 2020 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.
|
||||
*/
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package github
|
||||
|
||||
@@ -38,7 +27,7 @@ const (
|
||||
)
|
||||
|
||||
func init() {
|
||||
identityprovider.RegisterOAuthProvider(&ldapProviderFactory{})
|
||||
identityprovider.RegisterOAuthProviderFactory(&ldapProviderFactory{})
|
||||
}
|
||||
|
||||
type github struct {
|
||||
|
||||
@@ -1,20 +1,7 @@
|
||||
/*
|
||||
|
||||
Copyright 2020 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.
|
||||
|
||||
*/
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package github
|
||||
|
||||
@@ -29,7 +16,7 @@ import (
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/server/options"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gexec"
|
||||
"golang.org/x/oauth2"
|
||||
@@ -45,7 +32,7 @@ func TestGithub(t *testing.T) {
|
||||
RunSpecs(t, "GitHub Identity Provider Suite")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func(done Done) {
|
||||
var _ = BeforeSuite(func() {
|
||||
githubServer = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var data map[string]interface{}
|
||||
switch r.RequestURI {
|
||||
@@ -69,8 +56,7 @@ var _ = BeforeSuite(func(done Done) {
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(data)
|
||||
}))
|
||||
close(done)
|
||||
}, 60)
|
||||
})
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
By("tearing down the test environment")
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 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 gitlab
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider"
|
||||
"kubesphere.io/kubesphere/pkg/server/options"
|
||||
)
|
||||
|
||||
const (
|
||||
userInfoURL = "https://gitlab.com/api/v4/user"
|
||||
authURL = "https://gitlab.com/oauth/authorize"
|
||||
tokenURL = "https://gitlab.com/oauth/token"
|
||||
)
|
||||
|
||||
func init() {
|
||||
identityprovider.RegisterOAuthProvider(&gitlabProviderFactory{})
|
||||
}
|
||||
|
||||
type gitlab struct {
|
||||
// ClientID is the application's ID.
|
||||
ClientID string `json:"clientID" yaml:"clientID"`
|
||||
|
||||
// ClientSecret is the application's secret.
|
||||
ClientSecret string `json:"-" yaml:"clientSecret"`
|
||||
|
||||
// Endpoint contains the resource server's token endpoint
|
||||
// URLs. These are constants specific to each server and are
|
||||
// often available via site-specific packages, such as
|
||||
// google.Endpoint or sso.endpoint.
|
||||
Endpoint endpoint `json:"endpoint" yaml:"endpoint"`
|
||||
|
||||
// RedirectURL is the URL to redirect users going through
|
||||
// the OAuth flow, after the resource owner's URLs.
|
||||
RedirectURL string `json:"redirectURL" yaml:"redirectURL"`
|
||||
|
||||
// Used to turn off TLS certificate checks
|
||||
InsecureSkipVerify bool `json:"insecureSkipVerify" yaml:"insecureSkipVerify"`
|
||||
|
||||
// Scope specifies optional requested permissions.
|
||||
Scopes []string `json:"scopes" yaml:"scopes"`
|
||||
|
||||
Config *oauth2.Config `json:"-" yaml:"-"`
|
||||
}
|
||||
|
||||
// endpoint represents an OAuth 2.0 provider's authorization and token
|
||||
// endpoint URLs.
|
||||
type endpoint struct {
|
||||
AuthURL string `json:"authURL" yaml:"authURL"`
|
||||
TokenURL string `json:"tokenURL" yaml:"tokenURL"`
|
||||
UserInfoURL string `json:"userInfoURL" yaml:"userInfoURL"`
|
||||
}
|
||||
|
||||
type gitlabIdentity struct {
|
||||
ID int64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
State string `json:"state"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
WebURL string `json:"web_url"`
|
||||
}
|
||||
|
||||
type gitlabProviderFactory struct {
|
||||
}
|
||||
|
||||
func (g *gitlabProviderFactory) Type() string {
|
||||
return "GitlabIdentityProvider"
|
||||
}
|
||||
|
||||
func (g *gitlabProviderFactory) Create(opts options.DynamicOptions) (identityprovider.OAuthProvider, error) {
|
||||
var gitlab gitlab
|
||||
if err := mapstructure.Decode(opts, &gitlab); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if gitlab.Endpoint.AuthURL == "" {
|
||||
gitlab.Endpoint.AuthURL = authURL
|
||||
}
|
||||
if gitlab.Endpoint.TokenURL == "" {
|
||||
gitlab.Endpoint.TokenURL = tokenURL
|
||||
}
|
||||
if gitlab.Endpoint.UserInfoURL == "" {
|
||||
gitlab.Endpoint.UserInfoURL = userInfoURL
|
||||
}
|
||||
// fixed options
|
||||
opts["endpoint"] = options.DynamicOptions{
|
||||
"authURL": gitlab.Endpoint.AuthURL,
|
||||
"tokenURL": gitlab.Endpoint.TokenURL,
|
||||
"userInfoURL": gitlab.Endpoint.UserInfoURL,
|
||||
}
|
||||
gitlab.Config = &oauth2.Config{
|
||||
ClientID: gitlab.ClientID,
|
||||
ClientSecret: gitlab.ClientSecret,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: gitlab.Endpoint.AuthURL,
|
||||
TokenURL: gitlab.Endpoint.TokenURL,
|
||||
},
|
||||
RedirectURL: gitlab.RedirectURL,
|
||||
Scopes: gitlab.Scopes,
|
||||
}
|
||||
return &gitlab, nil
|
||||
}
|
||||
|
||||
func (g gitlabIdentity) GetUserID() string {
|
||||
return strconv.FormatInt(g.ID, 10)
|
||||
}
|
||||
|
||||
func (g gitlabIdentity) GetUsername() string {
|
||||
return g.Username
|
||||
}
|
||||
|
||||
func (g gitlabIdentity) GetEmail() string {
|
||||
return g.Email
|
||||
}
|
||||
|
||||
func (g *gitlab) IdentityExchangeCallback(req *http.Request) (identityprovider.Identity, error) {
|
||||
// OAuth2 callback, see also https://tools.ietf.org/html/rfc6749#section-4.1.2
|
||||
code := req.URL.Query().Get("code")
|
||||
ctx := req.Context()
|
||||
if g.InsecureSkipVerify {
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, client)
|
||||
}
|
||||
token, err := g.Config.Exchange(ctx, code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := oauth2.NewClient(ctx, oauth2.StaticTokenSource(token)).Get(g.Endpoint.UserInfoURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var gitlabIdentity gitlabIdentity
|
||||
err = json.Unmarshal(data, &gitlabIdentity)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return gitlabIdentity, nil
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
package gitlab
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider"
|
||||
"kubesphere.io/kubesphere/pkg/server/options"
|
||||
)
|
||||
|
||||
func Test_gitlabProviderFactory_Create(t *testing.T) {
|
||||
type args struct {
|
||||
opts options.DynamicOptions
|
||||
}
|
||||
|
||||
mustUnmarshalYAML := func(data string) options.DynamicOptions {
|
||||
var dynamicOptions options.DynamicOptions
|
||||
_ = yaml.Unmarshal([]byte(data), &dynamicOptions)
|
||||
return dynamicOptions
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want identityprovider.OAuthProvider
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "should create successfully",
|
||||
args: args{opts: mustUnmarshalYAML(`
|
||||
clientID: 035c18fc229c686e4652d7034
|
||||
clientSecret: 75c82b42e54aaf25186140f5
|
||||
endpoint:
|
||||
userInfoUrl: "https://gitlab.com/api/v4/user"
|
||||
authURL: "https://gitlab.com/oauth/authorize"
|
||||
tokenURL: "https://gitlab.com/oauth/token"
|
||||
redirectURL: "https://ks-console.kubesphere-system.svc/oauth/redirect/gitlab"
|
||||
scopes:
|
||||
- read
|
||||
`)},
|
||||
want: &gitlab{
|
||||
ClientID: "035c18fc229c686e4652d7034",
|
||||
ClientSecret: "75c82b42e54aaf25186140f5",
|
||||
Endpoint: endpoint{
|
||||
AuthURL: "https://gitlab.com/oauth/authorize",
|
||||
TokenURL: "https://gitlab.com/oauth/token",
|
||||
UserInfoURL: "https://gitlab.com/api/v4/user",
|
||||
},
|
||||
RedirectURL: "https://ks-console.kubesphere-system.svc/oauth/redirect/gitlab",
|
||||
Scopes: []string{"read"},
|
||||
Config: &oauth2.Config{
|
||||
ClientID: "035c18fc229c686e4652d7034",
|
||||
ClientSecret: "75c82b42e54aaf25186140f5",
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: "https://gitlab.com/oauth/authorize",
|
||||
TokenURL: "https://gitlab.com/oauth/token",
|
||||
AuthStyle: oauth2.AuthStyleAutoDetect,
|
||||
},
|
||||
RedirectURL: "https://ks-console.kubesphere-system.svc/oauth/redirect/gitlab",
|
||||
Scopes: []string{"read"},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := &gitlabProviderFactory{}
|
||||
got, err := g.Create(tt.args.opts)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Create() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Create() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
18
pkg/apiserver/authentication/identityprovider/identity.go
Normal file
18
pkg/apiserver/authentication/identityprovider/identity.go
Normal file
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package identityprovider
|
||||
|
||||
// Identity represents the account mapped to kubesphere
|
||||
type Identity interface {
|
||||
// GetUserID required
|
||||
// Identifier for the End-User at the Issuer.
|
||||
GetUserID() string
|
||||
// GetUsername optional
|
||||
// The username which the End-User wishes to be referred to kubesphere.
|
||||
GetUsername() string
|
||||
// GetEmail optional
|
||||
GetEmail() string
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 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 identityprovider
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
)
|
||||
|
||||
var (
|
||||
oauthProviderFactories = make(map[string]OAuthProviderFactory)
|
||||
genericProviderFactories = make(map[string]GenericProviderFactory)
|
||||
identityProviderNotFound = errors.New("identity provider not found")
|
||||
oauthProviders = make(map[string]OAuthProvider)
|
||||
genericProviders = make(map[string]GenericProvider)
|
||||
)
|
||||
|
||||
// Identity represents the account mapped to kubesphere
|
||||
type Identity interface {
|
||||
// GetUserID required
|
||||
// Identifier for the End-User at the Issuer.
|
||||
GetUserID() string
|
||||
// GetUsername optional
|
||||
// The username which the End-User wishes to be referred to kubesphere.
|
||||
GetUsername() string
|
||||
// GetEmail optional
|
||||
GetEmail() string
|
||||
}
|
||||
|
||||
// SetupWithOptions will verify the configuration and initialize the identityProviders
|
||||
func SetupWithOptions(options []oauth.IdentityProviderOptions) error {
|
||||
// Clear all providers when reloading configuration
|
||||
oauthProviders = make(map[string]OAuthProvider)
|
||||
genericProviders = make(map[string]GenericProvider)
|
||||
|
||||
for _, o := range options {
|
||||
if oauthProviders[o.Name] != nil || genericProviders[o.Name] != nil {
|
||||
err := fmt.Errorf("duplicate identity provider found: %s, name must be unique", o.Name)
|
||||
klog.Error(err)
|
||||
return err
|
||||
}
|
||||
if genericProviderFactories[o.Type] == nil && oauthProviderFactories[o.Type] == nil {
|
||||
err := fmt.Errorf("identity provider %s with type %s is not supported", o.Name, o.Type)
|
||||
klog.Error(err)
|
||||
return err
|
||||
}
|
||||
if factory, ok := oauthProviderFactories[o.Type]; ok {
|
||||
if provider, err := factory.Create(o.Provider); err != nil {
|
||||
// don’t return errors, decoupling external dependencies
|
||||
klog.Error(fmt.Sprintf("failed to create identity provider %s: %s", o.Name, err))
|
||||
} else {
|
||||
oauthProviders[o.Name] = provider
|
||||
klog.V(4).Infof("create identity provider %s successfully", o.Name)
|
||||
}
|
||||
}
|
||||
if factory, ok := genericProviderFactories[o.Type]; ok {
|
||||
if provider, err := factory.Create(o.Provider); err != nil {
|
||||
klog.Error(fmt.Sprintf("failed to create identity provider %s: %s", o.Name, err))
|
||||
} else {
|
||||
genericProviders[o.Name] = provider
|
||||
klog.V(4).Infof("create identity provider %s successfully", o.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetGenericProvider returns GenericProvider with given name
|
||||
func GetGenericProvider(providerName string) (GenericProvider, error) {
|
||||
if provider, ok := genericProviders[providerName]; ok {
|
||||
return provider, nil
|
||||
}
|
||||
return nil, identityProviderNotFound
|
||||
}
|
||||
|
||||
// GetOAuthProvider returns OAuthProvider with given name
|
||||
func GetOAuthProvider(providerName string) (OAuthProvider, error) {
|
||||
if provider, ok := oauthProviders[providerName]; ok {
|
||||
return provider, nil
|
||||
}
|
||||
return nil, identityProviderNotFound
|
||||
}
|
||||
|
||||
// RegisterOAuthProvider register OAuthProviderFactory with the specified type
|
||||
func RegisterOAuthProvider(factory OAuthProviderFactory) {
|
||||
oauthProviderFactories[factory.Type()] = factory
|
||||
}
|
||||
|
||||
// RegisterGenericProvider registers GenericProviderFactory with the specified type
|
||||
func RegisterGenericProvider(factory GenericProviderFactory) {
|
||||
genericProviderFactories[factory.Type()] = factory
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package identityprovider
|
||||
|
||||
var (
|
||||
oauthProviderFactories = make(map[string]OAuthProviderFactory)
|
||||
genericProviderFactories = make(map[string]GenericProviderFactory)
|
||||
)
|
||||
|
||||
// RegisterOAuthProviderFactory register OAuthProviderFactory with the specified type
|
||||
func RegisterOAuthProviderFactory(factory OAuthProviderFactory) {
|
||||
oauthProviderFactories[factory.Type()] = factory
|
||||
}
|
||||
|
||||
// RegisterGenericProviderFactory registers GenericProviderFactory with the specified type
|
||||
func RegisterGenericProviderFactory(factory GenericProviderFactory) {
|
||||
genericProviderFactories[factory.Type()] = factory
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
/*
|
||||
|
||||
Copyright 2020 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 identityprovider
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/server/options"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
)
|
||||
|
||||
type emptyOAuthProviderFactory struct {
|
||||
typeName string
|
||||
}
|
||||
|
||||
func (e emptyOAuthProviderFactory) Type() string {
|
||||
return e.typeName
|
||||
}
|
||||
|
||||
type emptyOAuthProvider struct {
|
||||
}
|
||||
|
||||
type emptyIdentity struct {
|
||||
}
|
||||
|
||||
func (e emptyIdentity) GetUserID() string {
|
||||
return "test"
|
||||
}
|
||||
|
||||
func (e emptyIdentity) GetUsername() string {
|
||||
return "test"
|
||||
}
|
||||
|
||||
func (e emptyIdentity) GetEmail() string {
|
||||
return "test@test.com"
|
||||
}
|
||||
|
||||
func (e emptyOAuthProvider) IdentityExchangeCallback(req *http.Request) (Identity, error) {
|
||||
return emptyIdentity{}, nil
|
||||
}
|
||||
|
||||
func (e emptyOAuthProviderFactory) Create(options options.DynamicOptions) (OAuthProvider, error) {
|
||||
return emptyOAuthProvider{}, nil
|
||||
}
|
||||
|
||||
type emptyGenericProviderFactory struct {
|
||||
typeName string
|
||||
}
|
||||
|
||||
func (e emptyGenericProviderFactory) Type() string {
|
||||
return e.typeName
|
||||
}
|
||||
|
||||
type emptyGenericProvider struct {
|
||||
}
|
||||
|
||||
func (e emptyGenericProvider) Authenticate(username string, password string) (Identity, error) {
|
||||
return emptyIdentity{}, nil
|
||||
}
|
||||
|
||||
func (e emptyGenericProviderFactory) Create(options options.DynamicOptions) (GenericProvider, error) {
|
||||
return emptyGenericProvider{}, nil
|
||||
}
|
||||
|
||||
func TestSetupWith(t *testing.T) {
|
||||
RegisterOAuthProvider(emptyOAuthProviderFactory{typeName: "GitHubIdentityProvider"})
|
||||
RegisterOAuthProvider(emptyOAuthProviderFactory{typeName: "OIDCIdentityProvider"})
|
||||
RegisterGenericProvider(emptyGenericProviderFactory{typeName: "LDAPIdentityProvider"})
|
||||
type args struct {
|
||||
options []oauth.IdentityProviderOptions
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "ldap",
|
||||
args: args{options: []oauth.IdentityProviderOptions{
|
||||
{
|
||||
Name: "ldap",
|
||||
MappingMethod: "auto",
|
||||
Type: "LDAPIdentityProvider",
|
||||
Provider: options.DynamicOptions{},
|
||||
},
|
||||
}},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "conflict",
|
||||
args: args{options: []oauth.IdentityProviderOptions{
|
||||
{
|
||||
Name: "ldap",
|
||||
MappingMethod: "auto",
|
||||
Type: "LDAPIdentityProvider",
|
||||
Provider: options.DynamicOptions{},
|
||||
},
|
||||
{
|
||||
Name: "ldap",
|
||||
MappingMethod: "auto",
|
||||
Type: "LDAPIdentityProvider",
|
||||
Provider: options.DynamicOptions{},
|
||||
},
|
||||
}},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "not supported",
|
||||
args: args{options: []oauth.IdentityProviderOptions{
|
||||
{
|
||||
Name: "test",
|
||||
MappingMethod: "auto",
|
||||
Type: "NotSupported",
|
||||
Provider: options.DynamicOptions{},
|
||||
},
|
||||
}},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := SetupWithOptions(tt.args.options); (err != nil) != tt.wantErr {
|
||||
t.Errorf("SetupWithOptions() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package identityprovider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
toolscache "k8s.io/client-go/tools/cache"
|
||||
"k8s.io/klog/v2"
|
||||
runtimecache "sigs.k8s.io/controller-runtime/pkg/cache"
|
||||
)
|
||||
|
||||
var SharedIdentityProviderController = NewController()
|
||||
|
||||
type Controller struct {
|
||||
identityProviders *sync.Map
|
||||
identityProviderConfigs *sync.Map
|
||||
}
|
||||
|
||||
func NewController() *Controller {
|
||||
return &Controller{identityProviders: &sync.Map{}, identityProviderConfigs: &sync.Map{}}
|
||||
}
|
||||
|
||||
func (c *Controller) WatchConfigurationChanges(ctx context.Context, cache runtimecache.Cache) error {
|
||||
informer, err := cache.GetInformer(ctx, &v1.Secret{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("get informer failed: %w", err)
|
||||
}
|
||||
|
||||
_, err = informer.AddEventHandler(toolscache.FilteringResourceEventHandler{
|
||||
FilterFunc: func(obj interface{}) bool {
|
||||
return IsIdentityProviderConfiguration(obj.(*v1.Secret))
|
||||
},
|
||||
Handler: &toolscache.ResourceEventHandlerFuncs{
|
||||
AddFunc: func(obj interface{}) {
|
||||
c.OnConfigurationChange(obj.(*v1.Secret))
|
||||
},
|
||||
UpdateFunc: func(old, new interface{}) {
|
||||
c.OnConfigurationChange(new.(*v1.Secret))
|
||||
},
|
||||
DeleteFunc: func(obj interface{}) {
|
||||
c.OnConfigurationDelete(obj.(*v1.Secret))
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("add event handler failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) OnConfigurationDelete(secret *v1.Secret) {
|
||||
configuration, err := UnmarshalFrom(secret)
|
||||
if err != nil {
|
||||
klog.Errorf("failed to unmarshal secret data: %s", err)
|
||||
return
|
||||
}
|
||||
c.identityProviders.Delete(configuration.Name)
|
||||
c.identityProviderConfigs.Delete(configuration.Name)
|
||||
}
|
||||
|
||||
func (c *Controller) OnConfigurationChange(secret *v1.Secret) {
|
||||
configuration, err := UnmarshalFrom(secret)
|
||||
if err != nil {
|
||||
klog.Errorf("failed to unmarshal secret data: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
if genericProviderFactories[configuration.Type] == nil && oauthProviderFactories[configuration.Type] == nil {
|
||||
klog.Errorf("identity provider %s with type %s is not supported", configuration.Name, configuration.Type)
|
||||
return
|
||||
}
|
||||
|
||||
if factory, ok := oauthProviderFactories[configuration.Type]; ok {
|
||||
if provider, err := factory.Create(configuration.ProviderOptions); err != nil {
|
||||
// don’t return errors, decoupling external dependencies
|
||||
klog.Error(fmt.Sprintf("failed to create identity provider %s: %s", configuration.Name, err))
|
||||
} else {
|
||||
c.identityProviders.Store(configuration.Name, provider)
|
||||
c.identityProviderConfigs.Store(configuration.Name, configuration)
|
||||
klog.Infof("create identity provider %s successfully", configuration.Name)
|
||||
}
|
||||
}
|
||||
if factory, ok := genericProviderFactories[configuration.Type]; ok {
|
||||
if provider, err := factory.Create(configuration.ProviderOptions); err != nil {
|
||||
klog.Error(fmt.Sprintf("failed to create identity provider %s: %s", configuration.Name, err))
|
||||
} else {
|
||||
c.identityProviders.Store(configuration.Name, provider)
|
||||
c.identityProviderConfigs.Store(configuration.Name, configuration)
|
||||
klog.V(4).Infof("create identity provider %s successfully", configuration.Name)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (c *Controller) GetGenericProvider(providerName string) (GenericProvider, bool) {
|
||||
if obj, ok := c.identityProviders.Load(providerName); ok {
|
||||
if provider, ok := obj.(GenericProvider); ok {
|
||||
return provider, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (c *Controller) GetOAuthProvider(providerName string) (OAuthProvider, bool) {
|
||||
if obj, ok := c.identityProviders.Load(providerName); ok {
|
||||
if provider, ok := obj.(OAuthProvider); ok {
|
||||
return provider, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (c *Controller) ListConfigurations() []*Configuration {
|
||||
configurations := make([]*Configuration, 0)
|
||||
c.identityProviderConfigs.Range(func(key, value any) bool {
|
||||
if configuration, ok := value.(*Configuration); ok {
|
||||
configurations = append(configurations, configuration)
|
||||
}
|
||||
return true
|
||||
})
|
||||
return configurations
|
||||
}
|
||||
@@ -1,18 +1,7 @@
|
||||
/*
|
||||
Copyright 2020 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.
|
||||
*/
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package ldap
|
||||
|
||||
@@ -39,7 +28,7 @@ const (
|
||||
)
|
||||
|
||||
func init() {
|
||||
identityprovider.RegisterGenericProvider(&ldapProviderFactory{})
|
||||
identityprovider.RegisterGenericProviderFactory(&ldapProviderFactory{})
|
||||
}
|
||||
|
||||
type ldapProvider struct {
|
||||
|
||||
@@ -1,18 +1,7 @@
|
||||
/*
|
||||
Copyright 2020 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.
|
||||
*/
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package ldap
|
||||
|
||||
|
||||
@@ -1,18 +1,7 @@
|
||||
/*
|
||||
Copyright 2020 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.
|
||||
*/
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package identityprovider
|
||||
|
||||
|
||||
@@ -1,18 +1,7 @@
|
||||
/*
|
||||
Copyright 2020 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.
|
||||
*/
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package oidc
|
||||
|
||||
@@ -36,7 +25,7 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
identityprovider.RegisterOAuthProvider(&oidcProviderFactory{})
|
||||
identityprovider.RegisterOAuthProviderFactory(&oidcProviderFactory{})
|
||||
}
|
||||
|
||||
type oidcProvider struct {
|
||||
|
||||
@@ -1,20 +1,7 @@
|
||||
/*
|
||||
|
||||
Copyright 2020 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.
|
||||
|
||||
*/
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package oidc
|
||||
|
||||
@@ -36,7 +23,7 @@ import (
|
||||
"kubesphere.io/kubesphere/pkg/server/options"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gexec"
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
@@ -53,7 +40,7 @@ func TestOIDC(t *testing.T) {
|
||||
RunSpecs(t, "OIDC Identity Provider Suite")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func(done Done) {
|
||||
var _ = BeforeSuite(func() {
|
||||
privateKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
|
||||
Expect(err).Should(BeNil())
|
||||
jwk := jose.JSONWebKey{
|
||||
@@ -152,8 +139,7 @@ var _ = BeforeSuite(func(done Done) {
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(data)
|
||||
}))
|
||||
close(done)
|
||||
}, 60)
|
||||
})
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
By("tearing down the test environment")
|
||||
|
||||
@@ -1,26 +1,17 @@
|
||||
/*
|
||||
Copyright 2021 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.
|
||||
*/
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package oauth
|
||||
|
||||
import "fmt"
|
||||
|
||||
// The following error type is defined in https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
|
||||
var (
|
||||
// ErrorInvalidClient
|
||||
type ErrorType string
|
||||
|
||||
// The following error type is defined in https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1
|
||||
const (
|
||||
// InvalidClient
|
||||
// Client authentication failed (e.g., unknown client, no
|
||||
// client authentication included, or unsupported
|
||||
// authentication method). The authorization server MAY
|
||||
@@ -31,79 +22,98 @@ var (
|
||||
// respond with an HTTP 401 (Unauthorized) status code and
|
||||
// include the "WWW-Authenticate" response header field
|
||||
// matching the authentication scheme used by the client.
|
||||
ErrorInvalidClient = Error{Type: "invalid_client"}
|
||||
InvalidClient ErrorType = "invalid_client"
|
||||
|
||||
// ErrorInvalidRequest The request is missing a required parameter,
|
||||
// includes an unsupported parameter value (other than grant type),
|
||||
// repeats a parameter, includes multiple credentials,
|
||||
// utilizes more than one mechanism for authenticating the client,
|
||||
// InvalidRequest
|
||||
// The request is missing a required parameter, includes an unsupported parameter value (other than grant type),
|
||||
// repeats a parameter, includes multiple credentials, utilizes more than one mechanism for authenticating the client,
|
||||
// or is otherwise malformed.
|
||||
ErrorInvalidRequest = Error{Type: "invalid_request"}
|
||||
InvalidRequest ErrorType = "invalid_request"
|
||||
|
||||
// ErrorInvalidGrant
|
||||
// InvalidGrant
|
||||
// The provided authorization grant (e.g., authorization code,
|
||||
// resource owner credentials) or refresh token is invalid, expired, revoked,
|
||||
// does not match the redirection URI used in the authorization request,
|
||||
// or was issued to another client.
|
||||
ErrorInvalidGrant = Error{Type: "invalid_grant"}
|
||||
InvalidGrant ErrorType = "invalid_grant"
|
||||
|
||||
// ErrorUnsupportedGrantType
|
||||
// UnsupportedGrantType
|
||||
// The authorization grant type is not supported by the authorization server.
|
||||
ErrorUnsupportedGrantType = Error{Type: "unsupported_grant_type"}
|
||||
UnsupportedGrantType ErrorType = "unsupported_grant_type"
|
||||
|
||||
ErrorUnsupportedResponseType = Error{Type: "unsupported_response_type"}
|
||||
// UnsupportedResponseType
|
||||
// The authorization server does not support obtaining an authorization code using this method.
|
||||
UnsupportedResponseType ErrorType = "unsupported_response_type"
|
||||
|
||||
// ErrorUnauthorizedClient
|
||||
// UnauthorizedClient
|
||||
// The authenticated client is not authorized to use this authorization grant type.
|
||||
ErrorUnauthorizedClient = Error{Type: "unauthorized_client"}
|
||||
UnauthorizedClient ErrorType = "unauthorized_client"
|
||||
|
||||
// ErrorInvalidScope The requested scope is invalid, unknown, malformed,
|
||||
// InvalidScope The requested scope is invalid, unknown, malformed,
|
||||
// or exceeds the scope granted by the resource owner.
|
||||
ErrorInvalidScope = Error{Type: "invalid_scope"}
|
||||
InvalidScope ErrorType = "invalid_scope"
|
||||
|
||||
// ErrorLoginRequired The Authorization Server requires End-User authentication.
|
||||
// LoginRequired The Authorization Server requires End-User authentication.
|
||||
// This error MAY be returned when the prompt parameter value in the Authentication Request is none,
|
||||
// but the Authentication Request cannot be completed without displaying a user interface
|
||||
// for End-User authentication.
|
||||
ErrorLoginRequired = Error{Type: "login_required"}
|
||||
LoginRequired ErrorType = "login_required"
|
||||
|
||||
// ErrorServerError
|
||||
// InteractionRequired
|
||||
// The Authorization Server requires End-User interaction of some form to proceed.
|
||||
// This error MAY be returned when the prompt parameter value in the Authentication Request is none,
|
||||
// but the Authentication Request cannot be completed without displaying a user interface for End-User interaction.
|
||||
InteractionRequired ErrorType = "interaction_required"
|
||||
|
||||
// ServerError
|
||||
// The authorization server encountered an unexpected
|
||||
// condition that prevented it from fulfilling the request.
|
||||
// (This error code is needed because a 500 Internal Server
|
||||
// Error HTTP status code cannot be returned to the client
|
||||
// via an HTTP redirect.)
|
||||
ErrorServerError = Error{Type: "server_error"}
|
||||
ServerError ErrorType = "server_error"
|
||||
)
|
||||
|
||||
func NewInvalidRequest(error error) Error {
|
||||
err := ErrorInvalidRequest
|
||||
err.Description = error.Error()
|
||||
return err
|
||||
func NewError(errorType ErrorType, description string) *Error {
|
||||
return &Error{
|
||||
Type: errorType,
|
||||
Description: description,
|
||||
}
|
||||
}
|
||||
|
||||
func NewInvalidScope(error error) Error {
|
||||
err := ErrorInvalidScope
|
||||
err.Description = error.Error()
|
||||
return err
|
||||
func NewInvalidRequest(description string) *Error {
|
||||
return &Error{
|
||||
Type: InvalidRequest,
|
||||
Description: description,
|
||||
}
|
||||
}
|
||||
|
||||
func NewInvalidClient(error error) Error {
|
||||
err := ErrorInvalidClient
|
||||
err.Description = error.Error()
|
||||
return err
|
||||
func NewInvalidScope(description string) *Error {
|
||||
return &Error{
|
||||
Type: InvalidScope,
|
||||
Description: description,
|
||||
}
|
||||
}
|
||||
|
||||
func NewInvalidGrant(error error) Error {
|
||||
err := ErrorInvalidGrant
|
||||
err.Description = error.Error()
|
||||
return err
|
||||
func NewInvalidClient(description string) *Error {
|
||||
return &Error{
|
||||
Type: InvalidClient,
|
||||
Description: description,
|
||||
}
|
||||
}
|
||||
|
||||
func NewServerError(error error) Error {
|
||||
err := ErrorServerError
|
||||
err.Description = error.Error()
|
||||
return err
|
||||
func NewInvalidGrant(description string) *Error {
|
||||
return &Error{
|
||||
Type: InvalidGrant,
|
||||
Description: description,
|
||||
}
|
||||
}
|
||||
|
||||
func NewServerError(description string) *Error {
|
||||
return &Error{
|
||||
Type: ServerError,
|
||||
Description: description,
|
||||
}
|
||||
}
|
||||
|
||||
// Error wrapped OAuth error Response, for more details: https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
|
||||
@@ -115,7 +125,7 @@ type Error struct {
|
||||
// A single ASCII [USASCII] error code from the following:
|
||||
// Values for the "error" parameter MUST NOT include characters
|
||||
// outside the set %x20-21 / %x23-5B / %x5D-7E.
|
||||
Type string `json:"error"`
|
||||
Type ErrorType `json:"error"`
|
||||
// Description OPTIONAL. Human-readable ASCII [USASCII] text providing
|
||||
// additional information, used to assist the client developer in
|
||||
// understanding the error that occurred.
|
||||
@@ -124,6 +134,6 @@ type Error struct {
|
||||
Description string `json:"error_description,omitempty"`
|
||||
}
|
||||
|
||||
func (e Error) Error() string {
|
||||
func (e *Error) Error() string {
|
||||
return fmt.Sprintf("error=\"%s\", error_description=\"%s\"", e.Type, e.Description)
|
||||
}
|
||||
|
||||
239
pkg/apiserver/authentication/oauth/oauth_client.go
Normal file
239
pkg/apiserver/authentication/oauth/oauth_client.go
Normal file
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
errorsutil "k8s.io/apimachinery/pkg/util/errors"
|
||||
"k8s.io/klog/v2"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/constants"
|
||||
"kubesphere.io/kubesphere/pkg/utils/sliceutil"
|
||||
)
|
||||
|
||||
const (
|
||||
GrantMethodAuto = "auto"
|
||||
GrantMethodPrompt = "prompt"
|
||||
GrantMethodDeny = "deny"
|
||||
|
||||
ConfigTypeOAuthClient = "oauthclient"
|
||||
SecretTypeOAuthClient = "config.kubesphere.io/" + ConfigTypeOAuthClient
|
||||
SecretDataKey = "configuration.yaml"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrorClientNotFound = errors.New("the OAuth client was not found")
|
||||
ErrorRedirectURLNotAllowed = errors.New("redirect URL is not allowed")
|
||||
|
||||
ValidGrantMethods = []string{GrantMethodAuto, GrantMethodPrompt, GrantMethodDeny}
|
||||
)
|
||||
|
||||
// Client represents an OAuth client configuration.
|
||||
type Client struct {
|
||||
// Name is the unique identifier for the OAuth client. It is used as the client_id parameter
|
||||
// when making requests to <master>/oauth/authorize.
|
||||
Name string `json:"name" yaml:"name"`
|
||||
|
||||
// Secret is the unique secret associated with the client for secure communication.
|
||||
Secret string `json:"-" yaml:"secret"`
|
||||
|
||||
// Trusted indicates whether the client is considered a trusted client.
|
||||
Trusted bool `json:"trusted" yaml:"trusted"`
|
||||
|
||||
// GrantMethod determines how grant requests for this client should be handled. If no method is provided,
|
||||
// the cluster default grant handling method will be used. Valid grant handling methods are:
|
||||
// - auto: Always approves grant requests, useful for trusted clients.
|
||||
// - prompt: Prompts the end user for approval of grant requests, useful for third-party clients.
|
||||
// - deny: Always denies grant requests, useful for black-listed clients.
|
||||
GrantMethod string `json:"grantMethod" yaml:"grantMethod"`
|
||||
|
||||
// RespondWithChallenges indicates whether the client prefers authentication needed responses
|
||||
// in the form of challenges instead of redirects.
|
||||
RespondWithChallenges bool `json:"respondWithChallenges,omitempty" yaml:"respondWithChallenges,omitempty"`
|
||||
|
||||
// ScopeRestrictions describes which scopes this client can request. Each requested scope
|
||||
// is checked against each restriction. If any restriction matches, then the scope is allowed.
|
||||
// If no restriction matches, then the scope is denied.
|
||||
ScopeRestrictions []string `json:"scopeRestrictions,omitempty" yaml:"scopeRestrictions,omitempty"`
|
||||
|
||||
// RedirectURIs is a list of valid redirection URIs associated with the client.
|
||||
RedirectURIs []string `json:"redirectURIs,omitempty" yaml:"redirectURIs,omitempty"`
|
||||
|
||||
// AccessTokenMaxAge overrides the default maximum age for access tokens granted to this client.
|
||||
// The default value is 7200 seconds, and the minimum allowed value is 600 seconds.
|
||||
AccessTokenMaxAgeSeconds int64 `json:"accessTokenMaxAgeSeconds,omitempty" yaml:"accessTokenMaxAgeSeconds,omitempty"`
|
||||
|
||||
// AccessTokenInactivityTimeout overrides the default token inactivity timeout
|
||||
// for tokens granted to this client.
|
||||
AccessTokenInactivityTimeoutSeconds int64 `json:"accessTokenInactivityTimeoutSeconds,omitempty" yaml:"accessTokenInactivityTimeoutSeconds,omitempty"`
|
||||
}
|
||||
|
||||
type ClientGetter interface {
|
||||
GetOAuthClient(ctx context.Context, name string) (*Client, error)
|
||||
ListOAuthClients(ctx context.Context) ([]*Client, error)
|
||||
}
|
||||
|
||||
func NewOAuthClientGetter(reader client.Reader) ClientGetter {
|
||||
return &oauthClientGetter{reader}
|
||||
}
|
||||
|
||||
type oauthClientGetter struct {
|
||||
client.Reader
|
||||
}
|
||||
|
||||
func (o *oauthClientGetter) ListOAuthClients(ctx context.Context) ([]*Client, error) {
|
||||
clients := make([]*Client, 0)
|
||||
secrets := &v1.SecretList{}
|
||||
if err := o.List(ctx, secrets, client.InNamespace(constants.KubeSphereNamespace),
|
||||
client.MatchingLabels{constants.GenericConfigTypeLabel: ConfigTypeOAuthClient}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, secret := range secrets.Items {
|
||||
if secret.Type != SecretTypeOAuthClient {
|
||||
continue
|
||||
}
|
||||
if c, err := UnmarshalFrom(&secret); err != nil {
|
||||
klog.Errorf("failed to unmarshal secret data: %s", err)
|
||||
continue
|
||||
} else {
|
||||
clients = append(clients, c)
|
||||
}
|
||||
}
|
||||
return clients, nil
|
||||
}
|
||||
|
||||
// GetOAuthClient retrieves an OAuth client by name from the underlying storage.
|
||||
// It returns the OAuth client if found; otherwise, returns an error.
|
||||
func (o *oauthClientGetter) GetOAuthClient(ctx context.Context, name string) (*Client, error) {
|
||||
clients, err := o.ListOAuthClients(ctx)
|
||||
if err != nil {
|
||||
klog.Errorf("failed to list OAuth clients: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
for _, c := range clients {
|
||||
if c.Name == name {
|
||||
return c, nil
|
||||
}
|
||||
}
|
||||
return nil, ErrorClientNotFound
|
||||
}
|
||||
|
||||
// ValidateClient validates the properties of the provided OAuth 2.0 client.
|
||||
// It checks the client's grant method, access token inactivity timeout, and access
|
||||
// token max age for validity. If any validation fails, it returns an aggregated error.
|
||||
func ValidateClient(client Client) error {
|
||||
var validationErrors []error
|
||||
|
||||
// Validate grant method.
|
||||
if !sliceutil.HasString(ValidGrantMethods, client.GrantMethod) {
|
||||
validationErrors = append(validationErrors, fmt.Errorf("invalid grant method: %s", client.GrantMethod))
|
||||
}
|
||||
|
||||
// Validate access token inactivity timeout.
|
||||
if client.AccessTokenInactivityTimeoutSeconds != 0 && client.AccessTokenInactivityTimeoutSeconds < 600 {
|
||||
validationErrors = append(validationErrors, fmt.Errorf("invalid access token inactivity timeout: %d, the minimum value can only be 600", client.AccessTokenInactivityTimeoutSeconds))
|
||||
}
|
||||
|
||||
// Validate access token max age.
|
||||
if client.AccessTokenMaxAgeSeconds != 0 && client.AccessTokenMaxAgeSeconds < 600 {
|
||||
validationErrors = append(validationErrors, fmt.Errorf("invalid access token max age: %d, the minimum value can only be 600", client.AccessTokenMaxAgeSeconds))
|
||||
}
|
||||
|
||||
// Aggregate validation errors and return.
|
||||
return errorsutil.NewAggregate(validationErrors)
|
||||
}
|
||||
|
||||
// ResolveRedirectURL resolves the redirect URL for the OAuth 2.0 authorization process.
|
||||
// It takes an expected URL as a parameter and returns the resolved URL if it's allowed.
|
||||
// If the expected URL is not provided, it uses the first available RedirectURI from the client.
|
||||
func (c *Client) ResolveRedirectURL(expectURL string) (*url.URL, error) {
|
||||
// Check if RedirectURIs are specified for the client.
|
||||
if len(c.RedirectURIs) == 0 {
|
||||
return nil, ErrorRedirectURLNotAllowed
|
||||
}
|
||||
|
||||
// Get the list of redirectable URIs for the client.
|
||||
redirectAbleURIs := filterValidRedirectURIs(c.RedirectURIs)
|
||||
|
||||
// If the expected URL is not provided, use the first available RedirectURI.
|
||||
if expectURL == "" {
|
||||
if len(redirectAbleURIs) > 0 {
|
||||
return url.Parse(redirectAbleURIs[0])
|
||||
} else {
|
||||
// No RedirectURIs available for the client.
|
||||
return nil, ErrorRedirectURLNotAllowed
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the provided expected URL is allowed.
|
||||
if sliceutil.HasString(redirectAbleURIs, expectURL) {
|
||||
return url.Parse(expectURL)
|
||||
}
|
||||
|
||||
// The provided expected URL is not allowed.
|
||||
return nil, ErrorRedirectURLNotAllowed
|
||||
}
|
||||
|
||||
// IsValidScope checks whether the requested scope is valid for the client.
|
||||
// It compares each individual scope in the requested scope string with the client's
|
||||
// allowed scope restrictions. If all scopes are allowed, it returns true; otherwise, false.
|
||||
func (c *Client) IsValidScope(requestedScope string) bool {
|
||||
// Split the requested scope string into individual scopes.
|
||||
scopes := strings.Split(requestedScope, " ")
|
||||
|
||||
// Check each individual scope against the client's scope restrictions.
|
||||
for _, scope := range scopes {
|
||||
if !sliceutil.HasString(c.ScopeRestrictions, scope) {
|
||||
// Log a message indicating the disallowed scope.
|
||||
klog.V(4).Infof("Invalid scope: %s is not allowed for client %s", scope, c.Name)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// All scopes are valid.
|
||||
return true
|
||||
}
|
||||
|
||||
// filterValidRedirectURIs filters out invalid redirect URIs from the given slice.
|
||||
// It returns a new slice containing only valid URIs.
|
||||
func filterValidRedirectURIs(redirectURIs []string) []string {
|
||||
validURIs := make([]string, 0)
|
||||
for _, uri := range redirectURIs {
|
||||
// Check if the URI is valid by attempting to parse it.
|
||||
_, err := url.Parse(uri)
|
||||
if err == nil {
|
||||
// The URI is valid, add it to the list of valid URIs.
|
||||
validURIs = append(validURIs, uri)
|
||||
}
|
||||
}
|
||||
return validURIs
|
||||
}
|
||||
|
||||
func UnmarshalFrom(secret *v1.Secret) (*Client, error) {
|
||||
oc := &Client{}
|
||||
if err := yaml.Unmarshal(secret.Data[SecretDataKey], oc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return oc, nil
|
||||
}
|
||||
|
||||
func MarshalInto(client *Client, secret *v1.Secret) error {
|
||||
data, err := yaml.Marshal(client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
secret.Data = map[string][]byte{SecretDataKey: data}
|
||||
return nil
|
||||
}
|
||||
39
pkg/apiserver/authentication/oauth/oauth_client_test.go
Normal file
39
pkg/apiserver/authentication/oauth/oauth_client_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
func TestMarshalInto(t *testing.T) {
|
||||
want := &Client{
|
||||
Name: "test",
|
||||
Secret: "test",
|
||||
Trusted: false,
|
||||
GrantMethod: "auto",
|
||||
RedirectURIs: []string{"test"},
|
||||
AccessTokenMaxAgeSeconds: 10000,
|
||||
AccessTokenInactivityTimeoutSeconds: 10000,
|
||||
}
|
||||
secret := &v1.Secret{}
|
||||
if err := MarshalInto(want, secret); err != nil {
|
||||
t.Errorf("Error: %v", err)
|
||||
}
|
||||
|
||||
got, err := UnmarshalFrom(secret)
|
||||
if err != nil {
|
||||
klog.Errorf("failed to unmarshal secret data: %s", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(want, got) {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,7 @@
|
||||
/*
|
||||
|
||||
Copyright 2021 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.
|
||||
|
||||
*/
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package oauth
|
||||
|
||||
@@ -32,18 +19,19 @@ const (
|
||||
// ScopeProfile This scope value requests access to the End-User's default profile Claims,
|
||||
// which are: name, family_name, given_name, middle_name, nickname, preferred_username,
|
||||
// profile, picture, website, gender, birthdate, zoneinfo, locale, and updated_at.
|
||||
ScopeProfile = "profile"
|
||||
// ScopePhone This scope value requests access to the phone_number and phone_number_verified Claims.
|
||||
ScopePhone = "phone"
|
||||
// ScopeAddress This scope value requests access to the address Claim.
|
||||
ScopeAddress = "address"
|
||||
ResponseCode = "code"
|
||||
ResponseIDToken = "id_token"
|
||||
ResponseToken = "token"
|
||||
ScopeProfile = "profile"
|
||||
ResponseTypeCode = "code"
|
||||
ResponseTypeIDToken = "id_token"
|
||||
ResponseTypeToken = "token"
|
||||
GrantTypePassword = "password"
|
||||
GrantTypeRefreshToken = "refresh_token"
|
||||
GrantTypeCode = "code"
|
||||
GrantTypeAuthorizationCode = "authorization_code"
|
||||
GrantTypeOTP = "otp"
|
||||
)
|
||||
|
||||
var ValidScopes = []string{ScopeOpenID, ScopeEmail, ScopeProfile}
|
||||
var ValidResponseTypes = []string{ResponseCode, ResponseIDToken, ResponseToken}
|
||||
var ValidResponseTypes = []string{ResponseTypeCode, ResponseTypeIDToken, ResponseTypeToken}
|
||||
|
||||
func IsValidScopes(scopes []string) bool {
|
||||
for _, scope := range scopes {
|
||||
|
||||
@@ -1,18 +1,8 @@
|
||||
/*
|
||||
Copyright 2021 The KubeSphere Authors.
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
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 oauth
|
||||
|
||||
import "testing"
|
||||
|
||||
@@ -1,29 +1,14 @@
|
||||
/*
|
||||
Copyright 2020 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.
|
||||
*/
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/server/options"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/utils/sliceutil"
|
||||
)
|
||||
|
||||
type GrantHandlerType string
|
||||
@@ -31,36 +16,22 @@ type MappingMethod string
|
||||
type IdentityProviderType string
|
||||
|
||||
const (
|
||||
// GrantHandlerAuto auto-approves client authorization grant requests
|
||||
GrantHandlerAuto GrantHandlerType = "auto"
|
||||
// GrantHandlerPrompt prompts the user to approve new client authorization grant requests
|
||||
GrantHandlerPrompt GrantHandlerType = "prompt"
|
||||
// GrantHandlerDeny auto-denies client authorization grant requests
|
||||
GrantHandlerDeny GrantHandlerType = "deny"
|
||||
// MappingMethodAuto The default value.
|
||||
// The user will automatically create and mapping when login successful.
|
||||
// Fails if a user with that username is already mapped to another identity.
|
||||
MappingMethodAuto MappingMethod = "auto"
|
||||
// MappingMethodLookup Looks up an existing identity, user identity mapping, and user, but does not automatically
|
||||
// provision users or identities. Using this method requires you to manually provision users.
|
||||
MappingMethodLookup MappingMethod = "lookup"
|
||||
// MappingMethodMixed A user entity can be mapped with multiple identifyProvider.
|
||||
// MappingMethodMixed A user entity can be mapped with multiple identifyProvider.
|
||||
// not supported yet.
|
||||
MappingMethodMixed MappingMethod = "mixed"
|
||||
|
||||
DefaultIssuer string = "kubesphere"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrorClientNotFound = errors.New("the OAuth client was not found")
|
||||
ErrorProviderNotFound = errors.New("the identity provider was not found")
|
||||
ErrorRedirectURLNotAllowed = errors.New("redirect URL is not allowed")
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
// An Issuer Identifier is a case-sensitive URL using the https scheme that contains scheme,
|
||||
type IssuerOptions struct {
|
||||
// URL is a case-sensitive URL using the https scheme that contains scheme,
|
||||
// host, and optionally, port number and path components and no query or fragment components.
|
||||
Issuer string `json:"issuer,omitempty" yaml:"issuer,omitempty"`
|
||||
URL string `json:"url,omitempty" yaml:"url,omitempty"`
|
||||
|
||||
// secret to sign jwt token
|
||||
JWTSecret string `json:"-" yaml:"jwtSecret"`
|
||||
|
||||
// RSA private key file used to sign the id token
|
||||
SignKey string `json:"signKey,omitempty" yaml:"signKey,omitempty"`
|
||||
@@ -68,14 +39,9 @@ type Options struct {
|
||||
// Raw RSA private key. Base64 encoded PEM file
|
||||
SignKeyData string `json:"-,omitempty" yaml:"signKeyData,omitempty"`
|
||||
|
||||
// Register identity providers.
|
||||
IdentityProviders []IdentityProviderOptions `json:"identityProviders,omitempty" yaml:"identityProviders,omitempty"`
|
||||
|
||||
// Register additional OAuth clients.
|
||||
Clients []Client `json:"clients,omitempty" yaml:"clients,omitempty"`
|
||||
|
||||
// AccessTokenMaxAgeSeconds control the lifetime of access tokens. The default lifetime is 24 hours.
|
||||
// 0 means no expiration.
|
||||
// AccessTokenMaxAgeSeconds control the lifetime of access tokens.
|
||||
// The default lifetime is 24 hours.
|
||||
// Zero means no expiration.
|
||||
AccessTokenMaxAge time.Duration `json:"accessTokenMaxAge" yaml:"accessTokenMaxAge"`
|
||||
|
||||
// Inactivity timeout for tokens
|
||||
@@ -89,26 +55,34 @@ type Options struct {
|
||||
// - X: Tokens time out if there is no activity
|
||||
// The current minimum allowed value for X is 5 minutes
|
||||
AccessTokenInactivityTimeout time.Duration `json:"accessTokenInactivityTimeout" yaml:"accessTokenInactivityTimeout"`
|
||||
|
||||
// Token verification maximum time difference, default to 10s.
|
||||
// You should consider allowing a clock skew when checking the time-based values.
|
||||
// This should be values of a few seconds, and we don’t recommend using more than 30 seconds for this purpose,
|
||||
// as this would rather indicate problems with the server, rather than a common clock skew.
|
||||
MaximumClockSkew time.Duration `json:"maximumClockSkew" yaml:"maximumClockSkew"`
|
||||
}
|
||||
|
||||
type IdentityProviderOptions struct {
|
||||
// The provider name.
|
||||
Name string `json:"name" yaml:"name"`
|
||||
|
||||
// Defines how new identities are mapped to users when they login. Allowed values are:
|
||||
// - auto: The default value.The user will automatically create and mapping when login successful.
|
||||
// Fails if a user with that user name is already mapped to another identity.
|
||||
// Defines how new identities are mapped to users when they login.
|
||||
// Allowed values are:
|
||||
// - auto: The default value.The user will automatically create and mapping when login is successful.
|
||||
// Fails if a user with that username is already mapped to another identity.
|
||||
// - lookup: Looks up an existing identity, user identity mapping, and user, but does not automatically
|
||||
// provision users or identities. Using this method requires you to manually provision users.
|
||||
// - mixed: A user entity can be mapped with multiple identifyProvider.
|
||||
// provision users or identities.
|
||||
// Using this method requires you to manually provision users.
|
||||
// - mixed: A user entity can be mapped with multiple identifyProvider.
|
||||
MappingMethod MappingMethod `json:"mappingMethod" yaml:"mappingMethod"`
|
||||
|
||||
// DisableLoginConfirmation means that when the user login successfully,
|
||||
// reconfirm the account information is not required.
|
||||
// DisableLoginConfirmation Skip the login confirmation screen, so user cannot change its username.
|
||||
// Username is provided from ID Token.
|
||||
// Username from IDP must math [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*
|
||||
DisableLoginConfirmation bool `json:"disableLoginConfirmation" yaml:"disableLoginConfirmation"`
|
||||
|
||||
// The type of identify provider
|
||||
// The type of identity provider
|
||||
// OpenIDIdentityProvider LDAPIdentityProvider GitHubIdentityProvider
|
||||
Type string `json:"type" yaml:"type"`
|
||||
|
||||
@@ -125,7 +99,7 @@ type Token struct {
|
||||
// The Type method returns either this or "Bearer", the default.
|
||||
TokenType string `json:"token_type,omitempty"`
|
||||
|
||||
// RefreshToken is a token that's used by the application
|
||||
// RefreshToken is a token used by the application
|
||||
// (as opposed to the user) to refresh the access token
|
||||
// if it expires.
|
||||
RefreshToken string `json:"refresh_token,omitempty"`
|
||||
@@ -137,104 +111,10 @@ type Token struct {
|
||||
ExpiresIn int `json:"expires_in,omitempty"`
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
// The name of the OAuth client is used as the client_id parameter when making requests to <master>/oauth/authorize
|
||||
// and <master>/oauth/token.
|
||||
Name string `json:"name,omitempty" yaml:"name,omitempty"`
|
||||
|
||||
// Secret is the unique secret associated with a client
|
||||
Secret string `json:"-" yaml:"secret,omitempty"`
|
||||
|
||||
// RespondWithChallenges indicates whether the client wants authentication needed responses made
|
||||
// in the form of challenges instead of redirects
|
||||
RespondWithChallenges bool `json:"respondWithChallenges,omitempty" yaml:"respondWithChallenges,omitempty"`
|
||||
|
||||
// RedirectURIs is the valid redirection URIs associated with a client
|
||||
RedirectURIs []string `json:"redirectURIs,omitempty" yaml:"redirectURIs,omitempty"`
|
||||
|
||||
// GrantMethod determines how to handle grants for this client. If no method is provided, the
|
||||
// cluster default grant handling method will be used. Valid grant handling methods are:
|
||||
// - auto: always approves grant requests, useful for trusted clients
|
||||
// - prompt: prompts the end user for approval of grant requests, useful for third-party clients
|
||||
// - deny: always denies grant requests, useful for black-listed clients
|
||||
GrantMethod GrantHandlerType `json:"grantMethod,omitempty" yaml:"grantMethod,omitempty"`
|
||||
|
||||
// ScopeRestrictions describes which scopes this client can request. Each requested scope
|
||||
// is checked against each restriction. If any restriction matches, then the scope is allowed.
|
||||
// If no restriction matches, then the scope is denied.
|
||||
ScopeRestrictions []string `json:"scopeRestrictions,omitempty" yaml:"scopeRestrictions,omitempty"`
|
||||
|
||||
// AccessTokenMaxAge overrides the default access token max age for tokens granted to this client.
|
||||
AccessTokenMaxAge *time.Duration `json:"accessTokenMaxAge,omitempty" yaml:"accessTokenMaxAge,omitempty"`
|
||||
|
||||
// AccessTokenInactivityTimeout overrides the default token
|
||||
// inactivity timeout for tokens granted to this client.
|
||||
AccessTokenInactivityTimeout *time.Duration `json:"accessTokenInactivityTimeout,omitempty" yaml:"accessTokenInactivityTimeout,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
// AllowAllRedirectURI Allow any redirect URI if the redirectURI is defined in request
|
||||
AllowAllRedirectURI = "*"
|
||||
)
|
||||
|
||||
func (o *Options) OAuthClient(name string) (Client, error) {
|
||||
for _, found := range o.Clients {
|
||||
if found.Name == name {
|
||||
return found, nil
|
||||
}
|
||||
}
|
||||
return Client{}, ErrorClientNotFound
|
||||
}
|
||||
|
||||
func (o *Options) IdentityProviderOptions(name string) (*IdentityProviderOptions, error) {
|
||||
for _, found := range o.IdentityProviders {
|
||||
if found.Name == name {
|
||||
return &found, nil
|
||||
}
|
||||
}
|
||||
return nil, ErrorProviderNotFound
|
||||
}
|
||||
|
||||
func (c Client) anyRedirectAbleURI() []string {
|
||||
uris := make([]string, 0)
|
||||
for _, uri := range c.RedirectURIs {
|
||||
_, err := url.Parse(uri)
|
||||
if err == nil {
|
||||
uris = append(uris, uri)
|
||||
}
|
||||
}
|
||||
return uris
|
||||
}
|
||||
|
||||
func (c Client) ResolveRedirectURL(expectURL string) (*url.URL, error) {
|
||||
// RedirectURIs is empty
|
||||
if len(c.RedirectURIs) == 0 {
|
||||
return nil, ErrorRedirectURLNotAllowed
|
||||
}
|
||||
allowAllRedirectURI := sliceutil.HasString(c.RedirectURIs, AllowAllRedirectURI)
|
||||
redirectAbleURIs := c.anyRedirectAbleURI()
|
||||
|
||||
if expectURL == "" {
|
||||
// Need to specify at least one RedirectURI
|
||||
if len(redirectAbleURIs) > 0 {
|
||||
return url.Parse(redirectAbleURIs[0])
|
||||
} else {
|
||||
return nil, ErrorRedirectURLNotAllowed
|
||||
}
|
||||
}
|
||||
if allowAllRedirectURI || sliceutil.HasString(redirectAbleURIs, expectURL) {
|
||||
return url.Parse(expectURL)
|
||||
}
|
||||
|
||||
return nil, ErrorRedirectURLNotAllowed
|
||||
}
|
||||
|
||||
func NewOptions() *Options {
|
||||
return &Options{
|
||||
Issuer: DefaultIssuer,
|
||||
IdentityProviders: make([]IdentityProviderOptions, 0),
|
||||
Clients: make([]Client, 0),
|
||||
func NewIssuerOptions() *IssuerOptions {
|
||||
return &IssuerOptions{
|
||||
AccessTokenMaxAge: time.Hour * 2,
|
||||
AccessTokenInactivityTimeout: time.Hour * 2,
|
||||
MaximumClockSkew: 10 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 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 oauth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func TestClientResolveRedirectURL(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
Name string
|
||||
client Client
|
||||
wantErr bool
|
||||
expectURL string
|
||||
}{
|
||||
{
|
||||
Name: "custom client test",
|
||||
client: Client{
|
||||
Name: "custom",
|
||||
RespondWithChallenges: true,
|
||||
RedirectURIs: []string{AllowAllRedirectURI, "https://foo.bar.com/oauth/cb"},
|
||||
GrantMethod: GrantHandlerAuto,
|
||||
},
|
||||
wantErr: false,
|
||||
expectURL: "https://foo.bar.com/oauth/cb",
|
||||
},
|
||||
{
|
||||
Name: "custom client test",
|
||||
client: Client{
|
||||
Name: "custom",
|
||||
RespondWithChallenges: true,
|
||||
RedirectURIs: []string{"https://foo.bar.com/oauth/cb"},
|
||||
GrantMethod: GrantHandlerAuto,
|
||||
},
|
||||
wantErr: true,
|
||||
expectURL: "https://foo.bar.com/oauth/cb2",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
redirectURL, err := test.client.ResolveRedirectURL(test.expectURL)
|
||||
if (err != nil) != test.wantErr {
|
||||
t.Errorf("ResolveRedirectURL() error = %+v, wantErr %+v", err, test.wantErr)
|
||||
return
|
||||
}
|
||||
if redirectURL != nil && test.expectURL != redirectURL.String() {
|
||||
t.Errorf("expected redirect url: %s, got: %s", test.expectURL, redirectURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDynamicOptions_MarshalJSON(t *testing.T) {
|
||||
config := `
|
||||
accessTokenMaxAge: 1h
|
||||
accessTokenInactivityTimeout: 30m
|
||||
identityProviders:
|
||||
- name: ldap
|
||||
type: LDAPIdentityProvider
|
||||
mappingMethod: auto
|
||||
provider:
|
||||
host: xxxx.sn.mynetname.net:389
|
||||
managerDN: uid=root,cn=users,dc=xxxx,dc=sn,dc=mynetname,dc=net
|
||||
managerPassword: xxxx
|
||||
userSearchBase: dc=xxxx,dc=sn,dc=mynetname,dc=net
|
||||
loginAttribute: uid
|
||||
mailAttribute: mail
|
||||
- name: github
|
||||
type: GitHubIdentityProvider
|
||||
mappingMethod: mixed
|
||||
provider:
|
||||
clientID: 'xxxxxx'
|
||||
clientSecret: 'xxxxxx'
|
||||
endpoint:
|
||||
authURL: 'https://github.com/login/oauth/authorize'
|
||||
tokenURL: 'https://github.com/login/oauth/access_token'
|
||||
redirectURL: 'https://ks-console/oauth/redirect'
|
||||
scopes:
|
||||
- user
|
||||
`
|
||||
var options Options
|
||||
if err := yaml.Unmarshal([]byte(config), &options); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
expected := `{"identityProviders":[{"name":"ldap","mappingMethod":"auto","disableLoginConfirmation":false,"type":"LDAPIdentityProvider","provider":{"host":"xxxx.sn.mynetname.net:389","loginAttribute":"uid","mailAttribute":"mail","managerDN":"uid=root,cn=users,dc=xxxx,dc=sn,dc=mynetname,dc=net","userSearchBase":"dc=xxxx,dc=sn,dc=mynetname,dc=net"}},{"name":"github","mappingMethod":"mixed","disableLoginConfirmation":false,"type":"GitHubIdentityProvider","provider":{"clientID":"xxxxxx","endpoint":{"authURL":"https://github.com/login/oauth/authorize","tokenURL":"https://github.com/login/oauth/access_token"},"redirectURL":"https://ks-console/oauth/redirect","scopes":["user"]}}],"accessTokenMaxAge":3600000000000,"accessTokenInactivityTimeout":1800000000000}`
|
||||
output, _ := json.Marshal(options)
|
||||
if expected != string(output) {
|
||||
t.Errorf("expected: %s, but got: %s", expected, output)
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,7 @@
|
||||
/*
|
||||
|
||||
Copyright 2021 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.
|
||||
|
||||
*/
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package authentication
|
||||
|
||||
@@ -24,11 +11,9 @@ import (
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider"
|
||||
_ "kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider/aliyunidaas"
|
||||
_ "kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider/cas"
|
||||
_ "kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider/github"
|
||||
_ "kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider/gitlab"
|
||||
_ "kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider/ldap"
|
||||
_ "kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider/oidc"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
@@ -43,11 +28,7 @@ type Options struct {
|
||||
// A user will be blocked for 10m if he/she logins with incorrect credentials for at least 5 times in 10m.
|
||||
AuthenticateRateLimiterMaxTries int `json:"authenticateRateLimiterMaxTries" yaml:"authenticateRateLimiterMaxTries"`
|
||||
AuthenticateRateLimiterDuration time.Duration `json:"authenticateRateLimiterDuration" yaml:"authenticateRateLimiterDuration"`
|
||||
// Token verification maximum time difference, default to 10s.
|
||||
// You should consider allowing a clock skew when checking the time-based values.
|
||||
// This should be values of a few seconds, and we don’t recommend using more than 30 seconds for this purpose,
|
||||
// as this would rather indicate problems with the server, rather than a common clock skew.
|
||||
MaximumClockSkew time.Duration `json:"maximumClockSkew" yaml:"maximumClockSkew"`
|
||||
|
||||
// retention login history, records beyond this amount will be deleted
|
||||
LoginHistoryRetentionPeriod time.Duration `json:"loginHistoryRetentionPeriod" yaml:"loginHistoryRetentionPeriod"`
|
||||
// retention login history, records beyond this amount will be deleted
|
||||
@@ -55,39 +36,30 @@ type Options struct {
|
||||
LoginHistoryMaximumEntries int `json:"loginHistoryMaximumEntries,omitempty" yaml:"loginHistoryMaximumEntries,omitempty"`
|
||||
// allow multiple users login from different location at the same time
|
||||
MultipleLogin bool `json:"multipleLogin" yaml:"multipleLogin"`
|
||||
// secret to sign jwt token
|
||||
JwtSecret string `json:"-" yaml:"jwtSecret"`
|
||||
// OAuthOptions defines options needed for integrated oauth plugins
|
||||
OAuthOptions *oauth.Options `json:"oauthOptions" yaml:"oauthOptions"`
|
||||
// KubectlImage is the image address we use to create kubectl pod for users who have admin access to the cluster.
|
||||
KubectlImage string `json:"kubectlImage" yaml:"kubectlImage"`
|
||||
|
||||
// Issuer defines options needed for integrated oauth plugins
|
||||
Issuer *oauth.IssuerOptions `json:"issuer" yaml:"issuer"`
|
||||
}
|
||||
|
||||
func NewOptions() *Options {
|
||||
return &Options{
|
||||
AuthenticateRateLimiterMaxTries: 5,
|
||||
AuthenticateRateLimiterDuration: time.Minute * 30,
|
||||
MaximumClockSkew: 10 * time.Second,
|
||||
LoginHistoryRetentionPeriod: time.Hour * 24 * 7,
|
||||
LoginHistoryMaximumEntries: 100,
|
||||
OAuthOptions: oauth.NewOptions(),
|
||||
Issuer: oauth.NewIssuerOptions(),
|
||||
MultipleLogin: false,
|
||||
JwtSecret: "",
|
||||
KubectlImage: "kubesphere/kubectl:v1.0.0",
|
||||
}
|
||||
}
|
||||
|
||||
func (options *Options) Validate() []error {
|
||||
var errs []error
|
||||
if len(options.JwtSecret) == 0 {
|
||||
if len(options.Issuer.JWTSecret) == 0 {
|
||||
errs = append(errs, errors.New("JWT secret MUST not be empty"))
|
||||
}
|
||||
if options.AuthenticateRateLimiterMaxTries > options.LoginHistoryMaximumEntries {
|
||||
errs = append(errs, errors.New("authenticateRateLimiterMaxTries MUST not be greater than loginHistoryMaximumEntries"))
|
||||
}
|
||||
if err := identityprovider.SetupWithOptions(options.OAuthOptions.IdentityProviders); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
@@ -95,10 +67,9 @@ func (options *Options) AddFlags(fs *pflag.FlagSet, s *Options) {
|
||||
fs.IntVar(&options.AuthenticateRateLimiterMaxTries, "authenticate-rate-limiter-max-retries", s.AuthenticateRateLimiterMaxTries, "")
|
||||
fs.DurationVar(&options.AuthenticateRateLimiterDuration, "authenticate-rate-limiter-duration", s.AuthenticateRateLimiterDuration, "")
|
||||
fs.BoolVar(&options.MultipleLogin, "multiple-login", s.MultipleLogin, "Allow multiple login with the same account, disable means only one user can login at the same time.")
|
||||
fs.StringVar(&options.JwtSecret, "jwt-secret", s.JwtSecret, "Secret to sign jwt token, must not be empty.")
|
||||
fs.StringVar(&options.Issuer.JWTSecret, "jwt-secret", s.Issuer.JWTSecret, "Secret to sign jwt token, must not be empty.")
|
||||
fs.DurationVar(&options.LoginHistoryRetentionPeriod, "login-history-retention-period", s.LoginHistoryRetentionPeriod, "login-history-retention-period defines how long login history should be kept.")
|
||||
fs.IntVar(&options.LoginHistoryMaximumEntries, "login-history-maximum-entries", s.LoginHistoryMaximumEntries, "login-history-maximum-entries defines how many entries of login history should be kept.")
|
||||
fs.DurationVar(&options.OAuthOptions.AccessTokenMaxAge, "access-token-max-age", s.OAuthOptions.AccessTokenMaxAge, "access-token-max-age control the lifetime of access tokens, 0 means no expiration.")
|
||||
fs.StringVar(&s.KubectlImage, "kubectl-image", s.KubectlImage, "Setup the image used by kubectl terminal pod")
|
||||
fs.DurationVar(&options.MaximumClockSkew, "maximum-clock-skew", s.MaximumClockSkew, "The maximum time difference between the system clocks of the ks-apiserver that issued a JWT and the ks-apiserver that verified the JWT.")
|
||||
fs.DurationVar(&options.Issuer.AccessTokenMaxAge, "access-token-max-age", s.Issuer.AccessTokenMaxAge, "access-token-max-age control the lifetime of access tokens, 0 means no expiration.")
|
||||
fs.DurationVar(&options.Issuer.MaximumClockSkew, "maximum-clock-skew", s.Issuer.MaximumClockSkew, "The maximum time difference between the system clocks of the ks-apiserver that issued a JWT and the ks-apiserver that verified the JWT.")
|
||||
}
|
||||
|
||||
@@ -1,18 +1,7 @@
|
||||
/*
|
||||
Copyright 2020 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.
|
||||
*/
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package anonymous
|
||||
|
||||
|
||||
@@ -1,18 +1,7 @@
|
||||
/*
|
||||
Copyright 2020 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.
|
||||
*/
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package anonymous
|
||||
|
||||
|
||||
@@ -1,18 +1,7 @@
|
||||
/*
|
||||
Copyright 2020 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.
|
||||
*/
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package token
|
||||
|
||||
@@ -33,7 +22,7 @@ import (
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -93,9 +82,9 @@ type Claims struct {
|
||||
|
||||
// The following is well-known ID Token fields
|
||||
|
||||
// End-User's full name in displayable form including all name parts,
|
||||
// End-User's full url in displayable form including all url parts,
|
||||
// possibly including titles and suffixes, ordered according to the End-User's locale and preferences.
|
||||
Name string `json:"name,omitempty"`
|
||||
Name string `json:"url,omitempty"`
|
||||
// String value used to associate a Client session with an ID Token, and to mitigate replay attacks.
|
||||
// The value is passed through unmodified from the Authentication Request to the ID Token.
|
||||
Nonce string `json:"nonce,omitempty"`
|
||||
@@ -103,13 +92,13 @@ type Claims struct {
|
||||
Email string `json:"email,omitempty"`
|
||||
// End-User's locale, represented as a BCP47 [RFC5646] language tag.
|
||||
Locale string `json:"locale,omitempty"`
|
||||
// Shorthand name by which the End-User wishes to be referred to at the RP,
|
||||
// Shorthand url by which the End-User wishes to be referred to at the RP,
|
||||
PreferredUsername string `json:"preferred_username,omitempty"`
|
||||
}
|
||||
|
||||
type issuer struct {
|
||||
// Issuer Identity
|
||||
name string
|
||||
// Issuer Identifier
|
||||
url string
|
||||
// signing access_token and refresh_token
|
||||
secret []byte
|
||||
// signing id_token
|
||||
@@ -127,7 +116,7 @@ func (s *issuer) IssueTo(request *IssueRequest) (string, error) {
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
IssuedAt: jwt.NewNumericDate(issueAt),
|
||||
Subject: request.User.GetName(),
|
||||
Issuer: s.name,
|
||||
Issuer: s.url,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -253,19 +242,19 @@ func generatePrivateKeyData() ([]byte, error) {
|
||||
return pemData, nil
|
||||
}
|
||||
|
||||
func loadSignKey(options *authentication.Options) (*rsa.PrivateKey, string, error) {
|
||||
func loadSignKey(config *oauth.IssuerOptions) (*rsa.PrivateKey, string, error) {
|
||||
var signKey *rsa.PrivateKey
|
||||
var signKeyData []byte
|
||||
var err error
|
||||
|
||||
if options.OAuthOptions.SignKey != "" {
|
||||
signKeyData, err = os.ReadFile(options.OAuthOptions.SignKey)
|
||||
if config.SignKey != "" {
|
||||
signKeyData, err = os.ReadFile(config.SignKey)
|
||||
if err != nil {
|
||||
klog.Errorf("issuer: failed to read private key file %s: %v", options.OAuthOptions.SignKey, err)
|
||||
klog.Errorf("issuer: failed to read private key file %s: %v", config.SignKey, err)
|
||||
return nil, "", err
|
||||
}
|
||||
} else if options.OAuthOptions.SignKeyData != "" {
|
||||
signKeyData, err = base64.StdEncoding.DecodeString(options.OAuthOptions.SignKeyData)
|
||||
} else if config.SignKeyData != "" {
|
||||
signKeyData, err = base64.StdEncoding.DecodeString(config.SignKeyData)
|
||||
if err != nil {
|
||||
klog.Errorf("issuer: failed to decode sign key data: %s", err)
|
||||
return nil, "", err
|
||||
@@ -292,16 +281,16 @@ func loadSignKey(options *authentication.Options) (*rsa.PrivateKey, string, erro
|
||||
return signKey, keyID, nil
|
||||
}
|
||||
|
||||
func NewIssuer(options *authentication.Options) (Issuer, error) {
|
||||
func NewIssuer(config *oauth.IssuerOptions) (Issuer, error) {
|
||||
// TODO(hongming) automatically rotates keys
|
||||
signKey, keyID, err := loadSignKey(options)
|
||||
signKey, keyID, err := loadSignKey(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &issuer{
|
||||
name: options.OAuthOptions.Issuer,
|
||||
secret: []byte(options.JwtSecret),
|
||||
maximumClockSkew: options.MaximumClockSkew,
|
||||
url: config.URL,
|
||||
secret: []byte(config.JWTSecret),
|
||||
maximumClockSkew: config.MaximumClockSkew,
|
||||
signKey: &Keys{
|
||||
SigningKey: &jose.JSONWebKey{
|
||||
Key: signKey,
|
||||
|
||||
@@ -1,18 +1,8 @@
|
||||
/*
|
||||
Copyright 2021 The KubeSphere Authors.
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
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 token
|
||||
|
||||
import (
|
||||
@@ -26,7 +16,6 @@ import (
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
)
|
||||
|
||||
@@ -62,28 +51,26 @@ PsSsOHhPx0g+Wl8K2+Edg3FQRZ1m0rQFAZn66jd96u85aA9NH/bw3A3VYUdVJyHh
|
||||
|
||||
func TestNewIssuer(t *testing.T) {
|
||||
signKeyData := base64.StdEncoding.EncodeToString([]byte(privateKeyData))
|
||||
options := &authentication.Options{
|
||||
config := &oauth.IssuerOptions{
|
||||
URL: "https://ks-console.kubesphere-system.svc",
|
||||
SignKeyData: signKeyData,
|
||||
MaximumClockSkew: 10 * time.Second,
|
||||
JwtSecret: "test-secret",
|
||||
OAuthOptions: &oauth.Options{
|
||||
Issuer: "kubesphere",
|
||||
SignKeyData: signKeyData,
|
||||
},
|
||||
JWTSecret: "test-secret",
|
||||
}
|
||||
got, err := NewIssuer(options)
|
||||
got, err := NewIssuer(config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
signKey, keyID, err := loadSignKey(options)
|
||||
signKey, keyID, err := loadSignKey(config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
want := &issuer{
|
||||
name: options.OAuthOptions.Issuer,
|
||||
secret: []byte(options.JwtSecret),
|
||||
maximumClockSkew: options.MaximumClockSkew,
|
||||
url: config.URL,
|
||||
secret: []byte(config.JWTSecret),
|
||||
maximumClockSkew: config.MaximumClockSkew,
|
||||
signKey: &Keys{
|
||||
SigningKey: &jose.JSONWebKey{
|
||||
Key: signKey,
|
||||
@@ -100,21 +87,19 @@ func TestNewIssuer(t *testing.T) {
|
||||
},
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("NewIssuer() got = %v, want %v", got, want)
|
||||
t.Errorf("NewIssuerOptions() got = %v, want %v", got, want)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewIssuerGenerateSignKey(t *testing.T) {
|
||||
options := &authentication.Options{
|
||||
config := &oauth.IssuerOptions{
|
||||
URL: "https://ks-console.kubesphere-system.svc",
|
||||
MaximumClockSkew: 10 * time.Second,
|
||||
JwtSecret: "test-secret",
|
||||
OAuthOptions: &oauth.Options{
|
||||
Issuer: "kubesphere",
|
||||
},
|
||||
JWTSecret: "test-secret",
|
||||
}
|
||||
|
||||
got, err := NewIssuer(options)
|
||||
got, err := NewIssuer(config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -129,7 +114,7 @@ func TestNewIssuerGenerateSignKey(t *testing.T) {
|
||||
|
||||
func Test_issuer_IssueTo(t *testing.T) {
|
||||
type fields struct {
|
||||
name string
|
||||
url string
|
||||
secret []byte
|
||||
maximumClockSkew time.Duration
|
||||
}
|
||||
@@ -146,7 +131,7 @@ func Test_issuer_IssueTo(t *testing.T) {
|
||||
{
|
||||
name: "token is successfully issued",
|
||||
fields: fields{
|
||||
name: "kubesphere",
|
||||
url: "kubesphere",
|
||||
secret: []byte("kubesphere"),
|
||||
maximumClockSkew: 0,
|
||||
},
|
||||
@@ -173,7 +158,7 @@ func Test_issuer_IssueTo(t *testing.T) {
|
||||
{
|
||||
name: "token is successfully issued",
|
||||
fields: fields{
|
||||
name: "kubesphere",
|
||||
url: "kubesphere",
|
||||
secret: []byte("kubesphere"),
|
||||
maximumClockSkew: 0,
|
||||
},
|
||||
@@ -202,7 +187,7 @@ func Test_issuer_IssueTo(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &issuer{
|
||||
name: tt.fields.name,
|
||||
url: tt.fields.url,
|
||||
secret: tt.fields.secret,
|
||||
maximumClockSkew: tt.fields.maximumClockSkew,
|
||||
}
|
||||
@@ -220,7 +205,7 @@ func Test_issuer_IssueTo(t *testing.T) {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, got.TokenType, tt.want.TokenType)
|
||||
assert.Equal(t, got.Issuer, tt.fields.name)
|
||||
assert.Equal(t, got.Issuer, tt.fields.url)
|
||||
assert.Equal(t, got.Username, tt.want.Username)
|
||||
assert.Equal(t, got.Subject, tt.want.User.GetName())
|
||||
assert.NotZero(t, got.IssuedAt)
|
||||
@@ -230,7 +215,7 @@ func Test_issuer_IssueTo(t *testing.T) {
|
||||
|
||||
func Test_issuer_Verify(t *testing.T) {
|
||||
type fields struct {
|
||||
name string
|
||||
url string
|
||||
secret []byte
|
||||
maximumClockSkew time.Duration
|
||||
}
|
||||
@@ -247,7 +232,7 @@ func Test_issuer_Verify(t *testing.T) {
|
||||
{
|
||||
name: "token validation failed",
|
||||
fields: fields{
|
||||
name: "kubesphere",
|
||||
url: "kubesphere",
|
||||
secret: []byte("kubesphere"),
|
||||
maximumClockSkew: 0,
|
||||
},
|
||||
@@ -257,7 +242,7 @@ func Test_issuer_Verify(t *testing.T) {
|
||||
{
|
||||
name: "token is successfully verified",
|
||||
fields: fields{
|
||||
name: "kubesphere",
|
||||
url: "kubesphere",
|
||||
secret: []byte("kubesphere"),
|
||||
maximumClockSkew: 0,
|
||||
},
|
||||
@@ -277,7 +262,7 @@ func Test_issuer_Verify(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &issuer{
|
||||
name: tt.fields.name,
|
||||
url: tt.fields.url,
|
||||
secret: tt.fields.secret,
|
||||
maximumClockSkew: tt.fields.maximumClockSkew,
|
||||
}
|
||||
@@ -290,7 +275,7 @@ func Test_issuer_Verify(t *testing.T) {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, got.TokenType, tt.want.TokenType)
|
||||
assert.Equal(t, got.Issuer, tt.fields.name)
|
||||
assert.Equal(t, got.Issuer, tt.fields.url)
|
||||
assert.Equal(t, got.Username, tt.want.Username)
|
||||
assert.Equal(t, got.Subject, tt.want.User.GetName())
|
||||
assert.NotZero(t, got.IssuedAt)
|
||||
@@ -335,7 +320,7 @@ func Test_issuer_keyFunc(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s, err := NewIssuer(authentication.NewOptions())
|
||||
s, err := NewIssuer(oauth.NewIssuerOptions())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user