Signed-off-by: hongming <talonwan@yunify.com>
This commit is contained in:
hongming
2020-03-22 23:17:43 +08:00
parent cae7843832
commit aa05c2baf4
29 changed files with 626 additions and 367 deletions

View File

@@ -1,10 +0,0 @@
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, error)
// Verify verifies a token, and return a User if it's a valid token, otherwise return error
Verify(string) (User, error)
}

View File

@@ -1,77 +0,0 @@
package token
import (
"fmt"
"github.com/dgrijalva/jwt-go"
"kubesphere.io/kubesphere/pkg/api/iam"
"kubesphere.io/kubesphere/pkg/server/errors"
"time"
)
const DefaultIssuerName = "kubesphere"
var errInvalidToken = errors.New("invalid token")
type claims struct {
Username string `json:"username"`
UID string `json:"uid"`
Email string `json:"email"`
// Currently, we are not using any field in jwt.StandardClaims
jwt.StandardClaims
}
type jwtTokenIssuer struct {
name string
secret []byte
keyFunc jwt.Keyfunc
}
func (s *jwtTokenIssuer) Verify(tokenString string) (User, error) {
if len(tokenString) == 0 {
return nil, errInvalidToken
}
clm := &claims{}
_, err := jwt.ParseWithClaims(tokenString, clm, s.keyFunc)
if err != nil {
return nil, err
}
return &iam.User{Name: clm.Username, UID: clm.UID, Email: clm.Email}, nil
}
func (s *jwtTokenIssuer) IssueTo(user User) (string, error) {
clm := &claims{
Username: user.GetName(),
UID: user.GetUID(),
Email: user.GetEmail(),
StandardClaims: jwt.StandardClaims{
IssuedAt: time.Now().Unix(),
Issuer: s.name,
NotBefore: time.Now().Unix(),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, clm)
tokenString, err := token.SignedString(s.secret)
if err != nil {
return "", err
}
return tokenString, nil
}
func NewJwtTokenIssuer(issuerName string, secret []byte) Issuer {
return &jwtTokenIssuer{
name: issuerName,
secret: secret,
keyFunc: func(token *jwt.Token) (i interface{}, err error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); ok {
return secret, nil
} else {
return nil, fmt.Errorf("expect token signed with HMAC but got %v", token.Header["alg"])
}
},
}
}

View File

@@ -1,52 +0,0 @@
package token
import (
"github.com/google/go-cmp/cmp"
"kubesphere.io/kubesphere/pkg/api/iam"
"testing"
)
func TestJwtTokenIssuer(t *testing.T) {
issuer := NewJwtTokenIssuer(DefaultIssuerName, []byte("kubesphere"))
testCases := []struct {
description string
name string
uid string
email string
}{
{
name: "admin",
uid: "b8be6edd-2c92-4535-9b2a-df6326474458",
email: "admin@kubesphere.io",
},
{
name: "bar",
uid: "b8be6edd-2c92-4535-9b2a-df6326474452",
email: "bar@kubesphere.io",
},
}
for _, testCase := range testCases {
user := &iam.User{
Name: testCase.name,
UID: testCase.uid,
}
t.Run(testCase.description, func(t *testing.T) {
token, err := issuer.IssueTo(user)
if err != nil {
t.Fatal(err)
}
got, err := issuer.Verify(token)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(user, got); len(diff) != 0 {
t.Errorf("%T differ (-got, +expected), %s", user, diff)
}
})
}
}

View File

@@ -1,12 +0,0 @@
package token
type User interface {
// Name
GetName() string
// UID
GetUID() string
// Email
GetEmail() string
}

View File

@@ -13,8 +13,12 @@ import (
"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"
authenticationrequest "kubesphere.io/kubesphere/pkg/apiserver/authentication/request"
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"
@@ -35,6 +39,7 @@ import (
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"
"kubesphere.io/kubesphere/pkg/simple/client/cache"
"kubesphere.io/kubesphere/pkg/simple/client/devops"
"kubesphere.io/kubesphere/pkg/simple/client/k8s"
@@ -142,7 +147,7 @@ func (s *APIServer) installKubeSphereAPIs() {
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, s.AuthenticateOptions))
urlruntime.Must(oauth.AddToContainer(s.container, token.NewJwtTokenIssuer(token.DefaultIssuerName, s.AuthenticateOptions, s.CacheClient), &oauth2.SimpleConfigManager{}))
urlruntime.Must(servicemeshv1alpha2.AddToContainer(s.container))
}
@@ -184,10 +189,14 @@ func (s *APIServer) buildHandlerChain() {
excludedPaths := []string{"/oauth/authorize", "/oauth/token"}
pathAuthorizer, _ := path.NewAuthorizer(excludedPaths)
authorizer := unionauthorizer.New(pathAuthorizer, authorizerfactory.NewOPAAuthorizer(am.NewFakeAMOperator(cache.NewSimpleCache())))
authorizer := unionauthorizer.New(pathAuthorizer,
authorizerfactory.NewOPAAuthorizer(am.NewFakeAMOperator()))
handler = filters.WithAuthorization(handler, authorizer)
authn := unionauth.New(&authenticationrequest.AnonymousAuthenticator{}, bearertoken.New(jwttoken.NewTokenAuthenticator(s.CacheClient, s.AuthenticateOptions.JwtSecret)))
authn := unionauth.New(anonymous.NewAuthenticator(),
basictoken.New(basic.NewBasicAuthenticator(im.NewFakeOperator())),
bearertoken.New(jwttoken.NewTokenAuthenticator(
token.NewJwtTokenIssuer(token.DefaultIssuerName, s.AuthenticateOptions, s.CacheClient))))
handler = filters.WithAuthentication(handler, authn)
handler = filters.WithRequestInfo(handler, requestInfoResolver)
s.Server.Handler = handler

View File

@@ -0,0 +1,58 @@
/*
*
* 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 basic
import (
"context"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user"
"kubesphere.io/kubesphere/pkg/models/iam/im"
)
// TokenAuthenticator implements kubernetes token authenticate interface with our custom logic.
// TokenAuthenticator will retrieve user info from cache by given token. If empty or invalid token
// was given, authenticator will still give passed response at the condition user will be user.Anonymous
// and group from user.AllUnauthenticated. This helps requests be passed along the handler chain,
// because some resources are public accessible.
type basicAuthenticator struct {
im im.IdentityManagementInterface
}
func NewBasicAuthenticator(im im.IdentityManagementInterface) authenticator.Password {
return &basicAuthenticator{
im: im,
}
}
func (t *basicAuthenticator) AuthenticatePassword(ctx context.Context, username, password string) (*authenticator.Response, bool, error) {
providedUser, err := t.im.Authenticate(username, password)
if err != nil {
return nil, false, err
}
return &authenticator.Response{
User: &user.DefaultInfo{
Name: providedUser.GetName(),
UID: providedUser.GetUID(),
Groups: []string{user.AllAuthenticated},
},
}, true, nil
}

View File

@@ -4,44 +4,30 @@ import (
"context"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user"
"kubesphere.io/kubesphere/pkg/api/auth/token"
"kubesphere.io/kubesphere/pkg/server/errors"
"kubesphere.io/kubesphere/pkg/simple/client/cache"
token2 "kubesphere.io/kubesphere/pkg/apiserver/authentication/token"
)
var errTokenExpired = errors.New("expired token")
// TokenAuthenticator implements kubernetes token authenticate interface with our custom logic.
// TokenAuthenticator will retrieve user info from cache by given token. If empty or invalid token
// was given, authenticator will still give passed response at the condition user will be user.Anonymous
// and group from user.AllUnauthenticated. This helps requests be passed along the handler chain,
// because some resources are public accessible.
type tokenAuthenticator struct {
cacheClient cache.Interface
jwtTokenIssuer token.Issuer
jwtTokenIssuer token2.Issuer
}
func NewTokenAuthenticator(cacheClient cache.Interface, jwtSecret string) authenticator.Token {
func NewTokenAuthenticator(issuer token2.Issuer) authenticator.Token {
return &tokenAuthenticator{
cacheClient: cacheClient,
jwtTokenIssuer: token.NewJwtTokenIssuer(token.DefaultIssuerName, []byte(jwtSecret)),
jwtTokenIssuer: issuer,
}
}
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
}
// TODO implement token cache
//_, err = t.cacheClient.Get(tokenKeyForUsername(providedUser.Name(), token))
//if err != nil {
// return nil, false, errTokenExpired
//}
// Should we need to refresh token?
return &authenticator.Response{
User: &user.DefaultInfo{
Name: providedUser.GetName(),

View File

@@ -0,0 +1,30 @@
/*
*
* 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"
"golang.org/x/oauth2"
)
var ConfigNotFound = errors.New("config not found")
type Configuration interface {
Load(clientId string) (*oauth2.Config, error)
}

View File

@@ -0,0 +1,37 @@
/*
*
* 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
}

View File

@@ -1,24 +0,0 @@
package request
import (
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user"
"net/http"
"strings"
)
type AnonymousAuthenticator struct{}
func (a *AnonymousAuthenticator) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) {
auth := strings.TrimSpace(req.Header.Get("Authorization"))
if auth == "" {
return &authenticator.Response{
User: &user.DefaultInfo{
Name: user.Anonymous,
UID: "",
Groups: []string{user.AllUnauthenticated},
},
}, true, nil
}
return nil, false, nil
}

View File

@@ -0,0 +1,46 @@
/*
*
* 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 anonymous
import (
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user"
"net/http"
"strings"
)
type Authenticator struct{}
func NewAuthenticator() authenticator.Request {
return &Authenticator{}
}
func (a *Authenticator) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) {
auth := strings.TrimSpace(req.Header.Get("Authorization"))
if auth == "" {
return &authenticator.Response{
User: &user.DefaultInfo{
Name: user.Anonymous,
UID: "",
Groups: []string{user.AllUnauthenticated},
},
}, true, nil
}
return nil, false, nil
}

View File

@@ -0,0 +1,56 @@
/*
Copyright 2014 The Kubernetes 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 basictoken
import (
"errors"
"k8s.io/apiserver/pkg/authentication/authenticator"
"net/http"
)
type Authenticator struct {
auth authenticator.Password
}
func New(auth authenticator.Password) *Authenticator {
return &Authenticator{auth}
}
var invalidToken = errors.New("invalid basic token")
func (a *Authenticator) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) {
username, password, ok := req.BasicAuth()
if !ok {
return nil, false, nil
}
resp, ok, err := a.auth.AuthenticatePassword(req.Context(), username, password)
// if we authenticated successfully, go ahead and remove the bearer token so that no one
// is ever tempted to use it inside of the API server
if ok {
req.Header.Del("Authorization")
}
// If the token authenticator didn't error, provide a default error
if !ok && err == nil {
err = invalidToken
}
return resp, ok, err
}

View File

@@ -0,0 +1,31 @@
/*
*
* 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 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)
// Verify verifies a token, and return a User if it's a valid token, otherwise return error
Verify(string) (User, *Claims, error)
// Revoke a token,
Revoke(token string) error
}

View File

@@ -0,0 +1,124 @@
/*
*
* 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 token
import (
"fmt"
"github.com/dgrijalva/jwt-go"
"kubesphere.io/kubesphere/pkg/api/auth"
"kubesphere.io/kubesphere/pkg/api/iam"
"kubesphere.io/kubesphere/pkg/server/errors"
"kubesphere.io/kubesphere/pkg/simple/client/cache"
"time"
)
const DefaultIssuerName = "kubesphere"
var (
errInvalidToken = errors.New("invalid token")
errTokenExpired = errors.New("expired token")
)
type Claims struct {
Username string `json:"username"`
UID string `json:"uid"`
// Currently, we are not using any field in jwt.StandardClaims
jwt.StandardClaims
}
type jwtTokenIssuer struct {
name string
options *auth.AuthenticationOptions
cache cache.Interface
keyFunc jwt.Keyfunc
}
func (s *jwtTokenIssuer) Verify(tokenString string) (User, *Claims, error) {
if len(tokenString) == 0 {
return nil, nil, errInvalidToken
}
_, err := s.cache.Get(tokenCacheKey(tokenString))
if err != nil {
if err == cache.ErrNoSuchKey {
return nil, nil, errTokenExpired
}
return nil, nil, err
}
clm := &Claims{}
_, err = jwt.ParseWithClaims(tokenString, clm, s.keyFunc)
if err != nil {
return nil, nil, err
}
return &iam.User{Name: clm.Username, UID: clm.UID}, clm, nil
}
func (s *jwtTokenIssuer) IssueTo(user User) (string, *Claims, error) {
clm := &Claims{
Username: user.GetName(),
UID: user.GetUID(),
StandardClaims: jwt.StandardClaims{
IssuedAt: time.Now().Unix(),
Issuer: s.name,
NotBefore: time.Now().Unix(),
},
}
if s.options.TokenExpiration > 0 {
clm.ExpiresAt = clm.IssuedAt + int64(s.options.TokenExpiration.Seconds())
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, clm)
tokenString, err := token.SignedString([]byte(s.options.JwtSecret))
if err != nil {
return "", nil, err
}
s.cache.Set(tokenCacheKey(tokenString), tokenString, s.options.TokenExpiration)
return tokenString, clm, 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 {
return &jwtTokenIssuer{
name: issuerName,
options: options,
cache: cache,
keyFunc: func(token *jwt.Token) (i interface{}, err error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); ok {
return []byte(options.JwtSecret), nil
} else {
return nil, fmt.Errorf("expect token signed with HMAC but got %v", token.Header["alg"])
}
},
}
}
func tokenCacheKey(token string) string {
return fmt.Sprintf("kubesphere:tokens:%s", token)
}

View File

@@ -0,0 +1,72 @@
/*
*
* 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 token
import (
"github.com/google/go-cmp/cmp"
"kubesphere.io/kubesphere/pkg/api/auth"
"kubesphere.io/kubesphere/pkg/api/iam"
"kubesphere.io/kubesphere/pkg/simple/client/cache"
"testing"
)
func TestJwtTokenIssuer(t *testing.T) {
options := auth.NewAuthenticateOptions()
options.JwtSecret = "kubesphere"
issuer := NewJwtTokenIssuer(DefaultIssuerName, options, cache.NewSimpleCache())
testCases := []struct {
description string
name string
uid string
email string
}{
{
name: "admin",
uid: "b8be6edd-2c92-4535-9b2a-df6326474458",
},
{
name: "bar",
uid: "b8be6edd-2c92-4535-9b2a-df6326474452",
},
}
for _, testCase := range testCases {
user := &iam.User{
Name: testCase.name,
UID: testCase.uid,
}
t.Run(testCase.description, func(t *testing.T) {
token, _, err := issuer.IssueTo(user)
if err != nil {
t.Fatal(err)
}
got, _, err := issuer.Verify(token)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(user, got); len(diff) != 0 {
t.Errorf("%T differ (-got, +expected), %s", user, diff)
}
})
}
}

View File

@@ -1 +0,0 @@
package token

View File

@@ -0,0 +1,27 @@
/*
*
* 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 token
type User interface {
// Name
GetName() string
// UID
GetUID() string
}

View File

@@ -22,7 +22,6 @@ import (
"k8s.io/apiserver/pkg/authentication/user"
"kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizer"
"kubesphere.io/kubesphere/pkg/models/iam/am"
"kubesphere.io/kubesphere/pkg/simple/client/cache"
"testing"
)
@@ -46,7 +45,7 @@ resources_in_cluster1 {
},
}
operator := am.NewFakeAMOperator(cache.NewSimpleCache())
operator := am.NewFakeAMOperator()
operator.Prepare(platformRoles, nil, nil, nil)
opa := NewOPAAuthorizer(operator)

View File

@@ -19,7 +19,7 @@ type iamHandler struct {
func newIAMHandler(k8sClient k8s.Client, factory informers.InformerFactory, ldapClient ldappool.Interface, cacheClient cache.Interface, options *auth.AuthenticationOptions) *iamHandler {
return &iamHandler{
amOperator: am.NewAMOperator(k8sClient.Kubernetes(), factory.KubernetesSharedInformerFactory()),
imOperator: im.NewIMOperator(ldapClient, cacheClient, options),
imOperator: im.NewLDAPOperator(ldapClient),
}
}
@@ -48,6 +48,7 @@ func (h *iamHandler) ListUserRoles(req *restful.Request, resp *restful.Response)
}
func (h *iamHandler) ListRoles(req *restful.Request, resp *restful.Response) {
panic("implement me")
}
func (h *iamHandler) ListClusterRoles(req *restful.Request, resp *restful.Response) {

View File

@@ -44,13 +44,13 @@ func AddToContainer(c *restful.Container, k8sClient k8s.Client, factory informer
handler := newIAMHandler(k8sClient, factory, ldapClient, cacheClient, options)
// implemented by create CRD object.
ws.Route(ws.POST("/users"))
ws.Route(ws.DELETE("/users/{user}"))
ws.Route(ws.PUT("/users/{user}"))
ws.Route(ws.GET("/users/{user}"))
//ws.Route(ws.POST("/users"))
//ws.Route(ws.DELETE("/users/{user}"))
//ws.Route(ws.PUT("/users/{user}"))
//ws.Route(ws.GET("/users/{user}"))
// TODO move to resources api
ws.Route(ws.GET("/users"))
//ws.Route(ws.GET("/users"))
ws.Route(ws.GET("/namespaces/{namespace}/roles").
To(handler.ListRoles).
@@ -66,7 +66,7 @@ func AddToContainer(c *restful.Container, k8sClient k8s.Client, factory informer
Metadata(restfulspec.KeyOpenAPITags, []string{constants.AccessManagementTag}))
// TODO merge
ws.Route(ws.GET("/namespaces/{namespace}/roles/{role}/users"))
//ws.Route(ws.GET("/namespaces/{namespace}/roles/{role}/users"))
ws.Route(ws.GET("/namespaces/{namespace}/users").
To(handler.ListNamespaceUsers).
Doc("List all users in the specified namespace.").

View File

@@ -19,19 +19,25 @@
package oauth
import (
"fmt"
"github.com/emicklei/go-restful"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/klog"
"kubesphere.io/kubesphere/pkg/api"
"kubesphere.io/kubesphere/pkg/api/auth"
"kubesphere.io/kubesphere/pkg/api/auth/token"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/token"
"kubesphere.io/kubesphere/pkg/apiserver/request"
"net/http"
)
type oauthHandler struct {
issuer token.Issuer
config oauth.Configuration
}
func newOAUTHHandler(issuer token.Issuer) *oauthHandler {
return &oauthHandler{issuer: issuer}
func newOAUTHHandler(issuer token.Issuer, config oauth.Configuration) *oauthHandler {
return &oauthHandler{issuer: issuer, config: config}
}
// Implement webhook authentication interface
@@ -53,7 +59,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)
@@ -71,3 +77,45 @@ func (h *oauthHandler) TokenReviewHandler(req *restful.Request, resp *restful.Re
resp.WriteEntity(success)
}
func (h *oauthHandler) AuthorizeHandler(req *restful.Request, resp *restful.Response) {
user, ok := request.UserFrom(req.Request.Context())
clientId := req.QueryParameter("client_id")
responseType := req.QueryParameter("response_type")
conf, err := h.config.Load(clientId)
if err != nil {
err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err))
resp.WriteError(http.StatusUnauthorized, err)
return
}
if responseType != "token" {
err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: response type %s is not supported", responseType))
resp.WriteError(http.StatusUnauthorized, err)
return
}
if !ok {
err := apierrors.NewUnauthorized("Unauthorized")
resp.WriteError(http.StatusUnauthorized, err)
return
}
accessToken, clm, err := h.issuer.IssueTo(user)
if err != nil {
err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err))
resp.WriteError(http.StatusUnauthorized, err)
return
}
redirectURL := fmt.Sprintf("%s?access_token=%s&token_type=Bearer", conf.RedirectURL, accessToken)
expiresIn := clm.ExpiresAt - clm.IssuedAt
if expiresIn > 0 {
redirectURL = fmt.Sprintf("%s&expires_in=%v", redirectURL, expiresIn)
}
http.Redirect(resp, req.Request, redirectURL, http.StatusFound)
}

View File

@@ -23,18 +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/api/auth/token"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/token"
"kubesphere.io/kubesphere/pkg/constants"
"net/http"
)
func AddToContainer(c *restful.Container, options *auth.AuthenticationOptions) error {
ws := restful.WebService{}
func AddToContainer(c *restful.Container, issuer token.Issuer, configuration oauth.Configuration) error {
ws := &restful.WebService{}
ws.Path("/oauth").
Consumes(restful.MIME_JSON).
Produces(restful.MIME_JSON)
handler := newOAUTHHandler(token.NewJwtTokenIssuer(token.DefaultIssuerName, []byte(options.JwtSecret)))
handler := newOAUTHHandler(issuer, configuration)
// Implement webhook authentication interface
// https://kubernetes.io/docs/reference/access-authn-authz/authentication/#webhook-token-authentication
@@ -46,16 +47,17 @@ func AddToContainer(c *restful.Container, options *auth.AuthenticationOptions) e
Metadata(restfulspec.KeyOpenAPITags, []string{constants.IdentityManagementTag}))
// TODO Built-in oauth2 server (provider)
// Low priority
c.Add(ws.Route(ws.POST("/authorize")))
// 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
c.Add(ws.Route(ws.POST("/token")))
// oauth2 client callback
c.Add(ws.Route(ws.POST("/callback/{callback}")))
// 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}"))
c.Add(ws)
return nil
}

View File

@@ -21,6 +21,7 @@ package am
import (
"encoding/json"
"fmt"
"k8s.io/apiserver/pkg/authentication/user"
"kubesphere.io/kubesphere/pkg/simple/client/cache"
)
@@ -124,6 +125,15 @@ func (f FakeRole) GetRego() string {
return f.Rego
}
func NewFakeAMOperator(cache cache.Interface) *FakeOperator {
return &FakeOperator{cache: cache}
func NewFakeAMOperator() *FakeOperator {
operator := &FakeOperator{cache: cache.NewSimpleCache()}
operator.saveFakeRole(platformRoleCacheKey("admin"), FakeRole{
Name: "admin",
Rego: "package authz\ndefault allow = true",
})
operator.saveFakeRole(platformRoleCacheKey(user.Anonymous), FakeRole{
Name: "admin",
Rego: "package authz\ndefault allow = false",
})
return operator
}

View File

@@ -0,0 +1,25 @@
/*
*
* 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 im
import "kubesphere.io/kubesphere/pkg/simple/client/ldap"
func NewFakeOperator() IdentityManagementInterface {
return NewLDAPOperator(ldap.NewSimpleLdap())
}

View File

@@ -18,19 +18,9 @@
package im
import (
"fmt"
"github.com/pkg/errors"
"golang.org/x/oauth2"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/klog"
"kubesphere.io/kubesphere/pkg/api/auth"
"kubesphere.io/kubesphere/pkg/api/auth/token"
"kubesphere.io/kubesphere/pkg/api/iam"
"kubesphere.io/kubesphere/pkg/models"
"kubesphere.io/kubesphere/pkg/server/params"
"kubesphere.io/kubesphere/pkg/simple/client/cache"
"kubesphere.io/kubesphere/pkg/simple/client/ldap"
"time"
)
type IdentityManagementInterface interface {
@@ -38,17 +28,11 @@ type IdentityManagementInterface interface {
DeleteUser(username string) error
ModifyUser(user *iam.User) (*iam.User, error)
DescribeUser(username string) (*iam.User, error)
Login(username, password, ip string) (*oauth2.Token, error)
ListUsers(conditions *params.Conditions, orderBy string, reverse bool, limit, offset int) (*models.PageableResponse, error)
GetUserRoles(username string) ([]*rbacv1.Role, error)
GetUserRole(namespace string, username string) (*rbacv1.Role, error)
Authenticate(username, password string) (*iam.User, error)
}
type imOperator struct {
authenticateOptions *auth.AuthenticationOptions
ldapClient ldap.Interface
cacheClient cache.Interface
issuer token.Issuer
ldapClient ldap.Interface
}
var (
@@ -57,164 +41,54 @@ var (
UserNotExists = errors.New("user not exists")
)
func NewIMOperator(ldapClient ldap.Interface, cacheClient cache.Interface, options *auth.AuthenticationOptions) *imOperator {
func NewLDAPOperator(ldapClient ldap.Interface) IdentityManagementInterface {
return &imOperator{
ldapClient: ldapClient,
cacheClient: cacheClient,
authenticateOptions: options,
issuer: token.NewJwtTokenIssuer(token.DefaultIssuerName, []byte(options.JwtSecret)),
ldapClient: ldapClient,
}
}
func (im *imOperator) ModifyUser(user *iam.User) (*iam.User, error) {
err := im.ldapClient.Update(user)
if err != nil {
return nil, err
}
// clear auth failed record
if user.Password != "" {
records, err := im.cacheClient.Keys(authenticationFailedKeyForUsername(user.Name, "*"))
if err == nil {
im.cacheClient.Del(records...)
}
}
return im.ldapClient.Get(user.Name)
}
func (im *imOperator) Login(username, password, ip string) (*oauth2.Token, error) {
records, err := im.cacheClient.Keys(authenticationFailedKeyForUsername(username, "*"))
if err != nil {
return nil, err
}
if len(records) > im.authenticateOptions.MaxAuthenticateRetries {
return nil, AuthRateLimitExceeded
}
func (im *imOperator) Authenticate(username, password string) (*iam.User, error) {
user, err := im.ldapClient.Get(username)
if err != nil {
return nil, err
}
err = im.ldapClient.Verify(user.Name, password)
if err != nil {
if err == ldap.ErrInvalidCredentials {
im.cacheClient.Set(authenticationFailedKeyForUsername(username, fmt.Sprintf("%d", time.Now().UnixNano())), "", 30*time.Minute)
}
return nil, err
}
issuedToken, err := im.issuer.IssueTo(user)
err = im.ldapClient.Authenticate(user.Name, password)
if err != nil {
return nil, err
}
// TODO: I think we should come up with a better strategy to prevent multiple login.
tokenKey := tokenKeyForUsername(user.Name, issuedToken)
if !im.authenticateOptions.MultipleLogin {
// multi login not allowed, remove the previous token
sessions, err := im.cacheClient.Keys(tokenKey)
if err != nil {
return nil, err
}
if len(sessions) > 0 {
klog.V(4).Infoln("revoke token", sessions)
err = im.cacheClient.Del(sessions...)
if err != nil {
return nil, err
}
}
}
// save token with expiration time
if err = im.cacheClient.Set(tokenKey, issuedToken, im.authenticateOptions.TokenExpiration); err != nil {
return nil, err
}
im.logLogin(user.Name, ip, time.Now())
return &oauth2.Token{AccessToken: issuedToken}, nil
}
func (im *imOperator) logLogin(username, ip string, loginTime time.Time) {
if ip != "" {
_ = im.cacheClient.Set(loginKeyForUsername(username, loginTime.UTC().Format("2006-01-02T15:04:05Z"), ip), "", 30*24*time.Hour)
}
}
func (im *imOperator) LoginHistory(username string) ([]string, error) {
keys, err := im.cacheClient.Keys(loginKeyForUsername(username, "*", "*"))
if err != nil {
return nil, err
}
return keys, nil
}
func (im *imOperator) ListUsers(conditions *params.Conditions, orderBy string, reverse bool, limit, offset int) (*models.PageableResponse, error) {
panic("implement me")
return user, nil
}
func (im *imOperator) DescribeUser(username string) (*iam.User, error) {
return im.ldapClient.Get(username)
}
func (im *imOperator) getLastLoginTime(username string) string {
return ""
}
func (im *imOperator) DeleteUser(username string) error {
return im.ldapClient.Delete(username)
}
func (im *imOperator) CreateUser(user *iam.User) (*iam.User, error) {
err := im.ldapClient.Create(user)
if err != nil {
return nil, err
}
return user, nil
}
func (im *imOperator) VerifyToken(tokenString string) (*iam.User, error) {
providedUser, err := im.issuer.Verify(tokenString)
if err != nil {
return nil, err
}
user, err := im.ldapClient.Get(providedUser.GetName())
if err != nil {
return nil, err
}
return user, nil
}
func (im *imOperator) uidNumberNext() int {
// TODO fix me
return 0
}
func (im *imOperator) GetUserRoles(username string) ([]*rbacv1.Role, error) {
panic("implement me")
}
func (im *imOperator) GetUserRole(namespace string, username string) (*rbacv1.Role, error) {
panic("implement me")
}
func authenticationFailedKeyForUsername(username, failedTimestamp string) string {
return fmt.Sprintf("kubesphere:authfailed:%s:%s", username, failedTimestamp)
}
func tokenKeyForUsername(username, token string) string {
return fmt.Sprintf("kubesphere:users:%s:token:%s", username, token)
}
func loginKeyForUsername(username, loginTimestamp, ip string) string {
return fmt.Sprintf("kubesphere:users:%s:login-log:%s:%s", username, loginTimestamp, ip)
}

View File

@@ -16,6 +16,6 @@ type Interface interface {
// Get gets a user by its username from ldap, return ErrUserNotExists if user not exists
Get(name string) (*iam.User, error)
// Verify checks if (name, password) is valid, return ErrInvalidCredentials if not
Verify(name string, password string) error
// Authenticate checks if (name, password) is valid, return ErrInvalidCredentials if not
Authenticate(name string, password string) error
}

View File

@@ -347,7 +347,7 @@ func (l *ldapInterfaceImpl) Update(newUser *iam.User) error {
}
func (l *ldapInterfaceImpl) Verify(username, password string) error {
func (l *ldapInterfaceImpl) Authenticate(username, password string) error {
conn, err := l.newConn()
if err != nil {
return err

View File

@@ -60,7 +60,7 @@ func (s simpleLdap) Get(name string) (*iam.User, error) {
}
}
func (s simpleLdap) Verify(name string, password string) error {
func (s simpleLdap) Authenticate(name string, password string) error {
if user, err := s.Get(name); err != nil {
return err
} else {

View File

@@ -85,12 +85,12 @@ func TestSimpleLdap(t *testing.T) {
t.Fatal(err)
}
err = ldapClient.Verify(foo.Name, foo.Password)
err = ldapClient.Authenticate(foo.Name, foo.Password)
if err != nil {
t.Fatalf("should pass but got an error %v", err)
}
err = ldapClient.Verify(foo.Name, "gibberish")
err = ldapClient.Authenticate(foo.Name, "gibberish")
if err == nil || err != ErrInvalidCredentials {
t.Fatalf("expected error ErrInvalidCrenentials but got %v", err)
}