@@ -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)
|
||||
}
|
||||
@@ -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"])
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package token
|
||||
|
||||
type User interface {
|
||||
// Name
|
||||
GetName() string
|
||||
|
||||
// UID
|
||||
GetUID() string
|
||||
|
||||
// Email
|
||||
GetEmail() string
|
||||
}
|
||||
@@ -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
|
||||
|
||||
58
pkg/apiserver/authentication/authenticators/basic/basic.go
Normal file
58
pkg/apiserver/authentication/authenticators/basic/basic.go
Normal 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
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
30
pkg/apiserver/authentication/oauth/oauth.go
Normal file
30
pkg/apiserver/authentication/oauth/oauth.go
Normal 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)
|
||||
}
|
||||
37
pkg/apiserver/authentication/oauth/simple_config.go
Normal file
37
pkg/apiserver/authentication/oauth/simple_config.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
46
pkg/apiserver/authentication/request/anonymous/anonymous.go
Normal file
46
pkg/apiserver/authentication/request/anonymous/anonymous.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
31
pkg/apiserver/authentication/token/issuer.go
Normal file
31
pkg/apiserver/authentication/token/issuer.go
Normal 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
|
||||
}
|
||||
124
pkg/apiserver/authentication/token/jwt.go
Normal file
124
pkg/apiserver/authentication/token/jwt.go
Normal 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)
|
||||
}
|
||||
72
pkg/apiserver/authentication/token/jwt_test.go
Normal file
72
pkg/apiserver/authentication/token/jwt_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
package token
|
||||
27
pkg/apiserver/authentication/token/user.go
Normal file
27
pkg/apiserver/authentication/token/user.go
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.").
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
25
pkg/models/iam/im/fake_operator.go
Normal file
25
pkg/models/iam/im/fake_operator.go
Normal 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())
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user