github.com/minio/console@v1.4.1/api/admin_groups.go (about)

     1  // This file is part of MinIO Console Server
     2  // Copyright (c) 2021 MinIO, Inc.
     3  //
     4  // This program is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Affero General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // This program is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    12  // GNU Affero General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Affero General Public License
    15  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package api
    18  
    19  import (
    20  	"context"
    21  
    22  	"github.com/go-openapi/errors"
    23  	"github.com/go-openapi/runtime/middleware"
    24  	"github.com/minio/console/api/operations"
    25  	"github.com/minio/console/pkg/utils"
    26  	"github.com/minio/madmin-go/v3"
    27  
    28  	groupApi "github.com/minio/console/api/operations/group"
    29  
    30  	"github.com/minio/console/models"
    31  )
    32  
    33  func registerGroupsHandlers(api *operations.ConsoleAPI) {
    34  	// List Groups
    35  	api.GroupListGroupsHandler = groupApi.ListGroupsHandlerFunc(func(params groupApi.ListGroupsParams, session *models.Principal) middleware.Responder {
    36  		listGroupsResponse, err := getListGroupsResponse(session, params)
    37  		if err != nil {
    38  			return groupApi.NewListGroupsDefault(err.Code).WithPayload(err.APIError)
    39  		}
    40  		return groupApi.NewListGroupsOK().WithPayload(listGroupsResponse)
    41  	})
    42  	// Group Info
    43  	api.GroupGroupInfoHandler = groupApi.GroupInfoHandlerFunc(func(params groupApi.GroupInfoParams, session *models.Principal) middleware.Responder {
    44  		groupInfo, err := getGroupInfoResponse(session, params)
    45  		if err != nil {
    46  			return groupApi.NewGroupInfoDefault(err.Code).WithPayload(err.APIError)
    47  		}
    48  		return groupApi.NewGroupInfoOK().WithPayload(groupInfo)
    49  	})
    50  	// Add Group
    51  	api.GroupAddGroupHandler = groupApi.AddGroupHandlerFunc(func(params groupApi.AddGroupParams, session *models.Principal) middleware.Responder {
    52  		if err := getAddGroupResponse(session, params); err != nil {
    53  			return groupApi.NewAddGroupDefault(err.Code).WithPayload(err.APIError)
    54  		}
    55  		return groupApi.NewAddGroupCreated()
    56  	})
    57  	// Remove Group
    58  	api.GroupRemoveGroupHandler = groupApi.RemoveGroupHandlerFunc(func(params groupApi.RemoveGroupParams, session *models.Principal) middleware.Responder {
    59  		if err := getRemoveGroupResponse(session, params); err != nil {
    60  			return groupApi.NewRemoveGroupDefault(err.Code).WithPayload(err.APIError)
    61  		}
    62  		return groupApi.NewRemoveGroupNoContent()
    63  	})
    64  	// Update Group
    65  	api.GroupUpdateGroupHandler = groupApi.UpdateGroupHandlerFunc(func(params groupApi.UpdateGroupParams, session *models.Principal) middleware.Responder {
    66  		groupUpdateResp, err := getUpdateGroupResponse(session, params)
    67  		if err != nil {
    68  			return groupApi.NewUpdateGroupDefault(err.Code).WithPayload(err.APIError)
    69  		}
    70  		return groupApi.NewUpdateGroupOK().WithPayload(groupUpdateResp)
    71  	})
    72  }
    73  
    74  // getListGroupsResponse performs listGroups() and serializes it to the handler's output
    75  func getListGroupsResponse(session *models.Principal, params groupApi.ListGroupsParams) (*models.ListGroupsResponse, *CodedAPIError) {
    76  	ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
    77  	defer cancel()
    78  	mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
    79  	if err != nil {
    80  		return nil, ErrorWithContext(ctx, err)
    81  	}
    82  	// create a MinIO Admin Client interface implementation
    83  	// defining the client to be used
    84  	adminClient := AdminClient{Client: mAdmin}
    85  
    86  	groups, err := adminClient.listGroups(ctx)
    87  	if err != nil {
    88  		return nil, ErrorWithContext(ctx, err)
    89  	}
    90  
    91  	// serialize output
    92  	listGroupsResponse := &models.ListGroupsResponse{
    93  		Groups: groups,
    94  		Total:  int64(len(groups)),
    95  	}
    96  
    97  	return listGroupsResponse, nil
    98  }
    99  
   100  // groupInfo calls MinIO server get Group's info
   101  func groupInfo(ctx context.Context, client MinioAdmin, group string) (*madmin.GroupDesc, error) {
   102  	groupDesc, err := client.getGroupDescription(ctx, group)
   103  	if err != nil {
   104  		return nil, err
   105  	}
   106  	return groupDesc, nil
   107  }
   108  
   109  // getGroupInfoResponse performs groupInfo() and serializes it to the handler's output
   110  func getGroupInfoResponse(session *models.Principal, params groupApi.GroupInfoParams) (*models.Group, *CodedAPIError) {
   111  	ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
   112  	defer cancel()
   113  	mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
   114  	if err != nil {
   115  		return nil, ErrorWithContext(ctx, err)
   116  	}
   117  	// create a MinIO Admin Client interface implementation
   118  	// defining the client to be used
   119  	adminClient := AdminClient{Client: mAdmin}
   120  
   121  	groupName, err := utils.DecodeBase64(params.Name)
   122  	if err != nil {
   123  		return nil, ErrorWithContext(ctx, err)
   124  	}
   125  
   126  	groupDesc, err := groupInfo(ctx, adminClient, groupName)
   127  	if err != nil {
   128  		return nil, ErrorWithContext(ctx, err)
   129  	}
   130  
   131  	groupResponse := &models.Group{
   132  		Members: groupDesc.Members,
   133  		Name:    groupDesc.Name,
   134  		Policy:  groupDesc.Policy,
   135  		Status:  groupDesc.Status,
   136  	}
   137  
   138  	return groupResponse, nil
   139  }
   140  
   141  // addGroupAdd a MinIO group with the defined members
   142  func addGroup(ctx context.Context, client MinioAdmin, group string, members []string) error {
   143  	gAddRemove := madmin.GroupAddRemove{
   144  		Group:    group,
   145  		Members:  members,
   146  		IsRemove: false,
   147  	}
   148  	err := client.updateGroupMembers(ctx, gAddRemove)
   149  	if err != nil {
   150  		return err
   151  	}
   152  	return nil
   153  }
   154  
   155  // getAddGroupResponse performs addGroup() and serializes it to the handler's output
   156  func getAddGroupResponse(session *models.Principal, params groupApi.AddGroupParams) *CodedAPIError {
   157  	ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
   158  	defer cancel()
   159  	// AddGroup request needed to proceed
   160  	if params.Body == nil {
   161  		return ErrorWithContext(ctx, ErrGroupBodyNotInRequest)
   162  	}
   163  	groupRequest := params.Body
   164  	mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
   165  	if err != nil {
   166  		return ErrorWithContext(ctx, err)
   167  	}
   168  	// create a MinIO Admin Client interface implementation
   169  	// defining the client to be used
   170  	adminClient := AdminClient{Client: mAdmin}
   171  
   172  	groupList, _ := adminClient.listGroups(ctx)
   173  
   174  	for _, b := range groupList {
   175  		if b == *groupRequest.Group {
   176  			return ErrorWithContext(ctx, ErrGroupAlreadyExists)
   177  		}
   178  	}
   179  
   180  	if err := addGroup(ctx, adminClient, *groupRequest.Group, groupRequest.Members); err != nil {
   181  		return ErrorWithContext(ctx, err)
   182  	}
   183  	return nil
   184  }
   185  
   186  // removeGroup deletes a minIO group only if it has no members
   187  func removeGroup(ctx context.Context, client MinioAdmin, group string) error {
   188  	gAddRemove := madmin.GroupAddRemove{
   189  		Group:    group,
   190  		Members:  []string{},
   191  		IsRemove: true,
   192  	}
   193  	err := client.updateGroupMembers(ctx, gAddRemove)
   194  	if err != nil {
   195  		return err
   196  	}
   197  	return nil
   198  }
   199  
   200  // getRemoveGroupResponse performs removeGroup() and serializes it to the handler's output
   201  func getRemoveGroupResponse(session *models.Principal, params groupApi.RemoveGroupParams) *CodedAPIError {
   202  	ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
   203  	defer cancel()
   204  	if params.Name == "" {
   205  		return ErrorWithContext(ctx, ErrGroupNameNotInRequest)
   206  	}
   207  	mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
   208  	if err != nil {
   209  		return ErrorWithContext(ctx, err)
   210  	}
   211  	// Create a MinIO Admin Client interface implementation
   212  	// defining the client to be used
   213  	adminClient := AdminClient{Client: mAdmin}
   214  
   215  	groupName, err := utils.DecodeBase64(params.Name)
   216  	if err != nil {
   217  		return ErrorWithContext(ctx, err)
   218  	}
   219  
   220  	if err := removeGroup(ctx, adminClient, groupName); err != nil {
   221  		minioError := madmin.ToErrorResponse(err)
   222  		err2 := ErrorWithContext(ctx, err)
   223  		if minioError.Code == "XMinioAdminNoSuchGroup" {
   224  			err2.Code = 404
   225  		}
   226  		return err2
   227  	}
   228  	return nil
   229  }
   230  
   231  // updateGroup updates a group by adding/removing members and setting the status to the desired one
   232  //
   233  // isRemove: whether remove members or not
   234  func updateGroupMembers(ctx context.Context, client MinioAdmin, group string, members []string, isRemove bool) error {
   235  	gAddRemove := madmin.GroupAddRemove{
   236  		Group:    group,
   237  		Members:  members,
   238  		IsRemove: isRemove,
   239  	}
   240  	err := client.updateGroupMembers(ctx, gAddRemove)
   241  	if err != nil {
   242  		return err
   243  	}
   244  	return nil
   245  }
   246  
   247  // addOrDeleteMembers updates a group members by adding or deleting them based on the expectedMembers
   248  func addOrDeleteMembers(ctx context.Context, client MinioAdmin, group *madmin.GroupDesc, expectedMembers []string) error {
   249  	// get members to delete/add
   250  	membersToDelete := DifferenceArrays(group.Members, expectedMembers)
   251  	membersToAdd := DifferenceArrays(expectedMembers, group.Members)
   252  	// delete members if any to be deleted
   253  	if len(membersToDelete) > 0 {
   254  		err := updateGroupMembers(ctx, client, group.Name, membersToDelete, true)
   255  		if err != nil {
   256  			return err
   257  		}
   258  	}
   259  	// add members if any to be added
   260  	if len(membersToAdd) > 0 {
   261  		err := updateGroupMembers(ctx, client, group.Name, membersToAdd, false)
   262  		if err != nil {
   263  			return err
   264  		}
   265  	}
   266  	return nil
   267  }
   268  
   269  func setGroupStatus(ctx context.Context, client MinioAdmin, group, status string) error {
   270  	var setStatus madmin.GroupStatus
   271  	switch status {
   272  	case "enabled":
   273  		setStatus = madmin.GroupEnabled
   274  	case "disabled":
   275  		setStatus = madmin.GroupDisabled
   276  	default:
   277  		return errors.New(500, "status not valid")
   278  	}
   279  	return client.setGroupStatus(ctx, group, setStatus)
   280  }
   281  
   282  // getUpdateGroupResponse updates a group by adding or removing it's members depending on the request,
   283  // also sets the group's status if status in the request is different than the current one.
   284  // Then serializes the output to be used by the handler.
   285  func getUpdateGroupResponse(session *models.Principal, params groupApi.UpdateGroupParams) (*models.Group, *CodedAPIError) {
   286  	ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
   287  	defer cancel()
   288  	if params.Name == "" {
   289  		return nil, ErrorWithContext(ctx, ErrGroupNameNotInRequest)
   290  	}
   291  	if params.Body == nil {
   292  		return nil, ErrorWithContext(ctx, ErrGroupBodyNotInRequest)
   293  	}
   294  	expectedGroupUpdate := params.Body
   295  
   296  	groupName, err := utils.DecodeBase64(params.Name)
   297  	if err != nil {
   298  		return nil, ErrorWithContext(ctx, err)
   299  	}
   300  
   301  	mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
   302  	if err != nil {
   303  		return nil, ErrorWithContext(ctx, err)
   304  	}
   305  	// create a MinIO Admin Client interface implementation
   306  	// defining the client to be used
   307  	adminClient := AdminClient{Client: mAdmin}
   308  
   309  	groupUpdated, err := groupUpdate(ctx, adminClient, groupName, expectedGroupUpdate)
   310  	if err != nil {
   311  		return nil, ErrorWithContext(ctx, err)
   312  	}
   313  	groupResponse := &models.Group{
   314  		Name:    groupUpdated.Name,
   315  		Members: groupUpdated.Members,
   316  		Policy:  groupUpdated.Policy,
   317  		Status:  groupUpdated.Status,
   318  	}
   319  	return groupResponse, nil
   320  }
   321  
   322  // groupUpdate updates a group given the expected parameters, compares the expected parameters against the current ones
   323  // and updates them accordingly, status is only updated if the expected status is different than the current one.
   324  // Then fetches the group again to return the object updated.
   325  func groupUpdate(ctx context.Context, client MinioAdmin, groupName string, expectedGroup *models.UpdateGroupRequest) (*madmin.GroupDesc, error) {
   326  	expectedMembers := expectedGroup.Members
   327  	expectedStatus := *expectedGroup.Status
   328  	// get current members and status
   329  	groupDescription, err := groupInfo(ctx, client, groupName)
   330  	if err != nil {
   331  		LogInfo("error getting group info: %v", err)
   332  		return nil, err
   333  	}
   334  	// update group members
   335  	err = addOrDeleteMembers(ctx, client, groupDescription, expectedMembers)
   336  	if err != nil {
   337  		LogInfo("error updating group: %v", err)
   338  		return nil, err
   339  	}
   340  	// update group status only if different from current status
   341  	if expectedStatus != groupDescription.Status {
   342  		err = setGroupStatus(ctx, client, groupDescription.Name, expectedStatus)
   343  		if err != nil {
   344  			LogInfo("error updating group's status: %v", err)
   345  			return nil, err
   346  		}
   347  	}
   348  	// return latest group info to verify that changes were applied correctly
   349  	groupDescription, err = groupInfo(ctx, client, groupName)
   350  	if err != nil {
   351  		LogInfo("error getting group info: %v", err)
   352  		return nil, err
   353  	}
   354  	return groupDescription, nil
   355  }