Skip to content

Commit

Permalink
Set the policy mapping for a user or group (minio#8036)
Browse files Browse the repository at this point in the history
Add API to set policy mapping for a user or group

Contains a breaking Admin APIs change.

- Also enforce all applicable policies
- Removes the previous /set-user-policy API

 Bump up peerRESTVersion

Add get user info API to show groups of a user
  • Loading branch information
donatello authored and kannappanr committed Aug 13, 2019
1 parent bc79b43 commit bf9b619
Show file tree
Hide file tree
Showing 10 changed files with 301 additions and 78 deletions.
51 changes: 37 additions & 14 deletions cmd/admin-handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -1020,6 +1020,33 @@ func (a adminAPIHandlers) ListUsers(w http.ResponseWriter, r *http.Request) {
writeSuccessResponseJSON(w, econfigData)
}

// GetUserInfo - GET /minio/admin/v1/user-info
func (a adminAPIHandlers) GetUserInfo(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "GetUserInfo")

objectAPI := validateAdminReq(ctx, w, r)
if objectAPI == nil {
return
}

vars := mux.Vars(r)
name := vars["accessKey"]

userInfo, err := globalIAMSys.GetUserInfo(name)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}

data, err := json.Marshal(userInfo)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}

writeSuccessResponseJSON(w, data)
}

// UpdateGroupMembers - PUT /minio/admin/v1/update-group-members
func (a adminAPIHandlers) UpdateGroupMembers(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "UpdateGroupMembers")
Expand Down Expand Up @@ -1353,37 +1380,33 @@ func (a adminAPIHandlers) AddCannedPolicy(w http.ResponseWriter, r *http.Request
}
}

// SetUserPolicy - PUT /minio/admin/v1/set-user-policy?accessKey=<access_key>&name=<policy_name>
func (a adminAPIHandlers) SetUserPolicy(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "SetUserPolicy")
// SetPolicyForUserOrGroup - PUT /minio/admin/v1/set-policy?policy=xxx&user-or-group=?[&is-group]
func (a adminAPIHandlers) SetPolicyForUserOrGroup(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "SetPolicyForUserOrGroup")

objectAPI := validateAdminReq(ctx, w, r)
if objectAPI == nil {
return
}

vars := mux.Vars(r)
accessKey := vars["accessKey"]
policyName := vars["name"]
policyName := vars["policyName"]
entityName := vars["userOrGroup"]
isGroup := vars["isGroup"] == "true"

// Deny if WORM is enabled
if globalWORMEnabled {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
return
}

// Custom IAM policies not allowed for admin user.
if accessKey == globalServerConfig.GetCredential().AccessKey {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
return
}

if err := globalIAMSys.PolicyDBSet(accessKey, policyName, false); err != nil {
if err := globalIAMSys.PolicyDBSet(entityName, policyName, isGroup); err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}

// Notify all other Minio peers to reload user
for _, nerr := range globalNotificationSys.LoadUser(accessKey, false) {
// Notify all other MinIO peers to reload policy
for _, nerr := range globalNotificationSys.LoadPolicyMapping(entityName, isGroup) {
if nerr.Err != nil {
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
logger.LogIf(ctx, nerr.Err)
Expand Down
10 changes: 8 additions & 2 deletions cmd/admin-router.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,20 +96,26 @@ func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool)

// Add user IAM
adminV1Router.Methods(http.MethodPut).Path("/add-user").HandlerFunc(httpTraceHdrs(adminAPI.AddUser)).Queries("accessKey", "{accessKey:.*}")
adminV1Router.Methods(http.MethodPut).Path("/set-user-policy").HandlerFunc(httpTraceHdrs(adminAPI.SetUserPolicy)).
Queries("accessKey", "{accessKey:.*}").Queries("name", "{name:.*}")
adminV1Router.Methods(http.MethodPut).Path("/set-user-status").HandlerFunc(httpTraceHdrs(adminAPI.SetUserStatus)).
Queries("accessKey", "{accessKey:.*}").Queries("status", "{status:.*}")

// Remove policy IAM
adminV1Router.Methods(http.MethodDelete).Path("/remove-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.RemoveCannedPolicy)).Queries("name", "{name:.*}")

// Set user or group policy
adminV1Router.Methods(http.MethodPut).Path("/set-user-or-group-policy").
HandlerFunc(httpTraceHdrs(adminAPI.SetPolicyForUserOrGroup)).
Queries("policyName", "{policyName:.*}", "userOrGroup", "{userOrGroup:.*}", "isGroup", "{isGroup:true|false}")

// Remove user IAM
adminV1Router.Methods(http.MethodDelete).Path("/remove-user").HandlerFunc(httpTraceHdrs(adminAPI.RemoveUser)).Queries("accessKey", "{accessKey:.*}")

// List users
adminV1Router.Methods(http.MethodGet).Path("/list-users").HandlerFunc(httpTraceHdrs(adminAPI.ListUsers))

// User info
adminV1Router.Methods(http.MethodGet).Path("/user-info").HandlerFunc(httpTraceHdrs(adminAPI.GetUserInfo)).Queries("accessKey", "{accessKey:.*}")

// Add/Remove members from group
adminV1Router.Methods(http.MethodPut).Path("/update-group-members").HandlerFunc(httpTraceHdrs(adminAPI.UpdateGroupMembers))

Expand Down
11 changes: 11 additions & 0 deletions cmd/iam-etcd-store.go
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,7 @@ func (ies *IAMEtcdStore) reloadFromEvent(sys *IAMSys, event *etcd.Event) {
policyPrefix := strings.HasPrefix(string(event.Kv.Key), iamConfigPoliciesPrefix)
policyDBUsersPrefix := strings.HasPrefix(string(event.Kv.Key), iamConfigPolicyDBUsersPrefix)
policyDBSTSUsersPrefix := strings.HasPrefix(string(event.Kv.Key), iamConfigPolicyDBSTSUsersPrefix)
policyDBGroupsPrefix := strings.HasPrefix(string(event.Kv.Key), iamConfigPolicyDBGroupsPrefix)

switch {
case eventCreate:
Expand Down Expand Up @@ -563,6 +564,11 @@ func (ies *IAMEtcdStore) reloadFromEvent(sys *IAMSys, event *etcd.Event) {
iamConfigPolicyDBSTSUsersPrefix)
user := strings.TrimSuffix(policyMapFile, ".json")
ies.loadMappedPolicy(user, true, false, sys.iamUserPolicyMap)
case policyDBGroupsPrefix:
policyMapFile := strings.TrimPrefix(string(event.Kv.Key),
iamConfigPolicyDBGroupsPrefix)
user := strings.TrimSuffix(policyMapFile, ".json")
ies.loadMappedPolicy(user, false, true, sys.iamGroupPolicyMap)
}
case eventDelete:
switch {
Expand Down Expand Up @@ -594,6 +600,11 @@ func (ies *IAMEtcdStore) reloadFromEvent(sys *IAMSys, event *etcd.Event) {
iamConfigPolicyDBSTSUsersPrefix)
user := strings.TrimSuffix(policyMapFile, ".json")
delete(sys.iamUserPolicyMap, user)
case policyDBGroupsPrefix:
policyMapFile := strings.TrimPrefix(string(event.Kv.Key),
iamConfigPolicyDBGroupsPrefix)
user := strings.TrimSuffix(policyMapFile, ".json")
delete(sys.iamGroupPolicyMap, user)
}
}
}
130 changes: 105 additions & 25 deletions cmd/iam.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,33 @@ func (sys *IAMSys) LoadPolicy(objAPI ObjectLayer, policyName string) error {
return nil
}

// LoadPolicyMapping - loads the mapped policy for a user or group
// from storage into server memory.
func (sys *IAMSys) LoadPolicyMapping(objAPI ObjectLayer, userOrGroup string, isGroup bool) error {
if objAPI == nil {
return errInvalidArgument
}

sys.Lock()
defer sys.Unlock()

if globalEtcdClient == nil {
var err error
if isGroup {
err = sys.store.loadMappedPolicy(userOrGroup, false, isGroup, sys.iamGroupPolicyMap)
} else {
err = sys.store.loadMappedPolicy(userOrGroup, false, isGroup, sys.iamUserPolicyMap)
}

// Ignore policy not mapped error
if err != nil && err != errConfigNotFound {
return err
}
}
// When etcd is set, we use watch APIs so this code is not needed.
return nil
}

// LoadUser - reloads a specific user from backend disks or etcd.
func (sys *IAMSys) LoadUser(objAPI ObjectLayer, accessKey string, isSTS bool) error {
if objAPI == nil {
Expand Down Expand Up @@ -516,6 +543,29 @@ func (sys *IAMSys) ListUsers() (map[string]madmin.UserInfo, error) {
return users, nil
}

// GetUserInfo - get info on a user.
func (sys *IAMSys) GetUserInfo(name string) (u madmin.UserInfo, err error) {
objectAPI := newObjectLayerFn()
if objectAPI == nil {
return u, errServerNotInitialized
}

sys.RLock()
defer sys.RUnlock()

creds, found := sys.iamUsersMap[name]
if !found {
return u, errNoSuchUser
}

u = madmin.UserInfo{
PolicyName: sys.iamUserPolicyMap[name].Policy,
Status: madmin.AccountStatus(creds.Status),
MemberOf: sys.iamUserGroupMemberships[name].ToSlice(),
}
return u, nil
}

// SetUserStatus - sets current user status, supports disabled or enabled.
func (sys *IAMSys) SetUserStatus(accessKey string, status madmin.AccountStatus) error {
objectAPI := newObjectLayerFn()
Expand Down Expand Up @@ -776,6 +826,16 @@ func (sys *IAMSys) SetGroupStatus(group string, enabled bool) error {

// GetGroupDescription - builds up group description
func (sys *IAMSys) GetGroupDescription(group string) (gd madmin.GroupDesc, err error) {
ps, err := sys.PolicyDBGet(group, true)
if err != nil {
return gd, err
}
// A group may be mapped to at most one policy.
policy := ""
if len(ps) > 0 {
policy = ps[0]
}

sys.RLock()
defer sys.RUnlock()

Expand All @@ -784,17 +844,6 @@ func (sys *IAMSys) GetGroupDescription(group string) (gd madmin.GroupDesc, err e
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,
Expand Down Expand Up @@ -837,21 +886,28 @@ func (sys *IAMSys) policyDBSet(objectAPI ObjectLayer, name, policy string, isSTS
if name == "" || policy == "" {
return errInvalidArgument
}
if _, ok := sys.iamUsersMap[name]; !ok {
return errNoSuchUser
}
if _, ok := sys.iamPolicyDocsMap[policy]; !ok {
return errNoSuchPolicy
}
if _, ok := sys.iamUsersMap[name]; !ok {
return errNoSuchUser
if !isGroup {
if _, ok := sys.iamUsersMap[name]; !ok {
return errNoSuchUser
}
} else {
if _, ok := sys.iamGroupsMap[name]; !ok {
return errNoSuchGroup
}
}

mp := newMappedPolicy(policy)
if err := sys.store.saveMappedPolicy(name, isSTS, isGroup, mp); err != nil {
return err
}
sys.iamUserPolicyMap[name] = mp
if !isGroup {
sys.iamUserPolicyMap[name] = mp
} else {
sys.iamGroupPolicyMap[name] = mp
}
return nil
}

Expand Down Expand Up @@ -991,18 +1047,42 @@ func (sys *IAMSys) IsAllowed(args iampolicy.Args) bool {
return sys.IsAllowedSTS(args)
}

// Policies don't apply to the owner.
if args.IsOwner {
return true
}

policies, err := sys.PolicyDBGet(args.AccountName, false)
if err != nil {
logger.LogIf(context.Background(), err)
return false
}

if len(policies) == 0 {
// No policy found.
return false
}

// Policies were found, evaluate all of them.
sys.RLock()
defer sys.RUnlock()

// If policy is available for given user, check the policy.
if mp, found := sys.iamUserPolicyMap[args.AccountName]; found {
p, ok := sys.iamPolicyDocsMap[mp.Policy]
return ok && p.IsAllowed(args)
var availablePolicies []iampolicy.Policy
for _, pname := range policies {
p, found := sys.iamPolicyDocsMap[pname]
if found {
availablePolicies = append(availablePolicies, p)
}
}

// As policy is not available and OPA is not configured,
// return the owner value.
return args.IsOwner
if len(availablePolicies) == 0 {
return false
}
combinedPolicy := availablePolicies[0]
for i := 1; i < len(availablePolicies); i++ {
combinedPolicy.Statements = append(combinedPolicy.Statements,
availablePolicies[i].Statements...)
}
return combinedPolicy.IsAllowed(args)
}

// Set default canned policies only if not already overridden by users.
Expand Down
15 changes: 15 additions & 0 deletions cmd/notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,21 @@ func (sys *NotificationSys) LoadPolicy(policyName string) []NotificationPeerErr
return ng.Wait()
}

// LoadPolicyMapping - reloads a policy mapping across all peers
func (sys *NotificationSys) LoadPolicyMapping(userOrGroup string, isGroup bool) []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.LoadPolicyMapping(userOrGroup, isGroup)
}, idx, *client.host)
}
return ng.Wait()
}

// DeleteUser - deletes a specific user across all peers
func (sys *NotificationSys) DeleteUser(accessKey string) []NotificationPeerErr {
ng := WithNPeers(len(sys.peerClients))
Expand Down
16 changes: 16 additions & 0 deletions cmd/peer-rest-client.go
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,22 @@ func (client *peerRESTClient) LoadPolicy(policyName string) (err error) {
return nil
}

// LoadPolicyMapping - reload a specific policy mapping
func (client *peerRESTClient) LoadPolicyMapping(userOrGroup string, isGroup bool) error {
values := make(url.Values)
values.Set(peerRESTUserOrGroup, userOrGroup)
if isGroup {
values.Set(peerRESTIsGroup, "")
}

respBody, err := client.call(peerRESTMethodLoadPolicyMapping, values, nil, -1)
if err != nil {
return err
}
defer http.DrainBody(respBody)
return nil
}

// DeleteUser - delete a specific user.
func (client *peerRESTClient) DeleteUser(accessKey string) (err error) {
values := make(url.Values)
Expand Down
Loading

0 comments on commit bf9b619

Please sign in to comment.