未验证 提交 9b9d4021 编写于 作者: H hongming

implement identity provider and built-in oauth server

Signed-off-by: Nhongming <talonwan@yunify.com>
上级 59002cd1
......@@ -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,
......
......@@ -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")
}
......
......@@ -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
}
......
......@@ -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,
}
}
......
......@@ -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
......
......@@ -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
}
......
......@@ -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)
}
/*
*
* 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
}
/*
*
* 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 <master>/oauth/authorize
// and <master>/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,
}
}
......@@ -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.")
}
......@@ -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
......
......@@ -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,
......
......@@ -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)
}
......
......@@ -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 {
......
......@@ -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: &notification.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)
}
})
}
......@@ -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
}
......@@ -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),
......
......@@ -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)
......
......@@ -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
}
......@@ -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)
......
......@@ -16,22 +16,30 @@
* /
*/
package oauth
package serverconfig
import "golang.org/x/oauth2"
import (
"github.com/emicklei/go-restful"
apiserverconfig "kubesphere.io/kubesphere/pkg/apiserver/config"
)
type SimpleConfigManager struct {
}
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())
}))
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
c.Add(configs)
return nil
}
......@@ -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
}
/*
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
}
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.")
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册