/* * Please refer to the LICENSE file in the root directory of the project. * https://github.com/kubesphere/kubesphere/blob/master/LICENSE */ package v1beta1 import ( "fmt" "sync" "kubesphere.io/kubesphere/pkg/apiserver/authorization/rbac" "kubesphere.io/kubesphere/pkg/apiserver/rest" "github.com/emicklei/go-restful/v3" rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" utilerrors "k8s.io/apimachinery/pkg/util/errors" authuser "k8s.io/apiserver/pkg/authentication/user" iamv1beta1 "kubesphere.io/api/iam/v1beta1" "kubesphere.io/kubesphere/pkg/api" "kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizer" "kubesphere.io/kubesphere/pkg/apiserver/query" apirequest "kubesphere.io/kubesphere/pkg/apiserver/request" "kubesphere.io/kubesphere/pkg/models/auth" "kubesphere.io/kubesphere/pkg/models/iam/am" "kubesphere.io/kubesphere/pkg/models/iam/im" resv1beta1 "kubesphere.io/kubesphere/pkg/models/resources/v1beta1" servererr "kubesphere.io/kubesphere/pkg/server/errors" ) type Member struct { Username string `json:"username"` RoleRef string `json:"roleRef"` } type GroupMember struct { UserName string `json:"userName"` GroupName string `json:"groupName"` } type PasswordReset struct { CurrentPassword string `json:"currentPassword"` Password string `json:"password"` } type TOTOAuthKeyBind struct { AuthKey string `json:"authKey"` OTP string `json:"otp"` } type TOTPAuthKey struct { AuthKey string `json:"authKey"` } type handler struct { im im.IdentityManagementInterface am am.AccessManagementInterface authorizer authorizer.Authorizer } func NewHandler(im im.IdentityManagementInterface, am am.AccessManagementInterface) rest.Handler { return &handler{im: im, am: am, authorizer: rbac.NewRBACAuthorizer(am)} } func NewFakeHandler() rest.Handler { return &handler{} } func (h *handler) DescribeUser(request *restful.Request, response *restful.Response) { username := request.PathParameter("user") user, err := h.im.DescribeUser(username) if err != nil { if errors.IsNotFound(err) { api.HandleNotFound(response, request, err) return } api.HandleInternalError(response, request, err) return } response.WriteEntity(user) } func (h *handler) ListUsers(request *restful.Request, response *restful.Response) { globalRole := request.QueryParameter("globalrole") if globalRole != "" { result, err := h.listUserByGlobalRole(globalRole) if err != nil { api.HandleInternalError(response, request, err) return } response.WriteEntity(result) return } queryParam := query.ParseQueryParameter(request) result, err := h.im.ListUsers(queryParam) if err != nil { api.HandleInternalError(response, request, err) return } response.WriteEntity(result) } func (h *handler) listUserByGlobalRole(roleName string) (*api.ListResult, error) { result := &api.ListResult{Items: make([]runtime.Object, 0)} bindings, err := h.am.ListGlobalRoleBindings("", roleName) if err != nil { return nil, err } for _, binding := range bindings { for _, subject := range binding.Subjects { if subject.Kind == rbacv1.UserKind { user, err := h.im.DescribeUser(subject.Name) if err != nil { return nil, err } result.Items = append(result.Items, user) result.TotalItems += 1 } } } return result, nil } func (h *handler) CreateUser(req *restful.Request, resp *restful.Response) { var user iamv1beta1.User err := req.ReadEntity(&user) if err != nil { api.HandleBadRequest(resp, req, err) return } operator, ok := apirequest.UserFrom(req.Request.Context()) if ok && operator.GetName() == iamv1beta1.PreRegistrationUser { extra := operator.GetExtra() // The token used for registration must contain additional information if len(extra[iamv1beta1.ExtraIdentityProvider]) != 1 || len(extra[iamv1beta1.ExtraUID]) != 1 { err = errors.NewBadRequest("invalid registration token") api.HandleBadRequest(resp, req, err) return } if user.Annotations == nil { user.Labels = make(map[string]string) } user.Annotations[fmt.Sprintf("%s.%s", iamv1beta1.IdentityProviderAnnotation, extra[iamv1beta1.ExtraIdentityProvider][0])] = extra[iamv1beta1.ExtraUID][0] delete(user.Annotations, iamv1beta1.GlobalRoleAnnotation) } globalRole := user.Annotations[iamv1beta1.GlobalRoleAnnotation] if globalRole != "" { if _, err = h.am.GetGlobalRole(globalRole); err != nil { api.HandleError(resp, req, err) return } } created, err := h.im.CreateUser(&user) if err != nil { api.HandleError(resp, req, err) return } if globalRole != "" { if err := h.am.CreateOrUpdateGlobalRoleBinding(user.Name, globalRole); err != nil { api.HandleError(resp, req, err) return } } // ensure encrypted password will not be output created.Spec.EncryptedPassword = "" resp.WriteEntity(created) } func (h *handler) UpdateUser(request *restful.Request, response *restful.Response) { username := request.PathParameter("user") var user iamv1beta1.User err := request.ReadEntity(&user) if err != nil { api.HandleBadRequest(response, request, err) return } if username != user.Name { err := fmt.Errorf("the name of the object (%s) does not match the name on the URL (%s)", user.Name, username) api.HandleBadRequest(response, request, err) return } globalRole := user.Annotations[iamv1beta1.GlobalRoleAnnotation] updated, err := h.im.UpdateUser(&user) if err != nil { api.HandleError(response, request, err) return } operator, ok := apirequest.UserFrom(request.Request.Context()) if globalRole != "" && ok { if err = h.updateGlobalRoleBinding(operator, updated, globalRole); err != nil { api.HandleError(response, request, err) return } } response.WriteEntity(updated) } func (h *handler) updateGlobalRoleBinding(operator authuser.Info, user *iamv1beta1.User, globalRole string) error { oldGlobalRole, err := h.am.GetGlobalRoleOfUser(user.Name) if err != nil && !errors.IsNotFound(err) { return err } if oldGlobalRole != nil && oldGlobalRole.Name == globalRole { return nil } userManagement := authorizer.AttributesRecord{ Resource: iamv1beta1.ResourcesPluralUser, Verb: "update", ResourceScope: apirequest.GlobalScope, ResourceRequest: true, User: operator, } decision, _, err := h.authorizer.Authorize(userManagement) if err != nil { return err } if decision != authorizer.DecisionAllow { return errors.NewForbidden(iamv1beta1.Resource(iamv1beta1.ResourcesSingularUser), user.Name, fmt.Errorf("update global role binding is not allowed")) } if err := h.am.CreateOrUpdateGlobalRoleBinding(user.Name, globalRole); err != nil { return err } return nil } func (h *handler) ModifyPassword(request *restful.Request, response *restful.Response) { username := request.PathParameter("user") var passwordReset PasswordReset err := request.ReadEntity(&passwordReset) if err != nil { api.HandleBadRequest(response, request, err) return } operator, ok := apirequest.UserFrom(request.Request.Context()) if !ok { err = errors.NewInternalError(fmt.Errorf("cannot obtain user info")) api.HandleInternalError(response, request, err) return } userManagement := authorizer.AttributesRecord{ Resource: "users/password", Verb: "update", ResourceScope: apirequest.GlobalScope, ResourceRequest: true, User: operator, } decision, _, err := h.authorizer.Authorize(userManagement) if err != nil { api.HandleInternalError(response, request, err) return } // only the user manager can modify the password without verifying the old password // if old password is defined must be verified if decision != authorizer.DecisionAllow || passwordReset.CurrentPassword != "" { if err = h.im.PasswordVerify(username, passwordReset.CurrentPassword); err != nil { if err == auth.IncorrectPasswordError { err = errors.NewBadRequest("incorrect old password") } api.HandleError(response, request, err) return } } err = h.im.ModifyPassword(username, passwordReset.Password) if err != nil { api.HandleError(response, request, err) return } response.WriteEntity(servererr.None) } func (h *handler) DeleteUser(request *restful.Request, response *restful.Response) { username := request.PathParameter("user") err := h.im.DeleteUser(username) if err != nil { api.HandleError(response, request, err) return } response.WriteEntity(servererr.None) } func (h *handler) ListUserLoginRecords(request *restful.Request, response *restful.Response) { username := request.PathParameter("user") queryParam := query.ParseQueryParameter(request) result, err := h.im.ListLoginRecords(username, queryParam) if err != nil { api.HandleError(response, request, err) return } response.WriteEntity(result) } func (h *handler) ListClusterMembers(request *restful.Request, response *restful.Response) { roleName := request.QueryParameter("clusterrole") result := &api.ListResult{Items: make([]runtime.Object, 0)} bindings, err := h.am.ListClusterRoleBindings("", roleName) if err != nil { api.HandleError(response, request, err) return } filtered := []runtime.Object{} for _, binding := range bindings { for _, subject := range binding.Subjects { if subject.Kind == rbacv1.UserKind { filtered = append(filtered, &iamv1beta1.User{ ObjectMeta: metav1.ObjectMeta{ Name: subject.Name, Annotations: map[string]string{ iamv1beta1.ClusterRoleAnnotation: binding.RoleRef.Name, }, }, }) break } } } list, _, totalCount := resv1beta1.DefaultList(filtered, query.ParseQueryParameter(request), resv1beta1.DefaultCompare, resv1beta1.DefaultFilter) result.Items = list result.TotalItems = totalCount _ = response.WriteEntity(result) } func (h *handler) ListWorkspaceMembers(request *restful.Request, response *restful.Response) { workspace := request.PathParameter("workspace") roleName := request.QueryParameter("workspacerole") bindings, err := h.am.ListWorkspaceRoleBindings("", roleName, nil, workspace) result := &api.ListResult{Items: make([]runtime.Object, 0)} if err != nil { api.HandleError(response, request, err) return } filtered := []runtime.Object{} for _, binding := range bindings { for _, subject := range binding.Subjects { if subject.Kind == rbacv1.UserKind { filtered = append(filtered, &iamv1beta1.User{ ObjectMeta: metav1.ObjectMeta{ Name: subject.Name, Annotations: map[string]string{ iamv1beta1.WorkspaceRoleAnnotation: binding.RoleRef.Name, }, }, }) break } } } list, _, totalCount := resv1beta1.DefaultList(filtered, query.ParseQueryParameter(request), resv1beta1.DefaultCompare, resv1beta1.DefaultFilter) result.Items = list result.TotalItems = totalCount _ = response.WriteEntity(result) } func (h *handler) ListNamespaceMembers(request *restful.Request, response *restful.Response) { namespace := request.PathParameter("namespace") roleName := request.QueryParameter("role") bindings, err := h.am.ListRoleBindings("", roleName, nil, namespace) if err != nil { api.HandleError(response, request, err) return } result := &api.ListResult{Items: make([]runtime.Object, 0)} filtered := []runtime.Object{} for _, binding := range bindings { for _, subject := range binding.Subjects { if subject.Kind == rbacv1.UserKind { filtered = append(filtered, &iamv1beta1.User{ ObjectMeta: metav1.ObjectMeta{ Name: subject.Name, Annotations: map[string]string{ iamv1beta1.RoleAnnotation: binding.RoleRef.Name, }, }, }) break } } } list, _, totalCount := resv1beta1.DefaultList(filtered, query.ParseQueryParameter(request), resv1beta1.DefaultCompare, resv1beta1.DefaultFilter) result.Items = list result.TotalItems = totalCount _ = response.WriteEntity(result) } func (h *handler) CreateClusterMembers(request *restful.Request, response *restful.Response) { var members []Member err := request.ReadEntity(&members) if err != nil { api.HandleBadRequest(response, request, err) return } for _, member := range members { err := h.am.CreateOrUpdateClusterRoleBinding(member.Username, member.RoleRef) if err != nil { api.HandleError(response, request, err) return } } response.WriteEntity(members) } func (h *handler) RemoveClusterMember(request *restful.Request, response *restful.Response) { username := request.PathParameter("clustermember") err := h.am.RemoveUserFromCluster(username) if err != nil { api.HandleError(response, request, err) return } response.WriteEntity(servererr.None) } func (h *handler) CreateNamespaceMembers(request *restful.Request, response *restful.Response) { namespace := request.PathParameter("namespace") var members []Member err := request.ReadEntity(&members) if err != nil { api.HandleBadRequest(response, request, err) return } for _, member := range members { err := h.am.CreateOrUpdateNamespaceRoleBinding(member.Username, namespace, member.RoleRef) if err != nil { api.HandleError(response, request, err) return } } response.WriteEntity(members) } func (h *handler) RemoveNamespaceMember(request *restful.Request, response *restful.Response) { username := request.PathParameter("member") namespace := request.PathParameter("namespace") err := h.am.RemoveUserFromNamespace(username, namespace) if err != nil { api.HandleError(response, request, err) return } response.WriteEntity(servererr.None) } func (h *handler) ListRoleTemplateOfUser(request *restful.Request, response *restful.Response) { username := request.PathParameter("username") scope := request.QueryParameter("scope") namespace := request.QueryParameter("namespace") workspace := request.QueryParameter("workspace") userInfo, exist := apirequest.UserFrom(request.Request.Context()) if !exist { err := errors.NewInternalError(fmt.Errorf("cannot obtain user info")) api.HandleInternalError(response, request, err) return } if userInfo.GetName() != username { userManagement := authorizer.AttributesRecord{ Resource: "users", Verb: "get", ResourceScope: apirequest.GlobalScope, ResourceRequest: true, User: userInfo, } decision, _, err := h.authorizer.Authorize(userManagement) if err != nil { api.HandleInternalError(response, request, err) return } if decision != authorizer.DecisionAllow { err := errors.NewForbidden(iamv1beta1.Resource("users"), username, fmt.Errorf("not allow to get users")) api.HandleError(response, request, err) return } } result := &api.ListResult{Items: make([]runtime.Object, 0)} var roleTemplateNames []string switch scope { case iamv1beta1.ScopeGlobal: globalRole, err := h.am.GetGlobalRoleOfUser(username) if err != nil && !errors.IsNotFound(err) { api.HandleError(response, request, err) return } if globalRole != nil && globalRole.AggregationRoleTemplates != nil { roleTemplateNames = globalRole.AggregationRoleTemplates.TemplateNames } case iamv1beta1.ScopeWorkspace: workspaceRoles, err := h.am.GetWorkspaceRoleOfUser(username, nil, workspace) if err != nil { api.HandleError(response, request, err) return } for _, workspaceRole := range workspaceRoles { if workspaceRole.AggregationRoleTemplates != nil { roleTemplateNames = append(roleTemplateNames, workspaceRole.AggregationRoleTemplates.TemplateNames...) } } case iamv1beta1.ScopeCluster: clusterRole, err := h.am.GetClusterRoleOfUser(username) if err != nil && !errors.IsNotFound(err) { api.HandleError(response, request, err) return } if clusterRole != nil && clusterRole.AggregationRoleTemplates != nil { roleTemplateNames = clusterRole.AggregationRoleTemplates.TemplateNames } case iamv1beta1.ScopeNamespace: roles, err := h.am.GetNamespaceRoleOfUser(username, nil, namespace) if err != nil { api.HandleError(response, request, err) return } for _, role := range roles { if role.AggregationRoleTemplates != nil { roleTemplateNames = append(roleTemplateNames, role.AggregationRoleTemplates.TemplateNames...) } } } for _, name := range roleTemplateNames { template, err := h.am.GetRoleTemplate(name) if err != nil { if errors.IsNotFound(err) { continue } api.HandleError(response, request, err) return } result.Items = append(result.Items, template) result.TotalItems += 1 } _ = response.WriteEntity(result) } func (h *handler) CreateSubjectAccessReview(request *restful.Request, response *restful.Response) { data := make(map[string]interface{}) err := request.ReadEntity(&data) if err != nil { api.HandleBadRequest(response, request, err) return } uns := unstructured.Unstructured{Object: data} unsConverter := runtime.DefaultUnstructuredConverter if uns.IsList() { list := &iamv1beta1.SubjectAccessReviewList{} err := unsConverter.FromUnstructured(uns.Object, list) if err != nil { api.HandleBadRequest(response, request, err) return } result, err := h.handleList(*list) if err != nil { api.HandleBadRequest(response, request, err) return } _ = response.WriteEntity(result) return } review := &iamv1beta1.SubjectAccessReview{} err = unsConverter.FromUnstructured(uns.Object, review) if err != nil { api.HandleBadRequest(response, request, err) return } result, err := h.handleSingle(*review) if err != nil { api.HandleBadRequest(response, request, err) return } _ = response.WriteEntity(result) } func (h *handler) handleList(list iamv1beta1.SubjectAccessReviewList) (iamv1beta1.SubjectAccessReviewList, error) { var ( wg sync.WaitGroup errList []error newItems []iamv1beta1.SubjectAccessReview accessReviewMux sync.Mutex errMux sync.Mutex ) for _, review := range list.Items { wg.Add(1) go func(review iamv1beta1.SubjectAccessReview) { defer wg.Done() accessReview, err := h.handleSingle(review) if err != nil { errMux.Lock() errList = append(errList, err) errMux.Unlock() return } accessReviewMux.Lock() newItems = append(newItems, accessReview) accessReviewMux.Unlock() }(review) } wg.Wait() list.Items = newItems return list, utilerrors.NewAggregate(errList) } func (h *handler) handleSingle(review iamv1beta1.SubjectAccessReview) (iamv1beta1.SubjectAccessReview, error) { decision, reason, err := h.authorizer.Authorize(prepareAttribute(review)) if err != nil { return iamv1beta1.SubjectAccessReview{}, err } review.Status = iamv1beta1.SubjectAccessReviewStatus{ Allowed: decision == authorizer.DecisionAllow, Denied: decision == authorizer.DecisionDeny, Reason: reason, } return review, nil } func prepareAttribute(subjectAccessReview iamv1beta1.SubjectAccessReview) authorizer.AttributesRecord { attr := authorizer.AttributesRecord{ User: &iamv1beta1.DefaultInfo{ Name: subjectAccessReview.Spec.User, UID: subjectAccessReview.Spec.UID, Groups: subjectAccessReview.Spec.Groups, }, } if subjectAccessReview.Spec.ResourceAttributes != nil { attr.ResourceRequest = true attr.Verb = subjectAccessReview.Spec.ResourceAttributes.Verb attr.APIGroup = subjectAccessReview.Spec.ResourceAttributes.Group attr.APIVersion = subjectAccessReview.Spec.ResourceAttributes.Version attr.Workspace = subjectAccessReview.Spec.ResourceAttributes.Workspace attr.Namespace = subjectAccessReview.Spec.ResourceAttributes.Namespace attr.Resource = subjectAccessReview.Spec.ResourceAttributes.Resource attr.Name = subjectAccessReview.Spec.ResourceAttributes.Name attr.Subresource = subjectAccessReview.Spec.ResourceAttributes.Subresource if attr.Namespace != "" { attr.ResourceScope = apirequest.NamespaceScope } else if attr.Workspace != "" { attr.ResourceScope = apirequest.WorkspaceScope } else { attr.ResourceScope = subjectAccessReview.Spec.ResourceAttributes.ResourceScope } } else if subjectAccessReview.Spec.NonResourceAttributes != nil { attr.ResourceRequest = false attr.Verb = subjectAccessReview.Spec.NonResourceAttributes.Verb attr.Path = subjectAccessReview.Spec.NonResourceAttributes.Path } return attr } func (h *handler) CreateWorkspaceMembers(request *restful.Request, response *restful.Response) { workspace := request.PathParameter("workspace") var members []Member err := request.ReadEntity(&members) if err != nil { api.HandleBadRequest(response, request, err) return } for _, member := range members { err := h.am.CreateOrUpdateUserWorkspaceRoleBinding(member.Username, workspace, member.RoleRef) if err != nil { api.HandleError(response, request, err) return } } response.WriteEntity(members) } func (h *handler) RemoveWorkspaceMember(request *restful.Request, response *restful.Response) { workspace := request.PathParameter("workspace") username := request.PathParameter("workspacemember") err := h.am.RemoveUserFromWorkspace(username, workspace) if err != nil { api.HandleError(response, request, err) return } response.WriteEntity(servererr.None) } func (h *handler) DescribeWorkspaceMember(request *restful.Request, response *restful.Response) { workspace := request.PathParameter("workspace") memberName := request.PathParameter("workspacemember") bindings, err := h.am.ListWorkspaceRoleBindings(memberName, "", nil, workspace) if err != nil { api.HandleInternalError(response, request, err) return } if len(bindings) == 0 { api.HandleBadRequest(response, request, NewErrMemberNotExist(memberName)) return } user, err := h.im.DescribeUser(memberName) if err != nil { api.HandleError(response, request, err) return } user.Annotations[iamv1beta1.WorkspaceRoleAnnotation] = bindings[0].RoleRef.Name _ = response.WriteEntity(user) } func (h *handler) UpdateWorkspaceMember(request *restful.Request, response *restful.Response) { workspace := request.PathParameter("workspace") memberName := request.PathParameter("workspacemember") var member Member err := request.ReadEntity(&member) if err != nil { api.HandleBadRequest(response, request, err) return } if memberName != member.Username { api.HandleBadRequest(response, request, NewErrIncorrectUsername(memberName)) return } bindings, err := h.am.ListWorkspaceRoleBindings(memberName, "", nil, workspace) if err != nil { api.HandleInternalError(response, request, err) return } if len(bindings) == 0 { api.HandleBadRequest(response, request, NewErrMemberNotExist(member.Username)) return } err = h.am.CreateOrUpdateUserWorkspaceRoleBinding(member.Username, workspace, member.RoleRef) if err != nil { api.HandleError(response, request, err) return } response.WriteEntity(servererr.None) } func (h *handler) UpdateClusterMember(request *restful.Request, response *restful.Response) { memberName := request.PathParameter("clustermember") var member Member err := request.ReadEntity(&member) if err != nil { api.HandleBadRequest(response, request, err) return } if memberName != member.Username { api.HandleBadRequest(response, request, NewErrIncorrectUsername(memberName)) return } bindings, err := h.am.ListClusterRoleBindings(memberName, "") if err != nil { api.HandleInternalError(response, request, err) return } if len(bindings) == 0 { api.HandleBadRequest(response, request, NewErrMemberNotExist(member.Username)) return } err = h.am.CreateOrUpdateClusterRoleBinding(member.Username, member.RoleRef) if err != nil { api.HandleError(response, request, err) return } response.WriteEntity(servererr.None) } func (h *handler) UpdateNamespaceMember(request *restful.Request, response *restful.Response) { namespace := request.PathParameter("namespace") memberName := request.PathParameter("namespacemember") var member Member err := request.ReadEntity(&member) if err != nil { api.HandleBadRequest(response, request, err) return } if memberName != member.Username { api.HandleBadRequest(response, request, NewErrIncorrectUsername(memberName)) return } bindings, err := h.am.ListRoleBindings(member.Username, "", nil, namespace) if err != nil { api.HandleInternalError(response, request, err) return } if len(bindings) == 0 { api.HandleBadRequest(response, request, NewErrMemberNotExist(member.Username)) return } err = h.am.CreateOrUpdateNamespaceRoleBinding(member.Username, namespace, member.RoleRef) if err != nil { api.HandleError(response, request, err) return } response.WriteEntity(servererr.None) } func NewErrMemberNotExist(username string) error { return fmt.Errorf("member %s not exist", username) } func NewErrIncorrectUsername(username string) error { return fmt.Errorf("incorrect username %s, the username must equal to the member", username) }