From 9b9d4021ec9d99695ad571794344e7bcc41a5518 Mon Sep 17 00:00:00 2001 From: hongming Date: Thu, 26 Mar 2020 06:43:20 +0800 Subject: [PATCH] implement identity provider and built-in oauth server Signed-off-by: hongming --- cmd/controller-manager/app/options/options.go | 6 +- cmd/controller-manager/app/server.go | 5 +- cmd/ks-apiserver/app/options/options.go | 59 +- cmd/ks-apiserver/app/server.go | 13 +- pkg/apiserver/apiserver.go | 24 +- .../authenticators/jwttoken/jwt_token.go | 2 +- .../identity_provider.go} | 13 +- pkg/apiserver/authentication/oauth/github.go | 157 ++++++ .../authentication/oauth/oauth_options.go | 203 +++++++ .../authentication/oauth/simple_config.go | 37 -- .../options/authenticate_options.go} | 14 +- pkg/apiserver/authentication/token/issuer.go | 4 +- pkg/apiserver/authentication/token/jwt.go | 30 +- .../authentication/token/jwt_test.go | 8 +- pkg/apiserver/config/config.go | 76 ++- pkg/apiserver/config/config_test.go | 84 ++- .../workspace/workspace_controller.go | 517 +----------------- pkg/kapis/iam/v1alpha2/handler.go | 4 +- pkg/kapis/iam/v1alpha2/register.go | 4 +- pkg/kapis/oauth/handler.go | 86 ++- pkg/kapis/oauth/register.go | 16 +- pkg/kapis/serverconfig/register.go | 45 ++ pkg/models/iam/am/fake_operator.go | 4 +- pkg/simple/client/kubesphere/kubesphere.go | 349 ------------ pkg/simple/client/kubesphere/options.go | 40 -- 25 files changed, 637 insertions(+), 1163 deletions(-) rename pkg/apiserver/authentication/{oauth/oauth.go => identityprovider/identity_provider.go} (76%) create mode 100644 pkg/apiserver/authentication/oauth/github.go create mode 100644 pkg/apiserver/authentication/oauth/oauth_options.go delete mode 100644 pkg/apiserver/authentication/oauth/simple_config.go rename pkg/{api/auth/authenticate.go => apiserver/authentication/options/authenticate_options.go} (84%) create mode 100644 pkg/kapis/serverconfig/register.go delete mode 100644 pkg/simple/client/kubesphere/kubesphere.go delete mode 100644 pkg/simple/client/kubesphere/options.go diff --git a/cmd/controller-manager/app/options/options.go b/cmd/controller-manager/app/options/options.go index 3f8f8f2a..272bc400 100644 --- a/cmd/controller-manager/app/options/options.go +++ b/cmd/controller-manager/app/options/options.go @@ -9,7 +9,6 @@ import ( kubesphereconfig "kubesphere.io/kubesphere/pkg/apiserver/config" "kubesphere.io/kubesphere/pkg/simple/client/devops/jenkins" "kubesphere.io/kubesphere/pkg/simple/client/k8s" - "kubesphere.io/kubesphere/pkg/simple/client/kubesphere" "kubesphere.io/kubesphere/pkg/simple/client/openpitrix" "kubesphere.io/kubesphere/pkg/simple/client/s3" "strings" @@ -21,9 +20,7 @@ type KubeSphereControllerManagerOptions struct { DevopsOptions *jenkins.Options S3Options *s3.Options OpenPitrixOptions *openpitrix.Options - KubeSphereOptions *kubesphere.Options - - LeaderElection *leaderelection.LeaderElectionConfig + LeaderElection *leaderelection.LeaderElectionConfig } func NewKubeSphereControllerManagerOptions() *KubeSphereControllerManagerOptions { @@ -32,7 +29,6 @@ func NewKubeSphereControllerManagerOptions() *KubeSphereControllerManagerOptions DevopsOptions: jenkins.NewDevopsOptions(), S3Options: s3.NewS3Options(), OpenPitrixOptions: openpitrix.NewOptions(), - KubeSphereOptions: kubesphere.NewKubeSphereOptions(), LeaderElection: &leaderelection.LeaderElectionConfig{ LeaseDuration: 30 * time.Second, RenewDeadline: 15 * time.Second, diff --git a/cmd/controller-manager/app/server.go b/cmd/controller-manager/app/server.go index ea99db9f..6aa8e3c8 100644 --- a/cmd/controller-manager/app/server.go +++ b/cmd/controller-manager/app/server.go @@ -38,7 +38,6 @@ import ( "kubesphere.io/kubesphere/pkg/controller/workspace" "kubesphere.io/kubesphere/pkg/informers" "kubesphere.io/kubesphere/pkg/simple/client/k8s" - kclient "kubesphere.io/kubesphere/pkg/simple/client/kubesphere" "kubesphere.io/kubesphere/pkg/simple/client/openpitrix" "kubesphere.io/kubesphere/pkg/utils/term" "os" @@ -96,8 +95,6 @@ func Run(s *options.KubeSphereControllerManagerOptions, stopCh <-chan struct{}) return err } - kubesphereClient := kclient.NewKubeSphereClient(s.KubeSphereOptions) - openpitrixClient, err := openpitrix.NewClient(s.OpenPitrixOptions) if err != nil { klog.Errorf("Failed to create openpitrix client %v", err) @@ -136,7 +133,7 @@ func Run(s *options.KubeSphereControllerManagerOptions, stopCh <-chan struct{}) } klog.V(0).Info("Setting up controllers") - err = workspace.Add(mgr, kubesphereClient) + err = workspace.Add(mgr) if err != nil { klog.Fatal("Unable to create workspace controller") } diff --git a/cmd/ks-apiserver/app/options/options.go b/cmd/ks-apiserver/app/options/options.go index 5c41826b..1267a09f 100644 --- a/cmd/ks-apiserver/app/options/options.go +++ b/cmd/ks-apiserver/app/options/options.go @@ -6,8 +6,9 @@ import ( "fmt" cliflag "k8s.io/component-base/cli/flag" "k8s.io/klog" - "kubesphere.io/kubesphere/pkg/api/auth" "kubesphere.io/kubesphere/pkg/apiserver" + authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options" + apiserverconfig "kubesphere.io/kubesphere/pkg/apiserver/config" "kubesphere.io/kubesphere/pkg/informers" genericoptions "kubesphere.io/kubesphere/pkg/server/options" "kubesphere.io/kubesphere/pkg/simple/client/cache" @@ -29,42 +30,32 @@ import ( type ServerRunOptions struct { ConfigFile string GenericServerRunOptions *genericoptions.ServerRunOptions - KubernetesOptions *k8s.KubernetesOptions - DevopsOptions *jenkins.Options - SonarQubeOptions *sonarqube.Options - ServiceMeshOptions *servicemesh.Options - MySQLOptions *mysql.Options - MonitoringOptions *prometheus.Options - S3Options *s3.Options - OpenPitrixOptions *openpitrix.Options - LoggingOptions *esclient.Options - LdapOptions *ldap.Options - CacheOptions *cache.Options - AuthenticateOptions *auth.AuthenticationOptions + *apiserverconfig.Config // DebugMode bool } func NewServerRunOptions() *ServerRunOptions { - - s := ServerRunOptions{ + s := &ServerRunOptions{ GenericServerRunOptions: genericoptions.NewServerRunOptions(), - KubernetesOptions: k8s.NewKubernetesOptions(), - DevopsOptions: jenkins.NewDevopsOptions(), - SonarQubeOptions: sonarqube.NewSonarQubeOptions(), - ServiceMeshOptions: servicemesh.NewServiceMeshOptions(), - MySQLOptions: mysql.NewMySQLOptions(), - MonitoringOptions: prometheus.NewPrometheusOptions(), - S3Options: s3.NewS3Options(), - OpenPitrixOptions: openpitrix.NewOptions(), - LoggingOptions: esclient.NewElasticSearchOptions(), - LdapOptions: ldap.NewOptions(), - CacheOptions: cache.NewRedisOptions(), - AuthenticateOptions: auth.NewAuthenticateOptions(), + Config: &apiserverconfig.Config{ + KubernetesOptions: k8s.NewKubernetesOptions(), + DevopsOptions: jenkins.NewDevopsOptions(), + SonarQubeOptions: sonarqube.NewSonarQubeOptions(), + ServiceMeshOptions: servicemesh.NewServiceMeshOptions(), + MySQLOptions: mysql.NewMySQLOptions(), + MonitoringOptions: prometheus.NewPrometheusOptions(), + S3Options: s3.NewS3Options(), + OpenPitrixOptions: openpitrix.NewOptions(), + LoggingOptions: esclient.NewElasticSearchOptions(), + LdapOptions: ldap.NewOptions(), + RedisOptions: cache.NewRedisOptions(), + AuthenticationOptions: authoptions.NewAuthenticateOptions(), + }, } - return &s + return s } func (s *ServerRunOptions) Flags() (fss cliflag.NamedFlagSets) { @@ -72,12 +63,12 @@ func (s *ServerRunOptions) Flags() (fss cliflag.NamedFlagSets) { fs.BoolVar(&s.DebugMode, "debug", false, "Don't enable this if you don't know what it means.") s.GenericServerRunOptions.AddFlags(fs, s.GenericServerRunOptions) s.KubernetesOptions.AddFlags(fss.FlagSet("kubernetes"), s.KubernetesOptions) - s.AuthenticateOptions.AddFlags(fss.FlagSet("authenticate"), s.AuthenticateOptions) + s.AuthenticationOptions.AddFlags(fss.FlagSet("authentication"), s.AuthenticationOptions) s.MySQLOptions.AddFlags(fss.FlagSet("mysql"), s.MySQLOptions) s.DevopsOptions.AddFlags(fss.FlagSet("devops"), s.DevopsOptions) s.SonarQubeOptions.AddFlags(fss.FlagSet("sonarqube"), s.SonarQubeOptions) s.LdapOptions.AddFlags(fss.FlagSet("ldap"), s.LdapOptions) - s.CacheOptions.AddFlags(fss.FlagSet("cache"), s.CacheOptions) + s.RedisOptions.AddFlags(fss.FlagSet("redis"), s.RedisOptions) s.S3Options.AddFlags(fss.FlagSet("s3"), s.S3Options) s.OpenPitrixOptions.AddFlags(fss.FlagSet("openpitrix"), s.OpenPitrixOptions) s.ServiceMeshOptions.AddFlags(fss.FlagSet("servicemesh"), s.ServiceMeshOptions) @@ -100,7 +91,7 @@ const fakeInterface string = "FAKE" // NewAPIServer creates an APIServer instance using given options func (s *ServerRunOptions) NewAPIServer(stopCh <-chan struct{}) (*apiserver.APIServer, error) { apiServer := &apiserver.APIServer{ - AuthenticateOptions: s.AuthenticateOptions, + Config: s.Config, } kubernetesClient, err := k8s.NewKubernetesClient(s.KubernetesOptions) @@ -156,11 +147,11 @@ func (s *ServerRunOptions) NewAPIServer(stopCh <-chan struct{}) (*apiserver.APIS } var cacheClient cache.Interface - if s.CacheOptions.Host != "" { - if s.CacheOptions.Host == fakeInterface && s.DebugMode { + if s.RedisOptions.Host != "" { + if s.RedisOptions.Host == fakeInterface && s.DebugMode { apiServer.CacheClient = cache.NewSimpleCache() } else { - cacheClient, err = cache.NewRedisClient(s.CacheOptions, stopCh) + cacheClient, err = cache.NewRedisClient(s.RedisOptions, stopCh) if err != nil { return nil, err } diff --git a/cmd/ks-apiserver/app/server.go b/cmd/ks-apiserver/app/server.go index 78caa399..2f6d68f2 100644 --- a/cmd/ks-apiserver/app/server.go +++ b/cmd/ks-apiserver/app/server.go @@ -37,18 +37,7 @@ func NewAPIServerCommand() *cobra.Command { if err == nil { s = &options.ServerRunOptions{ GenericServerRunOptions: s.GenericServerRunOptions, - KubernetesOptions: conf.KubernetesOptions, - DevopsOptions: conf.DevopsOptions, - SonarQubeOptions: conf.SonarQubeOptions, - ServiceMeshOptions: conf.ServiceMeshOptions, - MySQLOptions: conf.MySQLOptions, - MonitoringOptions: conf.MonitoringOptions, - S3Options: conf.S3Options, - OpenPitrixOptions: conf.OpenPitrixOptions, - LoggingOptions: conf.LoggingOptions, - LdapOptions: conf.LdapOptions, - CacheOptions: conf.RedisOptions, - AuthenticateOptions: conf.AuthenticateOptions, + Config: conf, } } diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index 7f83560d..c39a7be5 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -12,31 +12,29 @@ import ( unionauth "k8s.io/apiserver/pkg/authentication/request/union" "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" "k8s.io/klog" - "kubesphere.io/kubesphere/pkg/api/auth" "kubesphere.io/kubesphere/pkg/apiserver/authentication/authenticators/basic" "kubesphere.io/kubesphere/pkg/apiserver/authentication/authenticators/jwttoken" - oauth2 "kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth" "kubesphere.io/kubesphere/pkg/apiserver/authentication/request/anonymous" "kubesphere.io/kubesphere/pkg/apiserver/authentication/request/basictoken" "kubesphere.io/kubesphere/pkg/apiserver/authentication/token" "kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizerfactory" "kubesphere.io/kubesphere/pkg/apiserver/authorization/path" unionauthorizer "kubesphere.io/kubesphere/pkg/apiserver/authorization/union" + apiserverconfig "kubesphere.io/kubesphere/pkg/apiserver/config" "kubesphere.io/kubesphere/pkg/apiserver/dispatch" "kubesphere.io/kubesphere/pkg/apiserver/filters" "kubesphere.io/kubesphere/pkg/apiserver/request" "kubesphere.io/kubesphere/pkg/informers" - devopsv1alpha2 "kubesphere.io/kubesphere/pkg/kapis/devops/v1alpha2" iamv1alpha2 "kubesphere.io/kubesphere/pkg/kapis/iam/v1alpha2" loggingv1alpha2 "kubesphere.io/kubesphere/pkg/kapis/logging/v1alpha2" monitoringv1alpha2 "kubesphere.io/kubesphere/pkg/kapis/monitoring/v1alpha2" - "kubesphere.io/kubesphere/pkg/kapis/oauth" + oauth "kubesphere.io/kubesphere/pkg/kapis/oauth" openpitrixv1 "kubesphere.io/kubesphere/pkg/kapis/openpitrix/v1" operationsv1alpha2 "kubesphere.io/kubesphere/pkg/kapis/operations/v1alpha2" resourcesv1alpha2 "kubesphere.io/kubesphere/pkg/kapis/resources/v1alpha2" resourcev1alpha3 "kubesphere.io/kubesphere/pkg/kapis/resources/v1alpha3" + "kubesphere.io/kubesphere/pkg/kapis/serverconfig" servicemeshv1alpha2 "kubesphere.io/kubesphere/pkg/kapis/servicemesh/metrics/v1alpha2" - tenantv1alpha2 "kubesphere.io/kubesphere/pkg/kapis/tenant/v1alpha2" terminalv1alpha2 "kubesphere.io/kubesphere/pkg/kapis/terminal/v1alpha2" "kubesphere.io/kubesphere/pkg/models/iam/am" "kubesphere.io/kubesphere/pkg/models/iam/im" @@ -75,7 +73,7 @@ type APIServer struct { // Server *http.Server - AuthenticateOptions *auth.AuthenticationOptions + Config *apiserverconfig.Config // webservice container, where all webservice defines container *restful.Container @@ -135,19 +133,19 @@ func (s *APIServer) PrepareRun() error { } func (s *APIServer) installKubeSphereAPIs() { - + urlruntime.Must(serverconfig.AddToContainer(s.container, s.Config)) urlruntime.Must(resourcev1alpha3.AddToContainer(s.container, s.InformerFactory)) // Need to refactor devops api registration, too much dependencies - urlruntime.Must(devopsv1alpha2.AddToContainer(s.container, s.DevopsClient, s.DBClient.Database(), nil, s.KubernetesClient.KubeSphere(), s.InformerFactory.KubeSphereSharedInformerFactory(), s.S3Client)) + //urlruntime.Must(devopsv1alpha2.AddToContainer(s.container, s.DevopsClient, s.DBClient.Database(), nil, s.KubernetesClient.KubeSphere(), s.InformerFactory.KubeSphereSharedInformerFactory(), s.S3Client)) urlruntime.Must(loggingv1alpha2.AddToContainer(s.container, s.KubernetesClient, s.LoggingClient)) urlruntime.Must(monitoringv1alpha2.AddToContainer(s.container, s.KubernetesClient, s.MonitoringClient)) urlruntime.Must(openpitrixv1.AddToContainer(s.container, s.InformerFactory, s.OpenpitrixClient)) urlruntime.Must(operationsv1alpha2.AddToContainer(s.container, s.KubernetesClient.Kubernetes())) urlruntime.Must(resourcesv1alpha2.AddToContainer(s.container, s.KubernetesClient.Kubernetes(), s.InformerFactory)) - urlruntime.Must(tenantv1alpha2.AddToContainer(s.container, s.KubernetesClient, s.InformerFactory, s.DBClient.Database())) + //urlruntime.Must(tenantv1alpha2.AddToContainer(s.container, s.KubernetesClient, s.InformerFactory, s.DBClient.Database())) urlruntime.Must(terminalv1alpha2.AddToContainer(s.container, s.KubernetesClient.Kubernetes(), s.KubernetesClient.Config())) - urlruntime.Must(iamv1alpha2.AddToContainer(s.container, s.KubernetesClient, s.InformerFactory, s.LdapClient, s.CacheClient, s.AuthenticateOptions)) - urlruntime.Must(oauth.AddToContainer(s.container, token.NewJwtTokenIssuer(token.DefaultIssuerName, s.AuthenticateOptions, s.CacheClient), &oauth2.SimpleConfigManager{})) + urlruntime.Must(iamv1alpha2.AddToContainer(s.container, s.KubernetesClient, s.InformerFactory, s.LdapClient, s.CacheClient, s.Config.AuthenticationOptions)) + urlruntime.Must(oauth.AddToContainer(s.container, token.NewJwtTokenIssuer(token.DefaultIssuerName, s.Config.AuthenticationOptions, s.CacheClient), s.Config.AuthenticationOptions)) urlruntime.Must(servicemeshv1alpha2.AddToContainer(s.container)) } @@ -187,7 +185,7 @@ func (s *APIServer) buildHandlerChain() { handler = filters.WithKubeAPIServer(handler, s.KubernetesClient.Config(), &errorResponder{}) handler = filters.WithMultipleClusterDispatcher(handler, dispatch.DefaultClusterDispatch) - excludedPaths := []string{"/oauth/authorize", "/oauth/token"} + excludedPaths := []string{"/oauth/*", "/server/configs/*"} pathAuthorizer, _ := path.NewAuthorizer(excludedPaths) authorizer := unionauthorizer.New(pathAuthorizer, authorizerfactory.NewOPAAuthorizer(am.NewFakeAMOperator())) @@ -196,7 +194,7 @@ func (s *APIServer) buildHandlerChain() { authn := unionauth.New(anonymous.NewAuthenticator(), basictoken.New(basic.NewBasicAuthenticator(im.NewFakeOperator())), bearertoken.New(jwttoken.NewTokenAuthenticator( - token.NewJwtTokenIssuer(token.DefaultIssuerName, s.AuthenticateOptions, s.CacheClient)))) + token.NewJwtTokenIssuer(token.DefaultIssuerName, s.Config.AuthenticationOptions, s.CacheClient)))) handler = filters.WithAuthentication(handler, authn) handler = filters.WithRequestInfo(handler, requestInfoResolver) s.Server.Handler = handler diff --git a/pkg/apiserver/authentication/authenticators/jwttoken/jwt_token.go b/pkg/apiserver/authentication/authenticators/jwttoken/jwt_token.go index bdbe8bd4..a8bea8f1 100644 --- a/pkg/apiserver/authentication/authenticators/jwttoken/jwt_token.go +++ b/pkg/apiserver/authentication/authenticators/jwttoken/jwt_token.go @@ -23,7 +23,7 @@ func NewTokenAuthenticator(issuer token2.Issuer) authenticator.Token { } func (t *tokenAuthenticator) AuthenticateToken(ctx context.Context, token string) (*authenticator.Response, bool, error) { - providedUser, _, err := t.jwtTokenIssuer.Verify(token) + providedUser, err := t.jwtTokenIssuer.Verify(token) if err != nil { return nil, false, err } diff --git a/pkg/apiserver/authentication/oauth/oauth.go b/pkg/apiserver/authentication/identityprovider/identity_provider.go similarity index 76% rename from pkg/apiserver/authentication/oauth/oauth.go rename to pkg/apiserver/authentication/identityprovider/identity_provider.go index 12a9a397..7601723a 100644 --- a/pkg/apiserver/authentication/oauth/oauth.go +++ b/pkg/apiserver/authentication/identityprovider/identity_provider.go @@ -16,15 +16,10 @@ * / */ -package oauth +package identityprovider -import ( - "errors" - "golang.org/x/oauth2" -) +import "k8s.io/apiserver/pkg/authentication/user" -var ConfigNotFound = errors.New("config not found") - -type Configuration interface { - Load(clientId string) (*oauth2.Config, error) +type OAuth2Interface interface { + IdentityExchange(code string) (user.Info, error) } diff --git a/pkg/apiserver/authentication/oauth/github.go b/pkg/apiserver/authentication/oauth/github.go new file mode 100644 index 00000000..ac12c13c --- /dev/null +++ b/pkg/apiserver/authentication/oauth/github.go @@ -0,0 +1,157 @@ +/* + * + * 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 ( + "context" + "encoding/json" + "golang.org/x/oauth2" + "io/ioutil" + "k8s.io/apiserver/pkg/authentication/user" + "time" +) + +const ( + UserInfoURL = "https://api.github.com/user" +) + +type Github 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 github.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"` + + // Scope specifies optional requested permissions. + Scopes []string `json:"scopes" yaml:"scopes"` +} + +// 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"` +} + +type GithubIdentity struct { + Login string `json:"login"` + ID string `json:"id"` + NodeID string `json:"node_id"` + AvatarURL string `json:"avatar_url"` + GravatarID string `json:"gravatar_id"` + URL string `json:"url"` + HTMLURL string `json:"html_url"` + FollowersURL string `json:"followers_url"` + FollowingURL string `json:"following_url"` + GistsURL string `json:"gists_url"` + StarredURL string `json:"starred_url"` + SubscriptionsURL string `json:"subscriptions_url"` + OrganizationsURL string `json:"organizations_url"` + ReposURL string `json:"repos_url"` + EventsURL string `json:"events_url"` + ReceivedEventsURL string `json:"received_events_url"` + Type string `json:"type"` + SiteAdmin bool `json:"site_admin"` + Name string `json:"name"` + Company string `json:"company"` + Blog string `json:"blog"` + Location string `json:"location"` + Email string `json:"email"` + Hireable bool `json:"hireable"` + Bio string `json:"bio"` + PublicRepos int `json:"public_repos"` + PublicGists int `json:"public_gists"` + Followers int `json:"followers"` + Following int `json:"following"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + PrivateGists int `json:"private_gists"` + TotalPrivateRepos int `json:"total_private_repos"` + OwnedPrivateRepos int `json:"owned_private_repos"` + DiskUsage int `json:"disk_usage"` + Collaborators int `json:"collaborators"` +} + +func (g GithubIdentity) GetName() string { + return g.Login +} + +func (g GithubIdentity) GetUID() string { + return g.ID +} + +func (g GithubIdentity) GetGroups() []string { + return nil +} + +func (g GithubIdentity) GetExtra() map[string][]string { + return nil +} + +func (g *Github) IdentityExchange(code string) (user.Info, error) { + config := oauth2.Config{ + ClientID: g.ClientID, + ClientSecret: g.ClientSecret, + Endpoint: oauth2.Endpoint{ + AuthURL: g.Endpoint.AuthURL, + TokenURL: g.Endpoint.TokenURL, + AuthStyle: oauth2.AuthStyleAutoDetect, + }, + RedirectURL: g.RedirectURL, + Scopes: g.Scopes, + } + token, err := config.Exchange(context.Background(), code) + + if err != nil { + return nil, err + } + + resp, err := oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(token)).Get(UserInfoURL) + + if err != nil { + return nil, err + } + + data, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + + if err != nil { + return nil, err + } + + var githubIdentity GithubIdentity + + err = json.Unmarshal(data, &githubIdentity) + + if err != nil { + return nil, err + } + + return githubIdentity, nil +} diff --git a/pkg/apiserver/authentication/oauth/oauth_options.go b/pkg/apiserver/authentication/oauth/oauth_options.go new file mode 100644 index 00000000..2d0c441e --- /dev/null +++ b/pkg/apiserver/authentication/oauth/oauth_options.go @@ -0,0 +1,203 @@ +/* + * + * 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 ( + "errors" + "kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider" + "kubesphere.io/kubesphere/pkg/utils/sliceutil" +) + +type GrantHandlerType string +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 user name 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 MappingMethod = "mixed" + + IdentityProviderTypeGithub = "github" +) + +var ( + ErrorClientNotFound = errors.New("the OAuth client was not found") + ErrorRedirectURLNotAllowed = errors.New("redirect URL is not allowed") + ErrorIdentityProviderNotFound = errors.New("the identity provider was not found") +) + +type Options struct { + // LDAPPasswordIdentityProvider provider is used by default. + IdentityProviders []IdentityProvider `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 int `json:"accessTokenMaxAgeSeconds" yaml:"accessTokenMaxAgeSeconds"` + + // Inactivity timeout for tokens + // The value represents the maximum amount of time that can occur between + // consecutive uses of the token. Tokens become invalid if they are not + // used within this temporal window. The user will need to acquire a new + // token to regain access once a token times out. + // This value needs to be set only if the default set in configuration is + // not appropriate for this client. Valid values are: + // - 0: Tokens for this client never time out + // - X: Tokens time out if there is no activity for X seconds + // The current minimum allowed value for X is 300 (5 minutes) + AccessTokenInactivityTimeoutSeconds int `json:"accessTokenInactivityTimeoutSeconds" yaml:"accessTokenInactivityTimeoutSeconds"` +} + +type IdentityProvider 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. + // - 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. + MappingMethod MappingMethod `json:"mappingMethod" yaml:"mappingMethod"` + + // When true, unauthenticated token requests from web clients (like the web console) + // are redirected to a login page (with WWW-Authenticate challenge header) backed by this provider. + LoginRedirect bool `json:"loginRedirect" yaml:"loginRedirect"` + + // The type of identify provider + Type string `json:"type" yaml:"type"` + + // Provider options + Github *Github `json:"github" yaml:"github" ` +} + +type Token struct { + // AccessToken is the token that authorizes and authenticates + // the requests. + AccessToken string `json:"access_token"` + + // TokenType is the type of token. + // 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 + // (as opposed to the user) to refresh the access token + // if it expires. + RefreshToken string `json:"refresh_token,omitempty"` + + // ExpiresIn is the optional expiration second of the access token. + 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 /oauth/authorize + // and /oauth/token. + Name string + + // 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"` + + // AccessTokenMaxAgeSeconds overrides the default access token max age for tokens granted to this client. + AccessTokenMaxAgeSeconds *int `json:"accessTokenMaxAgeSeconds,omitempty" yaml:"accessTokenMaxAgeSeconds,omitempty"` + + // AccessTokenInactivityTimeoutSeconds overrides the default token + // inactivity timeout for tokens granted to this client. + AccessTokenInactivityTimeoutSeconds *int `json:"accessTokenInactivityTimeoutSeconds,omitempty" yaml:"accessTokenInactivityTimeoutSeconds,omitempty"` +} + +func (o *Options) GetOAuthClient(name string) (Client, error) { + for _, found := range o.Clients { + if found.Name == name { + return found, nil + } + } + return Client{}, ErrorClientNotFound +} +func (o *Options) GetIdentityProvider(name string) (IdentityProvider, error) { + for _, found := range o.IdentityProviders { + if found.Name == name { + return found, nil + } + } + return IdentityProvider{}, ErrorClientNotFound +} + +func (c Client) ResolveRedirectURL(expectURL string) (string, error) { + if len(c.RedirectURIs) == 0 { + return "", ErrorRedirectURLNotAllowed + } + if expectURL == "" { + return c.RedirectURIs[0], nil + } + if sliceutil.HasString(c.RedirectURIs, expectURL) { + return expectURL, nil + } + return "", ErrorRedirectURLNotAllowed +} + +func (i IdentityProvider) GetOAuth2IdentityProviderInstance() (identityprovider.OAuth2Interface, error) { + switch i.Type { + case IdentityProviderTypeGithub: + if i.Github != nil { + return i.Github, nil + } + } + return nil, ErrorIdentityProviderNotFound +} + +func NewOptions() *Options { + return &Options{ + IdentityProviders: make([]IdentityProvider, 0), + Clients: make([]Client, 0), + AccessTokenMaxAgeSeconds: 86400, + AccessTokenInactivityTimeoutSeconds: 0, + } +} diff --git a/pkg/apiserver/authentication/oauth/simple_config.go b/pkg/apiserver/authentication/oauth/simple_config.go deleted file mode 100644 index 7e059756..00000000 --- a/pkg/apiserver/authentication/oauth/simple_config.go +++ /dev/null @@ -1,37 +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 "golang.org/x/oauth2" - -type SimpleConfigManager struct { -} - -func (s *SimpleConfigManager) Load(clientId string) (*oauth2.Config, error) { - if clientId == "kubesphere-console-client" { - return &oauth2.Config{ - ClientID: "8b21fef43889a28f2bd6", - ClientSecret: "xb21fef43889a28f2bd6", - Endpoint: oauth2.Endpoint{AuthURL: "http://ks-apiserver.kubesphere-system.svc/oauth/authorize", TokenURL: "http://ks-apiserver.kubesphere.io/oauth/token"}, - RedirectURL: "http://ks-console.kubesphere-system.svc/oauth/token/implicit", - Scopes: nil, - }, nil - } - return nil, ConfigNotFound -} diff --git a/pkg/api/auth/authenticate.go b/pkg/apiserver/authentication/options/authenticate_options.go similarity index 84% rename from pkg/api/auth/authenticate.go rename to pkg/apiserver/authentication/options/authenticate_options.go index 522e3163..a9759904 100644 --- a/pkg/api/auth/authenticate.go +++ b/pkg/apiserver/authentication/options/authenticate_options.go @@ -16,11 +16,12 @@ * / */ -package auth +package options import ( "fmt" "github.com/spf13/pflag" + "kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth" "time" ) @@ -32,15 +33,13 @@ type AuthenticationOptions struct { // maximum retries when authenticate failed MaxAuthenticateRetries int `json:"maxAuthenticateRetries" yaml:"maxAuthenticateRetries"` - // token validation duration, will refresh token expiration for each user request - // 0 means never expire - TokenExpiration time.Duration `json:"tokenExpiration" yaml:"tokenExpiration"` - // allow multiple users login at the same time MultipleLogin bool `json:"multipleLogin" yaml:"multipleLogin"` // secret to signed jwt token - JwtSecret string `json:"jwtSecret" yaml:"jwtSecret"` + JwtSecret string `json:"-" yaml:"jwtSecret"` + + OAuthOptions *oauth.Options `json:"oauthOptions" yaml:"oauthOptions"` } func NewAuthenticateOptions() *AuthenticationOptions { @@ -48,7 +47,7 @@ func NewAuthenticateOptions() *AuthenticationOptions { AuthenticateRateLimiterMaxTries: 5, AuthenticateRateLimiterDuration: time.Minute * 30, MaxAuthenticateRetries: 0, - TokenExpiration: 0, + OAuthOptions: oauth.NewOptions(), MultipleLogin: false, JwtSecret: "", } @@ -68,7 +67,6 @@ func (options *AuthenticationOptions) AddFlags(fs *pflag.FlagSet, s *Authenticat fs.IntVar(&options.AuthenticateRateLimiterMaxTries, "authenticate-rate-limiter-max-retries", s.AuthenticateRateLimiterMaxTries, "") fs.DurationVar(&options.AuthenticateRateLimiterDuration, "authenticate-rate-limiter-duration", s.AuthenticateRateLimiterDuration, "") fs.IntVar(&options.MaxAuthenticateRetries, "authenticate-max-retries", s.MaxAuthenticateRetries, "") - fs.DurationVar(&options.TokenExpiration, "token-expiration", s.TokenExpiration, "Token expire duration, for example 30m/2h/1d, 0 means token never expire unless server restart.") 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.") } diff --git a/pkg/apiserver/authentication/token/issuer.go b/pkg/apiserver/authentication/token/issuer.go index 5c1fdfa1..65eab50d 100644 --- a/pkg/apiserver/authentication/token/issuer.go +++ b/pkg/apiserver/authentication/token/issuer.go @@ -21,10 +21,10 @@ package token // Issuer issues token to user, tokens are required to perform mutating requests to resources type Issuer interface { // IssueTo issues a token a User, return error if issuing process failed - IssueTo(User) (string, *Claims, error) + IssueTo(user User, expiresInSecond int) (string, error) // Verify verifies a token, and return a User if it's a valid token, otherwise return error - Verify(string) (User, *Claims, error) + Verify(string) (User, error) // Revoke a token, Revoke(token string) error diff --git a/pkg/apiserver/authentication/token/jwt.go b/pkg/apiserver/authentication/token/jwt.go index 10e8db30..b6b6881f 100644 --- a/pkg/apiserver/authentication/token/jwt.go +++ b/pkg/apiserver/authentication/token/jwt.go @@ -21,8 +21,8 @@ package token import ( "fmt" "github.com/dgrijalva/jwt-go" - "kubesphere.io/kubesphere/pkg/api/auth" "kubesphere.io/kubesphere/pkg/api/iam" + authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options" "kubesphere.io/kubesphere/pkg/server/errors" "kubesphere.io/kubesphere/pkg/simple/client/cache" "time" @@ -44,35 +44,35 @@ type Claims struct { type jwtTokenIssuer struct { name string - options *auth.AuthenticationOptions + options *authoptions.AuthenticationOptions cache cache.Interface keyFunc jwt.Keyfunc } -func (s *jwtTokenIssuer) Verify(tokenString string) (User, *Claims, error) { +func (s *jwtTokenIssuer) Verify(tokenString string) (User, error) { if len(tokenString) == 0 { - return nil, nil, errInvalidToken + return nil, errInvalidToken } _, err := s.cache.Get(tokenCacheKey(tokenString)) if err != nil { if err == cache.ErrNoSuchKey { - return nil, nil, errTokenExpired + return nil, errTokenExpired } - return nil, nil, err + return nil, err } clm := &Claims{} _, err = jwt.ParseWithClaims(tokenString, clm, s.keyFunc) if err != nil { - return nil, nil, err + return nil, err } - return &iam.User{Name: clm.Username, UID: clm.UID}, clm, nil + return &iam.User{Name: clm.Username, UID: clm.UID}, nil } -func (s *jwtTokenIssuer) IssueTo(user User) (string, *Claims, error) { +func (s *jwtTokenIssuer) IssueTo(user User, expiresInSecond int) (string, error) { clm := &Claims{ Username: user.GetName(), UID: user.GetUID(), @@ -83,8 +83,8 @@ func (s *jwtTokenIssuer) IssueTo(user User) (string, *Claims, error) { }, } - if s.options.TokenExpiration > 0 { - clm.ExpiresAt = clm.IssuedAt + int64(s.options.TokenExpiration.Seconds()) + if expiresInSecond > 0 { + clm.ExpiresAt = clm.IssuedAt + int64(expiresInSecond) } token := jwt.NewWithClaims(jwt.SigningMethodHS256, clm) @@ -92,19 +92,19 @@ func (s *jwtTokenIssuer) IssueTo(user User) (string, *Claims, error) { tokenString, err := token.SignedString([]byte(s.options.JwtSecret)) if err != nil { - return "", nil, err + return "", err } - s.cache.Set(tokenCacheKey(tokenString), tokenString, s.options.TokenExpiration) + s.cache.Set(tokenCacheKey(tokenString), tokenString, time.Second*time.Duration(expiresInSecond)) - return tokenString, clm, nil + return tokenString, nil } func (s *jwtTokenIssuer) Revoke(token string) error { return s.cache.Del(tokenCacheKey(token)) } -func NewJwtTokenIssuer(issuerName string, options *auth.AuthenticationOptions, cache cache.Interface) Issuer { +func NewJwtTokenIssuer(issuerName string, options *authoptions.AuthenticationOptions, cache cache.Interface) Issuer { return &jwtTokenIssuer{ name: issuerName, options: options, diff --git a/pkg/apiserver/authentication/token/jwt_test.go b/pkg/apiserver/authentication/token/jwt_test.go index bc7c0b38..dac84abb 100644 --- a/pkg/apiserver/authentication/token/jwt_test.go +++ b/pkg/apiserver/authentication/token/jwt_test.go @@ -20,14 +20,14 @@ package token import ( "github.com/google/go-cmp/cmp" - "kubesphere.io/kubesphere/pkg/api/auth" "kubesphere.io/kubesphere/pkg/api/iam" + authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options" "kubesphere.io/kubesphere/pkg/simple/client/cache" "testing" ) func TestJwtTokenIssuer(t *testing.T) { - options := auth.NewAuthenticateOptions() + options := authoptions.NewAuthenticateOptions() options.JwtSecret = "kubesphere" issuer := NewJwtTokenIssuer(DefaultIssuerName, options, cache.NewSimpleCache()) @@ -54,12 +54,12 @@ func TestJwtTokenIssuer(t *testing.T) { } t.Run(testCase.description, func(t *testing.T) { - token, _, err := issuer.IssueTo(user) + token, err := issuer.IssueTo(user, 0) if err != nil { t.Fatal(err) } - got, _, err := issuer.Verify(token) + got, err := issuer.Verify(token) if err != nil { t.Fatal(err) } diff --git a/pkg/apiserver/config/config.go b/pkg/apiserver/config/config.go index d3cfb7d0..574194b2 100644 --- a/pkg/apiserver/config/config.go +++ b/pkg/apiserver/config/config.go @@ -5,13 +5,12 @@ import ( "github.com/emicklei/go-restful" "github.com/spf13/viper" "k8s.io/apimachinery/pkg/runtime/schema" - "kubesphere.io/kubesphere/pkg/api/auth" + authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options" "kubesphere.io/kubesphere/pkg/apiserver/runtime" "kubesphere.io/kubesphere/pkg/simple/client/alerting" "kubesphere.io/kubesphere/pkg/simple/client/cache" "kubesphere.io/kubesphere/pkg/simple/client/devops/jenkins" "kubesphere.io/kubesphere/pkg/simple/client/k8s" - "kubesphere.io/kubesphere/pkg/simple/client/kubesphere" "kubesphere.io/kubesphere/pkg/simple/client/ldap" "kubesphere.io/kubesphere/pkg/simple/client/logging/elasticsearch" "kubesphere.io/kubesphere/pkg/simple/client/monitoring/prometheus" @@ -63,23 +62,18 @@ const ( // Config defines everything needed for apiserver to deal with external services type Config struct { - MySQLOptions *mysql.Options `json:"mysql,omitempty" yaml:"mysql,omitempty" mapstructure:"mysql"` - DevopsOptions *jenkins.Options `json:"devops,omitempty" yaml:"devops,omitempty" mapstructure:"devops"` - SonarQubeOptions *sonarqube.Options `json:"sonarqube,omitempty" yaml:"sonarQube,omitempty" mapstructure:"sonarqube"` - KubernetesOptions *k8s.KubernetesOptions `json:"kubernetes,omitempty" yaml:"kubernetes,omitempty" mapstructure:"kubernetes"` - ServiceMeshOptions *servicemesh.Options `json:"servicemesh,omitempty" yaml:"servicemesh,omitempty" mapstructure:"servicemesh"` - LdapOptions *ldap.Options `json:"ldap,omitempty" yaml:"ldap,omitempty" mapstructure:"ldap"` - RedisOptions *cache.Options `json:"redis,omitempty" yaml:"redis,omitempty" mapstructure:"redis"` - S3Options *s3.Options `json:"s3,omitempty" yaml:"s3,omitempty" mapstructure:"s3"` - OpenPitrixOptions *openpitrix.Options `json:"openpitrix,omitempty" yaml:"openpitrix,omitempty" mapstructure:"openpitrix"` - MonitoringOptions *prometheus.Options `json:"monitoring,omitempty" yaml:"monitoring,omitempty" mapstructure:"monitoring"` - LoggingOptions *elasticsearch.Options `json:"logging,omitempty" yaml:"logging,omitempty" mapstructure:"logging"` - - // Options below are only loaded from configuration file, no command line flags for these options now. - KubeSphereOptions *kubesphere.Options `json:"-" yaml:"kubesphere,omitempty" mapstructure:"kubesphere"` - - AuthenticateOptions *auth.AuthenticationOptions `json:"authentication,omitempty" yaml:"authenticate,omitempty" mapstructure:"authenticate"` - + MySQLOptions *mysql.Options `json:"mysql,omitempty" yaml:"mysql,omitempty" mapstructure:"mysql"` + DevopsOptions *jenkins.Options `json:"devops,omitempty" yaml:"devops,omitempty" mapstructure:"devops"` + SonarQubeOptions *sonarqube.Options `json:"sonarqube,omitempty" yaml:"sonarQube,omitempty" mapstructure:"sonarqube"` + KubernetesOptions *k8s.KubernetesOptions `json:"kubernetes,omitempty" yaml:"kubernetes,omitempty" mapstructure:"kubernetes"` + ServiceMeshOptions *servicemesh.Options `json:"servicemesh,omitempty" yaml:"servicemesh,omitempty" mapstructure:"servicemesh"` + LdapOptions *ldap.Options `json:"ldap,omitempty" yaml:"ldap,omitempty" mapstructure:"ldap"` + RedisOptions *cache.Options `json:"redis,omitempty" yaml:"redis,omitempty" mapstructure:"redis"` + S3Options *s3.Options `json:"s3,omitempty" yaml:"s3,omitempty" mapstructure:"s3"` + OpenPitrixOptions *openpitrix.Options `json:"openpitrix,omitempty" yaml:"openpitrix,omitempty" mapstructure:"openpitrix"` + MonitoringOptions *prometheus.Options `json:"monitoring,omitempty" yaml:"monitoring,omitempty" mapstructure:"monitoring"` + LoggingOptions *elasticsearch.Options `json:"logging,omitempty" yaml:"logging,omitempty" mapstructure:"logging"` + AuthenticationOptions *authoptions.AuthenticationOptions `json:"authentication,omitempty" yaml:"authentication,omitempty" mapstructure:"authentication"` // Options used for enabling components, not actually used now. Once we switch Alerting/Notification API to kubesphere, // we can add these options to kubesphere command lines AlertingOptions *alerting.Options `json:"alerting,omitempty" yaml:"alerting,omitempty" mapstructure:"alerting"` @@ -89,21 +83,20 @@ type Config struct { // newConfig creates a default non-empty Config func New() *Config { return &Config{ - MySQLOptions: mysql.NewMySQLOptions(), - DevopsOptions: jenkins.NewDevopsOptions(), - SonarQubeOptions: sonarqube.NewSonarQubeOptions(), - KubernetesOptions: k8s.NewKubernetesOptions(), - ServiceMeshOptions: servicemesh.NewServiceMeshOptions(), - LdapOptions: ldap.NewOptions(), - RedisOptions: cache.NewRedisOptions(), - S3Options: s3.NewS3Options(), - OpenPitrixOptions: openpitrix.NewOptions(), - MonitoringOptions: prometheus.NewPrometheusOptions(), - KubeSphereOptions: kubesphere.NewKubeSphereOptions(), - AlertingOptions: alerting.NewAlertingOptions(), - NotificationOptions: notification.NewNotificationOptions(), - LoggingOptions: elasticsearch.NewElasticSearchOptions(), - AuthenticateOptions: auth.NewAuthenticateOptions(), + MySQLOptions: mysql.NewMySQLOptions(), + DevopsOptions: jenkins.NewDevopsOptions(), + SonarQubeOptions: sonarqube.NewSonarQubeOptions(), + KubernetesOptions: k8s.NewKubernetesOptions(), + ServiceMeshOptions: servicemesh.NewServiceMeshOptions(), + LdapOptions: ldap.NewOptions(), + RedisOptions: cache.NewRedisOptions(), + S3Options: s3.NewS3Options(), + OpenPitrixOptions: openpitrix.NewOptions(), + MonitoringOptions: prometheus.NewPrometheusOptions(), + AlertingOptions: alerting.NewAlertingOptions(), + NotificationOptions: notification.NewNotificationOptions(), + LoggingOptions: elasticsearch.NewElasticSearchOptions(), + AuthenticationOptions: authoptions.NewAuthenticateOptions(), } } @@ -125,17 +118,9 @@ func TryLoadFromDisk() (*Config, error) { } conf := New() + if err := viper.Unmarshal(conf); err != nil { return nil, err - } else { - // make sure kubesphere options always exists - if conf.KubeSphereOptions == nil { - conf.KubeSphereOptions = kubesphere.NewKubeSphereOptions() - } else { - ksOptions := kubesphere.NewKubeSphereOptions() - conf.KubeSphereOptions.ApplyTo(ksOptions) - conf.KubeSphereOptions = ksOptions - } } return conf, nil @@ -151,7 +136,7 @@ func (conf *Config) InstallAPI(c *restful.Container) { ws.Route(ws.GET("/configz"). To(func(request *restful.Request, response *restful.Response) { conf.stripEmptyOptions() - response.WriteAsJson(conf.toMap()) + response.WriteAsJson(conf.ToMap()) }). Doc("Get system components configuration"). Produces(restful.MIME_JSON). @@ -163,7 +148,8 @@ func (conf *Config) InstallAPI(c *restful.Container) { // convertToMap simply converts config to map[string]bool // to hide sensitive information -func (conf *Config) toMap() map[string]bool { +func (conf *Config) ToMap() map[string]bool { + conf.stripEmptyOptions() result := make(map[string]bool, 0) if conf == nil { diff --git a/pkg/apiserver/config/config_test.go b/pkg/apiserver/config/config_test.go index 66d95214..dca9d800 100644 --- a/pkg/apiserver/config/config_test.go +++ b/pkg/apiserver/config/config_test.go @@ -2,14 +2,15 @@ package config import ( "fmt" + "github.com/google/go-cmp/cmp" "gopkg.in/yaml.v2" "io/ioutil" - "kubesphere.io/kubesphere/pkg/api/auth" + "kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth" + authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options" "kubesphere.io/kubesphere/pkg/simple/client/alerting" "kubesphere.io/kubesphere/pkg/simple/client/cache" "kubesphere.io/kubesphere/pkg/simple/client/devops/jenkins" "kubesphere.io/kubesphere/pkg/simple/client/k8s" - "kubesphere.io/kubesphere/pkg/simple/client/kubesphere" "kubesphere.io/kubesphere/pkg/simple/client/ldap" "kubesphere.io/kubesphere/pkg/simple/client/logging/elasticsearch" "kubesphere.io/kubesphere/pkg/simple/client/monitoring/prometheus" @@ -26,7 +27,7 @@ import ( ) func newTestConfig() *Config { - conf := &Config{ + var conf = &Config{ MySQLOptions: &mysql.Options{ Host: "10.68.96.5:3306", Username: "root", @@ -96,22 +97,46 @@ func newTestConfig() *Config { IndexPrefix: "elk", Version: "6", }, - KubeSphereOptions: &kubesphere.Options{ - APIServer: "http://ks-apiserver.kubesphere-system.svc", - AccountServer: "http://ks-account.kubesphere-system.svc", - }, AlertingOptions: &alerting.Options{ Endpoint: "http://alerting.kubesphere-alerting-system.svc:9200", }, NotificationOptions: ¬ification.Options{ Endpoint: "http://notification.kubesphere-alerting-system.svc:9200", }, - AuthenticateOptions: &auth.AuthenticationOptions{ + AuthenticationOptions: &authoptions.AuthenticationOptions{ AuthenticateRateLimiterMaxTries: 5, AuthenticateRateLimiterDuration: 30 * time.Minute, MaxAuthenticateRetries: 6, - TokenExpiration: 30 * time.Minute, + JwtSecret: "xxxxxx", MultipleLogin: false, + OAuthOptions: &oauth.Options{ + IdentityProviders: []oauth.IdentityProvider{{ + Name: "github", + MappingMethod: "auto", + LoginRedirect: true, + Type: oauth.IdentityProviderTypeGithub, + Github: &oauth.Github{ + ClientID: "de6ff7bed0304e487b6e", + ClientSecret: "xxxxxx-xxxxx-xxxxx", + Endpoint: oauth.Endpoint{ + AuthURL: "https://github.com/login/oauth/authorize", + TokenURL: "https://github.com/login/oauth/token", + }, + RedirectURL: "https://ks-console.kubesphere-system.svc/oauth/callbak/github", + Scopes: []string{"user"}, + }, + }}, + Clients: []oauth.Client{{ + Name: "kubesphere-console-client", + Secret: "xxxxxx-xxxxxx-xxxxxx", + RespondWithChallenges: true, + RedirectURIs: []string{"http://ks-console.kubesphere-system.svc/oauth/token/implicit"}, + GrantMethod: oauth.GrantHandlerAuto, + AccessTokenInactivityTimeoutSeconds: nil, + }}, + AccessTokenMaxAgeSeconds: 86400, + AccessTokenInactivityTimeoutSeconds: 0, + }, }, } return conf @@ -153,47 +178,8 @@ func TestGet(t *testing.T) { t.Fatal(err) } + cmp.Diff(conf, conf2) if diff := reflectutils.Equal(conf, conf2); diff != nil { t.Fatal(diff) } } - -func TestKubeSphereOptions(t *testing.T) { - conf := newTestConfig() - - t.Run("save nil kubesphere options", func(t *testing.T) { - savedConf := *conf - savedConf.KubeSphereOptions = nil - saveTestConfig(t, &savedConf) - defer cleanTestConfig(t) - - loadedConf, err := TryLoadFromDisk() - if err != nil { - t.Fatal(err) - } - - if diff := reflectutils.Equal(conf, loadedConf); diff != nil { - t.Fatal(diff) - } - }) - - t.Run("save partially kubesphere options", func(t *testing.T) { - savedConf := *conf - savedConf.KubeSphereOptions.APIServer = "http://example.com" - savedConf.KubeSphereOptions.AccountServer = "" - - saveTestConfig(t, &savedConf) - defer cleanTestConfig(t) - - loadedConf, err := TryLoadFromDisk() - if err != nil { - t.Fatal(err) - } - - savedConf.KubeSphereOptions.AccountServer = "http://ks-account.kubesphere-system.svc" - - if diff := reflectutils.Equal(&savedConf, loadedConf); diff != nil { - t.Fatal(diff) - } - }) -} diff --git a/pkg/controller/workspace/workspace_controller.go b/pkg/controller/workspace/workspace_controller.go index ebc2d613..5792d226 100644 --- a/pkg/controller/workspace/workspace_controller.go +++ b/pkg/controller/workspace/workspace_controller.go @@ -20,30 +20,17 @@ package workspace import ( "context" - "fmt" - corev1 "k8s.io/api/core/v1" - rbac "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/tools/record" - log "k8s.io/klog" tenantv1alpha1 "kubesphere.io/kubesphere/pkg/apis/tenant/v1alpha1" - "kubesphere.io/kubesphere/pkg/constants" - "kubesphere.io/kubesphere/pkg/models" - "kubesphere.io/kubesphere/pkg/simple/client/kubesphere" "kubesphere.io/kubesphere/pkg/utils/sliceutil" - "reflect" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" - "sync" ) const ( @@ -54,14 +41,14 @@ const ( // Add creates a new Workspace Controller and adds it to the Manager with default RBAC. The Manager will set fields on the Controller // and Start it when the Manager is Started. -func Add(mgr manager.Manager, kubesphereClient kubesphere.Interface) error { - return add(mgr, newReconciler(mgr, kubesphereClient)) +func Add(mgr manager.Manager) error { + return add(mgr, newReconciler(mgr)) } // newReconciler returns a new reconcile.Reconciler -func newReconciler(mgr manager.Manager, kubesphereClient kubesphere.Interface) reconcile.Reconciler { +func newReconciler(mgr manager.Manager) reconcile.Reconciler { return &ReconcileWorkspace{Client: mgr.GetClient(), scheme: mgr.GetScheme(), - recorder: mgr.GetEventRecorderFor("workspace-controller"), ksclient: kubesphereClient} + recorder: mgr.GetEventRecorderFor("workspace-controller")} } // add adds a new Controller to mgr with r as the reconcile.Reconciler @@ -88,7 +75,6 @@ type ReconcileWorkspace struct { client.Client scheme *runtime.Scheme recorder record.EventRecorder - ksclient kubesphere.Interface } // Reconcile reads that state of the cluster for a Workspace object and makes changes based on the state read @@ -125,9 +111,6 @@ func (r *ReconcileWorkspace) Reconcile(request reconcile.Request) (reconcile.Res // The object is being deleted if sliceutil.HasString(instance.ObjectMeta.Finalizers, finalizer) { // our finalizer is present, so lets handle our external dependency - if err := r.deleteDevOpsProjects(instance); err != nil { - return reconcile.Result{}, err - } // remove our finalizer from the list and update it. instance.ObjectMeta.Finalizers = sliceutil.RemoveString(instance.ObjectMeta.Finalizers, func(item string) bool { @@ -142,497 +125,5 @@ func (r *ReconcileWorkspace) Reconcile(request reconcile.Request) (reconcile.Res return reconcile.Result{}, nil } - if err = r.createWorkspaceAdmin(instance); err != nil { - return reconcile.Result{}, err - } - - if err = r.createWorkspaceRegular(instance); err != nil { - return reconcile.Result{}, err - } - - if err = r.createWorkspaceViewer(instance); err != nil { - return reconcile.Result{}, err - } - - if err = r.createWorkspaceRoleBindings(instance); err != nil { - return reconcile.Result{}, err - } - - if err = r.bindNamespaces(instance); err != nil { - return reconcile.Result{}, err - } - return reconcile.Result{}, nil } - -func (r *ReconcileWorkspace) createWorkspaceAdmin(instance *tenantv1alpha1.Workspace) error { - found := &rbac.ClusterRole{} - - admin := getWorkspaceAdmin(instance.Name) - - if err := controllerutil.SetControllerReference(instance, admin, r.scheme); err != nil { - return err - } - - err := r.Get(context.TODO(), types.NamespacedName{Name: admin.Name}, found) - - if err != nil && errors.IsNotFound(err) { - log.Info("Creating workspace role", "workspace", instance.Name, "name", admin.Name) - err = r.Create(context.TODO(), admin) - if err != nil { - return err - } - found = admin - } else if err != nil { - // Error reading the object - requeue the request. - return err - } - - // Update the found object and write the result back if there are any changes - if !reflect.DeepEqual(admin.Rules, found.Rules) || !reflect.DeepEqual(admin.Labels, found.Labels) || !reflect.DeepEqual(admin.Annotations, found.Annotations) { - found.Rules = admin.Rules - found.Labels = admin.Labels - found.Annotations = admin.Annotations - log.Info("Updating workspace role", "workspace", instance.Name, "name", admin.Name) - err = r.Update(context.TODO(), found) - if err != nil { - return err - } - } - return nil -} - -func (r *ReconcileWorkspace) createWorkspaceRegular(instance *tenantv1alpha1.Workspace) error { - found := &rbac.ClusterRole{} - - regular := getWorkspaceRegular(instance.Name) - - if err := controllerutil.SetControllerReference(instance, regular, r.scheme); err != nil { - return err - } - - err := r.Get(context.TODO(), types.NamespacedName{Name: regular.Name}, found) - - if err != nil && errors.IsNotFound(err) { - - log.Info("Creating workspace role", "workspace", instance.Name, "name", regular.Name) - err = r.Create(context.TODO(), regular) - // Error reading the object - requeue the request. - if err != nil { - return err - } - found = regular - } else if err != nil { - // Error reading the object - requeue the request. - return err - } - - // Update the found object and write the result back if there are any changes - if !reflect.DeepEqual(regular.Rules, found.Rules) || !reflect.DeepEqual(regular.Labels, found.Labels) || !reflect.DeepEqual(regular.Annotations, found.Annotations) { - found.Rules = regular.Rules - found.Labels = regular.Labels - found.Annotations = regular.Annotations - log.Info("Updating workspace role", "workspace", instance.Name, "name", regular.Name) - err = r.Update(context.TODO(), found) - if err != nil { - return err - } - } - - return nil -} - -func (r *ReconcileWorkspace) createWorkspaceViewer(instance *tenantv1alpha1.Workspace) error { - found := &rbac.ClusterRole{} - - viewer := getWorkspaceViewer(instance.Name) - - if err := controllerutil.SetControllerReference(instance, viewer, r.scheme); err != nil { - return err - } - - err := r.Get(context.TODO(), types.NamespacedName{Name: viewer.Name}, found) - - if err != nil && errors.IsNotFound(err) { - log.Info("Creating workspace role", "workspace", instance.Name, "name", viewer.Name) - err = r.Create(context.TODO(), viewer) - // Error reading the object - requeue the request. - if err != nil { - return err - } - found = viewer - } else if err != nil { - // Error reading the object - requeue the request. - return err - } - - // Update the found object and write the result back if there are any changes - if !reflect.DeepEqual(viewer.Rules, found.Rules) || !reflect.DeepEqual(viewer.Labels, found.Labels) || !reflect.DeepEqual(viewer.Annotations, found.Annotations) { - found.Rules = viewer.Rules - found.Labels = viewer.Labels - found.Annotations = viewer.Annotations - log.Info("Updating workspace role", "workspace", instance.Name, "name", viewer.Name) - err = r.Update(context.TODO(), found) - if err != nil { - return err - } - } - - return nil -} - -func (r *ReconcileWorkspace) createGroup(instance *tenantv1alpha1.Workspace) error { - _, err := r.ksclient.DescribeGroup(instance.Name) - - group := &models.Group{ - Name: instance.Name, - } - - if err != nil && kubesphere.IsNotFound(err) { - log.Info("Creating group", "group name", instance.Name) - _, err = r.ksclient.CreateGroup(group) - if err != nil { - if kubesphere.IsExist(err) { - return nil - } - return err - } - } else if err != nil { - return err - } - - return nil -} - -func (r *ReconcileWorkspace) deleteGroup(instance *tenantv1alpha1.Workspace) error { - log.Info("Creating group", "group name", instance.Name) - if err := r.ksclient.DeleteGroup(instance.Name); err != nil { - if kubesphere.IsNotFound(err) { - return nil - } - return err - } - return nil -} - -func (r *ReconcileWorkspace) deleteDevOpsProjects(instance *tenantv1alpha1.Workspace) error { - var wg sync.WaitGroup - - log.Info("Delete DevOps Projects") - for { - projects, err := r.ksclient.ListWorkspaceDevOpsProjects(instance.Name) - if err != nil { - log.Error(err, "Failed to Get Workspace's DevOps Projects", "ws", instance.Name) - return err - } - if projects.TotalCount == 0 { - return nil - } - errChan := make(chan error, len(projects.Items)) - for _, project := range projects.Items { - wg.Add(1) - go func(workspace, devops string) { - err = r.ksclient.DeleteWorkspaceDevOpsProjects(workspace, devops) - errChan <- err - wg.Done() - }(instance.Name, project.ProjectId) - } - wg.Wait() - close(errChan) - for err := range errChan { - if err != nil { - log.Error(err, "delete devops project error") - return err - } - } - - } -} - -func (r *ReconcileWorkspace) createWorkspaceRoleBindings(instance *tenantv1alpha1.Workspace) error { - - adminRoleBinding := &rbac.ClusterRoleBinding{} - adminRoleBinding.Name = getWorkspaceAdminRoleBindingName(instance.Name) - adminRoleBinding.Labels = map[string]string{constants.WorkspaceLabelKey: instance.Name} - adminRoleBinding.RoleRef = rbac.RoleRef{APIGroup: "rbac.authorization.k8s.io", Kind: "ClusterRole", Name: getWorkspaceAdminRoleName(instance.Name)} - - workspaceManager := rbac.Subject{APIGroup: "rbac.authorization.k8s.io", Kind: "User", Name: instance.Spec.Manager} - - if workspaceManager.Name != "" { - adminRoleBinding.Subjects = []rbac.Subject{workspaceManager} - } else { - adminRoleBinding.Subjects = []rbac.Subject{} - } - - if err := controllerutil.SetControllerReference(instance, adminRoleBinding, r.scheme); err != nil { - return err - } - - foundRoleBinding := &rbac.ClusterRoleBinding{} - - err := r.Get(context.TODO(), types.NamespacedName{Name: adminRoleBinding.Name}, foundRoleBinding) - - if err != nil && errors.IsNotFound(err) { - log.Info("Creating workspace role binding", "workspace", instance.Name, "name", adminRoleBinding.Name) - err = r.Create(context.TODO(), adminRoleBinding) - // Error reading the object - requeue the request. - if err != nil { - return err - } - foundRoleBinding = adminRoleBinding - } else if err != nil { - // Error reading the object - requeue the request. - return err - } - - // Update the found object and write the result back if there are any changes - if !reflect.DeepEqual(adminRoleBinding.RoleRef, foundRoleBinding.RoleRef) { - log.Info("Deleting conflict workspace role binding", "workspace", instance.Name, "name", adminRoleBinding.Name) - err = r.Delete(context.TODO(), foundRoleBinding) - if err != nil { - return err - } - return fmt.Errorf("conflict workspace role binding %s, waiting for recreate", foundRoleBinding.Name) - } - - if workspaceManager.Name != "" && !hasSubject(foundRoleBinding.Subjects, workspaceManager) { - foundRoleBinding.Subjects = append(foundRoleBinding.Subjects, workspaceManager) - log.Info("Updating workspace role binding", "workspace", instance.Name, "name", adminRoleBinding.Name) - err = r.Update(context.TODO(), foundRoleBinding) - if err != nil { - return err - } - } - - regularRoleBinding := &rbac.ClusterRoleBinding{} - regularRoleBinding.Name = getWorkspaceRegularRoleBindingName(instance.Name) - regularRoleBinding.Labels = map[string]string{constants.WorkspaceLabelKey: instance.Name} - regularRoleBinding.RoleRef = rbac.RoleRef{APIGroup: "rbac.authorization.k8s.io", Kind: "ClusterRole", Name: getWorkspaceRegularRoleName(instance.Name)} - regularRoleBinding.Subjects = []rbac.Subject{} - - if err = controllerutil.SetControllerReference(instance, regularRoleBinding, r.scheme); err != nil { - return err - } - - err = r.Get(context.TODO(), types.NamespacedName{Name: regularRoleBinding.Name}, foundRoleBinding) - - if err != nil && errors.IsNotFound(err) { - log.Info("Creating workspace role binding", "workspace", instance.Name, "name", regularRoleBinding.Name) - err = r.Create(context.TODO(), regularRoleBinding) - // Error reading the object - requeue the request. - if err != nil { - return err - } - foundRoleBinding = regularRoleBinding - } else if err != nil { - // Error reading the object - requeue the request. - return err - } - - // Update the found object and write the result back if there are any changes - if !reflect.DeepEqual(regularRoleBinding.RoleRef, foundRoleBinding.RoleRef) { - log.Info("Deleting conflict workspace role binding", "workspace", instance.Name, "name", regularRoleBinding.Name) - err = r.Delete(context.TODO(), foundRoleBinding) - if err != nil { - return err - } - return fmt.Errorf("conflict workspace role binding %s, waiting for recreate", foundRoleBinding.Name) - } - - viewerRoleBinding := &rbac.ClusterRoleBinding{} - viewerRoleBinding.Name = getWorkspaceViewerRoleBindingName(instance.Name) - viewerRoleBinding.Labels = map[string]string{constants.WorkspaceLabelKey: instance.Name} - viewerRoleBinding.RoleRef = rbac.RoleRef{APIGroup: "rbac.authorization.k8s.io", Kind: "ClusterRole", Name: getWorkspaceViewerRoleName(instance.Name)} - viewerRoleBinding.Subjects = []rbac.Subject{} - - if err = controllerutil.SetControllerReference(instance, viewerRoleBinding, r.scheme); err != nil { - return err - } - - err = r.Get(context.TODO(), types.NamespacedName{Name: viewerRoleBinding.Name}, foundRoleBinding) - - if err != nil && errors.IsNotFound(err) { - log.Info("Creating workspace role binding", "workspace", instance.Name, "name", viewerRoleBinding.Name) - err = r.Create(context.TODO(), viewerRoleBinding) - // Error reading the object - requeue the request. - if err != nil { - return err - } - foundRoleBinding = viewerRoleBinding - } else if err != nil { - // Error reading the object - requeue the request. - return err - } - - // Update the found object and write the result back if there are any changes - if !reflect.DeepEqual(viewerRoleBinding.RoleRef, foundRoleBinding.RoleRef) { - log.Info("Deleting conflict workspace role binding", "workspace", instance.Name, "name", viewerRoleBinding.Name) - err = r.Delete(context.TODO(), foundRoleBinding) - if err != nil { - return err - } - return fmt.Errorf("conflict workspace role binding %s, waiting for recreate", foundRoleBinding.Name) - } - - return nil -} - -func (r *ReconcileWorkspace) bindNamespaces(instance *tenantv1alpha1.Workspace) error { - - nsList := &corev1.NamespaceList{} - options := client.ListOptions{LabelSelector: labels.SelectorFromSet(labels.Set{constants.WorkspaceLabelKey: instance.Name})} - err := r.List(context.TODO(), nsList, &options) - - if err != nil { - log.Error(err, fmt.Sprintf("list workspace %s namespace failed", instance.Name)) - return err - } - - for _, namespace := range nsList.Items { - if !metav1.IsControlledBy(&namespace, instance) { - if err := controllerutil.SetControllerReference(instance, &namespace, r.scheme); err != nil { - return err - } - log.Info("Bind workspace", "namespace", namespace.Name, "workspace", instance.Name) - err = r.Update(context.TODO(), &namespace) - if err != nil { - return err - } - } - } - - return nil -} - -func hasSubject(subjects []rbac.Subject, user rbac.Subject) bool { - for _, subject := range subjects { - if reflect.DeepEqual(subject, user) { - return true - } - } - return false -} - -func getWorkspaceAdminRoleName(workspaceName string) string { - return fmt.Sprintf("workspace:%s:admin", workspaceName) -} -func getWorkspaceRegularRoleName(workspaceName string) string { - return fmt.Sprintf("workspace:%s:regular", workspaceName) -} -func getWorkspaceViewerRoleName(workspaceName string) string { - return fmt.Sprintf("workspace:%s:viewer", workspaceName) -} - -func getWorkspaceAdminRoleBindingName(workspaceName string) string { - return fmt.Sprintf("workspace:%s:admin", workspaceName) -} - -func getWorkspaceRegularRoleBindingName(workspaceName string) string { - return fmt.Sprintf("workspace:%s:regular", workspaceName) -} - -func getWorkspaceViewerRoleBindingName(workspaceName string) string { - return fmt.Sprintf("workspace:%s:viewer", workspaceName) -} - -func getWorkspaceAdmin(workspaceName string) *rbac.ClusterRole { - admin := &rbac.ClusterRole{} - admin.Name = getWorkspaceAdminRoleName(workspaceName) - admin.Labels = map[string]string{constants.WorkspaceLabelKey: workspaceName} - admin.Annotations = map[string]string{constants.DisplayNameAnnotationKey: constants.WorkspaceAdmin, constants.DescriptionAnnotationKey: workspaceAdminDescription, constants.CreatorAnnotationKey: constants.System} - admin.Rules = []rbac.PolicyRule{ - { - Verbs: []string{"*"}, - APIGroups: []string{"*"}, - ResourceNames: []string{workspaceName}, - Resources: []string{"workspaces", "workspaces/*"}, - }, - { - Verbs: []string{"watch"}, - APIGroups: []string{""}, - Resources: []string{"namespaces"}, - }, - { - Verbs: []string{"list"}, - APIGroups: []string{"iam.kubesphere.io"}, - Resources: []string{"users"}, - }, - { - Verbs: []string{"get", "list"}, - APIGroups: []string{"openpitrix.io"}, - Resources: []string{"categories"}, - }, - { - Verbs: []string{"*"}, - APIGroups: []string{"openpitrix.io"}, - Resources: []string{"applications", "apps", "apps/versions", "apps/events", "apps/action", "apps/audits", "repos", "repos/action", "attachments"}, - }, - } - - return admin -} - -func getWorkspaceRegular(workspaceName string) *rbac.ClusterRole { - regular := &rbac.ClusterRole{} - regular.Name = getWorkspaceRegularRoleName(workspaceName) - regular.Labels = map[string]string{constants.WorkspaceLabelKey: workspaceName} - regular.Annotations = map[string]string{constants.DisplayNameAnnotationKey: constants.WorkspaceRegular, constants.DescriptionAnnotationKey: workspaceRegularDescription, constants.CreatorAnnotationKey: constants.System} - regular.Rules = []rbac.PolicyRule{ - { - Verbs: []string{"get"}, - APIGroups: []string{"*"}, - Resources: []string{"workspaces"}, - ResourceNames: []string{workspaceName}, - }, { - Verbs: []string{"create"}, - APIGroups: []string{"tenant.kubesphere.io"}, - Resources: []string{"workspaces/namespaces", "workspaces/devops"}, - ResourceNames: []string{workspaceName}, - }, - { - Verbs: []string{"get"}, - APIGroups: []string{"iam.kubesphere.io"}, - ResourceNames: []string{workspaceName}, - Resources: []string{"workspaces/members"}, - }, - { - Verbs: []string{"get", "list"}, - APIGroups: []string{"openpitrix.io"}, - Resources: []string{"apps/events", "apps/action", "apps/audits", "categories"}, - }, - - { - Verbs: []string{"*"}, - APIGroups: []string{"openpitrix.io"}, - Resources: []string{"applications", "apps", "apps/versions", "repos", "repos/action", "attachments"}, - }, - } - - return regular -} - -func getWorkspaceViewer(workspaceName string) *rbac.ClusterRole { - viewer := &rbac.ClusterRole{} - viewer.Name = getWorkspaceViewerRoleName(workspaceName) - viewer.Labels = map[string]string{constants.WorkspaceLabelKey: workspaceName} - viewer.Annotations = map[string]string{constants.DisplayNameAnnotationKey: constants.WorkspaceViewer, constants.DescriptionAnnotationKey: workspaceViewerDescription, constants.CreatorAnnotationKey: constants.System} - viewer.Rules = []rbac.PolicyRule{ - { - Verbs: []string{"get", "list"}, - APIGroups: []string{"*"}, - ResourceNames: []string{workspaceName}, - Resources: []string{"workspaces", "workspaces/*"}, - }, - { - Verbs: []string{"watch"}, - APIGroups: []string{""}, - Resources: []string{"namespaces"}, - }, - { - Verbs: []string{"get", "list"}, - APIGroups: []string{"openpitrix.io"}, - Resources: []string{"applications", "apps", "apps/events", "apps/action", "apps/audits", "apps/versions", "repos", "categories", "attachments"}, - }, - } - return viewer -} diff --git a/pkg/kapis/iam/v1alpha2/handler.go b/pkg/kapis/iam/v1alpha2/handler.go index 6f53f307..5c93193c 100644 --- a/pkg/kapis/iam/v1alpha2/handler.go +++ b/pkg/kapis/iam/v1alpha2/handler.go @@ -2,7 +2,7 @@ package v1alpha2 import ( "github.com/emicklei/go-restful" - "kubesphere.io/kubesphere/pkg/api/auth" + authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options" "kubesphere.io/kubesphere/pkg/informers" "kubesphere.io/kubesphere/pkg/models/iam/am" "kubesphere.io/kubesphere/pkg/models/iam/im" @@ -16,7 +16,7 @@ type iamHandler struct { imOperator im.IdentityManagementInterface } -func newIAMHandler(k8sClient k8s.Client, factory informers.InformerFactory, ldapClient ldappool.Interface, cacheClient cache.Interface, options *auth.AuthenticationOptions) *iamHandler { +func newIAMHandler(k8sClient k8s.Client, factory informers.InformerFactory, ldapClient ldappool.Interface, cacheClient cache.Interface, options *authoptions.AuthenticationOptions) *iamHandler { return &iamHandler{ amOperator: am.NewAMOperator(k8sClient.Kubernetes(), factory.KubernetesSharedInformerFactory()), imOperator: im.NewLDAPOperator(ldapClient), diff --git a/pkg/kapis/iam/v1alpha2/register.go b/pkg/kapis/iam/v1alpha2/register.go index f41652a9..2a082d41 100644 --- a/pkg/kapis/iam/v1alpha2/register.go +++ b/pkg/kapis/iam/v1alpha2/register.go @@ -22,7 +22,7 @@ import ( "github.com/emicklei/go-restful-openapi" "k8s.io/apimachinery/pkg/runtime/schema" "kubesphere.io/kubesphere/pkg/api" - "kubesphere.io/kubesphere/pkg/api/auth" + authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options" "kubesphere.io/kubesphere/pkg/apiserver/runtime" "kubesphere.io/kubesphere/pkg/constants" "kubesphere.io/kubesphere/pkg/informers" @@ -38,7 +38,7 @@ const groupName = "iam.kubesphere.io" var GroupVersion = schema.GroupVersion{Group: groupName, Version: "v1alpha2"} -func AddToContainer(c *restful.Container, k8sClient k8s.Client, factory informers.InformerFactory, ldapClient ldappool.Interface, cacheClient cache.Interface, options *auth.AuthenticationOptions) error { +func AddToContainer(c *restful.Container, k8sClient k8s.Client, factory informers.InformerFactory, ldapClient ldappool.Interface, cacheClient cache.Interface, options *authoptions.AuthenticationOptions) error { ws := runtime.NewWebService(GroupVersion) handler := newIAMHandler(k8sClient, factory, ldapClient, cacheClient, options) diff --git a/pkg/kapis/oauth/handler.go b/pkg/kapis/oauth/handler.go index e7826097..b673aa45 100644 --- a/pkg/kapis/oauth/handler.go +++ b/pkg/kapis/oauth/handler.go @@ -26,18 +26,19 @@ import ( "kubesphere.io/kubesphere/pkg/api" "kubesphere.io/kubesphere/pkg/api/auth" "kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth" + authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options" "kubesphere.io/kubesphere/pkg/apiserver/authentication/token" "kubesphere.io/kubesphere/pkg/apiserver/request" "net/http" ) type oauthHandler struct { - issuer token.Issuer - config oauth.Configuration + issuer token.Issuer + options *authoptions.AuthenticationOptions } -func newOAUTHHandler(issuer token.Issuer, config oauth.Configuration) *oauthHandler { - return &oauthHandler{issuer: issuer, config: config} +func newOAUTHHandler(issuer token.Issuer, options *authoptions.AuthenticationOptions) *oauthHandler { + return &oauthHandler{issuer: issuer, options: options} } // Implement webhook authentication interface @@ -59,7 +60,7 @@ func (h *oauthHandler) TokenReviewHandler(req *restful.Request, resp *restful.Re return } - user, _, err := h.issuer.Verify(tokenReview.Spec.Token) + user, err := h.issuer.Verify(tokenReview.Spec.Token) if err != nil { klog.Errorln(err) @@ -82,8 +83,9 @@ func (h *oauthHandler) AuthorizeHandler(req *restful.Request, resp *restful.Resp user, ok := request.UserFrom(req.Request.Context()) clientId := req.QueryParameter("client_id") responseType := req.QueryParameter("response_type") + redirectURI := req.QueryParameter("redirect_uri") - conf, err := h.config.Load(clientId) + conf, err := h.options.OAuthOptions.GetOAuthClient(clientId) if err != nil { err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err)) @@ -103,7 +105,21 @@ func (h *oauthHandler) AuthorizeHandler(req *restful.Request, resp *restful.Resp return } - accessToken, clm, err := h.issuer.IssueTo(user) + expiresIn := h.options.OAuthOptions.AccessTokenMaxAgeSeconds + + if conf.AccessTokenMaxAgeSeconds != nil { + expiresIn = *conf.AccessTokenMaxAgeSeconds + } + + accessToken, err := h.issuer.IssueTo(user, expiresIn) + + if err != nil { + err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err)) + resp.WriteError(http.StatusUnauthorized, err) + return + } + + redirectURL, err := conf.ResolveRedirectURL(redirectURI) if err != nil { err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err)) @@ -111,11 +127,63 @@ func (h *oauthHandler) AuthorizeHandler(req *restful.Request, resp *restful.Resp return } - redirectURL := fmt.Sprintf("%s?access_token=%s&token_type=Bearer", conf.RedirectURL, accessToken) - expiresIn := clm.ExpiresAt - clm.IssuedAt + redirectURL = fmt.Sprintf("%s#access_token=%s&token_type=Bearer", redirectURL, accessToken) + if expiresIn > 0 { redirectURL = fmt.Sprintf("%s&expires_in=%v", redirectURL, expiresIn) } + resp.Header().Set("Content-Type", "text/plain") http.Redirect(resp, req.Request, redirectURL, http.StatusFound) } + +func (h *oauthHandler) OAuthCallBackHandler(req *restful.Request, resp *restful.Response) { + + code := req.QueryParameter("code") + name := req.PathParameter("callback") + + if code == "" { + err := apierrors.NewUnauthorized("Unauthorized: missing code") + resp.WriteError(http.StatusUnauthorized, err) + } + + idP, err := h.options.OAuthOptions.GetIdentityProvider(name) + + if err != nil { + err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err)) + resp.WriteError(http.StatusUnauthorized, err) + } + + oauthIdentityProvider, err := idP.GetOAuth2IdentityProviderInstance() + + if err != nil { + err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err)) + resp.WriteError(http.StatusUnauthorized, err) + } + + user, err := oauthIdentityProvider.IdentityExchange(code) + + if err != nil { + err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err)) + resp.WriteError(http.StatusUnauthorized, err) + } + + expiresIn := h.options.OAuthOptions.AccessTokenMaxAgeSeconds + + accessToken, err := h.issuer.IssueTo(user, expiresIn) + + if err != nil { + err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err)) + resp.WriteError(http.StatusUnauthorized, err) + return + } + + result := oauth.Token{ + AccessToken: accessToken, + TokenType: "Bearer", + ExpiresIn: expiresIn, + } + + resp.WriteEntity(result) + return +} diff --git a/pkg/kapis/oauth/register.go b/pkg/kapis/oauth/register.go index 846cb842..85b2de12 100644 --- a/pkg/kapis/oauth/register.go +++ b/pkg/kapis/oauth/register.go @@ -23,19 +23,19 @@ import ( restfulspec "github.com/emicklei/go-restful-openapi" "kubesphere.io/kubesphere/pkg/api" "kubesphere.io/kubesphere/pkg/api/auth" - "kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth" + authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options" "kubesphere.io/kubesphere/pkg/apiserver/authentication/token" "kubesphere.io/kubesphere/pkg/constants" "net/http" ) -func AddToContainer(c *restful.Container, issuer token.Issuer, configuration oauth.Configuration) error { +func AddToContainer(c *restful.Container, issuer token.Issuer, options *authoptions.AuthenticationOptions) error { ws := &restful.WebService{} ws.Path("/oauth"). Consumes(restful.MIME_JSON). Produces(restful.MIME_JSON) - handler := newOAUTHHandler(issuer, configuration) + handler := newOAUTHHandler(issuer, options) // Implement webhook authentication interface // https://kubernetes.io/docs/reference/access-authn-authz/authentication/#webhook-token-authentication @@ -46,16 +46,14 @@ func AddToContainer(c *restful.Container, issuer token.Issuer, configuration oau Returns(http.StatusOK, api.StatusOK, auth.TokenReview{}). Metadata(restfulspec.KeyOpenAPITags, []string{constants.IdentityManagementTag})) - // TODO Built-in oauth2 server (provider) - // web console use 'Resource Owner Password Credentials Grant' or 'Client Credentials Grant' request for an OAuth token - // https://tools.ietf.org/html/rfc6749#section-4.3 - // https://tools.ietf.org/html/rfc6749#section-4.4 - + // Only support implicit grant flow + // https://tools.ietf.org/html/rfc6749#section-4.2 // curl -u admin:P@88w0rd 'http://ks-apiserver.kubesphere-system.svc/oauth/authorize?client_id=kubesphere-console-client&response_type=token' -v ws.Route(ws.GET("/authorize"). To(handler.AuthorizeHandler)) //ws.Route(ws.POST("/token")) - //ws.Route(ws.POST("/callback/{callback}")) + ws.Route(ws.GET("/callback/{callback}"). + To(handler.OAuthCallBackHandler)) c.Add(ws) diff --git a/pkg/kapis/serverconfig/register.go b/pkg/kapis/serverconfig/register.go new file mode 100644 index 00000000..1a68bd92 --- /dev/null +++ b/pkg/kapis/serverconfig/register.go @@ -0,0 +1,45 @@ +/* + * + * 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 serverconfig + +import ( + "github.com/emicklei/go-restful" + apiserverconfig "kubesphere.io/kubesphere/pkg/apiserver/config" +) + +func AddToContainer(c *restful.Container, config *apiserverconfig.Config) error { + configs := &restful.WebService{} + + configs.Path("/server/configs"). + Consumes(restful.MIME_JSON). + Produces(restful.MIME_JSON) + + // information about the authorization server are published. + configs.Route(configs.GET("/oauth-configz").To(func(request *restful.Request, response *restful.Response) { + response.WriteEntity(config.AuthenticationOptions.OAuthOptions) + })) + + // information about the server configuration + configs.Route(configs.GET("/configz").To(func(request *restful.Request, response *restful.Response) { + response.WriteAsJson(config.ToMap()) + })) + + c.Add(configs) + return nil +} diff --git a/pkg/models/iam/am/fake_operator.go b/pkg/models/iam/am/fake_operator.go index 13a5c525..2d83d413 100644 --- a/pkg/models/iam/am/fake_operator.go +++ b/pkg/models/iam/am/fake_operator.go @@ -133,7 +133,9 @@ func NewFakeAMOperator() *FakeOperator { }) operator.saveFakeRole(platformRoleCacheKey(user.Anonymous), FakeRole{ Name: "admin", - Rego: "package authz\ndefault allow = false", + Rego: `package authz +default allow = false +`, }) return operator } diff --git a/pkg/simple/client/kubesphere/kubesphere.go b/pkg/simple/client/kubesphere/kubesphere.go deleted file mode 100644 index c4a9c54f..00000000 --- a/pkg/simple/client/kubesphere/kubesphere.go +++ /dev/null @@ -1,349 +0,0 @@ -/* - - 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. - -*/ -package kubesphere - -import ( - "bytes" - "encoding/json" - "fmt" - "io/ioutil" - "k8s.io/klog" - "kubesphere.io/kubesphere/pkg/api/devops/v1alpha2" - "kubesphere.io/kubesphere/pkg/constants" - "kubesphere.io/kubesphere/pkg/models" - "net/http" - "strings" -) - -type Interface interface { - CreateGroup(group *models.Group) (*models.Group, error) - UpdateGroup(group *models.Group) (*models.Group, error) - DescribeGroup(name string) (*models.Group, error) - DeleteGroup(name string) error - ListUsers() (*models.PageableResponse, error) - ListWorkspaceDevOpsProjects(workspace string) (*v1alpha2.PageableDevOpsProject, error) - DeleteWorkspaceDevOpsProjects(workspace, devops string) error -} - -type Client struct { - client *http.Client - - apiServer string - accountServer string -} - -func NewKubeSphereClient(options *Options) *Client { - return &Client{ - client: &http.Client{}, - apiServer: options.APIServer, - accountServer: options.AccountServer, - } -} - -type Error struct { - status int - message string -} - -func (e Error) Error() string { - return fmt.Sprintf("status: %d,message: %s", e.status, e.message) -} - -func (c *Client) CreateGroup(group *models.Group) (*models.Group, error) { - data, err := json.Marshal(group) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/kapis/iam.kubesphere.io/v1alpha2/groups", c.accountServer), bytes.NewReader(data)) - - if err != nil { - klog.Error(err) - return nil, err - } - req.Header.Add("Content-Type", "application/json") - - resp, err := c.client.Do(req) - - if err != nil { - klog.Error(err) - return nil, err - } - defer resp.Body.Close() - data, err = ioutil.ReadAll(resp.Body) - - if err != nil { - klog.Error(err) - return nil, err - } - - if resp.StatusCode > http.StatusOK { - return nil, Error{resp.StatusCode, string(data)} - } - - err = json.Unmarshal(data, group) - - if err != nil { - klog.Error(err) - return nil, err - } - - return group, nil -} - -func (c *Client) UpdateGroup(group *models.Group) (*models.Group, error) { - data, err := json.Marshal(group) - - if err != nil { - klog.Error(err) - return nil, err - } - - req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/kapis/iam.kubesphere.io/v1alpha2/groups/%s", c.accountServer, group.Name), bytes.NewReader(data)) - - if err != nil { - klog.Error(err) - return nil, err - } - - req.Header.Add("Content-Type", "application/json") - resp, err := c.client.Do(req) - - if err != nil { - return nil, err - } - defer resp.Body.Close() - data, err = ioutil.ReadAll(resp.Body) - - if err != nil { - klog.Error(err) - return nil, err - } - - if resp.StatusCode > http.StatusOK { - return nil, Error{resp.StatusCode, string(data)} - } - - err = json.Unmarshal(data, group) - - if err != nil { - klog.Error(err) - return nil, err - } - - return group, nil -} - -func (c *Client) DeleteGroup(name string) error { - req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("%s/kapis/iam.kubesphere.io/v1alpha2/groups/%s", c.accountServer, name), nil) - - if err != nil { - klog.Error(err) - return err - } - - resp, err := c.client.Do(req) - - if err != nil { - klog.Error(err) - return err - } - defer resp.Body.Close() - data, err := ioutil.ReadAll(resp.Body) - - if err != nil { - klog.Error(err) - return err - } - - if resp.StatusCode > http.StatusOK { - return Error{resp.StatusCode, string(data)} - } - - return nil -} - -func (c *Client) DescribeGroup(name string) (*models.Group, error) { - req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/kapis/iam.kubesphere.io/v1alpha2/groups/%s", c.accountServer, name), nil) - - if err != nil { - klog.Error(err) - return nil, err - } - resp, err := c.client.Do(req) - - if err != nil { - klog.Error(err) - return nil, err - } - defer resp.Body.Close() - data, err := ioutil.ReadAll(resp.Body) - - if err != nil { - klog.Error(err) - return nil, err - } - - if resp.StatusCode > http.StatusOK { - return nil, Error{resp.StatusCode, string(data)} - } - - var group models.Group - err = json.Unmarshal(data, &group) - - if err != nil { - klog.Error(err) - return nil, err - } - - return &group, nil -} - -func (c *Client) ListUsers() (*models.PageableResponse, error) { - req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/kapis/iam.kubesphere.io/v1alpha2/users", c.accountServer), nil) - - if err != nil { - return nil, err - } - req.Header.Add("Authorization", c.accountServer) - resp, err := c.client.Do(req) - - if err != nil { - klog.Error(err) - return nil, err - } - defer resp.Body.Close() - data, err := ioutil.ReadAll(resp.Body) - - if err != nil { - klog.Error(err) - return nil, err - } - - if resp.StatusCode > http.StatusOK { - return nil, Error{resp.StatusCode, string(data)} - } - - var result models.PageableResponse - err = json.Unmarshal(data, &result) - - if err != nil { - klog.Error(err) - return nil, err - } - - return &result, nil -} - -func (c *Client) ListWorkspaceDevOpsProjects(workspace string) (*v1alpha2.PageableDevOpsProject, error) { - req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/kapis/tenant.kubesphere.io/v1alpha2/workspaces/%s/devops", c.apiServer, workspace), nil) - - if err != nil { - klog.Error(err) - return nil, err - } - - req.Header.Add(constants.UserNameHeader, constants.AdminUserName) - - klog.Info(req.Method, req.URL) - resp, err := c.client.Do(req) - - if err != nil { - klog.Error(err) - return nil, err - } - defer resp.Body.Close() - data, err := ioutil.ReadAll(resp.Body) - - if err != nil { - klog.Error(err) - return nil, err - } - if resp.StatusCode > http.StatusOK { - klog.Error(req.Method, req.URL, resp.StatusCode, string(data)) - return nil, Error{resp.StatusCode, string(data)} - } - - var result v1alpha2.PageableDevOpsProject - err = json.Unmarshal(data, &result) - - if err != nil { - klog.Error(err) - return nil, err - } - return &result, nil - -} - -func (c *Client) DeleteWorkspaceDevOpsProjects(workspace, devops string) error { - req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("%s/kapis/tenant.kubesphere.io/v1alpha2/workspaces/%s/devops/%s", c.apiServer, workspace, devops), nil) - - if err != nil { - klog.Error(err) - return err - } - req.Header.Add(constants.UserNameHeader, constants.AdminUserName) - - klog.Info(req.Method, req.URL) - resp, err := c.client.Do(req) - - if err != nil { - klog.Error(err) - return err - } - defer resp.Body.Close() - data, err := ioutil.ReadAll(resp.Body) - - if err != nil { - klog.Error(err) - return err - } - if resp.StatusCode > http.StatusOK { - klog.Error(req.Method, req.URL, resp.StatusCode, string(data)) - return Error{resp.StatusCode, string(data)} - } - - return nil -} - -func IsNotFound(err error) bool { - if e, ok := err.(Error); ok { - if e.status == http.StatusNotFound { - return true - } - if strings.Contains(e.message, "not exist") { - return true - } - if strings.Contains(e.message, "not found") { - return true - } - } - return false -} - -func IsExist(err error) bool { - if e, ok := err.(Error); ok { - if e.status == http.StatusConflict { - return true - } - if strings.Contains(e.message, "Already Exists") { - return true - } - } - return false -} diff --git a/pkg/simple/client/kubesphere/options.go b/pkg/simple/client/kubesphere/options.go deleted file mode 100644 index c4e134c7..00000000 --- a/pkg/simple/client/kubesphere/options.go +++ /dev/null @@ -1,40 +0,0 @@ -package kubesphere - -import "github.com/spf13/pflag" - -type Options struct { - APIServer string `json:"apiServer" yaml:"apiServer"` - AccountServer string `json:"accountServer" yaml:"accountServer"` -} - -// NewKubeSphereOptions create a default options -func NewKubeSphereOptions() *Options { - return &Options{ - APIServer: "http://ks-apiserver.kubesphere-system.svc", - AccountServer: "http://ks-account.kubesphere-system.svc", - } -} - -func (s *Options) ApplyTo(options *Options) { - if s.AccountServer != "" { - options.AccountServer = s.AccountServer - } - - if s.APIServer != "" { - options.APIServer = s.APIServer - } -} - -func (s *Options) Validate() []error { - errs := []error{} - - return errs -} - -func (s *Options) AddFlags(fs *pflag.FlagSet) { - fs.StringVar(&s.APIServer, "kubesphere-apiserver-host", s.APIServer, ""+ - "KubeSphere apiserver host address.") - - fs.StringVar(&s.AccountServer, "kubesphere-account-host", s.AccountServer, ""+ - "KubeSphere account server host address.") -} -- GitLab