improve IAM module

Signed-off-by: hongming <talonwan@yunify.com>
This commit is contained in:
hongming
2020-05-22 09:35:05 +08:00
parent 0d12529051
commit 8f93266ec0
640 changed files with 50221 additions and 18179 deletions

View File

@@ -11,9 +11,6 @@ import (
unionauth "k8s.io/apiserver/pkg/authentication/request/union"
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
"k8s.io/klog"
clusterv1alpha1 "kubesphere.io/kubesphere/pkg/apis/cluster/v1alpha1"
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
tenantv1alpha1 "kubesphere.io/kubesphere/pkg/apis/tenant/v1alpha1"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/authenticators/basic"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/authenticators/jwttoken"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/request/anonymous"
@@ -156,8 +153,10 @@ func (s *APIServer) installKubeSphereAPIs() {
urlruntime.Must(openpitrixv1.AddToContainer(s.container, s.InformerFactory, s.OpenpitrixClient))
urlruntime.Must(networkv1alpha2.AddToContainer(s.container, s.Config.NetworkOptions.WeaveScopeHost))
urlruntime.Must(operationsv1alpha2.AddToContainer(s.container, s.KubernetesClient.Kubernetes()))
urlruntime.Must(resourcesv1alpha2.AddToContainer(s.container, s.KubernetesClient.Kubernetes(), s.InformerFactory))
urlruntime.Must(tenantv1alpha2.AddToContainer(s.container, s.InformerFactory, s.EventsClient))
urlruntime.Must(resourcesv1alpha2.AddToContainer(s.container, s.KubernetesClient.Kubernetes(), s.InformerFactory,
s.KubernetesClient.Master()))
urlruntime.Must(tenantv1alpha2.AddToContainer(s.container, s.InformerFactory, s.KubernetesClient.Kubernetes(),
s.KubernetesClient.KubeSphere(), s.EventsClient))
urlruntime.Must(terminalv1alpha2.AddToContainer(s.container, s.KubernetesClient.Kubernetes(), s.KubernetesClient.Config()))
urlruntime.Must(clusterkapisv1alpha1.AddToContainer(s.container,
s.InformerFactory.KubernetesSharedInformerFactory(),
@@ -167,9 +166,10 @@ func (s *APIServer) installKubeSphereAPIs() {
s.Config.MultiClusterOptions.AgentImage))
urlruntime.Must(iamapi.AddToContainer(s.container,
im.NewOperator(s.KubernetesClient.KubeSphere(), s.InformerFactory),
am.NewAMOperator(s.InformerFactory),
am.NewOperator(s.InformerFactory, s.KubernetesClient.KubeSphere(), s.KubernetesClient.Kubernetes()),
s.Config.AuthenticationOptions))
urlruntime.Must(oauth.AddToContainer(s.container,
im.NewOperator(s.KubernetesClient.KubeSphere(), s.InformerFactory),
token.NewJwtTokenIssuer(token.DefaultIssuerName, s.Config.AuthenticationOptions, s.CacheClient),
s.Config.AuthenticationOptions))
urlruntime.Must(servicemeshv1alpha2.AddToContainer(s.container))
@@ -212,13 +212,6 @@ func (s *APIServer) buildHandlerChain() {
requestInfoResolver := &request.RequestInfoFactory{
APIPrefixes: sets.NewString("api", "apis", "kapis", "kapi"),
GrouplessAPIPrefixes: sets.NewString("api", "kapi"),
GlobalResources: []schema.GroupResource{
{Group: iamv1alpha2.SchemeGroupVersion.Group, Resource: iamv1alpha2.ResourcesPluralUser},
{Group: iamv1alpha2.SchemeGroupVersion.Group, Resource: iamv1alpha2.ResourcesPluralGlobalRole},
{Group: iamv1alpha2.SchemeGroupVersion.Group, Resource: iamv1alpha2.ResourcesPluralGlobalRoleBinding},
{Group: tenantv1alpha1.SchemeGroupVersion.Group, Resource: tenantv1alpha1.ResourcePluralWorkspace},
{Group: clusterv1alpha1.SchemeGroupVersion.Group, Resource: clusterv1alpha1.ResourcesPluralCluster},
},
}
handler := s.Server.Handler
@@ -241,7 +234,8 @@ func (s *APIServer) buildHandlerChain() {
case authorizationoptions.RBAC:
excludedPaths := []string{"/oauth/*", "/kapis/config.kubesphere.io/*"}
pathAuthorizer, _ := path.NewAuthorizer(excludedPaths)
authorizers = unionauthorizer.New(pathAuthorizer, authorizerfactory.NewOPAAuthorizer(am.NewAMOperator(s.InformerFactory)), authorizerfactory.NewRBACAuthorizer(am.NewAMOperator(s.InformerFactory)))
amOperator := am.NewReadOnlyOperator(s.InformerFactory)
authorizers = unionauthorizer.New(pathAuthorizer, authorizerfactory.NewRBACAuthorizer(amOperator))
}
handler = filters.WithAuthorization(handler, authorizers)
@@ -330,12 +324,14 @@ func (s *APIServer) waitForResourceSync(stopCh <-chan struct{}) error {
ksGVRs := []schema.GroupVersionResource{
{Group: "tenant.kubesphere.io", Version: "v1alpha1", Resource: "workspaces"},
{Group: "tenant.kubesphere.io", Version: "v1alpha2", Resource: "workspacetemplates"},
{Group: "iam.kubesphere.io", Version: "v1alpha2", Resource: "users"},
{Group: "iam.kubesphere.io", Version: "v1alpha2", Resource: "globalroles"},
{Group: "iam.kubesphere.io", Version: "v1alpha2", Resource: "globalrolebindings"},
{Group: "iam.kubesphere.io", Version: "v1alpha2", Resource: "workspaceroles"},
{Group: "iam.kubesphere.io", Version: "v1alpha2", Resource: "workspacerolebindings"},
{Group: "cluster.kubesphere.io", Version: "v1alpha1", Resource: "clusters"},
{Group: "devops.kubesphere.io", Version: "v1alpha3", Resource: "devopsprojects"},
}
devopsGVRs := []schema.GroupVersionResource{

View File

@@ -0,0 +1,172 @@
/*
*
* 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 github
import (
"context"
"encoding/json"
"golang.org/x/oauth2"
"gopkg.in/yaml.v3"
"io/ioutil"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
"time"
)
const (
UserInfoURL = "https://api.github.com/user"
)
type Github struct {
// ClientID is the application's ID.
ClientID string `json:"clientID" yaml:"clientID"`
// ClientSecret is the application's secret.
ClientSecret string `json:"-" yaml:"clientSecret"`
// Endpoint contains the resource server's token endpoint
// URLs. These are constants specific to each server and are
// often available via site-specific packages, such as
// google.Endpoint or github.Endpoint.
Endpoint Endpoint `json:"endpoint" yaml:"endpoint"`
// RedirectURL is the URL to redirect users going through
// the OAuth flow, after the resource owner's URLs.
RedirectURL string `json:"redirectURL" yaml:"redirectURL"`
// Scope specifies optional requested permissions.
Scopes []string `json:"scopes" yaml:"scopes"`
}
// Endpoint represents an OAuth 2.0 provider's authorization and token
// endpoint URLs.
type Endpoint struct {
AuthURL string `json:"authURL" yaml:"authURL"`
TokenURL string `json:"tokenURL" yaml:"tokenURL"`
}
type GithubIdentity struct {
Login string `json:"login"`
ID int `json:"id"`
NodeID string `json:"node_id"`
AvatarURL string `json:"avatar_url"`
GravatarID string `json:"gravatar_id"`
URL string `json:"url"`
HTMLURL string `json:"html_url"`
FollowersURL string `json:"followers_url"`
FollowingURL string `json:"following_url"`
GistsURL string `json:"gists_url"`
StarredURL string `json:"starred_url"`
SubscriptionsURL string `json:"subscriptions_url"`
OrganizationsURL string `json:"organizations_url"`
ReposURL string `json:"repos_url"`
EventsURL string `json:"events_url"`
ReceivedEventsURL string `json:"received_events_url"`
Type string `json:"type"`
SiteAdmin bool `json:"site_admin"`
Name string `json:"name"`
Company string `json:"company"`
Blog string `json:"blog"`
Location string `json:"location"`
Email string `json:"email"`
Hireable bool `json:"hireable"`
Bio string `json:"bio"`
PublicRepos int `json:"public_repos"`
PublicGists int `json:"public_gists"`
Followers int `json:"followers"`
Following int `json:"following"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
PrivateGists int `json:"private_gists"`
TotalPrivateRepos int `json:"total_private_repos"`
OwnedPrivateRepos int `json:"owned_private_repos"`
DiskUsage int `json:"disk_usage"`
Collaborators int `json:"collaborators"`
}
func init() {
identityprovider.RegisterOAuthProviderCodec(&Github{})
}
func (g *Github) Type() string {
return "GitHubIdentityProvider"
}
func (g *Github) Setup(options *oauth.DynamicOptions) (identityprovider.OAuthProvider, error) {
data, err := yaml.Marshal(options)
if err != nil {
return nil, err
}
var provider Github
err = yaml.Unmarshal(data, &provider)
if err != nil {
return nil, err
}
return &provider, nil
}
func (g GithubIdentity) GetName() string {
return g.Login
}
func (g GithubIdentity) GetEmail() string {
return g.Email
}
func (g *Github) IdentityExchange(code string) (identityprovider.Identity, error) {
config := oauth2.Config{
ClientID: g.ClientID,
ClientSecret: g.ClientSecret,
Endpoint: oauth2.Endpoint{
AuthURL: g.Endpoint.AuthURL,
TokenURL: g.Endpoint.TokenURL,
AuthStyle: oauth2.AuthStyleAutoDetect,
},
RedirectURL: g.RedirectURL,
Scopes: g.Scopes,
}
token, err := config.Exchange(context.Background(), code)
if err != nil {
return nil, err
}
resp, err := oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(token)).Get(UserInfoURL)
if err != nil {
return nil, err
}
data, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return nil, err
}
var githubIdentity GithubIdentity
err = json.Unmarshal(data, &githubIdentity)
if err != nil {
return nil, err
}
return githubIdentity, nil
}

View File

@@ -20,44 +20,31 @@ package identityprovider
import (
"errors"
"k8s.io/apiserver/pkg/authentication/user"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
)
var (
ErrorIdentityProviderNotFound = errors.New("the identity provider was not found")
ErrorAlreadyRegistered = errors.New("the identity provider was not found")
oauthProviderCodecs = map[string]OAuthProviderCodec{}
oauthProviders = make(map[string]OAuthProvider, 0)
)
type OAuthProvider interface {
IdentityExchange(code string) (user.Info, error)
}
type OAuthProviderCodec interface {
Type() string
Decode(options *oauth.DynamicOptions) (OAuthProvider, error)
Encode(provider OAuthProvider) (*oauth.DynamicOptions, error)
Setup(options *oauth.DynamicOptions) (OAuthProvider, error)
IdentityExchange(code string) (Identity, error)
}
type Identity interface {
GetName() string
GetEmail() string
}
func ResolveOAuthProvider(providerType string, options *oauth.DynamicOptions) (OAuthProvider, error) {
if codec, ok := oauthProviderCodecs[providerType]; ok {
return codec.Decode(options)
func GetOAuthProvider(providerType string, options *oauth.DynamicOptions) (OAuthProvider, error) {
if provider, ok := oauthProviders[providerType]; ok {
return provider.Setup(options)
}
return nil, ErrorIdentityProviderNotFound
}
func ResolveOAuthOptions(providerType string, provider OAuthProvider) (*oauth.DynamicOptions, error) {
if codec, ok := oauthProviderCodecs[providerType]; ok {
return codec.Encode(provider)
}
return nil, ErrorIdentityProviderNotFound
}
func RegisterOAuthProviderCodec(codec OAuthProviderCodec) error {
if _, ok := oauthProviderCodecs[codec.Type()]; ok {
return ErrorAlreadyRegistered
}
oauthProviderCodecs[codec.Type()] = codec
return nil
func RegisterOAuthProviderCodec(provider OAuthProvider) {
oauthProviders[provider.Type()] = provider
}

View File

@@ -52,7 +52,7 @@ var (
)
type Options struct {
// LDAPPasswordIdentityProvider provider is used by default.
// Register identity providers.
IdentityProviders []IdentityProviderOptions `json:"identityProviders,omitempty" yaml:"identityProviders,omitempty"`
// Register additional OAuth clients.
@@ -89,15 +89,12 @@ type IdentityProviderOptions struct {
// - mixed: A user entity can be mapped with multiple identifyProvider.
MappingMethod MappingMethod `json:"mappingMethod" yaml:"mappingMethod"`
// When true, unauthenticated token requests from web clients (like the web console)
// are redirected to a login page (with WWW-Authenticate challenge header) backed by this provider.
LoginRedirect bool `json:"loginRedirect" yaml:"loginRedirect"`
// The type of identify provider
// OpenIDIdentityProvider LDAPIdentityProvider GitHubIdentityProvider
Type string `json:"type" yaml:"type"`
// The options of identify provider
Provider *DynamicOptions `json:"provider,omitempty" yaml:"provider,omitempty"`
Provider *DynamicOptions `json:"provider,omitempty" yaml:"provider"`
}
type Token struct {

View File

@@ -21,6 +21,7 @@ package options
import (
"fmt"
"github.com/spf13/pflag"
_ "kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider/github"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
"time"
)
@@ -55,7 +56,6 @@ func NewAuthenticateOptions() *AuthenticationOptions {
func (options *AuthenticationOptions) Validate() []error {
var errs []error
if len(options.JwtSecret) == 0 {
errs = append(errs, fmt.Errorf("jwt secret is empty"))
}

View File

@@ -1,175 +0,0 @@
/*
*
* Copyright 2020 The KubeSphere Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* /
*/
package authorizerfactory
import (
"context"
"github.com/open-policy-agent/opa/rego"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/klog"
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
"kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizer"
"kubesphere.io/kubesphere/pkg/models/iam/am"
)
type opaAuthorizer struct {
am am.AccessManagementInterface
}
const (
defaultRegoQuery = "data.authz.allow"
)
// 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
// Error returned when an internal error occurs
// Reason must be returned when access is denied
globalRole, err := o.am.GetGlobalRoleOfUser(attr.GetUser().GetName())
if err != nil {
if errors.IsNotFound(err) {
return authorizer.DecisionNoOpinion, "", nil
}
return authorizer.DecisionNoOpinion, "", err
}
// check global policy rules
if authorized, reason, err = o.makeDecision(globalRole, attr); authorized == authorizer.DecisionAllow {
return authorized, reason, nil
}
// it's global resource, permission denied
if attr.GetResourceScope() == iamv1alpha2.GlobalScope {
return authorizer.DecisionNoOpinion, "", nil
}
if attr.GetResourceScope() == iamv1alpha2.WorkspaceScope {
workspaceRole, err := o.am.GetWorkspaceRoleOfUser(attr.GetUser().GetName(), attr.GetWorkspace())
if err != nil {
if errors.IsNotFound(err) {
return authorizer.DecisionNoOpinion, "", nil
}
return authorizer.DecisionNoOpinion, "", err
}
// check workspace role policy rules
if authorized, reason, err := o.makeDecision(workspaceRole, attr); authorized == authorizer.DecisionAllow {
return authorized, reason, err
} else if err != nil {
return authorizer.DecisionNoOpinion, "", err
}
return authorizer.DecisionNoOpinion, "", nil
}
if attr.GetResourceScope() == iamv1alpha2.NamespaceScope {
role, err := o.am.GetNamespaceRoleOfUser(attr.GetUser().GetName(), attr.GetNamespace())
if err != nil {
if errors.IsNotFound(err) {
return authorizer.DecisionNoOpinion, "", nil
}
return authorizer.DecisionNoOpinion, "", err
}
// check namespace role policy rules
if authorized, reason, err := o.makeDecision(role, attr); authorized == authorizer.DecisionAllow {
return authorized, reason, err
} else if err != nil {
return authorizer.DecisionNoOpinion, "", err
}
return authorizer.DecisionNoOpinion, "", nil
}
clusterRole, err := o.am.GetClusterRoleOfUser(attr.GetUser().GetName(), attr.GetCluster())
if errors.IsNotFound(err) {
return authorizer.DecisionNoOpinion, "", nil
}
if err != nil {
return authorizer.DecisionNoOpinion, "", err
}
// check cluster role policy rules
if authorized, reason, err := o.makeDecision(clusterRole, attr); authorized == authorizer.DecisionAllow {
return authorized, reason, nil
} else if err != nil {
return authorizer.DecisionNoOpinion, "", err
}
return authorizer.DecisionNoOpinion, "", nil
}
// Make decision base on role
func (o *opaAuthorizer) makeDecision(role interface{}, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
regoPolicy := ""
// override
if globalRole, ok := role.(*iamv1alpha2.GlobalRole); ok {
if overrideRego, ok := globalRole.Annotations[iamv1alpha2.RegoOverrideAnnotation]; ok {
regoPolicy = overrideRego
}
} else if workspaceRole, ok := role.(*iamv1alpha2.WorkspaceRole); ok {
if overrideRego, ok := workspaceRole.Annotations[iamv1alpha2.RegoOverrideAnnotation]; ok {
regoPolicy = overrideRego
}
} else if clusterRole, ok := role.(*rbacv1.ClusterRole); ok {
if overrideRego, ok := clusterRole.Annotations[iamv1alpha2.RegoOverrideAnnotation]; ok {
regoPolicy = overrideRego
}
} else if role, ok := role.(*rbacv1.Role); ok {
if overrideRego, ok := role.Annotations[iamv1alpha2.RegoOverrideAnnotation]; ok {
regoPolicy = overrideRego
}
}
if regoPolicy == "" {
return authorizer.DecisionNoOpinion, "", nil
}
// Call the rego.New function to create an object that can be prepared or evaluated
// After constructing a new rego.Rego object you can call PrepareForEval() to obtain an executable query
query, err := rego.New(rego.Query(defaultRegoQuery), rego.Module("authz.rego", regoPolicy)).PrepareForEval(context.Background())
if err != nil {
klog.Errorf("syntax error:%s,refer: %s+v", err, role)
return authorizer.DecisionNoOpinion, "", err
}
// 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))
if err != nil {
klog.Errorf("syntax error:%s,refer: %s+v", err, role)
return authorizer.DecisionNoOpinion, "", err
}
if len(results) > 0 && results[0].Expressions[0].Value == true {
return authorizer.DecisionAllow, "", nil
}
return authorizer.DecisionNoOpinion, "", nil
}
func NewOPAAuthorizer(am am.AccessManagementInterface) *opaAuthorizer {
return &opaAuthorizer{am: am}
}

View File

@@ -1,252 +0,0 @@
/*
*
* Copyright 2020 The KubeSphere Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* /
*/
package authorizerfactory
import (
"fmt"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apiserver/pkg/authentication/user"
fakek8s "k8s.io/client-go/kubernetes/fake"
iamvealpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
"kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizer"
"kubesphere.io/kubesphere/pkg/client/clientset/versioned/fake"
factory "kubesphere.io/kubesphere/pkg/informers"
"kubesphere.io/kubesphere/pkg/models/iam/am"
"testing"
)
func TestGlobalRole(t *testing.T) {
operator, err := prepare()
if err != nil {
t.Fatal(err)
}
opa := NewOPAAuthorizer(operator)
tests := []struct {
name string
request authorizer.AttributesRecord
expectedDecision authorizer.Decision
}{
{
name: "admin can list nodes",
request: authorizer.AttributesRecord{
User: &user.DefaultInfo{
Name: "admin",
UID: "0",
Groups: []string{"admin"},
Extra: nil,
},
Verb: "list",
APIVersion: "v1",
Resource: "nodes",
KubernetesRequest: true,
ResourceRequest: true,
Path: "/api/v1/nodes",
},
expectedDecision: authorizer.DecisionAllow,
},
{
name: "anonymous can not list nodes",
request: authorizer.AttributesRecord{
User: &user.DefaultInfo{
Name: user.Anonymous,
UID: "0",
Groups: []string{"admin"},
Extra: nil,
},
Verb: "list",
APIVersion: "v1",
Resource: "nodes",
KubernetesRequest: true,
ResourceRequest: true,
Path: "/api/v1/nodes",
},
expectedDecision: authorizer.DecisionNoOpinion,
}, {
name: "tom can list nodes in cluster1",
request: authorizer.AttributesRecord{
User: &user.DefaultInfo{
Name: "tom",
},
Verb: "list",
Cluster: "cluster1",
APIVersion: "v1",
Resource: "nodes",
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",
APIVersion: "v1",
Resource: "nodes",
KubernetesRequest: true,
ResourceRequest: true,
Path: "/api/v1/clusters/cluster2/nodes",
},
expectedDecision: authorizer.DecisionNoOpinion,
},
}
for _, test := range tests {
decision, _, err := opa.Authorize(test.request)
if err != nil {
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)
}
}
}
func prepare() (am.AccessManagementInterface, error) {
globalRoles := []*iamvealpha2.GlobalRole{
{
TypeMeta: metav1.TypeMeta{
Kind: iamvealpha2.ResourceKindGlobalRole,
APIVersion: iamvealpha2.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: "global-admin",
Annotations: map[string]string{iamvealpha2.RegoOverrideAnnotation: "package authz\ndefault allow = true"},
},
}, {
TypeMeta: metav1.TypeMeta{
Kind: iamvealpha2.ResourceKindGlobalRole,
APIVersion: iamvealpha2.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: "anonymous",
Annotations: map[string]string{iamvealpha2.RegoOverrideAnnotation: "package authz\ndefault allow = false"},
},
}, {
TypeMeta: metav1.TypeMeta{
Kind: iamvealpha2.ResourceKindGlobalRole,
APIVersion: iamvealpha2.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: "cluster1-admin",
Annotations: map[string]string{iamvealpha2.RegoOverrideAnnotation: `package authz
default allow = false
allow {
resources_in_cluster1
}
resources_in_cluster1 {
input.Cluster == "cluster1"
}`},
},
},
}
roleBindings := []*iamvealpha2.GlobalRoleBinding{
{
TypeMeta: metav1.TypeMeta{
Kind: iamvealpha2.ResourceKindGlobalRoleBinding,
APIVersion: iamvealpha2.SchemeGroupVersion.String()},
ObjectMeta: metav1.ObjectMeta{
Name: "global-admin",
},
RoleRef: rbacv1.RoleRef{
APIGroup: iamvealpha2.SchemeGroupVersion.String(),
Kind: iamvealpha2.ResourceKindGlobalRole,
Name: "global-admin",
},
Subjects: []rbacv1.Subject{
{
Kind: iamvealpha2.ResourceKindUser,
APIGroup: iamvealpha2.SchemeGroupVersion.String(),
Name: "admin",
},
},
},
{
TypeMeta: metav1.TypeMeta{
Kind: iamvealpha2.ResourceKindGlobalRoleBinding,
APIVersion: iamvealpha2.SchemeGroupVersion.String()},
ObjectMeta: metav1.ObjectMeta{
Name: "anonymous",
},
RoleRef: rbacv1.RoleRef{
APIGroup: iamvealpha2.SchemeGroupVersion.String(),
Kind: iamvealpha2.ResourceKindGlobalRole,
Name: "anonymous",
},
Subjects: []rbacv1.Subject{
{
Kind: iamvealpha2.ResourceKindUser,
APIGroup: iamvealpha2.SchemeGroupVersion.String(),
Name: user.Anonymous,
},
},
},
{
TypeMeta: metav1.TypeMeta{
Kind: iamvealpha2.ResourceKindGlobalRoleBinding,
APIVersion: iamvealpha2.SchemeGroupVersion.String()},
ObjectMeta: metav1.ObjectMeta{
Name: "cluster1-admin",
},
RoleRef: rbacv1.RoleRef{
APIGroup: iamvealpha2.SchemeGroupVersion.String(),
Kind: iamvealpha2.ResourceKindGlobalRole,
Name: "cluster1-admin",
},
Subjects: []rbacv1.Subject{
{
Kind: iamvealpha2.ResourceKindUser,
APIGroup: iamvealpha2.SchemeGroupVersion.String(),
Name: "tom",
},
},
},
}
ksClient := fake.NewSimpleClientset()
k8sClient := fakek8s.NewSimpleClientset()
factory := factory.NewInformerFactories(k8sClient, ksClient, nil, nil, nil, nil)
for _, role := range globalRoles {
err := factory.KubeSphereSharedInformerFactory().Iam().V1alpha2().GlobalRoles().Informer().GetIndexer().Add(role)
if err != nil {
return nil, fmt.Errorf("add role:%s", err)
}
}
for _, roleBinding := range roleBindings {
err := factory.KubeSphereSharedInformerFactory().Iam().V1alpha2().GlobalRoleBindings().Informer().GetIndexer().Add(roleBinding)
if err != nil {
return nil, fmt.Errorf("add role binding:%s", err)
}
}
operator := am.NewAMOperator(factory)
return operator, nil
}

View File

@@ -20,10 +20,13 @@ package authorizerfactory
import (
"bytes"
"context"
"fmt"
"github.com/open-policy-agent/opa/rego"
"k8s.io/apiserver/pkg/authentication/serviceaccount"
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
"kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizer"
"kubesphere.io/kubesphere/pkg/apiserver/request"
"kubesphere.io/kubesphere/pkg/models/iam/am"
"kubesphere.io/kubesphere/pkg/utils/sliceutil"
@@ -35,6 +38,11 @@ import (
rbacv1helpers "kubesphere.io/kubesphere/pkg/apis/rbac/v1"
)
const (
defaultRegoQuery = "data.authz.allow"
defaultRegoFileName = "authz.rego"
)
type RBACAuthorizer struct {
am am.AccessManagementInterface
}
@@ -48,7 +56,12 @@ type authorizingVisitor struct {
errors []error
}
func (v *authorizingVisitor) visit(source fmt.Stringer, rule *rbacv1.PolicyRule, err error) bool {
func (v *authorizingVisitor) visit(source fmt.Stringer, regoPolicy string, rule *rbacv1.PolicyRule, err error) bool {
if regoPolicy != "" && regoPolicyAllows(v.requestAttributes, regoPolicy) {
v.allowed = true
v.reason = fmt.Sprintf("RBAC: allowed by %s", source.String())
return false
}
if rule != nil && ruleAllows(v.requestAttributes, rule) {
v.allowed = true
v.reason = fmt.Sprintf("RBAC: allowed by %s", source.String())
@@ -65,7 +78,7 @@ type ruleAccumulator struct {
errors []error
}
func (r *ruleAccumulator) visit(source fmt.Stringer, rule *rbacv1.PolicyRule, err error) bool {
func (r *ruleAccumulator) visit(source fmt.Stringer, _ string, rule *rbacv1.PolicyRule, err error) bool {
if rule != nil {
r.rules = append(r.rules, *rule)
}
@@ -155,15 +168,40 @@ func ruleAllows(requestAttributes authorizer.Attributes, rule *rbacv1.PolicyRule
rbacv1helpers.NonResourceURLMatches(rule, requestAttributes.GetPath())
}
func regoPolicyAllows(requestAttributes authorizer.Attributes, regoPolicy string) bool {
// Call the rego.New function to create an object that can be prepared or evaluated
// After constructing a new rego.Rego object you can call PrepareForEval() to obtain an executable query
query, err := rego.New(rego.Query(defaultRegoQuery), rego.Module(defaultRegoFileName, regoPolicy)).PrepareForEval(context.Background())
if err != nil {
klog.Warningf("syntax error:%s, content: %s", err, regoPolicy)
return false
}
// 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(requestAttributes))
if err != nil {
klog.Warningf("syntax error:%s, content: %s", err, regoPolicy)
return false
}
if len(results) > 0 && results[0].Expressions[0].Value == true {
return true
}
return false
}
func (r *RBACAuthorizer) rulesFor(requestAttributes authorizer.Attributes) ([]rbacv1.PolicyRule, error) {
visitor := &ruleAccumulator{}
r.visitRulesFor(requestAttributes, visitor.visit)
return visitor.rules, utilerrors.NewAggregate(visitor.errors)
}
func (r *RBACAuthorizer) visitRulesFor(requestAttributes authorizer.Attributes, visitor func(source fmt.Stringer, rule *rbacv1.PolicyRule, err error) bool) {
func (r *RBACAuthorizer) visitRulesFor(requestAttributes authorizer.Attributes, visitor func(source fmt.Stringer, regoPolicy string, rule *rbacv1.PolicyRule, err error) bool) {
if globalRoleBindings, err := r.am.ListGlobalRoleBindings(""); err != nil {
if !visitor(nil, nil, err) {
if !visitor(nil, "", nil, err) {
return
}
} else {
@@ -173,26 +211,27 @@ func (r *RBACAuthorizer) visitRulesFor(requestAttributes authorizer.Attributes,
if !applies {
continue
}
rules, err := r.am.GetRoleReferenceRules(globalRoleBinding.RoleRef, "")
regoPolicy, rules, err := r.am.GetRoleReferenceRules(globalRoleBinding.RoleRef, "")
if err != nil {
if !visitor(nil, nil, err) {
return
}
visitor(nil, "", nil, err)
continue
}
sourceDescriber.binding = globalRoleBinding
sourceDescriber.subject = &globalRoleBinding.Subjects[subjectIndex]
if !visitor(sourceDescriber, regoPolicy, nil, nil) {
return
}
for i := range rules {
if !visitor(sourceDescriber, &rules[i], nil) {
if !visitor(sourceDescriber, "", &rules[i], nil) {
return
}
}
}
}
if requestAttributes.GetResourceScope() == iamv1alpha2.WorkspaceScope {
if requestAttributes.GetResourceScope() == request.WorkspaceScope {
if workspaceRoleBindings, err := r.am.ListWorkspaceRoleBindings("", requestAttributes.GetWorkspace()); err != nil {
if !visitor(nil, nil, err) {
if !visitor(nil, "", nil, err) {
return
}
} else {
@@ -202,17 +241,18 @@ func (r *RBACAuthorizer) visitRulesFor(requestAttributes authorizer.Attributes,
if !applies {
continue
}
rules, err := r.am.GetRoleReferenceRules(workspaceRoleBinding.RoleRef, "")
regoPolicy, rules, err := r.am.GetRoleReferenceRules(workspaceRoleBinding.RoleRef, "")
if err != nil {
if !visitor(nil, nil, err) {
return
}
visitor(nil, "", nil, err)
continue
}
sourceDescriber.binding = workspaceRoleBinding
sourceDescriber.subject = &workspaceRoleBinding.Subjects[subjectIndex]
if !visitor(sourceDescriber, regoPolicy, nil, nil) {
return
}
for i := range rules {
if !visitor(sourceDescriber, &rules[i], nil) {
if !visitor(sourceDescriber, "", &rules[i], nil) {
return
}
}
@@ -220,9 +260,9 @@ func (r *RBACAuthorizer) visitRulesFor(requestAttributes authorizer.Attributes,
}
}
if requestAttributes.GetResourceScope() == iamv1alpha2.NamespaceScope {
if requestAttributes.GetResourceScope() == request.NamespaceScope {
if roleBindings, err := r.am.ListRoleBindings("", requestAttributes.GetNamespace()); err != nil {
if !visitor(nil, nil, err) {
if !visitor(nil, "", nil, err) {
return
}
} else {
@@ -232,17 +272,18 @@ func (r *RBACAuthorizer) visitRulesFor(requestAttributes authorizer.Attributes,
if !applies {
continue
}
rules, err := r.am.GetRoleReferenceRules(roleBinding.RoleRef, requestAttributes.GetNamespace())
regoPolicy, rules, err := r.am.GetRoleReferenceRules(roleBinding.RoleRef, requestAttributes.GetNamespace())
if err != nil {
if !visitor(nil, nil, err) {
return
}
visitor(nil, "", nil, err)
continue
}
sourceDescriber.binding = roleBinding
sourceDescriber.subject = &roleBinding.Subjects[subjectIndex]
if !visitor(sourceDescriber, regoPolicy, nil, nil) {
return
}
for i := range rules {
if !visitor(sourceDescriber, &rules[i], nil) {
if !visitor(sourceDescriber, "", &rules[i], nil) {
return
}
}
@@ -251,7 +292,7 @@ func (r *RBACAuthorizer) visitRulesFor(requestAttributes authorizer.Attributes,
}
if clusterRoleBindings, err := r.am.ListClusterRoleBindings(""); err != nil {
if !visitor(nil, nil, err) {
if !visitor(nil, "", nil, err) {
return
}
} else {
@@ -261,17 +302,18 @@ func (r *RBACAuthorizer) visitRulesFor(requestAttributes authorizer.Attributes,
if !applies {
continue
}
rules, err := r.am.GetRoleReferenceRules(clusterRoleBinding.RoleRef, "")
regoPolicy, rules, err := r.am.GetRoleReferenceRules(clusterRoleBinding.RoleRef, "")
if err != nil {
if !visitor(nil, nil, err) {
return
}
visitor(nil, "", nil, err)
continue
}
sourceDescriber.binding = clusterRoleBinding
sourceDescriber.subject = &clusterRoleBinding.Subjects[subjectIndex]
if !visitor(sourceDescriber, regoPolicy, nil, nil) {
return
}
for i := range rules {
if !visitor(sourceDescriber, &rules[i], nil) {
if !visitor(sourceDescriber, "", &rules[i], nil) {
return
}
}

View File

@@ -25,8 +25,8 @@ import (
"io"
fakeistio "istio.io/client-go/pkg/clientset/versioned/fake"
fakek8s "k8s.io/client-go/kubernetes/fake"
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
"kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizer"
"kubesphere.io/kubesphere/pkg/apiserver/request"
fakeks "kubesphere.io/kubesphere/pkg/client/clientset/versioned/fake"
"kubesphere.io/kubesphere/pkg/informers"
"kubesphere.io/kubesphere/pkg/models/iam/am"
@@ -208,10 +208,10 @@ func TestRBACAuthorizer(t *testing.T) {
t.Fatal(err)
}
scope := iamv1alpha2.ClusterScope
scope := request.ClusterScope
if tc.namespace != "" {
scope = iamv1alpha2.NamespaceScope
scope = request.NamespaceScope
}
rules, err := ruleResolver.rulesFor(authorizer.AttributesRecord{
@@ -274,7 +274,7 @@ func newMockRBACAuthorizer(staticRoles *StaticRoles) (*RBACAuthorizer, error) {
return nil, err
}
}
return NewRBACAuthorizer(am.NewAMOperator(fakeInformerFactory)), nil
return NewRBACAuthorizer(am.NewReadOnlyOperator(fakeInformerFactory)), nil
}
func TestAppliesTo(t *testing.T) {

View File

@@ -5,6 +5,7 @@ type Value string
const (
FieldName = "name"
FieldNames = "names"
FieldUID = "uid"
FieldCreationTimeStamp = "creationTimestamp"
FieldCreateTime = "createTime"

View File

@@ -5,12 +5,11 @@ import (
"fmt"
"k8s.io/apimachinery/pkg/api/validation/path"
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
metainternalversionscheme "k8s.io/apimachinery/pkg/apis/meta/internalversion/scheme"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/klog"
"kubesphere.io/kubesphere/pkg/api"
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
"net/http"
"strings"
@@ -56,7 +55,6 @@ type RequestInfo struct {
type RequestInfoFactory struct {
APIPrefixes sets.String
GrouplessAPIPrefixes sets.String
GlobalResources []schema.GroupResource
}
// NewRequestInfo returns the information from the http request. If error is not nil, RequestInfo holds the information as best it is known before the failure
@@ -211,7 +209,7 @@ func (r *RequestInfoFactory) NewRequestInfo(req *http.Request) (*RequestInfo, er
// if there's no name on the request and we thought it was a get before, then the actual verb is a list or a watch
if len(requestInfo.Name) == 0 && requestInfo.Verb == "get" {
opts := metainternalversion.ListOptions{}
if err := metainternalversion.ParameterCodec.DecodeParameters(req.URL.Query(), metav1.SchemeGroupVersion, &opts); err != nil {
if err := metainternalversionscheme.ParameterCodec.DecodeParameters(req.URL.Query(), metav1.SchemeGroupVersion, &opts); err != nil {
// An error in parsing request will result in default to "list" and not setting "name" field.
klog.Errorf("Couldn't parse request %#v: %v", req.URL.Query(), err)
// Reset opts to not rely on partial results from parsing.
@@ -273,20 +271,26 @@ func splitPath(path string) []string {
return strings.Split(path, "/")
}
const (
GlobalScope = "Global"
ClusterScope = "Cluster"
WorkspaceScope = "Workspace"
NamespaceScope = "Namespace"
)
func (r *RequestInfoFactory) resolveResourceScope(request RequestInfo) string {
for _, globalResource := range r.GlobalResources {
if globalResource.Group == request.APIGroup &&
globalResource.Resource == request.Resource {
return iamv1alpha2.GlobalScope
}
if request.Cluster != "" {
return ClusterScope
}
if request.Namespace != "" {
return iamv1alpha2.NamespaceScope
return NamespaceScope
}
if request.Workspace != "" {
return iamv1alpha2.WorkspaceScope
return WorkspaceScope
}
return iamv1alpha2.ClusterScope
return GlobalScope
}