Add IAM groups support (#7981)

This change adds admin APIs and IAM subsystem APIs to:

- add or remove members to a group (group addition and deletion is
  implicit on add and remove)

- enable/disable a group

- list and fetch group info
master
Aditya Manthramurthy 5 years ago committed by kannappanr
parent 5cd9f10a02
commit 414a7eca83
  1. 127
      cmd/admin-handlers.go
  2. 12
      cmd/admin-router.go
  3. 16
      cmd/api-errors.go
  4. 585
      cmd/iam.go
  5. 13
      cmd/notification.go
  6. 12
      cmd/peer-rest-client.go
  7. 2
      cmd/peer-rest-common.go
  8. 25
      cmd/peer-rest-server.go
  9. 7
      cmd/sts-handlers.go
  10. 7
      cmd/typed-errors.go
  11. 164
      pkg/madmin/group-commands.go

@ -23,6 +23,7 @@ import (
"encoding/json"
"errors"
"io"
"io/ioutil"
"net/http"
"os"
"sort"
@ -1019,6 +1020,130 @@ func (a adminAPIHandlers) ListUsers(w http.ResponseWriter, r *http.Request) {
writeSuccessResponseJSON(w, econfigData)
}
// UpdateGroupMembers - PUT /minio/admin/v1/update-group-members
func (a adminAPIHandlers) UpdateGroupMembers(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "UpdateGroupMembers")
objectAPI := validateAdminReq(ctx, w, r)
if objectAPI == nil {
return
}
defer r.Body.Close()
data, err := ioutil.ReadAll(r.Body)
if err != nil {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
return
}
var updReq madmin.GroupAddRemove
err = json.Unmarshal(data, &updReq)
if err != nil {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
return
}
if updReq.IsRemove {
err = globalIAMSys.RemoveUsersFromGroup(updReq.Group, updReq.Members)
} else {
err = globalIAMSys.AddUsersToGroup(updReq.Group, updReq.Members)
}
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
// Notify all other MinIO peers to load group.
for _, nerr := range globalNotificationSys.LoadGroup(updReq.Group) {
if nerr.Err != nil {
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
logger.LogIf(ctx, nerr.Err)
}
}
}
// GetGroup - /minio/admin/v1/group?group=mygroup1
func (a adminAPIHandlers) GetGroup(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "GetGroup")
objectAPI := validateAdminReq(ctx, w, r)
if objectAPI == nil {
return
}
vars := mux.Vars(r)
group := vars["group"]
gdesc, err := globalIAMSys.GetGroupDescription(group)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
body, err := json.Marshal(gdesc)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
writeSuccessResponseJSON(w, body)
}
// ListGroups - GET /minio/admin/v1/groups
func (a adminAPIHandlers) ListGroups(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "ListGroups")
objectAPI := validateAdminReq(ctx, w, r)
if objectAPI == nil {
return
}
groups := globalIAMSys.ListGroups()
body, err := json.Marshal(groups)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
writeSuccessResponseJSON(w, body)
}
// SetGroupStatus - PUT /minio/admin/v1/set-group-status?group=mygroup1&status=enabled
func (a adminAPIHandlers) SetGroupStatus(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "SetGroupStatus")
objectAPI := validateAdminReq(ctx, w, r)
if objectAPI == nil {
return
}
vars := mux.Vars(r)
group := vars["group"]
status := vars["status"]
var err error
if status == statusEnabled {
err = globalIAMSys.SetGroupStatus(group, true)
} else if status == statusDisabled {
err = globalIAMSys.SetGroupStatus(group, false)
} else {
err = errInvalidArgument
}
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
// Notify all other MinIO peers to reload user.
for _, nerr := range globalNotificationSys.LoadGroup(group) {
if nerr.Err != nil {
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
logger.LogIf(ctx, nerr.Err)
}
}
}
// SetUserStatus - PUT /minio/admin/v1/set-user-status?accessKey=<access_key>&status=[enabled|disabled]
func (a adminAPIHandlers) SetUserStatus(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "SetUserStatus")
@ -1253,7 +1378,7 @@ func (a adminAPIHandlers) SetUserPolicy(w http.ResponseWriter, r *http.Request)
return
}
if err := globalIAMSys.PolicyDBSet(accessKey, policyName); err != nil {
if err := globalIAMSys.PolicyDBSet(accessKey, policyName, false); err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
}

@ -110,6 +110,18 @@ func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool)
// List users
adminV1Router.Methods(http.MethodGet).Path("/list-users").HandlerFunc(httpTraceHdrs(adminAPI.ListUsers))
// Add/Remove members from group
adminV1Router.Methods(http.MethodPut).Path("/update-group-members").HandlerFunc(httpTraceHdrs(adminAPI.UpdateGroupMembers))
// Get Group
adminV1Router.Methods(http.MethodGet).Path("/group").HandlerFunc(httpTraceHdrs(adminAPI.GetGroup)).Queries("group", "{group:.*}")
// List Groups
adminV1Router.Methods(http.MethodGet).Path("/groups").HandlerFunc(httpTraceHdrs(adminAPI.ListGroups))
// Set Group Status
adminV1Router.Methods(http.MethodPut).Path("/set-group-status").HandlerFunc(httpTraceHdrs(adminAPI.SetGroupStatus)).Queries("group", "{group:.*}").Queries("status", "{status:.*}")
// List policies
adminV1Router.Methods(http.MethodGet).Path("/list-canned-policies").HandlerFunc(httpTraceHdrs(adminAPI.ListCannedPolicies))
}

@ -203,6 +203,8 @@ const (
ErrMalformedJSON
ErrAdminNoSuchUser
ErrAdminNoSuchGroup
ErrAdminGroupNotEmpty
ErrAdminNoSuchPolicy
ErrAdminInvalidArgument
ErrAdminInvalidAccessKey
@ -923,6 +925,16 @@ var errorCodes = errorCodeMap{
Description: "The specified user does not exist.",
HTTPStatusCode: http.StatusNotFound,
},
ErrAdminNoSuchGroup: {
Code: "XMinioAdminNoSuchGroup",
Description: "The specified group does not exist.",
HTTPStatusCode: http.StatusNotFound,
},
ErrAdminGroupNotEmpty: {
Code: "XMinioAdminGroupNotEmpty",
Description: "The specified group is not empty - cannot remove it.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrAdminNoSuchPolicy: {
Code: "XMinioAdminNoSuchPolicy",
Description: "The canned policy does not exist.",
@ -1500,6 +1512,10 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
apiErr = ErrAdminInvalidArgument
case errNoSuchUser:
apiErr = ErrAdminNoSuchUser
case errNoSuchGroup:
apiErr = ErrAdminNoSuchGroup
case errGroupNotEmpty:
apiErr = ErrAdminGroupNotEmpty
case errNoSuchPolicy:
apiErr = ErrAdminNoSuchPolicy
case errSignatureMismatch:

@ -42,6 +42,9 @@ const (
// IAM users directory.
iamConfigUsersPrefix = iamConfigPrefix + "/users/"
// IAM groups directory.
iamConfigGroupsPrefix = iamConfigPrefix + "/groups/"
// IAM policies directory.
iamConfigPoliciesPrefix = iamConfigPrefix + "/policies/"
@ -52,6 +55,7 @@ const (
iamConfigPolicyDBPrefix = iamConfigPrefix + "/policydb/"
iamConfigPolicyDBUsersPrefix = iamConfigPolicyDBPrefix + "users/"
iamConfigPolicyDBSTSUsersPrefix = iamConfigPolicyDBPrefix + "sts-users/"
iamConfigPolicyDBGroupsPrefix = iamConfigPolicyDBPrefix + "groups/"
// IAM identity file which captures identity credentials.
iamIdentityFile = "identity.json"
@ -59,12 +63,20 @@ const (
// IAM policy file which provides policies for each users.
iamPolicyFile = "policy.json"
// IAM group members file
iamGroupMembersFile = "members.json"
// IAM format file
iamFormatFile = "format.json"
iamFormatVersion1 = 1
)
const (
statusEnabled = "enabled"
statusDisabled = "disabled"
)
type iamFormat struct {
Version int `json:"version"`
}
@ -85,25 +97,23 @@ func getUserIdentityPath(user string, isSTS bool) string {
return pathJoin(basePath, user, iamIdentityFile)
}
func getGroupInfoPath(group string) string {
return pathJoin(iamConfigGroupsPrefix, group, iamGroupMembersFile)
}
func getPolicyDocPath(name string) string {
return pathJoin(iamConfigPoliciesPrefix, name, iamPolicyFile)
}
func getMappedPolicyPath(name string, isSTS bool) string {
if isSTS {
func getMappedPolicyPath(name string, isSTS, isGroup bool) string {
switch {
case isSTS:
return pathJoin(iamConfigPolicyDBSTSUsersPrefix, name+".json")
case isGroup:
return pathJoin(iamConfigPolicyDBGroupsPrefix, name+".json")
default:
return pathJoin(iamConfigPolicyDBUsersPrefix, name+".json")
}
return pathJoin(iamConfigPolicyDBUsersPrefix, name+".json")
}
// MappedPolicy represents a policy name mapped to a user or group
type MappedPolicy struct {
Version int `json:"version"`
Policy string `json:"policy"`
}
func newMappedPolicy(policy string) MappedPolicy {
return MappedPolicy{Version: 1, Policy: policy}
}
// UserIdentity represents a user's secret key and their status
@ -116,6 +126,27 @@ func newUserIdentity(creds auth.Credentials) UserIdentity {
return UserIdentity{Version: 1, Credentials: creds}
}
// GroupInfo contains info about a group
type GroupInfo struct {
Version int `json:"version"`
Status string `json:"status"`
Members []string `json:"members"`
}
func newGroupInfo(members []string) GroupInfo {
return GroupInfo{Version: 1, Status: statusEnabled, Members: members}
}
// MappedPolicy represents a policy name mapped to a user or group
type MappedPolicy struct {
Version int `json:"version"`
Policy string `json:"policy"`
}
func newMappedPolicy(policy string) MappedPolicy {
return MappedPolicy{Version: 1, Policy: policy}
}
func loadIAMConfigItem(objectAPI ObjectLayer, item interface{}, path string) error {
data, err := readConfig(context.Background(), objectAPI, path)
if err != nil {
@ -212,12 +243,18 @@ func saveIAMConfigItemEtcd(ctx context.Context, item interface{}, path string) e
// IAMSys - config system.
type IAMSys struct {
sync.RWMutex
// map of usernames to credentials
iamUsersMap map[string]auth.Credentials
// map of policy names to policy definitions
iamPolicyDocsMap map[string]iampolicy.Policy
// map of usernames to credentials
iamUsersMap map[string]auth.Credentials
// map of group names to group info
iamGroupsMap map[string]GroupInfo
// map of user names to groups they are a member of
iamUserGroupMemberships map[string]set.StringSet
// map of usernames/temporary access keys to policy names
iamUserPolicyMap map[string]MappedPolicy
// map of group names to policy names
iamGroupPolicyMap map[string]MappedPolicy
}
func loadPolicyDoc(objectAPI ObjectLayer, policy string, m map[string]iampolicy.Policy) error {
@ -260,7 +297,7 @@ func loadUser(objectAPI ObjectLayer, user string, isSTS bool,
idFile := getUserIdentityPath(user, isSTS)
// Delete expired identity - ignoring errors here.
deleteConfig(context.Background(), objectAPI, idFile)
deleteConfig(context.Background(), objectAPI, getMappedPolicyPath(user, isSTS))
deleteConfig(context.Background(), objectAPI, getMappedPolicyPath(user, isSTS, false))
return nil
}
@ -291,11 +328,38 @@ func loadUsers(objectAPI ObjectLayer, isSTS bool, m map[string]auth.Credentials)
return nil
}
func loadMappedPolicy(objectAPI ObjectLayer, name string, isSTS bool,
func loadGroup(objectAPI ObjectLayer, group string, m map[string]GroupInfo) error {
var g GroupInfo
err := loadIAMConfigItem(objectAPI, &g, getGroupInfoPath(group))
if err != nil {
return err
}
m[group] = g
return nil
}
func loadGroups(objectAPI ObjectLayer, m map[string]GroupInfo) error {
doneCh := make(chan struct{})
defer close(doneCh)
for item := range listIAMConfigItems(objectAPI, iamConfigGroupsPrefix, true, doneCh) {
if item.Err != nil {
return item.Err
}
group := item.Item
err := loadGroup(objectAPI, group, m)
if err != nil {
return err
}
}
return nil
}
func loadMappedPolicy(objectAPI ObjectLayer, name string, isSTS, isGroup bool,
m map[string]MappedPolicy) error {
var p MappedPolicy
err := loadIAMConfigItem(objectAPI, &p, getMappedPolicyPath(name, isSTS))
err := loadIAMConfigItem(objectAPI, &p, getMappedPolicyPath(name, isSTS, isGroup))
if err != nil {
return err
}
@ -303,7 +367,7 @@ func loadMappedPolicy(objectAPI ObjectLayer, name string, isSTS bool,
return nil
}
func loadMappedPolicies(objectAPI ObjectLayer, isSTS bool, m map[string]MappedPolicy) error {
func loadMappedPolicies(objectAPI ObjectLayer, isSTS, isGroup bool, m map[string]MappedPolicy) error {
doneCh := make(chan struct{})
defer close(doneCh)
basePath := iamConfigPolicyDBUsersPrefix
@ -317,7 +381,7 @@ func loadMappedPolicies(objectAPI ObjectLayer, isSTS bool, m map[string]MappedPo
policyFile := item.Item
userOrGroupName := strings.TrimSuffix(policyFile, ".json")
err := loadMappedPolicy(objectAPI, userOrGroupName, isSTS, m)
err := loadMappedPolicy(objectAPI, userOrGroupName, isSTS, isGroup, m)
if err != nil {
return err
}
@ -325,6 +389,51 @@ func loadMappedPolicies(objectAPI ObjectLayer, isSTS bool, m map[string]MappedPo
return nil
}
// LoadGroup - loads a specific group from storage, and updates the
// memberships cache. If the specified group does not exist in
// storage, it is removed from in-memory maps as well - this
// simplifies the implementation for group removal. This is called
// only via IAM notifications.
func (sys *IAMSys) LoadGroup(objAPI ObjectLayer, group string) error {
if objAPI == nil {
return errInvalidArgument
}
sys.Lock()
defer sys.Unlock()
if globalEtcdClient != nil {
// Watch APIs cover this case, so nothing to do.
return nil
}
err := loadGroup(objAPI, group, sys.iamGroupsMap)
if err != nil && err != errConfigNotFound {
return err
}
if err == errConfigNotFound {
// group does not exist - so remove from memory.
sys.removeGroupFromMembershipsMap(group)
delete(sys.iamGroupsMap, group)
delete(sys.iamGroupPolicyMap, group)
return nil
}
gi := sys.iamGroupsMap[group]
// Updating the group memberships cache happens in two steps:
//
// 1. Remove the group from each user's list of memberships.
// 2. Add the group to each member's list of memberships.
//
// This ensures that regardless of members being added or
// removed, the cache stays current.
sys.removeGroupFromMembershipsMap(group)
sys.updateGroupMembershipsMap(group, &gi)
return nil
}
// LoadPolicy - reloads a specific canned policy from backend disks or etcd.
func (sys *IAMSys) LoadPolicy(objAPI ObjectLayer, policyName string) error {
if objAPI == nil {
@ -356,7 +465,7 @@ func (sys *IAMSys) LoadUser(objAPI ObjectLayer, accessKey string, isSTS bool) er
if err != nil {
return err
}
err = loadMappedPolicy(objAPI, accessKey, isSTS, sys.iamUserPolicyMap)
err = loadMappedPolicy(objAPI, accessKey, isSTS, false, sys.iamUserPolicyMap)
if err != nil {
return err
}
@ -378,6 +487,7 @@ func (sys *IAMSys) reloadFromEvent(event *etcd.Event) {
eventCreate := event.IsModify() || event.IsCreate()
eventDelete := event.Type == etcd.EventTypeDelete
usersPrefix := strings.HasPrefix(string(event.Kv.Key), iamConfigUsersPrefix)
groupsPrefix := strings.HasPrefix(string(event.Kv.Key), iamConfigGroupsPrefix)
stsPrefix := strings.HasPrefix(string(event.Kv.Key), iamConfigSTSPrefix)
policyPrefix := strings.HasPrefix(string(event.Kv.Key), iamConfigPoliciesPrefix)
policyDBUsersPrefix := strings.HasPrefix(string(event.Kv.Key), iamConfigPolicyDBUsersPrefix)
@ -398,6 +508,13 @@ func (sys *IAMSys) reloadFromEvent(event *etcd.Event) {
accessKey := path.Dir(strings.TrimPrefix(string(event.Kv.Key),
iamConfigSTSPrefix))
loadEtcdUser(ctx, accessKey, true, sys.iamUsersMap)
case groupsPrefix:
group := path.Dir(strings.TrimPrefix(string(event.Kv.Key),
iamConfigGroupsPrefix))
loadEtcdGroup(ctx, group, sys.iamGroupsMap)
gi := sys.iamGroupsMap[group]
sys.removeGroupFromMembershipsMap(group)
sys.updateGroupMembershipsMap(group, &gi)
case policyPrefix:
policyName := path.Dir(strings.TrimPrefix(string(event.Kv.Key),
iamConfigPoliciesPrefix))
@ -406,12 +523,12 @@ func (sys *IAMSys) reloadFromEvent(event *etcd.Event) {
policyMapFile := strings.TrimPrefix(string(event.Kv.Key),
iamConfigPolicyDBUsersPrefix)
user := strings.TrimSuffix(policyMapFile, ".json")
loadEtcdMappedPolicy(ctx, user, false, sys.iamUserPolicyMap)
loadEtcdMappedPolicy(ctx, user, false, false, sys.iamUserPolicyMap)
case policyDBSTSUsersPrefix:
policyMapFile := strings.TrimPrefix(string(event.Kv.Key),
iamConfigPolicyDBSTSUsersPrefix)
user := strings.TrimSuffix(policyMapFile, ".json")
loadEtcdMappedPolicy(ctx, user, true, sys.iamUserPolicyMap)
loadEtcdMappedPolicy(ctx, user, true, false, sys.iamUserPolicyMap)
}
case eventDelete:
switch {
@ -423,6 +540,12 @@ func (sys *IAMSys) reloadFromEvent(event *etcd.Event) {
accessKey := path.Dir(strings.TrimPrefix(string(event.Kv.Key),
iamConfigSTSPrefix))
delete(sys.iamUsersMap, accessKey)
case groupsPrefix:
group := path.Dir(strings.TrimPrefix(string(event.Kv.Key),
iamConfigGroupsPrefix))
sys.removeGroupFromMembershipsMap(group)
delete(sys.iamGroupsMap, group)
delete(sys.iamGroupPolicyMap, group)
case policyPrefix:
policyName := path.Dir(strings.TrimPrefix(string(event.Kv.Key),
iamConfigPoliciesPrefix))
@ -541,7 +664,7 @@ func migrateUsersConfigToV1(objAPI ObjectLayer, isSTS bool) error {
// 2. copy policy file to new location.
mp := newMappedPolicy(policyName)
path := getMappedPolicyPath(user, isSTS)
path := getMappedPolicyPath(user, isSTS, false)
if err := saveIAMConfigItem(objAPI, mp, path); err != nil {
return err
}
@ -621,7 +744,7 @@ func migrateUsersConfigEtcdToV1(isSTS bool) error {
// 2. copy policy to new loc.
mp := newMappedPolicy(policyName)
path := getMappedPolicyPath(user, isSTS)
path := getMappedPolicyPath(user, isSTS, false)
if err := saveIAMConfigItemEtcd(ctx, mp, path); err != nil {
return err
}
@ -898,7 +1021,7 @@ func (sys *IAMSys) DeleteUser(accessKey string) error {
}
var err error
mappingPath := getMappedPolicyPath(accessKey, false)
mappingPath := getMappedPolicyPath(accessKey, false, false)
idPath := getUserIdentityPath(accessKey, false)
if globalEtcdClient != nil {
// It is okay to ignore errors when deleting policy.json for the user.
@ -950,7 +1073,7 @@ func (sys *IAMSys) SetTempUser(accessKey string, cred auth.Credentials, policyNa
mp := newMappedPolicy(policyName)
mappingPath := getMappedPolicyPath(accessKey, true)
mappingPath := getMappedPolicyPath(accessKey, true, false)
var err error
if globalEtcdClient != nil {
err = saveIAMConfigItemEtcd(context.Background(), mp, mappingPath)
@ -1072,7 +1195,7 @@ func (sys *IAMSys) SetUser(accessKey string, uinfo madmin.UserInfo) error {
// Set policy if specified.
if uinfo.PolicyName != "" {
return sys.policyDBSet(objectAPI, accessKey, uinfo.PolicyName, false)
return sys.policyDBSet(objectAPI, accessKey, uinfo.PolicyName, false, false)
}
return nil
}
@ -1119,10 +1242,243 @@ func (sys *IAMSys) GetUser(accessKey string) (cred auth.Credentials, ok bool) {
return cred, ok && cred.IsValid()
}
// AddUsersToGroup - adds users to a group, creating the group if
// needed. No error if user(s) already are in the group.
func (sys *IAMSys) AddUsersToGroup(group string, members []string) error {
objectAPI := newObjectLayerFn()
if objectAPI == nil {
return errServerNotInitialized
}
if group == "" {
return errInvalidArgument
}
sys.Lock()
defer sys.Unlock()
// Validate that all members exist.
for _, member := range members {
_, ok := sys.iamUsersMap[member]
if !ok {
return errNoSuchUser
}
}
gi, ok := sys.iamGroupsMap[group]
if !ok {
// Set group as enabled by default when it doesn't
// exist.
gi = newGroupInfo(members)
} else {
mergedMembers := append(gi.Members, members...)
uniqMembers := set.CreateStringSet(mergedMembers...).ToSlice()
gi.Members = uniqMembers
}
var err error
if globalEtcdClient == nil {
err = saveIAMConfigItem(objectAPI, gi, getGroupInfoPath(group))
} else {
err = saveIAMConfigItemEtcd(context.Background(), gi, getGroupInfoPath(group))
}
if err != nil {
return err
}
sys.iamGroupsMap[group] = gi
// update user-group membership map
for _, member := range members {
gset := sys.iamUserGroupMemberships[member]
if gset == nil {
gset = set.CreateStringSet(group)
} else {
gset.Add(group)
}
sys.iamUserGroupMemberships[member] = gset
}
return nil
}
// RemoveUsersFromGroup - remove users from group. If no users are
// given, and the group is empty, deletes the group as well.
func (sys *IAMSys) RemoveUsersFromGroup(group string, members []string) error {
objectAPI := newObjectLayerFn()
if objectAPI == nil {
return errServerNotInitialized
}
if group == "" {
return errInvalidArgument
}
sys.Lock()
defer sys.Unlock()
// Validate that all members exist.
for _, member := range members {
_, ok := sys.iamUsersMap[member]
if !ok {
return errNoSuchUser
}
}
gi, ok := sys.iamGroupsMap[group]
if !ok {
return errNoSuchGroup
}
// Check if attempting to delete a non-empty group.
if len(members) == 0 && len(gi.Members) != 0 {
return errGroupNotEmpty
}
if len(members) == 0 {
// len(gi.Members) == 0 here.
// Remove the group from storage.
giPath := getGroupInfoPath(group)
giPolicyPath := getMappedPolicyPath(group, false, true)
if globalEtcdClient == nil {
err := deleteConfig(context.Background(), objectAPI, giPath)
if err != nil {
return err
}
// Remove mapped policy from storage.
err = deleteConfig(context.Background(), objectAPI,
getMappedPolicyPath(group, false, true))
if err != nil {
return err
}
} else {
err := deleteKeyEtcd(context.Background(), globalEtcdClient, giPath)
if err != nil {
return err
}
err = deleteKeyEtcd(context.Background(), globalEtcdClient, giPolicyPath)
if err != nil {
return err
}
}
// Delete from server memory
delete(sys.iamGroupsMap, group)
delete(sys.iamGroupPolicyMap, group)
return nil
}
// Only removing members.
s := set.CreateStringSet(gi.Members...)
d := set.CreateStringSet(members...)
gi.Members = s.Difference(d).ToSlice()
err := saveIAMConfigItem(objectAPI, gi, getGroupInfoPath(group))
if err != nil {
return err
}
sys.iamGroupsMap[group] = gi
// update user-group membership map
for _, member := range members {
gset := sys.iamUserGroupMemberships[member]
if gset == nil {
continue
}
gset.Remove(group)
sys.iamUserGroupMemberships[member] = gset
}
return nil
}
// SetGroupStatus - enable/disabled a group
func (sys *IAMSys) SetGroupStatus(group string, enabled bool) error {
objectAPI := newObjectLayerFn()
if objectAPI == nil {
return errServerNotInitialized
}
sys.Lock()
defer sys.Unlock()
if group == "" {
return errInvalidArgument
}
gi, ok := sys.iamGroupsMap[group]
if !ok {
return errNoSuchGroup
}
if enabled {
gi.Status = statusEnabled
} else {
gi.Status = statusDisabled
}
var err error
if globalEtcdClient == nil {
err = saveIAMConfigItem(objectAPI, gi, getGroupInfoPath(group))
} else {
err = saveIAMConfigItemEtcd(context.Background(), gi, getGroupInfoPath(group))
}
if err != nil {
return err
}
sys.iamGroupsMap[group] = gi
return nil
}
// GetGroupDescription - builds up group description
func (sys *IAMSys) GetGroupDescription(group string) (gd madmin.GroupDesc, err error) {
sys.RLock()
defer sys.RUnlock()
gi, ok := sys.iamGroupsMap[group]
if !ok {
return gd, errNoSuchGroup
}
var p []string
p, err = sys.policyDBGet(group, true)
if err != nil {
return gd, err
}
policy := ""
if len(p) > 0 {
policy = p[0]
}
return madmin.GroupDesc{
Name: group,
Status: gi.Status,
Members: gi.Members,
Policy: policy,
}, nil
}
// ListGroups - lists groups
func (sys *IAMSys) ListGroups() (r []string) {
sys.RLock()
defer sys.RUnlock()
for k := range sys.iamGroupsMap {
r = append(r, k)
}
return r
}
// PolicyDBSet - sets a policy for a user or group in the
// PolicyDB. This function applies only long-term users. For STS
// users, policy is set directly by called sys.policyDBSet().
func (sys *IAMSys) PolicyDBSet(name, policy string) error {
func (sys *IAMSys) PolicyDBSet(name, policy string, isGroup bool) error {
objectAPI := newObjectLayerFn()
if objectAPI == nil {
return errServerNotInitialized
@ -1131,12 +1487,14 @@ func (sys *IAMSys) PolicyDBSet(name, policy string) error {
sys.Lock()
defer sys.Unlock()
return sys.policyDBSet(objectAPI, name, policy, false)
// isSTS is always false when called via PolicyDBSet as policy
// is never set by an external API call for STS users.
return sys.policyDBSet(objectAPI, name, policy, false, isGroup)
}
// policyDBSet - sets a policy for user in the policy db. Assumes that
// caller has sys.Lock().
func (sys *IAMSys) policyDBSet(objectAPI ObjectLayer, name, policy string, isSTS bool) error {
func (sys *IAMSys) policyDBSet(objectAPI ObjectLayer, name, policy string, isSTS, isGroup bool) error {
if name == "" || policy == "" {
return errInvalidArgument
}
@ -1156,7 +1514,7 @@ func (sys *IAMSys) policyDBSet(objectAPI ObjectLayer, name, policy string, isSTS
return errNoSuchUser
}
var err error
mappingPath := getMappedPolicyPath(name, isSTS)
mappingPath := getMappedPolicyPath(name, isSTS, isGroup)
if globalEtcdClient != nil {
err = saveIAMConfigItemEtcd(context.Background(), mp, mappingPath)
} else {
@ -1169,27 +1527,57 @@ func (sys *IAMSys) policyDBSet(objectAPI ObjectLayer, name, policy string, isSTS
return nil
}
// PolicyDBGet - gets policy set on a user
func (sys *IAMSys) PolicyDBGet(name string) (string, error) {
// PolicyDBGet - gets policy set on a user or group. Since a user may
// be a member of multiple groups, this function returns an array of
// applicable policies (each group is mapped to at most one policy).
func (sys *IAMSys) PolicyDBGet(name string, isGroup bool) ([]string, error) {
if name == "" {
return "", errInvalidArgument
return nil, errInvalidArgument
}
objectAPI := newObjectLayerFn()
if objectAPI == nil {
return "", errServerNotInitialized
return nil, errServerNotInitialized
}
sys.RLock()
defer sys.RUnlock()
return sys.policyDBGet(name, isGroup)
}
// This call assumes that caller has the sys.RLock()
func (sys *IAMSys) policyDBGet(name string, isGroup bool) ([]string, error) {
if isGroup {
if _, ok := sys.iamGroupsMap[name]; !ok {
return nil, errNoSuchGroup
}
policy := sys.iamGroupPolicyMap[name]
// returned policy could be empty
if policy.Policy == "" {
return nil, nil
}
return []string{policy.Policy}, nil
}
if _, ok := sys.iamUsersMap[name]; !ok {
return "", errNoSuchUser
return nil, errNoSuchUser
}
result := []string{}
policy := sys.iamUserPolicyMap[name]
// returned policy could be empty
return policy.Policy, nil
if policy.Policy != "" {
result = append(result, policy.Policy)
}
for _, group := range sys.iamUserGroupMemberships[name].ToSlice() {
p, ok := sys.iamGroupPolicyMap[group]
if ok && p.Policy != "" {
result = append(result, p.Policy)
}
}
return result, nil
}
// IsAllowedSTS is meant for STS based temporary credentials,
@ -1326,11 +1714,11 @@ func etcdKvsToSetPolicyDB(prefix string, kvs []*mvccpb.KeyValue) set.StringSet {
return items
}
func loadEtcdMappedPolicy(ctx context.Context, name string, isSTS bool,
func loadEtcdMappedPolicy(ctx context.Context, name string, isSTS, isGroup bool,
m map[string]MappedPolicy) error {
var p MappedPolicy
err := loadIAMConfigItemEtcd(ctx, &p, getMappedPolicyPath(name, isSTS))
err := loadIAMConfigItemEtcd(ctx, &p, getMappedPolicyPath(name, isSTS, isGroup))
if err != nil {
return err
}
@ -1338,7 +1726,7 @@ func loadEtcdMappedPolicy(ctx context.Context, name string, isSTS bool,
return nil
}
func loadEtcdMappedPolicies(isSTS bool, m map[string]MappedPolicy) error {
func loadEtcdMappedPolicies(isSTS, isGroup bool, m map[string]MappedPolicy) error {
ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout)
defer cancel()
basePrefix := iamConfigPolicyDBUsersPrefix
@ -1354,7 +1742,7 @@ func loadEtcdMappedPolicies(isSTS bool, m map[string]MappedPolicy) error {
// Reload config and policies for all users.
for _, user := range users.ToSlice() {
if err = loadEtcdMappedPolicy(ctx, user, isSTS, m); err != nil {
if err = loadEtcdMappedPolicy(ctx, user, isSTS, isGroup, m); err != nil {
return err
}
}
@ -1371,7 +1759,7 @@ func loadEtcdUser(ctx context.Context, user string, isSTS bool, m map[string]aut
if u.Credentials.IsExpired() {
// Delete expired identity.
deleteKeyEtcd(ctx, globalEtcdClient, getUserIdentityPath(user, isSTS))
deleteKeyEtcd(ctx, globalEtcdClient, getMappedPolicyPath(user, isSTS))
deleteKeyEtcd(ctx, globalEtcdClient, getMappedPolicyPath(user, isSTS, false))
return nil
}
@ -1394,7 +1782,7 @@ func loadEtcdUsers(isSTS bool, m map[string]auth.Credentials) error {
users := etcdKvsToSet(basePrefix, r.Kvs)
// Reload config and policies for all users.
// Reload config for all users.
for _, user := range users.ToSlice() {
if err = loadEtcdUser(ctx, user, isSTS, m); err != nil {
return err
@ -1403,6 +1791,35 @@ func loadEtcdUsers(isSTS bool, m map[string]auth.Credentials) error {
return nil
}
func loadEtcdGroup(ctx context.Context, group string, m map[string]GroupInfo) error {
var gi GroupInfo
err := loadIAMConfigItemEtcd(ctx, &gi, getGroupInfoPath(group))
if err != nil {
return err
}
m[group] = gi
return nil
}
func loadEtcdGroups(m map[string]GroupInfo) error {
ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout)
defer cancel()
r, err := globalEtcdClient.Get(ctx, iamConfigGroupsPrefix, etcd.WithPrefix(), etcd.WithKeysOnly())
if err != nil {
return err
}
groups := etcdKvsToSet(iamConfigGroupsPrefix, r.Kvs)
// Reload config for all groups.
for _, group := range groups.ToSlice() {
if err = loadEtcdGroup(ctx, group, m); err != nil {
return err
}
}
return nil
}
func loadEtcdPolicy(ctx context.Context, policyName string, m map[string]iampolicy.Policy) error {
var p iampolicy.Policy
err := loadIAMConfigItemEtcd(ctx, &p, getPolicyDocPath(policyName))
@ -1451,8 +1868,10 @@ func setDefaultCannedPolicies(policies map[string]iampolicy.Policy) {
func (sys *IAMSys) refreshEtcd() error {
iamUsersMap := make(map[string]auth.Credentials)
iamGroupsMap := make(map[string]GroupInfo)
iamPolicyDocsMap := make(map[string]iampolicy.Policy)
iamUserPolicyMap := make(map[string]MappedPolicy)
iamGroupPolicyMap := make(map[string]MappedPolicy)
if err := loadEtcdPolicies(iamPolicyDocsMap); err != nil {
return err
@ -1464,11 +1883,19 @@ func (sys *IAMSys) refreshEtcd() error {
if err := loadEtcdUsers(true, iamUsersMap); err != nil {
return err
}
if err := loadEtcdMappedPolicies(false, iamUserPolicyMap); err != nil {
if err := loadEtcdGroups(iamGroupsMap); err != nil {
return err
}
if err := loadEtcdMappedPolicies(false, false, iamUserPolicyMap); err != nil {
return err
}
// load STS users policy mapping into the same map
if err := loadEtcdMappedPolicies(true, iamUserPolicyMap); err != nil {
if err := loadEtcdMappedPolicies(true, false, iamUserPolicyMap); err != nil {
return err
}
// load group policy mapping
if err := loadEtcdMappedPolicies(false, true, iamGroupPolicyMap); err != nil {
return err
}
@ -1479,8 +1906,10 @@ func (sys *IAMSys) refreshEtcd() error {
defer sys.Unlock()
sys.iamUsersMap = iamUsersMap
sys.iamGroupsMap = iamGroupsMap
sys.iamUserPolicyMap = iamUserPolicyMap
sys.iamPolicyDocsMap = iamPolicyDocsMap
sys.iamGroupPolicyMap = iamGroupPolicyMap
return nil
}
@ -1488,8 +1917,10 @@ func (sys *IAMSys) refreshEtcd() error {
// Refresh IAMSys.
func (sys *IAMSys) refresh(objAPI ObjectLayer) error {
iamUsersMap := make(map[string]auth.Credentials)
iamGroupsMap := make(map[string]GroupInfo)
iamPolicyDocsMap := make(map[string]iampolicy.Policy)
iamUserPolicyMap := make(map[string]MappedPolicy)
iamGroupPolicyMap := make(map[string]MappedPolicy)
if err := loadPolicyDocs(objAPI, iamPolicyDocsMap); err != nil {
return err
@ -1501,12 +1932,19 @@ func (sys *IAMSys) refresh(objAPI ObjectLayer) error {
if err := loadUsers(objAPI, true, iamUsersMap); err != nil {
return err
}
if err := loadGroups(objAPI, iamGroupsMap); err != nil {
return err
}
if err := loadMappedPolicies(objAPI, false, iamUserPolicyMap); err != nil {
if err := loadMappedPolicies(objAPI, false, false, iamUserPolicyMap); err != nil {
return err
}
// load STS policy mappings into the same map
if err := loadMappedPolicies(objAPI, true, iamUserPolicyMap); err != nil {
if err := loadMappedPolicies(objAPI, true, false, iamUserPolicyMap); err != nil {
return err
}
// load policies mapped to groups
if err := loadMappedPolicies(objAPI, false, false, iamGroupPolicyMap); err != nil {
return err
}
@ -1519,15 +1957,60 @@ func (sys *IAMSys) refresh(objAPI ObjectLayer) error {
sys.iamUsersMap = iamUsersMap
sys.iamPolicyDocsMap = iamPolicyDocsMap
sys.iamUserPolicyMap = iamUserPolicyMap
sys.iamGroupPolicyMap = iamGroupPolicyMap
sys.iamGroupsMap = iamGroupsMap
sys.buildUserGroupMemberships()
return nil
}
// buildUserGroupMemberships - builds the memberships map. IMPORTANT:
// Assumes that sys.Lock is held by caller.
func (sys *IAMSys) buildUserGroupMemberships() {
for group, gi := range sys.iamGroupsMap {
sys.updateGroupMembershipsMap(group, &gi)
}
}
// updateGroupMembershipsMap - updates the memberships map for a
// group. IMPORTANT: Assumes sys.Lock() is held by caller.
func (sys *IAMSys) updateGroupMembershipsMap(group string, gi *GroupInfo) {
if gi == nil {
return
}
for _, member := range gi.Members {
v := sys.iamUserGroupMemberships[member]
if v == nil {
v = set.CreateStringSet(group)
} else {
v.Add(group)
}
sys.iamUserGroupMemberships[member] = v
}
}
// removeGroupFromMembershipsMap - removes the group from every member
// in the cache. IMPORTANT: Assumes sys.Lock() is held by caller.
func (sys *IAMSys) removeGroupFromMembershipsMap(group string) {
if _, ok := sys.iamUserGroupMemberships[group]; !ok {
return
}
for member, groups := range sys.iamUserGroupMemberships {
if !groups.Contains(group) {
continue
}
groups.Remove(group)
sys.iamUserGroupMemberships[member] = groups
}
}
// NewIAMSys - creates new config system object.
func NewIAMSys() *IAMSys {
return &IAMSys{
iamUsersMap: make(map[string]auth.Credentials),
iamPolicyDocsMap: make(map[string]iampolicy.Policy),
iamUserPolicyMap: make(map[string]MappedPolicy),
iamUsersMap: make(map[string]auth.Credentials),
iamPolicyDocsMap: make(map[string]iampolicy.Policy),
iamUserPolicyMap: make(map[string]MappedPolicy),
iamGroupsMap: make(map[string]GroupInfo),
iamUserGroupMemberships: make(map[string]set.StringSet),
}
}

@ -232,6 +232,19 @@ func (sys *NotificationSys) LoadUsers() []NotificationPeerErr {
return ng.Wait()
}
// LoadGroup - loads a specific group on all peers.
func (sys *NotificationSys) LoadGroup(group string) []NotificationPeerErr {
ng := WithNPeers(len(sys.peerClients))
for idx, client := range sys.peerClients {
if client == nil {
continue
}
client := client
ng.Go(context.Background(), func() error { return client.LoadGroup(group) }, idx, *client.host)
}
return ng.Wait()
}
// BackgroundHealStatus - returns background heal status of all peers
func (sys *NotificationSys) BackgroundHealStatus() []madmin.BgHealState {
states := make([]madmin.BgHealState, len(sys.peerClients))

@ -443,6 +443,18 @@ func (client *peerRESTClient) LoadUsers() (err error) {
return nil
}
// LoadGroup - send load group command to peers.
func (client *peerRESTClient) LoadGroup(group string) error {
values := make(url.Values)
values.Set(peerRESTGroup, group)
respBody, err := client.call(peerRESTMethodLoadGroup, values, nil, -1)
if err != nil {
return err
}
defer http.DrainBody(respBody)
return nil
}
// SignalService - sends signal to peer nodes.
func (client *peerRESTClient) SignalService(sig serviceSignal) error {
values := make(url.Values)

@ -34,6 +34,7 @@ const (
peerRESTMethodLoadPolicy = "loadpolicy"
peerRESTMethodDeletePolicy = "deletepolicy"
peerRESTMethodLoadUsers = "loadusers"
peerRESTMethodLoadGroup = "loadgroup"
peerRESTMethodStartProfiling = "startprofiling"
peerRESTMethodDownloadProfilingData = "downloadprofilingdata"
peerRESTMethodBucketPolicySet = "setbucketpolicy"
@ -50,6 +51,7 @@ const (
const (
peerRESTBucket = "bucket"
peerRESTUser = "user"
peerRESTGroup = "group"
peerRESTUserTemp = "user-temp"
peerRESTPolicy = "policy"
peerRESTSignal = "signal"

@ -260,6 +260,30 @@ func (s *peerRESTServer) LoadUsersHandler(w http.ResponseWriter, r *http.Request
w.(http.Flusher).Flush()
}
// LoadGroupHandler - reloads group along with members list.
func (s *peerRESTServer) LoadGroupHandler(w http.ResponseWriter, r *http.Request) {
if !s.IsValid(w, r) {
s.writeErrorResponse(w, errors.New("Invalid request"))
return
}
objAPI := newObjectLayerFn()
if objAPI == nil {
s.writeErrorResponse(w, errServerNotInitialized)
return
}
vars := mux.Vars(r)
group := vars[peerRESTGroup]
err := globalIAMSys.LoadGroup(objAPI, group)
if err != nil {
s.writeErrorResponse(w, err)
return
}
w.(http.Flusher).Flush()
}
// StartProfilingHandler - Issues the start profiling command.
func (s *peerRESTServer) StartProfilingHandler(w http.ResponseWriter, r *http.Request) {
if !s.IsValid(w, r) {
@ -800,6 +824,7 @@ func registerPeerRESTHandlers(router *mux.Router) {
subrouter.Methods(http.MethodPost).Path("/" + peerRESTMethodDeleteUser).HandlerFunc(httpTraceAll(server.LoadUserHandler)).Queries(restQueries(peerRESTUser)...)
subrouter.Methods(http.MethodPost).Path("/" + peerRESTMethodLoadUser).HandlerFunc(httpTraceAll(server.LoadUserHandler)).Queries(restQueries(peerRESTUser, peerRESTUserTemp)...)
subrouter.Methods(http.MethodPost).Path("/" + peerRESTMethodLoadUsers).HandlerFunc(httpTraceAll(server.LoadUsersHandler))
subrouter.Methods(http.MethodPost).Path("/" + peerRESTMethodLoadGroup).HandlerFunc(httpTraceAll(server.LoadGroupHandler)).Queries(restQueries(peerRESTGroup)...)
subrouter.Methods(http.MethodPost).Path("/" + peerRESTMethodStartProfiling).HandlerFunc(httpTraceAll(server.StartProfilingHandler)).Queries(restQueries(peerRESTProfiler)...)
subrouter.Methods(http.MethodPost).Path("/" + peerRESTMethodDownloadProfilingData).HandlerFunc(httpTraceHdrs(server.DownloadProflingDataHandler))

@ -183,13 +183,18 @@ func (sts *stsAPIHandlers) AssumeRole(w http.ResponseWriter, r *http.Request) {
return
}
policyName, err := globalIAMSys.PolicyDBGet(user.AccessKey)
policies, err := globalIAMSys.PolicyDBGet(user.AccessKey, false)
if err != nil {
logger.LogIf(ctx, err)
writeSTSErrorResponse(w, stsErrCodes.ToSTSErr(ErrSTSInvalidParameterValue))
return
}
policyName := ""
if len(policies) > 0 {
policyName = policies[0]
}
// This policy is the policy associated with the user
// requesting for temporary credentials. The temporary
// credentials will inherit the same policy requirements.

@ -80,6 +80,13 @@ var errInvalidDecompressedSize = errors.New("Invalid Decompressed Size")
// error returned in IAM subsystem when user doesn't exist.
var errNoSuchUser = errors.New("Specified user does not exist")
// error returned in IAM subsystem when groups doesn't exist.
var errNoSuchGroup = errors.New("Specified group does not exist")
// error returned in IAM subsystem when a non-empty group needs to be
// deleted.
var errGroupNotEmpty = errors.New("Specified group is not empty - cannot remove it")
// error returned in IAM subsystem when policy doesn't exist.
var errNoSuchPolicy = errors.New("Specified canned policy does not exist")

@ -0,0 +1,164 @@
/*
* MinIO Cloud Storage, (C) 2018-2019 MinIO, Inc.
*
* 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 madmin
import (
"encoding/json"
"io/ioutil"
"net/http"
"net/url"
)
// GroupAddRemove is type for adding/removing members to/from a group.
type GroupAddRemove struct {
Group string `json:"group"`
Members []string `json:"members"`
IsRemove bool `json:"isRemove"`
}
// UpdateGroupMembers - adds/removes users to/from a group. Server
// creates the group as needed. Group is removed if remove request is
// made on empty group.
func (adm *AdminClient) UpdateGroupMembers(g GroupAddRemove) error {
data, err := json.Marshal(g)
if err != nil {
return err
}
reqData := requestData{
relPath: "/v1/update-group-members",
content: data,
}
// Execute PUT on /minio/admin/v1/update-group-members
resp, err := adm.executeMethod("PUT", reqData)
defer closeResponse(resp)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return httpRespToErrorResponse(resp)
}
return nil
}
// GroupDesc is a type that holds group info along with the policy
// attached to it.
type GroupDesc struct {
Name string `json:"name"`
Status string `json:"status"`
Members []string `json:"members"`
Policy string `json:"policy"`
}
// GetGroupDescription - fetches information on a group.
func (adm *AdminClient) GetGroupDescription(group string) (*GroupDesc, error) {
v := url.Values{}
v.Set("group", group)
reqData := requestData{
relPath: "/v1/group",
queryValues: v,
}
resp, err := adm.executeMethod("GET", reqData)
defer closeResponse(resp)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, httpRespToErrorResponse(resp)
}
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
gd := GroupDesc{}
if err = json.Unmarshal(data, &gd); err != nil {
return nil, err
}
return &gd, nil
}
// ListGroups - lists all groups names present on the server.
func (adm *AdminClient) ListGroups() ([]string, error) {
reqData := requestData{
relPath: "/v1/groups",
}
resp, err := adm.executeMethod("GET", reqData)
defer closeResponse(resp)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, httpRespToErrorResponse(resp)
}
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
groups := []string{}
if err = json.Unmarshal(data, &groups); err != nil {
return nil, err
}
return groups, nil
}
// GroupStatus - group status.
type GroupStatus string
// GroupStatus values.
const (
GroupEnabled GroupStatus = "enabled"
GroupDisabled GroupStatus = "disabled"
)
// SetGroupStatus - sets the status of a group.
func (adm *AdminClient) SetGroupStatus(group string, status GroupStatus) error {
v := url.Values{}
v.Set("group", group)
v.Set("status", string(status))
reqData := requestData{
relPath: "/v1/set-group-status",
queryValues: v,
}
resp, err := adm.executeMethod("PUT", reqData)
defer closeResponse(resp)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return httpRespToErrorResponse(resp)
}
return nil
}
Loading…
Cancel
Save