github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/admin-handlers-idp-ldap.go (about) 1 // Copyright (c) 2015-2022 MinIO, Inc. 2 // 3 // This file is part of MinIO Object Storage stack 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package cmd 19 20 import ( 21 "encoding/json" 22 "errors" 23 "fmt" 24 "io" 25 "net/http" 26 "strings" 27 28 "github.com/minio/madmin-go/v3" 29 "github.com/minio/minio/internal/auth" 30 "github.com/minio/minio/internal/logger" 31 "github.com/minio/mux" 32 "github.com/minio/pkg/v2/policy" 33 ) 34 35 // ListLDAPPolicyMappingEntities lists users/groups mapped to given/all policies. 36 // 37 // GET <admin-prefix>/idp/ldap/policy-entities?[query-params] 38 // 39 // Query params: 40 // 41 // user=... -> repeatable query parameter, specifying users to query for 42 // policy mapping 43 // 44 // group=... -> repeatable query parameter, specifying groups to query for 45 // policy mapping 46 // 47 // policy=... -> repeatable query parameter, specifying policy to query for 48 // user/group mapping 49 // 50 // When all query parameters are omitted, returns mappings for all policies. 51 func (a adminAPIHandlers) ListLDAPPolicyMappingEntities(w http.ResponseWriter, r *http.Request) { 52 ctx := r.Context() 53 54 // Check authorization. 55 56 objectAPI, cred := validateAdminReq(ctx, w, r, 57 policy.ListGroupsAdminAction, policy.ListUsersAdminAction, policy.ListUserPoliciesAdminAction) 58 if objectAPI == nil { 59 return 60 } 61 62 // Validate API arguments. 63 64 q := madmin.PolicyEntitiesQuery{ 65 Users: r.Form["user"], 66 Groups: r.Form["group"], 67 Policy: r.Form["policy"], 68 } 69 70 // Query IAM 71 72 res, err := globalIAMSys.QueryLDAPPolicyEntities(r.Context(), q) 73 if err != nil { 74 writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) 75 return 76 } 77 78 // Encode result and send response. 79 80 data, err := json.Marshal(res) 81 if err != nil { 82 writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) 83 return 84 } 85 password := cred.SecretKey 86 econfigData, err := madmin.EncryptData(password, data) 87 if err != nil { 88 writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) 89 return 90 } 91 writeSuccessResponseJSON(w, econfigData) 92 } 93 94 // AttachDetachPolicyLDAP attaches or detaches policies from an LDAP entity 95 // (user or group). 96 // 97 // POST <admin-prefix>/idp/ldap/policy/{operation} 98 func (a adminAPIHandlers) AttachDetachPolicyLDAP(w http.ResponseWriter, r *http.Request) { 99 ctx := r.Context() 100 101 // Check authorization. 102 103 objectAPI, cred := validateAdminReq(ctx, w, r, policy.UpdatePolicyAssociationAction) 104 if objectAPI == nil { 105 return 106 } 107 108 if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 { 109 // More than maxConfigSize bytes were available 110 writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigTooLarge), r.URL) 111 return 112 } 113 114 // Ensure body content type is opaque to ensure that request body has not 115 // been interpreted as form data. 116 contentType := r.Header.Get("Content-Type") 117 if contentType != "application/octet-stream" { 118 writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrBadRequest), r.URL) 119 return 120 } 121 122 // Validate operation 123 operation := mux.Vars(r)["operation"] 124 if operation != "attach" && operation != "detach" { 125 writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminInvalidArgument), r.URL) 126 return 127 } 128 129 isAttach := operation == "attach" 130 131 // Validate API arguments in body. 132 password := cred.SecretKey 133 reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength)) 134 if err != nil { 135 logger.LogIf(ctx, err, logger.ErrorKind) 136 writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL) 137 return 138 } 139 140 var par madmin.PolicyAssociationReq 141 err = json.Unmarshal(reqBytes, &par) 142 if err != nil { 143 writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL) 144 return 145 } 146 147 if err := par.IsValid(); err != nil { 148 writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL) 149 return 150 } 151 152 // Call IAM subsystem 153 updatedAt, addedOrRemoved, _, err := globalIAMSys.PolicyDBUpdateLDAP(ctx, isAttach, par) 154 if err != nil { 155 writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) 156 return 157 } 158 159 respBody := madmin.PolicyAssociationResp{ 160 UpdatedAt: updatedAt, 161 } 162 if isAttach { 163 respBody.PoliciesAttached = addedOrRemoved 164 } else { 165 respBody.PoliciesDetached = addedOrRemoved 166 } 167 168 data, err := json.Marshal(respBody) 169 if err != nil { 170 writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) 171 return 172 } 173 174 encryptedData, err := madmin.EncryptData(password, data) 175 if err != nil { 176 writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) 177 return 178 } 179 180 writeSuccessResponseJSON(w, encryptedData) 181 } 182 183 // AddServiceAccountLDAP adds a new service account for provided LDAP username or DN 184 // 185 // PUT /minio/admin/v3/idp/ldap/add-service-account 186 func (a adminAPIHandlers) AddServiceAccountLDAP(w http.ResponseWriter, r *http.Request) { 187 ctx, cred, opts, createReq, targetUser, APIError := commonAddServiceAccount(r) 188 if APIError.Code != "" { 189 writeErrorResponseJSON(ctx, w, APIError, r.URL) 190 return 191 } 192 193 // fail if ldap is not enabled 194 if !globalIAMSys.LDAPConfig.Enabled() { 195 writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errors.New("LDAP not enabled")), r.URL) 196 return 197 } 198 199 // Find the user for the request sender (as it may be sent via a service 200 // account or STS account): 201 requestorUser := cred.AccessKey 202 requestorParentUser := cred.AccessKey 203 requestorGroups := cred.Groups 204 requestorIsDerivedCredential := false 205 if cred.IsServiceAccount() || cred.IsTemp() { 206 requestorParentUser = cred.ParentUser 207 requestorIsDerivedCredential = true 208 } 209 210 // Check if we are creating svc account for request sender. 211 isSvcAccForRequestor := false 212 if targetUser == requestorUser || targetUser == requestorParentUser { 213 isSvcAccForRequestor = true 214 } 215 216 var ( 217 targetGroups []string 218 err error 219 ) 220 221 // If we are creating svc account for request sender, ensure 222 // that targetUser is a real user (i.e. not derived 223 // credentials). 224 if isSvcAccForRequestor { 225 if requestorIsDerivedCredential { 226 if requestorParentUser == "" { 227 writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, 228 errors.New("service accounts cannot be generated for temporary credentials without parent")), r.URL) 229 return 230 } 231 targetUser = requestorParentUser 232 } 233 targetGroups = requestorGroups 234 235 // Deny if the target user is not LDAP 236 foundLDAPDN, err := globalIAMSys.LDAPConfig.GetValidatedDNForUsername(targetUser) 237 if err != nil { 238 writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) 239 return 240 } 241 if foundLDAPDN == "" { 242 err := errors.New("Specified user does not exist on LDAP server") 243 APIErr := errorCodes.ToAPIErrWithErr(ErrAdminNoSuchUser, err) 244 writeErrorResponseJSON(ctx, w, APIErr, r.URL) 245 return 246 } 247 248 // In case of LDAP/OIDC we need to set `opts.claims` to ensure 249 // it is associated with the LDAP/OIDC user properly. 250 for k, v := range cred.Claims { 251 if k == expClaim { 252 continue 253 } 254 opts.claims[k] = v 255 } 256 } else { 257 isDN := globalIAMSys.LDAPConfig.IsLDAPUserDN(targetUser) 258 259 opts.claims[ldapUserN] = targetUser // simple username 260 targetUser, targetGroups, err = globalIAMSys.LDAPConfig.LookupUserDN(targetUser) 261 if err != nil { 262 // if not found, check if DN 263 if strings.Contains(err.Error(), "not found") && isDN { 264 // warn user that DNs are not allowed 265 err = fmt.Errorf("Must use short username to add service account. %w", err) 266 } 267 writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) 268 return 269 } 270 opts.claims[ldapUser] = targetUser // DN 271 } 272 273 newCred, updatedAt, err := globalIAMSys.NewServiceAccount(ctx, targetUser, targetGroups, opts) 274 if err != nil { 275 writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) 276 return 277 } 278 279 createResp := madmin.AddServiceAccountResp{ 280 Credentials: madmin.Credentials{ 281 AccessKey: newCred.AccessKey, 282 SecretKey: newCred.SecretKey, 283 Expiration: newCred.Expiration, 284 }, 285 } 286 287 data, err := json.Marshal(createResp) 288 if err != nil { 289 writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) 290 return 291 } 292 293 encryptedData, err := madmin.EncryptData(cred.SecretKey, data) 294 if err != nil { 295 writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) 296 return 297 } 298 299 writeSuccessResponseJSON(w, encryptedData) 300 301 // Call hook for cluster-replication if the service account is not for a 302 // root user. 303 if newCred.ParentUser != globalActiveCred.AccessKey { 304 logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{ 305 Type: madmin.SRIAMItemSvcAcc, 306 SvcAccChange: &madmin.SRSvcAccChange{ 307 Create: &madmin.SRSvcAccCreate{ 308 Parent: newCred.ParentUser, 309 AccessKey: newCred.AccessKey, 310 SecretKey: newCred.SecretKey, 311 Groups: newCred.Groups, 312 Name: newCred.Name, 313 Description: newCred.Description, 314 Claims: opts.claims, 315 SessionPolicy: createReq.Policy, 316 Status: auth.AccountOn, 317 Expiration: createReq.Expiration, 318 }, 319 }, 320 UpdatedAt: updatedAt, 321 })) 322 } 323 } 324 325 // ListAccessKeysLDAP - GET /minio/admin/v3/idp/ldap/list-access-keys 326 func (a adminAPIHandlers) ListAccessKeysLDAP(w http.ResponseWriter, r *http.Request) { 327 ctx := r.Context() 328 329 // Get current object layer instance. 330 objectAPI := newObjectLayerFn() 331 if objectAPI == nil || globalNotificationSys == nil { 332 writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL) 333 return 334 } 335 336 cred, owner, s3Err := validateAdminSignature(ctx, r, "") 337 if s3Err != ErrNone { 338 writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL) 339 return 340 } 341 342 userDN := r.Form.Get("userDN") 343 344 // If listing is requested for a specific user (who is not the request 345 // sender), check that the user has permissions. 346 if userDN != "" && userDN != cred.ParentUser { 347 if !globalIAMSys.IsAllowed(policy.Args{ 348 AccountName: cred.AccessKey, 349 Groups: cred.Groups, 350 Action: policy.ListServiceAccountsAdminAction, 351 ConditionValues: getConditionValues(r, "", cred), 352 IsOwner: owner, 353 Claims: cred.Claims, 354 }) { 355 writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL) 356 return 357 } 358 } else { 359 if !globalIAMSys.IsAllowed(policy.Args{ 360 AccountName: cred.AccessKey, 361 Groups: cred.Groups, 362 Action: policy.ListServiceAccountsAdminAction, 363 ConditionValues: getConditionValues(r, "", cred), 364 IsOwner: owner, 365 Claims: cred.Claims, 366 DenyOnly: true, 367 }) { 368 writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL) 369 return 370 } 371 userDN = cred.AccessKey 372 if cred.ParentUser != "" { 373 userDN = cred.ParentUser 374 } 375 } 376 377 targetAccount, err := globalIAMSys.LDAPConfig.GetValidatedDNForUsername(userDN) 378 if err != nil { 379 writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) 380 return 381 } else if userDN == "" { 382 writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errNoSuchUser), r.URL) 383 return 384 } 385 386 listType := r.Form.Get("listType") 387 if listType != "sts-only" && listType != "svcacc-only" && listType != "" { 388 // default to both 389 listType = "" 390 } 391 392 var serviceAccounts []auth.Credentials 393 var stsKeys []auth.Credentials 394 395 if listType == "" || listType == "sts-only" { 396 stsKeys, err = globalIAMSys.ListSTSAccounts(ctx, targetAccount) 397 if err != nil { 398 writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) 399 return 400 } 401 } 402 if listType == "" || listType == "svcacc-only" { 403 serviceAccounts, err = globalIAMSys.ListServiceAccounts(ctx, targetAccount) 404 if err != nil { 405 writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) 406 return 407 } 408 } 409 410 var serviceAccountList []madmin.ServiceAccountInfo 411 var stsKeyList []madmin.ServiceAccountInfo 412 413 for _, svc := range serviceAccounts { 414 expiryTime := svc.Expiration 415 serviceAccountList = append(serviceAccountList, madmin.ServiceAccountInfo{ 416 AccessKey: svc.AccessKey, 417 Expiration: &expiryTime, 418 }) 419 } 420 for _, sts := range stsKeys { 421 expiryTime := sts.Expiration 422 stsKeyList = append(stsKeyList, madmin.ServiceAccountInfo{ 423 AccessKey: sts.AccessKey, 424 Expiration: &expiryTime, 425 }) 426 } 427 428 listResp := madmin.ListAccessKeysLDAPResp{ 429 ServiceAccounts: serviceAccountList, 430 STSKeys: stsKeyList, 431 } 432 433 data, err := json.Marshal(listResp) 434 if err != nil { 435 writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) 436 return 437 } 438 439 encryptedData, err := madmin.EncryptData(cred.SecretKey, data) 440 if err != nil { 441 writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) 442 return 443 } 444 445 writeSuccessResponseJSON(w, encryptedData) 446 }