Files
object-browser/restapi/admin_users.go
2020-06-22 20:56:52 -07:00

488 lines
15 KiB
Go

// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package restapi
import (
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/swag"
"github.com/minio/mcs/models"
"github.com/minio/mcs/restapi/operations"
"github.com/minio/mcs/restapi/operations/admin_api"
"github.com/minio/minio/pkg/madmin"
"context"
"fmt"
"log"
"strings"
)
func registerUsersHandlers(api *operations.McsAPI) {
// List Users
api.AdminAPIListUsersHandler = admin_api.ListUsersHandlerFunc(func(params admin_api.ListUsersParams, principal *models.Principal) middleware.Responder {
sessionID := string(*principal)
listUsersResponse, err := getListUsersResponse(sessionID)
if err != nil {
return admin_api.NewListUsersDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
return admin_api.NewListUsersOK().WithPayload(listUsersResponse)
})
// Add User
api.AdminAPIAddUserHandler = admin_api.AddUserHandlerFunc(func(params admin_api.AddUserParams, principal *models.Principal) middleware.Responder {
sessionID := string(*principal)
userResponse, err := getUserAddResponse(sessionID, params)
if err != nil {
return admin_api.NewAddUserDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
return admin_api.NewAddUserCreated().WithPayload(userResponse)
})
// Remove User
api.AdminAPIRemoveUserHandler = admin_api.RemoveUserHandlerFunc(func(params admin_api.RemoveUserParams, principal *models.Principal) middleware.Responder {
sessionID := string(*principal)
err := getRemoveUserResponse(sessionID, params)
if err != nil {
return admin_api.NewRemoveUserDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
return admin_api.NewRemoveUserNoContent()
})
// Update User-Groups
api.AdminAPIUpdateUserGroupsHandler = admin_api.UpdateUserGroupsHandlerFunc(func(params admin_api.UpdateUserGroupsParams, principal *models.Principal) middleware.Responder {
sessionID := string(*principal)
userUpdateResponse, err := getUpdateUserGroupsResponse(sessionID, params)
if err != nil {
return admin_api.NewUpdateUserGroupsDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
return admin_api.NewUpdateUserGroupsOK().WithPayload(userUpdateResponse)
})
// Get User
api.AdminAPIGetUserInfoHandler = admin_api.GetUserInfoHandlerFunc(func(params admin_api.GetUserInfoParams, principal *models.Principal) middleware.Responder {
sessionID := string(*principal)
userInfoResponse, err := getUserInfoResponse(sessionID, params)
if err != nil {
return admin_api.NewGetUserInfoDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
return admin_api.NewGetUserInfoOK().WithPayload(userInfoResponse)
})
// Update User
api.AdminAPIUpdateUserInfoHandler = admin_api.UpdateUserInfoHandlerFunc(func(params admin_api.UpdateUserInfoParams, principal *models.Principal) middleware.Responder {
sessionID := string(*principal)
userUpdateResponse, err := getUpdateUserResponse(sessionID, params)
if err != nil {
return admin_api.NewUpdateUserInfoDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
return admin_api.NewUpdateUserInfoOK().WithPayload(userUpdateResponse)
})
// Update User-Groups Bulk
api.AdminAPIBulkUpdateUsersGroupsHandler = admin_api.BulkUpdateUsersGroupsHandlerFunc(func(params admin_api.BulkUpdateUsersGroupsParams, principal *models.Principal) middleware.Responder {
sessionID := string(*principal)
err := getAddUsersListToGroupsResponse(sessionID, params)
if err != nil {
return admin_api.NewBulkUpdateUsersGroupsDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
return admin_api.NewBulkUpdateUsersGroupsOK()
})
}
func listUsers(ctx context.Context, client MinioAdmin) ([]*models.User, error) {
// Get list of all users in the MinIO
// This call requires explicit authentication, no anonymous requests are
// allowed for listing users.
userMap, err := client.listUsers(ctx)
if err != nil {
return []*models.User{}, err
}
var users []*models.User
for accessKey, user := range userMap {
userElem := &models.User{
AccessKey: accessKey,
Status: string(user.Status),
Policy: user.PolicyName,
MemberOf: user.MemberOf,
}
users = append(users, userElem)
}
return users, nil
}
// getListUsersResponse performs listUsers() and serializes it to the handler's output
func getListUsersResponse(sessionID string) (*models.ListUsersResponse, error) {
ctx := context.Background()
mAdmin, err := newMAdminClient(sessionID)
if err != nil {
log.Println("error creating Madmin Client:", err)
return nil, err
}
// create a minioClient interface implementation
// defining the client to be used
adminClient := adminClient{client: mAdmin}
users, err := listUsers(ctx, adminClient)
if err != nil {
log.Println("error listing users:", err)
return nil, err
}
// serialize output
listUsersResponse := &models.ListUsersResponse{
Users: users,
}
return listUsersResponse, nil
}
// addUser invokes adding a users on `MinioAdmin` and builds the response `models.User`
func addUser(ctx context.Context, client MinioAdmin, accessKey, secretKey *string, groups []string) (*models.User, error) {
// Calls into MinIO to add a new user if there's an error return it
if err := client.addUser(ctx, *accessKey, *secretKey); err != nil {
return nil, err
}
if len(groups) > 0 {
userElem, errUG := updateUserGroups(ctx, client, *accessKey, groups)
if errUG != nil {
return nil, errUG
}
return userElem, nil
}
userRet := &models.User{
AccessKey: *accessKey,
MemberOf: nil,
Policy: "",
Status: "",
}
return userRet, nil
}
func getUserAddResponse(sessionID string, params admin_api.AddUserParams) (*models.User, error) {
ctx := context.Background()
mAdmin, err := newMAdminClient(sessionID)
if err != nil {
log.Println("error creating Madmin Client:", err)
return nil, err
}
// create a minioClient interface implementation
// defining the client to be used
adminClient := adminClient{client: mAdmin}
user, err := addUser(ctx, adminClient, params.Body.AccessKey, params.Body.SecretKey, params.Body.Groups)
if err != nil {
log.Println("error adding user:", err)
return nil, err
}
return user, nil
}
//removeUser invokes removing an user on `MinioAdmin`, then we return the response from API
func removeUser(ctx context.Context, client MinioAdmin, accessKey string) error {
if err := client.removeUser(ctx, accessKey); err != nil {
return err
}
return nil
}
func getRemoveUserResponse(sessionID string, params admin_api.RemoveUserParams) error {
ctx := context.Background()
mAdmin, err := newMAdminClient(sessionID)
if err != nil {
log.Println("error creating Madmin Client:", err)
return err
}
// create a minioClient interface implementation
// defining the client to be used
adminClient := adminClient{client: mAdmin}
if err := removeUser(ctx, adminClient, params.Name); err != nil {
log.Println("error removing user:", err)
return err
}
log.Println("User removed successfully:", params.Name)
return nil
}
// getUserInfo calls MinIO server get the User Information
func getUserInfo(ctx context.Context, client MinioAdmin, accessKey string) (*madmin.UserInfo, error) {
userInfo, err := client.getUserInfo(ctx, accessKey)
if err != nil {
return nil, err
}
return &userInfo, nil
}
func getUserInfoResponse(sessionID string, params admin_api.GetUserInfoParams) (*models.User, error) {
ctx := context.Background()
mAdmin, err := newMAdminClient(sessionID)
if err != nil {
log.Println("error creating Madmin Client:", err)
return nil, err
}
// create a minioClient interface implementation
// defining the client to be used
adminClient := adminClient{client: mAdmin}
user, err := getUserInfo(ctx, adminClient, params.Name)
if err != nil {
log.Println("error getting user:", err)
return nil, err
}
userInformation := &models.User{
AccessKey: params.Name,
MemberOf: user.MemberOf,
Policy: user.PolicyName,
Status: string(user.Status),
}
return userInformation, nil
}
// updateUserGroups invokes getUserInfo() to get the old groups from the user,
// then we merge the list with the new groups list to have a shorter iteration between groups and we do a comparison between the current and old groups.
// We delete or update the groups according the location in each list and send the user with the new groups from `MinioAdmin` to the client
func updateUserGroups(ctx context.Context, client MinioAdmin, user string, groupsToAssign []string) (*models.User, error) {
parallelUserUpdate := func(groupName string, originGroups []string) chan error {
chProcess := make(chan error)
go func() error {
defer close(chProcess)
//Compare if groupName is in the arrays
isGroupPersistent := IsElementInArray(groupsToAssign, groupName)
isInOriginGroups := IsElementInArray(originGroups, groupName)
if isGroupPersistent && isInOriginGroups { // Group is already assigned and doesn't need to be updated
chProcess <- nil
return nil
}
isRemove := false // User is added by default
// User is deleted from the group
if !isGroupPersistent {
isRemove = true
}
userToAddRemove := []string{user}
updateReturn := updateGroupMembers(ctx, client, groupName, userToAddRemove, isRemove)
chProcess <- updateReturn
return updateReturn
}()
return chProcess
}
userInfoOr, err := getUserInfo(ctx, client, user)
if err != nil {
return nil, err
}
memberOf := userInfoOr.MemberOf
mergedGroupArray := UniqueKeys(append(memberOf, groupsToAssign...))
var listOfUpdates []chan error
// Each group must be updated individually because there is no way to update all the groups at once for a user,
// we are using the same logic as 'mc admin group add' command
for _, groupN := range mergedGroupArray {
proc := parallelUserUpdate(groupN, memberOf)
listOfUpdates = append(listOfUpdates, proc)
}
channelHasError := false
for _, chanRet := range listOfUpdates {
locError := <-chanRet
if locError != nil {
channelHasError = true
}
}
if channelHasError {
errRt := errors.New(500, "there was an error updating the groups")
return nil, errRt
}
userInfo, err := getUserInfo(ctx, client, user)
if err != nil {
return nil, err
}
userReturn := &models.User{
AccessKey: user,
MemberOf: userInfo.MemberOf,
Policy: userInfo.PolicyName,
Status: string(userInfo.Status),
}
return userReturn, nil
}
func getUpdateUserGroupsResponse(sessionID string, params admin_api.UpdateUserGroupsParams) (*models.User, error) {
ctx := context.Background()
mAdmin, err := newMAdminClient(sessionID)
if err != nil {
log.Println("error creating Madmin Client:", err)
return nil, err
}
// create a minioClient interface implementation
// defining the client to be used
adminClient := adminClient{client: mAdmin}
user, err := updateUserGroups(ctx, adminClient, params.Name, params.Body.Groups)
if err != nil {
log.Println("error updating users's groups:", params.Body.Groups)
return nil, err
}
return user, nil
}
// setUserStatus invokes setUserStatus from madmin to update user status
func setUserStatus(ctx context.Context, client MinioAdmin, user string, status string) error {
var setStatus madmin.AccountStatus
switch status {
case "enabled":
setStatus = madmin.AccountEnabled
case "disabled":
setStatus = madmin.AccountDisabled
default:
return errors.New(500, "status not valid")
}
if err := client.setUserStatus(ctx, user, setStatus); err != nil {
return err
}
return nil
}
func getUpdateUserResponse(sessionID string, params admin_api.UpdateUserInfoParams) (*models.User, error) {
ctx := context.Background()
mAdmin, err := newMAdminClient(sessionID)
if err != nil {
log.Println("error creating Madmin Client:", err)
return nil, err
}
// create a minioClient interface implementation
// defining the client to be used
adminClient := adminClient{client: mAdmin}
name := params.Name
status := *params.Body.Status
groups := params.Body.Groups
if err := setUserStatus(ctx, adminClient, name, status); err != nil {
log.Println("error updating user status:", status)
return nil, err
}
userElem, errUG := updateUserGroups(ctx, adminClient, name, groups)
if errUG != nil {
return nil, errUG
}
return userElem, nil
}
// addUsersListToGroups iterates over the user list & assigns the requested groups to each user.
func addUsersListToGroups(ctx context.Context, client MinioAdmin, usersToUpdate []string, groupsToAssign []string) error {
// We update each group with the complete usersList
parallelGroupsUpdate := func(groupToAssign string) chan error {
groupProcess := make(chan error)
go func() {
defer close(groupProcess)
// We add the users array to the group.
err := updateGroupMembers(ctx, client, groupToAssign, usersToUpdate, false)
groupProcess <- err
}()
return groupProcess
}
var groupsUpdateList []chan error
// We get each group name & add users accordingly
for _, groupName := range groupsToAssign {
// We update the group
proc := parallelGroupsUpdate(groupName)
groupsUpdateList = append(groupsUpdateList, proc)
}
errorsList := []string{} // We get the errors list because we want to have all errors at once.
for _, err := range groupsUpdateList {
errorFromUpdate := <-err // We store the error to avoid Data Race
if errorFromUpdate != nil {
// If there is an error, we store the errors strings so we can join them after we receive all errors
errorsList = append(errorsList, errorFromUpdate.Error()) // We wait until all the channels have been closed.
}
}
// If there are errors, we throw the final error with the errors inside
if len(errorsList) > 0 {
errGen := fmt.Errorf("error in users-groups assignation: %q", strings.Join(errorsList[:], ","))
return errGen
}
return nil
}
func getAddUsersListToGroupsResponse(sessionID string, params admin_api.BulkUpdateUsersGroupsParams) error {
ctx := context.Background()
mAdmin, err := newMAdminClient(sessionID)
if err != nil {
log.Println("error creating Madmin Client:", err)
return err
}
// create a minioClient interface implementation
// defining the client to be used
adminClient := adminClient{client: mAdmin}
usersList := params.Body.Users
groupsList := params.Body.Groups
if err := addUsersListToGroups(ctx, adminClient, usersList, groupsList); err != nil {
log.Println("error updating groups bulk users:", err.Error())
return err
}
return nil
}