diff --git a/pkg/api/auth/token/jwt.go b/pkg/api/auth/token/jwt.go index b0537c40d..88cec5685 100644 --- a/pkg/api/auth/token/jwt.go +++ b/pkg/api/auth/token/jwt.go @@ -3,6 +3,7 @@ package token import ( "fmt" "github.com/dgrijalva/jwt-go" + "kubesphere.io/kubesphere/pkg/api/iam" "kubesphere.io/kubesphere/pkg/server/errors" "time" ) @@ -12,9 +13,9 @@ const DefaultIssuerName = "kubesphere" var errInvalidToken = errors.New("invalid token") type claims struct { - Username string `json:"username"` - UID string `json:"uid"` - Groups []string `json:"groups"` + Username string `json:"username"` + UID string `json:"uid"` + Email string `json:"email"` // Currently, we are not using any field in jwt.StandardClaims jwt.StandardClaims } @@ -37,14 +38,14 @@ func (s *jwtTokenIssuer) Verify(tokenString string) (User, error) { return nil, err } - return &AuthUser{Name: clm.Username, UID: clm.UID, Groups: clm.Groups}, nil + 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(), - Groups: user.GetGroups(), + Email: user.GetEmail(), StandardClaims: jwt.StandardClaims{ IssuedAt: time.Now().Unix(), Issuer: s.name, diff --git a/pkg/api/auth/token/jwt_test.go b/pkg/api/auth/token/jwt_test.go index 3fe3f7dfe..193b1f853 100644 --- a/pkg/api/auth/token/jwt_test.go +++ b/pkg/api/auth/token/jwt_test.go @@ -2,6 +2,7 @@ package token import ( "github.com/google/go-cmp/cmp" + "kubesphere.io/kubesphere/pkg/api/iam" "testing" ) @@ -12,19 +13,22 @@ func TestJwtTokenIssuer(t *testing.T) { description string name string uid string + email string }{ { - name: "admin", - uid: "b8be6edd-2c92-4535-9b2a-df6326474458", + name: "admin", + uid: "b8be6edd-2c92-4535-9b2a-df6326474458", + email: "admin@kubesphere.io", }, { - name: "bar", - uid: "b8be6edd-2c92-4535-9b2a-df6326474452", + name: "bar", + uid: "b8be6edd-2c92-4535-9b2a-df6326474452", + email: "bar@kubesphere.io", }, } for _, testCase := range testCases { - user := &AuthUser{ + user := &iam.User{ Name: testCase.name, UID: testCase.uid, } diff --git a/pkg/api/auth/token/user.go b/pkg/api/auth/token/user.go index 55e8cfbda..2a86e2f5f 100644 --- a/pkg/api/auth/token/user.go +++ b/pkg/api/auth/token/user.go @@ -7,24 +7,6 @@ type User interface { // UID GetUID() string - // Groups - GetGroups() []string -} - -type AuthUser struct { - Name string - UID string - Groups []string -} - -func (a AuthUser) GetName() string { - return a.Name -} - -func (a AuthUser) GetUID() string { - return a.UID -} - -func (a AuthUser) GetGroups() []string { - return a.Groups + // Email + GetEmail() string } diff --git a/pkg/api/iam/user.go b/pkg/api/iam/user.go index 7c57f8386..526cf346f 100644 --- a/pkg/api/iam/user.go +++ b/pkg/api/iam/user.go @@ -6,30 +6,30 @@ import ( ) type User struct { - Username string `json:"username"` + Name string `json:"username"` UID string `json:"uid"` Email string `json:"email"` Lang string `json:"lang,omitempty"` Description string `json:"description"` - CreateTime time.Time `json:"create_time"` + CreateTime time.Time `json:"createTime"` Groups []string `json:"groups,omitempty"` Password string `json:"password,omitempty"` } func (u *User) GetName() string { - return u.Username + return u.Name } func (u *User) GetUID() string { return u.UID } -func (u *User) GetGroups() []string { - return u.Groups +func (u *User) GetEmail() string { + return u.Email } func (u *User) Validate() error { - if u.Username == "" { + if u.Name == "" { return errors.New("username can not be empty") } diff --git a/pkg/apiserver/authorization/authorizerfactory/opa.go b/pkg/apiserver/authorization/authorizerfactory/opa.go index 7b1d9ab69..609945477 100644 --- a/pkg/apiserver/authorization/authorizerfactory/opa.go +++ b/pkg/apiserver/authorization/authorizerfactory/opa.go @@ -29,20 +29,21 @@ type opaAuthorizer struct { am am.AccessManagementInterface } +// Make decision by request attributes func (o *opaAuthorizer) Authorize(attr authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { + // Make decisions based on the authorization policy of different levels of roles platformRole, err := o.am.GetPlatformRole(attr.GetUser().GetName()) if err != nil { return authorizer.DecisionDeny, "", err } // check platform role policy rules - if a, r, e := makeDecision(platformRole, attr); a == authorizer.DecisionAllow { - return a, r, e + if authorized, reason, err = makeDecision(platformRole, attr); authorized == authorizer.DecisionAllow { + return authorized, reason, err } // it's not in cluster resource, permission denied - // TODO declare implicit cluster info in request Info if attr.GetCluster() == "" { return authorizer.DecisionDeny, "permission undefined", nil } @@ -78,7 +79,7 @@ func (o *opaAuthorizer) Authorize(attr authorizer.Attributes) (authorized author } if attr.GetNamespace() != "" { - namespaceRole, err := o.am.GetNamespaceRole(attr.GetNamespace(), attr.GetUser().GetName()) + namespaceRole, err := o.am.GetNamespaceRole(attr.GetCluster(), attr.GetNamespace(), attr.GetUser().GetName()) if err != nil { return authorizer.DecisionDeny, "", err } @@ -102,6 +103,29 @@ func makeDecision(role am.Role, a authorizer.Attributes) (authorized authorizer. return authorizer.DecisionDeny, "", err } + // data example + //{ + // "User": { + // "Name": "admin", + // "UID": "0", + // "Groups": [ + // "admin" + // ], + // "Extra": null + // }, + // "Verb": "list", + // "Cluster": "cluster1", + // "Workspace": "", + // "Namespace": "", + // "APIGroup": "", + // "APIVersion": "v1", + // "Resource": "nodes", + // "Subresource": "", + // "Name": "", + // "KubernetesRequest": true, + // "ResourceRequest": true, + // "Path": "/api/v1/nodes" + //} // The policy decision is contained in the results returned by the Eval() call. You can inspect the decision and handle it accordingly. results, err := query.Eval(context.Background(), rego.EvalInput(a)) diff --git a/pkg/apiserver/authorization/authorizerfactory/opa_test.go b/pkg/apiserver/authorization/authorizerfactory/opa_test.go index f435e7f7e..34d6e91da 100644 --- a/pkg/apiserver/authorization/authorizerfactory/opa_test.go +++ b/pkg/apiserver/authorization/authorizerfactory/opa_test.go @@ -27,8 +27,29 @@ import ( ) func TestPlatformRole(t *testing.T) { + platformRoles := map[string]am.FakeRole{"admin": { + Name: "admin", + Rego: "package authz\ndefault allow = true", + }, "anonymous": { + Name: "anonymous", + Rego: "package authz\ndefault allow = false", + }, "tom": { + Name: "tom", + Rego: `package authz +default allow = false +allow { + resources_in_cluster1 +} +resources_in_cluster1 { + input.Cluster == "cluster1" +}`, + }, + } - opa := NewOPAAuthorizer(am.NewFakeAMOperator(cache.NewSimpleCache())) + operator := am.NewFakeAMOperator(cache.NewSimpleCache()) + operator.Prepare(platformRoles, nil, nil, nil) + + opa := NewOPAAuthorizer(operator) tests := []struct { name string @@ -36,7 +57,7 @@ func TestPlatformRole(t *testing.T) { expectedDecision authorizer.Decision }{ { - name: "list nodes", + name: "admin can list nodes", request: authorizer.AttributesRecord{ User: &user.DefaultInfo{ Name: "admin", @@ -60,7 +81,7 @@ func TestPlatformRole(t *testing.T) { expectedDecision: authorizer.DecisionAllow, }, { - name: "list nodes", + name: "anonymous can not list nodes", request: authorizer.AttributesRecord{ User: &user.DefaultInfo{ Name: user.Anonymous, @@ -82,13 +103,54 @@ func TestPlatformRole(t *testing.T) { Path: "/api/v1/nodes", }, expectedDecision: authorizer.DecisionDeny, + }, { + name: "tom can list nodes in cluster1", + request: authorizer.AttributesRecord{ + User: &user.DefaultInfo{ + Name: "tom", + }, + Verb: "list", + Cluster: "cluster1", + Workspace: "", + Namespace: "", + APIGroup: "", + APIVersion: "v1", + Resource: "nodes", + Subresource: "", + Name: "", + KubernetesRequest: true, + ResourceRequest: true, + Path: "/api/v1/clusters/cluster1/nodes", + }, + expectedDecision: authorizer.DecisionAllow, + }, + { + name: "tom can not list nodes in cluster2", + request: authorizer.AttributesRecord{ + User: &user.DefaultInfo{ + Name: "tom", + }, + Verb: "list", + Cluster: "cluster2", + Workspace: "", + Namespace: "", + APIGroup: "", + APIVersion: "v1", + Resource: "nodes", + Subresource: "", + Name: "", + KubernetesRequest: true, + ResourceRequest: true, + Path: "/api/v1/clusters/cluster2/nodes", + }, + expectedDecision: authorizer.DecisionDeny, }, } for _, test := range tests { decision, _, err := opa.Authorize(test.request) if err != nil { - t.Error(err) + t.Errorf("test failed: %s, %v", test.name, err) } if decision != test.expectedDecision { t.Errorf("%s: expected decision %v, actual %+v", test.name, test.expectedDecision, decision) diff --git a/pkg/kapis/oauth/handler.go b/pkg/kapis/oauth/handler.go index 530f1ec98..6213b5fde 100644 --- a/pkg/kapis/oauth/handler.go +++ b/pkg/kapis/oauth/handler.go @@ -65,7 +65,7 @@ func (h *oauthHandler) TokenReviewHandler(req *restful.Request, resp *restful.Re Kind: auth.KindTokenReview, Status: &auth.Status{ Authenticated: true, - User: map[string]interface{}{"username": user.GetName(), "uid": user.GetUID(), "groups": user.GetGroups()}, + User: map[string]interface{}{"username": user.GetName(), "uid": user.GetUID()}, }, } diff --git a/pkg/models/iam/am/am.go b/pkg/models/iam/am/am.go index 4212954e9..9a2da7f84 100644 --- a/pkg/models/iam/am/am.go +++ b/pkg/models/iam/am/am.go @@ -36,7 +36,7 @@ type AccessManagementInterface interface { GetPlatformRole(username string) (Role, error) GetClusterRole(cluster, username string) (Role, error) GetWorkspaceRole(workspace, username string) (Role, error) - GetNamespaceRole(namespace, username string) (Role, error) + GetNamespaceRole(cluster, namespace, username string) (Role, error) } type Role interface { @@ -73,10 +73,6 @@ func (am *amOperator) GetWorkspaceRole(workspace, username string) (Role, error) panic("implement me") } -func (am *amOperator) GetNamespaceRole(namespace, username string) (Role, error) { - panic("implement me") -} - -func (am *amOperator) GetDevOpsRole(namespace, username string) (Role, error) { +func (am *amOperator) GetNamespaceRole(cluster, namespace, username string) (Role, error) { panic("implement me") } diff --git a/pkg/models/iam/am/fake_operator.go b/pkg/models/iam/am/fake_operator.go index 8297e5654..94be3a566 100644 --- a/pkg/models/iam/am/fake_operator.go +++ b/pkg/models/iam/am/fake_operator.go @@ -19,55 +19,111 @@ package am import ( - "k8s.io/apiserver/pkg/authentication/user" + "encoding/json" + "fmt" "kubesphere.io/kubesphere/pkg/simple/client/cache" ) -type fakeRole struct { +type FakeRole struct { Name string Rego string } -type fakeOperator struct { +type FakeOperator struct { cache cache.Interface } -func newFakeRole(username string) Role { - if username == user.Anonymous { - return &fakeRole{ - Name: "anonymous", - Rego: "package authz\ndefault allow = false", +func (f FakeOperator) queryFakeRole(cacheKey string) (Role, error) { + data, err := f.cache.Get(cacheKey) + if err != nil { + if err == cache.ErrNoSuchKey { + return &FakeRole{ + Name: "DenyAll", + Rego: "package authz\ndefault allow = false", + }, nil + } + return nil, err + } + var role FakeRole + err = json.Unmarshal([]byte(data), &role) + if err != nil { + return nil, err + } + return role, nil +} + +func (f FakeOperator) saveFakeRole(cacheKey string, role FakeRole) error { + data, err := json.Marshal(role) + if err != nil { + return err + } + return f.cache.Set(cacheKey, string(data), 0) +} + +func (f FakeOperator) GetPlatformRole(username string) (Role, error) { + return f.queryFakeRole(platformRoleCacheKey(username)) +} + +func (f FakeOperator) GetClusterRole(cluster, username string) (Role, error) { + return f.queryFakeRole(clusterRoleCacheKey(cluster, username)) +} + +func (f FakeOperator) GetWorkspaceRole(workspace, username string) (Role, error) { + return f.queryFakeRole(workspaceRoleCacheKey(workspace, username)) +} + +func (f FakeOperator) GetNamespaceRole(cluster, namespace, username string) (Role, error) { + return f.queryFakeRole(namespaceRoleCacheKey(cluster, namespace, username)) +} + +func (f FakeOperator) Prepare(platformRoles map[string]FakeRole, clusterRoles map[string]map[string]FakeRole, workspaceRoles map[string]map[string]FakeRole, namespaceRoles map[string]map[string]map[string]FakeRole) { + + for username, role := range platformRoles { + f.saveFakeRole(platformRoleCacheKey(username), role) + } + for cluster, roles := range clusterRoles { + for username, role := range roles { + f.saveFakeRole(clusterRoleCacheKey(cluster, username), role) } } - return &fakeRole{ - Name: "admin", - Rego: "package authz\ndefault allow = true", + + for workspace, roles := range workspaceRoles { + for username, role := range roles { + f.saveFakeRole(workspaceRoleCacheKey(workspace, username), role) + } + } + + for cluster, nsRoles := range namespaceRoles { + for namespace, roles := range nsRoles { + for username, role := range roles { + f.saveFakeRole(namespaceRoleCacheKey(cluster, namespace, username), role) + } + } } } -func (f fakeOperator) GetPlatformRole(username string) (Role, error) { - return newFakeRole(username), nil +func namespaceRoleCacheKey(cluster, namespace, username string) string { + return fmt.Sprintf("cluster.%s.namespaces.%s.roles.%s", cluster, namespace, username) } -func (f fakeOperator) GetClusterRole(cluster, username string) (Role, error) { - return newFakeRole(username), nil +func clusterRoleCacheKey(cluster, username string) string { + return fmt.Sprintf("cluster.%s.roles.%s", cluster, username) +} +func workspaceRoleCacheKey(workspace, username string) string { + return fmt.Sprintf("workspace.%s.roles.%s", workspace, username) } -func (f fakeOperator) GetWorkspaceRole(workspace, username string) (Role, error) { - return newFakeRole(username), nil +func platformRoleCacheKey(username string) string { + return fmt.Sprintf("platform.roles.%s", username) } -func (f fakeOperator) GetNamespaceRole(namespace, username string) (Role, error) { - return newFakeRole(username), nil -} - -func (f fakeRole) GetName() string { +func (f FakeRole) GetName() string { return f.Name } -func (f fakeRole) GetRego() string { +func (f FakeRole) GetRego() string { return f.Rego } -func NewFakeAMOperator(cache cache.Interface) AccessManagementInterface { - return &fakeOperator{cache: cache} +func NewFakeAMOperator(cache cache.Interface) *FakeOperator { + return &FakeOperator{cache: cache} } diff --git a/pkg/models/iam/im/im.go b/pkg/models/iam/im/im.go index e4cb57448..07720d661 100644 --- a/pkg/models/iam/im/im.go +++ b/pkg/models/iam/im/im.go @@ -75,13 +75,13 @@ func (im *imOperator) ModifyUser(user *iam.User) (*iam.User, error) { // clear auth failed record if user.Password != "" { - records, err := im.cacheClient.Keys(authenticationFailedKeyForUsername(user.Username, "*")) + records, err := im.cacheClient.Keys(authenticationFailedKeyForUsername(user.Name, "*")) if err == nil { im.cacheClient.Del(records...) } } - return im.ldapClient.Get(user.Username) + return im.ldapClient.Get(user.Name) } func (im *imOperator) Login(username, password, ip string) (*oauth2.Token, error) { @@ -100,7 +100,7 @@ func (im *imOperator) Login(username, password, ip string) (*oauth2.Token, error return nil, err } - err = im.ldapClient.Verify(user.Username, password) + 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) @@ -114,7 +114,7 @@ func (im *imOperator) Login(username, password, ip string) (*oauth2.Token, error } // TODO: I think we should come up with a better strategy to prevent multiple login. - tokenKey := tokenKeyForUsername(user.Username, issuedToken) + tokenKey := tokenKeyForUsername(user.Name, issuedToken) if !im.authenticateOptions.MultipleLogin { // multi login not allowed, remove the previous token sessions, err := im.cacheClient.Keys(tokenKey) @@ -136,7 +136,7 @@ func (im *imOperator) Login(username, password, ip string) (*oauth2.Token, error return nil, err } - im.logLogin(user.Username, ip, time.Now()) + im.logLogin(user.Name, ip, time.Now()) return &oauth2.Token{AccessToken: issuedToken}, nil } diff --git a/pkg/simple/client/ldap/ldap.go b/pkg/simple/client/ldap/ldap.go index 025887c1d..d228e58fb 100644 --- a/pkg/simple/client/ldap/ldap.go +++ b/pkg/simple/client/ldap/ldap.go @@ -216,7 +216,7 @@ func (l *ldapInterfaceImpl) Get(name string) (*iam.User, error) { userEntry := searchResults.Entries[0] user := &iam.User{ - Username: userEntry.GetAttributeValue(ldapAttributeUserID), + Name: userEntry.GetAttributeValue(ldapAttributeUserID), Email: userEntry.GetAttributeValue(ldapAttributeMail), Lang: userEntry.GetAttributeValue(ldapAttributePreferredLanguage), Description: userEntry.GetAttributeValue(ldapAttributeDescription), @@ -229,12 +229,12 @@ func (l *ldapInterfaceImpl) Get(name string) (*iam.User, error) { } func (l *ldapInterfaceImpl) Create(user *iam.User) error { - if _, err := l.Get(user.Username); err != nil { + if _, err := l.Get(user.Name); err != nil { return ErrUserAlreadyExisted } createRequest := &ldap.AddRequest{ - DN: l.dnForUsername(user.Username), + DN: l.dnForUsername(user.Name), Attributes: []ldap.Attribute{ { Type: ldapAttributeObjectClass, @@ -242,7 +242,7 @@ func (l *ldapInterfaceImpl) Create(user *iam.User) error { }, { Type: ldapAttributeCommonName, - Vals: []string{user.Username}, + Vals: []string{user.Name}, }, { Type: ldapAttributeSerialNumber, @@ -254,11 +254,11 @@ func (l *ldapInterfaceImpl) Create(user *iam.User) error { }, { Type: ldapAttributeHomeDirectory, - Vals: []string{"/home/" + user.Username}, + Vals: []string{"/home/" + user.Name}, }, { Type: ldapAttributeUserID, - Vals: []string{user.Username}, + Vals: []string{user.Name}, }, { Type: ldapAttributeUserIDNumber, @@ -322,13 +322,13 @@ func (l *ldapInterfaceImpl) Update(newUser *iam.User) error { defer conn.Close() // check user existed - _, err = l.Get(newUser.Username) + _, err = l.Get(newUser.Name) if err != nil { return err } modifyRequest := &ldap.ModifyRequest{ - DN: l.dnForUsername(newUser.Username), + DN: l.dnForUsername(newUser.Name), } if newUser.Description != "" { diff --git a/pkg/simple/client/ldap/simple_ldap.go b/pkg/simple/client/ldap/simple_ldap.go index 78a82b4c6..deedd46a4 100644 --- a/pkg/simple/client/ldap/simple_ldap.go +++ b/pkg/simple/client/ldap/simple_ldap.go @@ -17,7 +17,7 @@ func NewSimpleLdap() Interface { // initialize with a admin user admin := &iam.User{ - Username: "admin", + Name: "admin", Email: "admin@kubesphere.io", Lang: "eng", Description: "administrator", @@ -25,21 +25,21 @@ func NewSimpleLdap() Interface { Groups: nil, Password: "P@88w0rd", } - sl.store[admin.Username] = admin + sl.store[admin.Name] = admin return sl } func (s simpleLdap) Create(user *iam.User) error { - s.store[user.Username] = user + s.store[user.Name] = user return nil } func (s simpleLdap) Update(user *iam.User) error { - _, err := s.Get(user.Username) + _, err := s.Get(user.Name) if err != nil { return err } - s.store[user.Username] = user + s.store[user.Name] = user return nil } diff --git a/pkg/simple/client/ldap/simple_ldap_test.go b/pkg/simple/client/ldap/simple_ldap_test.go index c50096b2c..c39144ba1 100644 --- a/pkg/simple/client/ldap/simple_ldap_test.go +++ b/pkg/simple/client/ldap/simple_ldap_test.go @@ -11,7 +11,7 @@ func TestSimpleLdap(t *testing.T) { ldapClient := NewSimpleLdap() foo := &iam.User{ - Username: "jerry", + Name: "jerry", Email: "jerry@kubesphere.io", Lang: "en", Description: "Jerry is kind and gentle.", @@ -27,7 +27,7 @@ func TestSimpleLdap(t *testing.T) { } // check if user really created - user, err := ldapClient.Get(foo.Username) + user, err := ldapClient.Get(foo.Name) if err != nil { t.Fatal(err) } @@ -35,7 +35,7 @@ func TestSimpleLdap(t *testing.T) { t.Fatalf("%T differ (-got, +want): %s", user, diff) } - _ = ldapClient.Delete(foo.Username) + _ = ldapClient.Delete(foo.Name) }) t.Run("should update user", func(t *testing.T) { @@ -51,7 +51,7 @@ func TestSimpleLdap(t *testing.T) { } // check if user really created - user, err := ldapClient.Get(foo.Username) + user, err := ldapClient.Get(foo.Name) if err != nil { t.Fatal(err) } @@ -59,7 +59,7 @@ func TestSimpleLdap(t *testing.T) { t.Fatalf("%T differ (-got, +want): %s", user, diff) } - _ = ldapClient.Delete(foo.Username) + _ = ldapClient.Delete(foo.Name) }) t.Run("should delete user", func(t *testing.T) { @@ -68,12 +68,12 @@ func TestSimpleLdap(t *testing.T) { t.Fatal(err) } - err = ldapClient.Delete(foo.Username) + err = ldapClient.Delete(foo.Name) if err != nil { t.Fatal(err) } - _, err = ldapClient.Get(foo.Username) + _, err = ldapClient.Get(foo.Name) if err == nil || err != ErrUserNotExists { t.Fatalf("expected ErrUserNotExists error, got %v", err) } @@ -85,12 +85,12 @@ func TestSimpleLdap(t *testing.T) { t.Fatal(err) } - err = ldapClient.Verify(foo.Username, foo.Password) + err = ldapClient.Verify(foo.Name, foo.Password) if err != nil { t.Fatalf("should pass but got an error %v", err) } - err = ldapClient.Verify(foo.Username, "gibberish") + err = ldapClient.Verify(foo.Name, "gibberish") if err == nil || err != ErrInvalidCredentials { t.Fatalf("expected error ErrInvalidCrenentials but got %v", err) }