feat: introduce listUsers, listPolicies for any bucket (#12372)

Bonus change LDAP settings such as user, group mappings
are now listed as part of `mc admin user list` and
`mc admin group list`

Additionally this PR also deprecates the `/v2` API
that is no longer in use.
This commit is contained in:
Harshavardhana 2021-05-27 10:15:02 -07:00 committed by GitHub
parent b5ebfd35b4
commit be541dba8a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 163 additions and 72 deletions

View File

@ -94,6 +94,42 @@ func (a adminAPIHandlers) RemoveUser(w http.ResponseWriter, r *http.Request) {
}
}
// ListUsers - GET /minio/admin/v3/list-users?bucket={bucket}
func (a adminAPIHandlers) ListBucketUsers(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "ListBucketUsers")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, cred := validateAdminUsersReq(ctx, w, r, iampolicy.ListUsersAdminAction)
if objectAPI == nil {
return
}
bucket := mux.Vars(r)["bucket"]
password := cred.SecretKey
allCredentials, err := globalIAMSys.ListBucketUsers(bucket)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
data, err := json.Marshal(allCredentials)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
econfigData, err := madmin.EncryptData(password, data)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
writeSuccessResponseJSON(w, econfigData)
}
// ListUsers - GET /minio/admin/v3/list-users
func (a adminAPIHandlers) ListUsers(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "ListUsers")
@ -1062,33 +1098,6 @@ func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Requ
writeSuccessResponseJSON(w, usageInfoJSON)
}
// InfoCannedPolicyV2 - GET /minio/admin/v2/info-canned-policy?name={policyName}
func (a adminAPIHandlers) InfoCannedPolicyV2(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "InfoCannedPolicyV2")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.GetPolicyAdminAction)
if objectAPI == nil {
return
}
policy, err := globalIAMSys.InfoPolicy(mux.Vars(r)["name"])
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
data, err := json.Marshal(policy)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
w.Write(data)
w.(http.Flusher).Flush()
}
// InfoCannedPolicy - GET /minio/admin/v3/info-canned-policy?name={policyName}
func (a adminAPIHandlers) InfoCannedPolicy(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "InfoCannedPolicy")
@ -1113,9 +1122,9 @@ func (a adminAPIHandlers) InfoCannedPolicy(w http.ResponseWriter, r *http.Reques
w.(http.Flusher).Flush()
}
// ListCannedPoliciesV2 - GET /minio/admin/v2/list-canned-policies
func (a adminAPIHandlers) ListCannedPoliciesV2(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "ListCannedPoliciesV2")
// ListBucketPolicies - GET /minio/admin/v3/list-canned-policies?bucket={bucket}
func (a adminAPIHandlers) ListBucketPolicies(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "ListBucketPolicies")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
@ -1124,27 +1133,29 @@ func (a adminAPIHandlers) ListCannedPoliciesV2(w http.ResponseWriter, r *http.Re
return
}
policies, err := globalIAMSys.ListPolicies()
bucket := mux.Vars(r)["bucket"]
policies, err := globalIAMSys.ListPolicies(bucket)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
policyMap := make(map[string][]byte, len(policies))
for k, p := range policies {
var err error
policyMap[k], err = json.Marshal(p)
var newPolicies = make(map[string]iampolicy.Policy)
for name, p := range policies {
_, err = json.Marshal(p)
if err != nil {
logger.LogIf(ctx, err)
continue
}
newPolicies[name] = p
}
if err = json.NewEncoder(w).Encode(policyMap); err != nil {
if err = json.NewEncoder(w).Encode(newPolicies); err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
w.(http.Flusher).Flush()
}
// ListCannedPolicies - GET /minio/admin/v3/list-canned-policies
@ -1158,7 +1169,7 @@ func (a adminAPIHandlers) ListCannedPolicies(w http.ResponseWriter, r *http.Requ
return
}
policies, err := globalIAMSys.ListPolicies()
policies, err := globalIAMSys.ListPolicies("")
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return

View File

@ -25,11 +25,9 @@ import (
)
const (
adminPathPrefix = minioReservedBucketPath + "/admin"
adminAPIVersionV2 = madmin.AdminAPIVersionV2
adminAPIVersion = madmin.AdminAPIVersion
adminAPIVersionPrefix = SlashSeparator + adminAPIVersion
adminAPIVersionV2Prefix = SlashSeparator + adminAPIVersionV2
adminPathPrefix = minioReservedBucketPath + "/admin"
adminAPIVersion = madmin.AdminAPIVersion
adminAPIVersionPrefix = SlashSeparator + adminAPIVersion
)
// adminAPIHandlers provides HTTP handlers for MinIO admin API.
@ -46,7 +44,6 @@ func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool)
adminVersions := []string{
adminAPIVersionPrefix,
adminAPIVersionV2Prefix,
}
for _, adminVersion := range adminVersions {
@ -127,19 +124,11 @@ func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool)
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/list-service-accounts").HandlerFunc(httpTraceHdrs(adminAPI.ListServiceAccounts))
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/delete-service-account").HandlerFunc(httpTraceHdrs(adminAPI.DeleteServiceAccount)).Queries("accessKey", "{accessKey:.*}")
if adminVersion == adminAPIVersionV2Prefix {
// Info policy IAM v2
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/info-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.InfoCannedPolicyV2)).Queries("name", "{name:.*}")
// List policies v2
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/list-canned-policies").HandlerFunc(httpTraceHdrs(adminAPI.ListCannedPoliciesV2))
} else {
// Info policy IAM latest
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/info-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.InfoCannedPolicy)).Queries("name", "{name:.*}")
// List policies latest
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/list-canned-policies").HandlerFunc(httpTraceHdrs(adminAPI.ListCannedPolicies))
}
// Info policy IAM latest
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/info-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.InfoCannedPolicy)).Queries("name", "{name:.*}")
// List policies latest
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/list-canned-policies").HandlerFunc(httpTraceHdrs(adminAPI.ListBucketPolicies)).Queries("bucket", "{bucket:.*}")
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/list-canned-policies").HandlerFunc(httpTraceHdrs(adminAPI.ListCannedPolicies))
// Remove policy IAM
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/remove-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.RemoveCannedPolicy)).Queries("name", "{name:.*}")
@ -153,11 +142,11 @@ func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool)
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/remove-user").HandlerFunc(httpTraceHdrs(adminAPI.RemoveUser)).Queries("accessKey", "{accessKey:.*}")
// List users
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/list-users").HandlerFunc(httpTraceHdrs(adminAPI.ListBucketUsers)).Queries("bucket", "{bucket:.*}")
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/list-users").HandlerFunc(httpTraceHdrs(adminAPI.ListUsers))
// User info
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/user-info").HandlerFunc(httpTraceHdrs(adminAPI.GetUserInfo)).Queries("accessKey", "{accessKey:.*}")
// Add/Remove members from group
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/update-group-members").HandlerFunc(httpTraceHdrs(adminAPI.UpdateGroupMembers))

View File

@ -747,7 +747,7 @@ func (sys *IAMSys) InfoPolicy(policyName string) (iampolicy.Policy, error) {
}
// ListPolicies - lists all canned policies.
func (sys *IAMSys) ListPolicies() (map[string]iampolicy.Policy, error) {
func (sys *IAMSys) ListPolicies(bucketName string) (map[string]iampolicy.Policy, error) {
if !sys.Initialized() {
return nil, errServerNotInitialized
}
@ -759,7 +759,11 @@ func (sys *IAMSys) ListPolicies() (map[string]iampolicy.Policy, error) {
policyDocsMap := make(map[string]iampolicy.Policy, len(sys.iamPolicyDocsMap))
for k, v := range sys.iamPolicyDocsMap {
policyDocsMap[k] = v
if bucketName != "" && v.MatchResource(bucketName) {
policyDocsMap[k] = v
} else {
policyDocsMap[k] = v
}
}
return policyDocsMap, nil
@ -921,16 +925,60 @@ func (sys *IAMSys) SetTempUser(accessKey string, cred auth.Credentials, policyNa
return nil
}
// ListBucketUsers - list all users who can access this 'bucket'
func (sys *IAMSys) ListBucketUsers(bucket string) (map[string]madmin.UserInfo, error) {
if bucket == "" {
return nil, errInvalidArgument
}
sys.store.rlock()
defer sys.store.runlock()
var users = make(map[string]madmin.UserInfo)
for k, v := range sys.iamUsersMap {
if v.IsTemp() || v.IsServiceAccount() {
continue
}
var policies []string
mp, ok := sys.iamUserPolicyMap[k]
if ok {
policies = append(policies, mp.toSlice()...)
for _, group := range sys.iamUserGroupMemberships[k].ToSlice() {
if nmp, ok := sys.iamGroupPolicyMap[group]; ok {
policies = append(policies, nmp.toSlice()...)
}
}
}
var matchesPolices []string
for _, p := range policies {
if sys.iamPolicyDocsMap[p].MatchResource(bucket) {
matchesPolices = append(matchesPolices, p)
}
}
if len(matchesPolices) > 0 {
users[k] = madmin.UserInfo{
PolicyName: strings.Join(matchesPolices, ","),
Status: func() madmin.AccountStatus {
if v.IsValid() {
return madmin.AccountEnabled
}
return madmin.AccountDisabled
}(),
MemberOf: sys.iamUserGroupMemberships[k].ToSlice(),
}
}
}
return users, nil
}
// ListUsers - list all users.
func (sys *IAMSys) ListUsers() (map[string]madmin.UserInfo, error) {
if !sys.Initialized() {
return nil, errServerNotInitialized
}
if sys.usersSysType != MinIOUsersSysType {
return nil, errIAMActionNotAllowed
}
<-sys.configLoaded
sys.store.rlock()
@ -948,6 +996,16 @@ func (sys *IAMSys) ListUsers() (map[string]madmin.UserInfo, error) {
}
return madmin.AccountDisabled
}(),
MemberOf: sys.iamUserGroupMemberships[k].ToSlice(),
}
}
}
if sys.usersSysType == LDAPUsersSysType {
for k, v := range sys.iamUserPolicyMap {
users[k] = madmin.UserInfo{
PolicyName: v.Policies,
Status: madmin.AccountEnabled,
}
}
}
@ -1013,15 +1071,21 @@ func (sys *IAMSys) GetUserInfo(name string) (u madmin.UserInfo, err error) {
sys.store.rlock()
// If the user has a mapped policy or is a member of a group, we
// return that info. Otherwise we return error.
mappedPolicy, ok1 := sys.iamUserPolicyMap[name]
memberships, ok2 := sys.iamUserGroupMemberships[name]
var groups []string
for _, v := range sys.iamUsersMap {
if v.ParentUser == name {
groups = v.Groups
break
}
}
mappedPolicy, ok := sys.iamUserPolicyMap[name]
sys.store.runlock()
if !ok1 && !ok2 {
if !ok {
return u, errNoSuchUser
}
return madmin.UserInfo{
PolicyName: mappedPolicy.Policies,
MemberOf: memberships.ToSlice(),
MemberOf: groups,
}, nil
}
@ -1741,10 +1805,6 @@ func (sys *IAMSys) ListGroups() (r []string, err error) {
return r, errServerNotInitialized
}
if sys.usersSysType != MinIOUsersSysType {
return nil, errIAMActionNotAllowed
}
<-sys.configLoaded
sys.store.rlock()
@ -1755,6 +1815,12 @@ func (sys *IAMSys) ListGroups() (r []string, err error) {
r = append(r, k)
}
if sys.usersSysType == LDAPUsersSysType {
for k := range sys.iamGroupPolicyMap {
r = append(r, k)
}
}
return r, nil
}

View File

@ -97,6 +97,16 @@ type Policy struct {
Statements []Statement `json:"Statement"`
}
// MatchResource matches resource with match resource patterns
func (iamp Policy) MatchResource(resource string) bool {
for _, statement := range iamp.Statements {
if statement.Resources.MatchResource(resource) {
return true
}
}
return false
}
// IsAllowed - checks given policy args is allowed to continue the Rest API.
func (iamp Policy) IsAllowed(args Args) bool {
// Check all deny statements. If any one statement denies, return false.

View File

@ -48,7 +48,12 @@ func (r Resource) IsValid() bool {
return r.Pattern != ""
}
// Match - matches object name with resource pattern.
// MatchResource matches object name with resource pattern only.
func (r Resource) MatchResource(resource string) bool {
return r.Match(resource, nil)
}
// Match - matches object name with resource pattern, including specific conditionals.
func (r Resource) Match(resource string, conditionValues map[string][]string) bool {
pattern := r.Pattern
for _, key := range condition.CommonKeys {

View File

@ -99,6 +99,16 @@ func (resourceSet ResourceSet) MarshalJSON() ([]byte, error) {
return json.Marshal(resources)
}
// MatchResource matches object name with resource patterns only.
func (resourceSet ResourceSet) MatchResource(resource string) bool {
for r := range resourceSet {
if r.MatchResource(resource) {
return true
}
}
return false
}
// Match - matches object name with anyone of resource pattern in resource set.
func (resourceSet ResourceSet) Match(resource string, conditionValues map[string][]string) bool {
for r := range resourceSet {