Add IAM API to attach/detach policies for LDAP (#16182)
This commit is contained in:
committed by
GitHub
parent
dfe73629a3
commit
e06127566d
@@ -19,8 +19,10 @@ package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
iampolicy "github.com/minio/pkg/iam/policy"
|
||||
@@ -86,3 +88,94 @@ func (a adminAPIHandlers) ListLDAPPolicyMappingEntities(w http.ResponseWriter, r
|
||||
}
|
||||
writeSuccessResponseJSON(w, econfigData)
|
||||
}
|
||||
|
||||
// AttachDetachPolicyLDAP attaches or detaches policies from an LDAP entity
|
||||
// (user or group).
|
||||
//
|
||||
// POST <admin-prefix>/idp/ldap/policy/{operation}
|
||||
func (a adminAPIHandlers) AttachDetachPolicyLDAP(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "AttachDetachPolicyLDAP")
|
||||
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
// Check authorization.
|
||||
|
||||
objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.UpdatePolicyAssociationAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 {
|
||||
// More than maxConfigSize bytes were available
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigTooLarge), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure body content type is opaque to ensure that request body has not
|
||||
// been interpreted as form data.
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
if contentType != "application/octet-stream" {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrBadRequest), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate operation
|
||||
operation := mux.Vars(r)["operation"]
|
||||
if operation != "attach" && operation != "detach" {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminInvalidArgument), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
isAttach := operation == "attach"
|
||||
|
||||
// Validate API arguments in body.
|
||||
password := cred.SecretKey
|
||||
reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err, logger.Application)
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
var par madmin.PolicyAssociationReq
|
||||
err = json.Unmarshal(reqBytes, &par)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if err := par.IsValid(); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Call IAM subsystem
|
||||
updatedAt, addedOrRemoved, err := globalIAMSys.PolicyDBUpdateLDAP(ctx, isAttach, par)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
respBody := madmin.PolicyAssociationResp{
|
||||
UpdatedAt: updatedAt,
|
||||
}
|
||||
if isAttach {
|
||||
respBody.PoliciesAttached = addedOrRemoved
|
||||
} else {
|
||||
respBody.PoliciesDetached = addedOrRemoved
|
||||
}
|
||||
|
||||
data, err := json.Marshal(respBody)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
encryptedData, err := madmin.EncryptData(password, data)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessResponseJSON(w, encryptedData)
|
||||
}
|
||||
|
||||
+1
-1
@@ -192,7 +192,7 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
|
||||
|
||||
// LDAP IAM operations
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/idp/ldap/policy-entities").HandlerFunc(gz(httpTraceHdrs(adminAPI.ListLDAPPolicyMappingEntities)))
|
||||
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/idp/ldap/policy/{operation}").HandlerFunc(gz(httpTraceHdrs(adminAPI.AttachDetachPolicyLDAP)))
|
||||
// -- END IAM APIs --
|
||||
|
||||
// GetBucketQuotaConfig
|
||||
|
||||
@@ -265,6 +265,7 @@ const (
|
||||
ErrAdminGroupNotEmpty
|
||||
ErrAdminNoSuchJob
|
||||
ErrAdminNoSuchPolicy
|
||||
ErrAdminPolicyChangeAlreadyApplied
|
||||
ErrAdminInvalidArgument
|
||||
ErrAdminInvalidAccessKey
|
||||
ErrAdminInvalidSecretKey
|
||||
@@ -1245,6 +1246,12 @@ var errorCodes = errorCodeMap{
|
||||
Description: "The canned policy does not exist.",
|
||||
HTTPStatusCode: http.StatusNotFound,
|
||||
},
|
||||
ErrAdminPolicyChangeAlreadyApplied: {
|
||||
Code: "XMinioAdminPolicyChangeAlreadyApplied",
|
||||
Description: "The specified policy change is already in effect.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
|
||||
ErrAdminInvalidArgument: {
|
||||
Code: "XMinioAdminInvalidArgument",
|
||||
Description: "Invalid arguments specified.",
|
||||
@@ -1966,6 +1973,8 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
||||
apiErr = ErrAdminNoSuchJob
|
||||
case errNoSuchPolicy:
|
||||
apiErr = ErrAdminNoSuchPolicy
|
||||
case errNoPolicyToAttachOrDetach:
|
||||
apiErr = ErrAdminPolicyChangeAlreadyApplied
|
||||
case errSignatureMismatch:
|
||||
apiErr = ErrSignatureDoesNotMatch
|
||||
case errInvalidRange:
|
||||
|
||||
+132
-131
File diff suppressed because one or more lines are too long
@@ -862,6 +862,78 @@ func (store *IAMStoreSys) ListGroups(ctx context.Context) (res []string, err err
|
||||
return
|
||||
}
|
||||
|
||||
// PolicyDBUpdate - adds or removes given policies to/from the user or group's
|
||||
// policy associations.
|
||||
func (store *IAMStoreSys) PolicyDBUpdate(ctx context.Context, name string, isGroup bool,
|
||||
userType IAMUserType, policies []string, isAttach bool) (updatedAt time.Time, addedOrRemoved []string,
|
||||
err error,
|
||||
) {
|
||||
if name == "" {
|
||||
return updatedAt, nil, errInvalidArgument
|
||||
}
|
||||
|
||||
cache := store.lock()
|
||||
defer store.unlock()
|
||||
|
||||
// Load existing policy mapping
|
||||
var mp MappedPolicy
|
||||
if !isGroup {
|
||||
mp = cache.iamUserPolicyMap[name]
|
||||
} else {
|
||||
if store.getUsersSysType() == MinIOUsersSysType {
|
||||
g, ok := cache.iamGroupsMap[name]
|
||||
if !ok {
|
||||
return updatedAt, nil, errNoSuchGroup
|
||||
}
|
||||
|
||||
if g.Status == statusDisabled {
|
||||
// TODO: return an error?
|
||||
return updatedAt, nil, nil
|
||||
}
|
||||
}
|
||||
mp = cache.iamGroupPolicyMap[name]
|
||||
}
|
||||
|
||||
// Compute net policy change effect and updated policy mapping
|
||||
existingPolicySet := mp.policySet()
|
||||
policiesToUpdate := set.CreateStringSet(policies...)
|
||||
newPolicyMapping := mp
|
||||
if isAttach {
|
||||
// new policies to attach => inputPolicies - existing (set difference)
|
||||
policiesToUpdate = policiesToUpdate.Difference(existingPolicySet)
|
||||
// validate that new policies to add are defined.
|
||||
for _, p := range policiesToUpdate.ToSlice() {
|
||||
if _, found := cache.iamPolicyDocsMap[p]; !found {
|
||||
return updatedAt, nil, errNoSuchPolicy
|
||||
}
|
||||
}
|
||||
newPolicyMapping.Policies = strings.Join(existingPolicySet.Union(policiesToUpdate).ToSlice(), ",")
|
||||
} else {
|
||||
// policies to detach => inputPolicies ∩ existing (intersection)
|
||||
policiesToUpdate = policiesToUpdate.Intersection(existingPolicySet)
|
||||
newPolicyMapping.Policies = strings.Join(existingPolicySet.Difference(policiesToUpdate).ToSlice(), ",")
|
||||
}
|
||||
newPolicyMapping.UpdatedAt = UTCNow()
|
||||
|
||||
// We return an error if the requested policy update will have no effect.
|
||||
if policiesToUpdate.IsEmpty() {
|
||||
return updatedAt, nil, errNoPolicyToAttachOrDetach
|
||||
}
|
||||
|
||||
addedOrRemoved = policiesToUpdate.ToSlice()
|
||||
|
||||
if err := store.saveMappedPolicy(ctx, name, userType, isGroup, newPolicyMapping); err != nil {
|
||||
return updatedAt, addedOrRemoved, err
|
||||
}
|
||||
if !isGroup {
|
||||
cache.iamUserPolicyMap[name] = newPolicyMapping
|
||||
} else {
|
||||
cache.iamGroupPolicyMap[name] = newPolicyMapping
|
||||
}
|
||||
cache.updatedAt = UTCNow()
|
||||
return cache.updatedAt, addedOrRemoved, nil
|
||||
}
|
||||
|
||||
// PolicyDBSet - update the policy mapping for the given user or group in
|
||||
// storage and in cache. We do not check for the existence of the user here
|
||||
// since users can be virtual, such as for:
|
||||
|
||||
+52
@@ -1497,6 +1497,58 @@ func (sys *IAMSys) PolicyDBSet(ctx context.Context, name, policy string, userTyp
|
||||
return updatedAt, nil
|
||||
}
|
||||
|
||||
// PolicyDBUpdateLDAP - adds or removes policies from a user or a group verified
|
||||
// to be in the LDAP directory.
|
||||
func (sys *IAMSys) PolicyDBUpdateLDAP(ctx context.Context, isAttach bool,
|
||||
r madmin.PolicyAssociationReq,
|
||||
) (updatedAt time.Time, addedOrRemoved []string, err error) {
|
||||
if !sys.Initialized() {
|
||||
return updatedAt, nil, errServerNotInitialized
|
||||
}
|
||||
|
||||
var dn string
|
||||
var isGroup bool
|
||||
if r.User != "" {
|
||||
dn, err = globalLDAPConfig.DoesUsernameExist(r.User)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return updatedAt, nil, err
|
||||
}
|
||||
if dn == "" {
|
||||
return updatedAt, nil, errNoSuchUser
|
||||
}
|
||||
isGroup = false
|
||||
} else {
|
||||
if exists, err := globalLDAPConfig.DoesGroupDNExist(r.Group); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return updatedAt, nil, err
|
||||
} else if !exists {
|
||||
return updatedAt, nil, errNoSuchGroup
|
||||
}
|
||||
dn = r.Group
|
||||
isGroup = true
|
||||
}
|
||||
|
||||
userType := stsUser
|
||||
updatedAt, addedOrRemoved, err = sys.store.PolicyDBUpdate(ctx, dn, isGroup,
|
||||
userType, r.Policies, isAttach)
|
||||
if err != nil {
|
||||
return updatedAt, nil, err
|
||||
}
|
||||
|
||||
// Notify all other MinIO peers to reload policy
|
||||
if !sys.HasWatcher() {
|
||||
for _, nerr := range globalNotificationSys.LoadPolicyMapping(dn, userType, isGroup) {
|
||||
if nerr.Err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
|
||||
logger.LogIf(ctx, nerr.Err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updatedAt, addedOrRemoved, nil
|
||||
}
|
||||
|
||||
// PolicyDBGet - gets policy set on a user or group. If a list of groups is
|
||||
// given, policies associated with them are included as well.
|
||||
func (sys *IAMSys) PolicyDBGet(name string, isGroup bool, groups ...string) ([]string, error) {
|
||||
|
||||
@@ -80,6 +80,10 @@ var errNoSuchAccount = errors.New("Specified account 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 policy attach/detach request has no
|
||||
// net effect, i.e. it is already applied.
|
||||
var errNoPolicyToAttachOrDetach = errors.New("Specified policy update has no net effect")
|
||||
|
||||
// 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")
|
||||
|
||||
Reference in New Issue
Block a user