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:
KubeSphere CI Bot
2024-09-06 11:05:52 +08:00
committed by GitHub
parent b5015ec7b9
commit 447a51f08b
8557 changed files with 546695 additions and 1146174 deletions

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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 {

View 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
}

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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")

View File

@@ -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
}

View File

@@ -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)
}
})
}
}

View 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
}

View File

@@ -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 {
// dont 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
}

View File

@@ -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
}

View File

@@ -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)
}
})
}
}

View File

@@ -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 {
// dont 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
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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

View File

@@ -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 {

View File

@@ -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")

View File

@@ -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)
}

View 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
}

View 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)
}
}

View File

@@ -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 {

View File

@@ -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"

View File

@@ -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 dont 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,
}
}

View File

@@ -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)
}
}

View File

@@ -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 dont 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.")
}

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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