mirror of
https://github.com/minio/minio.git
synced 2025-04-20 02:27:50 -04:00
Add New Accesskey Info and OpenID Accesskey List API endpoints (#21097)
* Add DisplayClaim config value * Add openid accesskey list * Get readable claim properly * latest stuff * Add functionality * undo go mod changes * fixed go mod * go mod fix * All fixes * dafault ID to sub * comment * Add info stuff * funcitonal info * lint * AllConfigs * New error * Update cmd/admin-handlers-idp-openid.go --------- Co-authored-by: Harshavardhana <harsha@minio.io>
This commit is contained in:
parent
3310f740f0
commit
cacec1ab21
246
cmd/admin-handlers-idp-openid.go
Normal file
246
cmd/admin-handlers-idp-openid.go
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
// Copyright (c) 2015-2025 MinIO, Inc.
|
||||||
|
//
|
||||||
|
// This file is part of MinIO Object Storage stack
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/minio/madmin-go/v3"
|
||||||
|
"github.com/minio/minio-go/v7/pkg/set"
|
||||||
|
"github.com/minio/pkg/v3/policy"
|
||||||
|
)
|
||||||
|
|
||||||
|
const dummyRoleARN = "dummy-internal"
|
||||||
|
|
||||||
|
// ListAccessKeysOpenIDBulk - GET /minio/admin/v3/idp/openid/list-access-keys-bulk
|
||||||
|
func (a adminAPIHandlers) ListAccessKeysOpenIDBulk(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
// Get current object layer instance.
|
||||||
|
objectAPI := newObjectLayerFn()
|
||||||
|
if objectAPI == nil || globalNotificationSys == nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cred, owner, s3Err := validateAdminSignature(ctx, r, "")
|
||||||
|
if s3Err != ErrNone {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !globalIAMSys.OpenIDConfig.Enabled {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminOpenIDNotEnabled), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userList := r.Form["users"]
|
||||||
|
isAll := r.Form.Get("all") == "true"
|
||||||
|
selfOnly := !isAll && len(userList) == 0
|
||||||
|
cfgName := r.Form.Get("configName")
|
||||||
|
allConfigs := r.Form.Get("allConfigs") == "true"
|
||||||
|
if cfgName == "" && !allConfigs {
|
||||||
|
cfgName = madmin.Default
|
||||||
|
}
|
||||||
|
|
||||||
|
if isAll && len(userList) > 0 {
|
||||||
|
// This should be checked on client side, so return generic error
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty DN list and not self, list access keys for all users
|
||||||
|
if isAll {
|
||||||
|
if !globalIAMSys.IsAllowed(policy.Args{
|
||||||
|
AccountName: cred.AccessKey,
|
||||||
|
Groups: cred.Groups,
|
||||||
|
Action: policy.ListUsersAdminAction,
|
||||||
|
ConditionValues: getConditionValues(r, "", cred),
|
||||||
|
IsOwner: owner,
|
||||||
|
Claims: cred.Claims,
|
||||||
|
}) {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if len(userList) == 1 && userList[0] == cred.ParentUser {
|
||||||
|
selfOnly = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !globalIAMSys.IsAllowed(policy.Args{
|
||||||
|
AccountName: cred.AccessKey,
|
||||||
|
Groups: cred.Groups,
|
||||||
|
Action: policy.ListServiceAccountsAdminAction,
|
||||||
|
ConditionValues: getConditionValues(r, "", cred),
|
||||||
|
IsOwner: owner,
|
||||||
|
Claims: cred.Claims,
|
||||||
|
DenyOnly: selfOnly,
|
||||||
|
}) {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if selfOnly && len(userList) == 0 {
|
||||||
|
selfDN := cred.AccessKey
|
||||||
|
if cred.ParentUser != "" {
|
||||||
|
selfDN = cred.ParentUser
|
||||||
|
}
|
||||||
|
userList = append(userList, selfDN)
|
||||||
|
}
|
||||||
|
|
||||||
|
listType := r.Form.Get("listType")
|
||||||
|
var listSTSKeys, listServiceAccounts bool
|
||||||
|
switch listType {
|
||||||
|
case madmin.AccessKeyListUsersOnly:
|
||||||
|
listSTSKeys = false
|
||||||
|
listServiceAccounts = false
|
||||||
|
case madmin.AccessKeyListSTSOnly:
|
||||||
|
listSTSKeys = true
|
||||||
|
listServiceAccounts = false
|
||||||
|
case madmin.AccessKeyListSvcaccOnly:
|
||||||
|
listSTSKeys = false
|
||||||
|
listServiceAccounts = true
|
||||||
|
case madmin.AccessKeyListAll:
|
||||||
|
listSTSKeys = true
|
||||||
|
listServiceAccounts = true
|
||||||
|
default:
|
||||||
|
err := errors.New("invalid list type")
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrInvalidRequest, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s := globalServerConfig.Clone()
|
||||||
|
roleArnMap := make(map[string]string)
|
||||||
|
// Map of configs to a map of users to their access keys
|
||||||
|
cfgToUsersMap := make(map[string]map[string]madmin.OpenIDUserAccessKeys)
|
||||||
|
configs, err := globalIAMSys.OpenIDConfig.GetConfigList(s)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, config := range configs {
|
||||||
|
if !allConfigs && cfgName != config.Name {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
arn := dummyRoleARN
|
||||||
|
if config.RoleARN != "" {
|
||||||
|
arn = config.RoleARN
|
||||||
|
}
|
||||||
|
roleArnMap[arn] = config.Name
|
||||||
|
newResp := make(map[string]madmin.OpenIDUserAccessKeys)
|
||||||
|
cfgToUsersMap[config.Name] = newResp
|
||||||
|
}
|
||||||
|
if len(roleArnMap) == 0 {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminNoSuchConfigTarget), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userSet := set.CreateStringSet(userList...)
|
||||||
|
accessKeys, err := globalIAMSys.ListAllAccessKeys(ctx)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, accessKey := range accessKeys {
|
||||||
|
// Filter out any disqualifying access keys
|
||||||
|
_, ok := accessKey.Claims[subClaim]
|
||||||
|
if !ok {
|
||||||
|
continue // OpenID access keys must have a sub claim
|
||||||
|
}
|
||||||
|
if (!listSTSKeys && !accessKey.IsServiceAccount()) || (!listServiceAccounts && accessKey.IsServiceAccount()) {
|
||||||
|
continue // skip if not the type we want
|
||||||
|
}
|
||||||
|
arn, ok := accessKey.Claims[roleArnClaim].(string)
|
||||||
|
if !ok {
|
||||||
|
if _, ok := accessKey.Claims[iamPolicyClaimNameOpenID()]; !ok {
|
||||||
|
continue // skip if no roleArn and no policy claim
|
||||||
|
}
|
||||||
|
}
|
||||||
|
matchingCfgName, ok := roleArnMap[arn]
|
||||||
|
if !ok {
|
||||||
|
continue // skip if not part of the target config
|
||||||
|
}
|
||||||
|
var id string
|
||||||
|
if idClaim := globalIAMSys.OpenIDConfig.GetUserIDClaim(matchingCfgName); idClaim != "" {
|
||||||
|
id, _ = accessKey.Claims[idClaim].(string)
|
||||||
|
}
|
||||||
|
if !userSet.IsEmpty() && !userSet.Contains(accessKey.ParentUser) && !userSet.Contains(id) {
|
||||||
|
continue // skip if not in the user list
|
||||||
|
}
|
||||||
|
openIDUserAccessKeys, ok := cfgToUsersMap[matchingCfgName][accessKey.ParentUser]
|
||||||
|
|
||||||
|
// Add new user to map if not already present
|
||||||
|
if !ok {
|
||||||
|
var readableClaim string
|
||||||
|
if rc := globalIAMSys.OpenIDConfig.GetUserReadableClaim(matchingCfgName); rc != "" {
|
||||||
|
readableClaim, _ = accessKey.Claims[rc].(string)
|
||||||
|
}
|
||||||
|
openIDUserAccessKeys = madmin.OpenIDUserAccessKeys{
|
||||||
|
MinioAccessKey: accessKey.ParentUser,
|
||||||
|
ID: id,
|
||||||
|
ReadableName: readableClaim,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
svcAccInfo := madmin.ServiceAccountInfo{
|
||||||
|
AccessKey: accessKey.AccessKey,
|
||||||
|
Expiration: &accessKey.Expiration,
|
||||||
|
}
|
||||||
|
if accessKey.IsServiceAccount() {
|
||||||
|
openIDUserAccessKeys.ServiceAccounts = append(openIDUserAccessKeys.ServiceAccounts, svcAccInfo)
|
||||||
|
} else {
|
||||||
|
openIDUserAccessKeys.STSKeys = append(openIDUserAccessKeys.STSKeys, svcAccInfo)
|
||||||
|
}
|
||||||
|
cfgToUsersMap[matchingCfgName][accessKey.ParentUser] = openIDUserAccessKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert map to slice and sort
|
||||||
|
resp := make([]madmin.ListAccessKeysOpenIDResp, 0, len(cfgToUsersMap))
|
||||||
|
for cfgName, usersMap := range cfgToUsersMap {
|
||||||
|
users := make([]madmin.OpenIDUserAccessKeys, 0, len(usersMap))
|
||||||
|
for _, user := range usersMap {
|
||||||
|
users = append(users, user)
|
||||||
|
}
|
||||||
|
sort.Slice(users, func(i, j int) bool {
|
||||||
|
return users[i].MinioAccessKey < users[j].MinioAccessKey
|
||||||
|
})
|
||||||
|
resp = append(resp, madmin.ListAccessKeysOpenIDResp{
|
||||||
|
ConfigName: cfgName,
|
||||||
|
Users: users,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
sort.Slice(resp, func(i, j int) bool {
|
||||||
|
return resp[i].ConfigName < resp[j].ConfigName
|
||||||
|
})
|
||||||
|
|
||||||
|
data, err := json.Marshal(resp)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptedData, err := madmin.EncryptData(cred.SecretKey, data)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeSuccessResponseJSON(w, encryptedData)
|
||||||
|
}
|
@ -2068,6 +2068,149 @@ func (a adminAPIHandlers) RevokeTokens(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeSuccessNoContent(w)
|
writeSuccessNoContent(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InfoAccessKey - GET /minio/admin/v3/info-access-key?access-key=<access-key>
|
||||||
|
func (a adminAPIHandlers) InfoAccessKey(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
// Get current object layer instance.
|
||||||
|
objectAPI := newObjectLayerFn()
|
||||||
|
if objectAPI == nil || globalNotificationSys == nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cred, owner, s3Err := validateAdminSignature(ctx, r, "")
|
||||||
|
if s3Err != ErrNone {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
accessKey := mux.Vars(r)["accessKey"]
|
||||||
|
if accessKey == "" {
|
||||||
|
accessKey = cred.AccessKey
|
||||||
|
}
|
||||||
|
|
||||||
|
u, ok := globalIAMSys.GetUser(ctx, accessKey)
|
||||||
|
targetCred := u.Credentials
|
||||||
|
|
||||||
|
if !globalIAMSys.IsAllowed(policy.Args{
|
||||||
|
AccountName: cred.AccessKey,
|
||||||
|
Groups: cred.Groups,
|
||||||
|
Action: policy.ListServiceAccountsAdminAction,
|
||||||
|
ConditionValues: getConditionValues(r, "", cred),
|
||||||
|
IsOwner: owner,
|
||||||
|
Claims: cred.Claims,
|
||||||
|
}) {
|
||||||
|
// If requested user does not exist and requestor is not allowed to list service accounts, return access denied.
|
||||||
|
if !ok {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
requestUser := cred.AccessKey
|
||||||
|
if cred.ParentUser != "" {
|
||||||
|
requestUser = cred.ParentUser
|
||||||
|
}
|
||||||
|
|
||||||
|
if requestUser != targetCred.ParentUser {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminNoSuchAccessKey), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
sessionPolicy *policy.Policy
|
||||||
|
err error
|
||||||
|
userType string
|
||||||
|
)
|
||||||
|
switch {
|
||||||
|
case targetCred.IsTemp():
|
||||||
|
userType = "STS"
|
||||||
|
_, sessionPolicy, err = globalIAMSys.GetTemporaryAccount(ctx, accessKey)
|
||||||
|
if err == errNoSuchTempAccount {
|
||||||
|
err = errNoSuchAccessKey
|
||||||
|
}
|
||||||
|
case targetCred.IsServiceAccount():
|
||||||
|
userType = "Service Account"
|
||||||
|
_, sessionPolicy, err = globalIAMSys.GetServiceAccount(ctx, accessKey)
|
||||||
|
if err == errNoSuchServiceAccount {
|
||||||
|
err = errNoSuchAccessKey
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
err = errNoSuchAccessKey
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// if session policy is nil or empty, then it is implied policy
|
||||||
|
impliedPolicy := sessionPolicy == nil || (sessionPolicy.Version == "" && len(sessionPolicy.Statements) == 0)
|
||||||
|
|
||||||
|
var svcAccountPolicy policy.Policy
|
||||||
|
|
||||||
|
if !impliedPolicy {
|
||||||
|
svcAccountPolicy = *sessionPolicy
|
||||||
|
} else {
|
||||||
|
policiesNames, err := globalIAMSys.PolicyDBGet(targetCred.ParentUser, targetCred.Groups...)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
svcAccountPolicy = globalIAMSys.GetCombinedPolicy(policiesNames...)
|
||||||
|
}
|
||||||
|
|
||||||
|
policyJSON, err := json.MarshalIndent(svcAccountPolicy, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var expiration *time.Time
|
||||||
|
if !targetCred.Expiration.IsZero() && !targetCred.Expiration.Equal(timeSentinel) {
|
||||||
|
expiration = &targetCred.Expiration
|
||||||
|
}
|
||||||
|
|
||||||
|
userProvider := guessUserProvider(targetCred)
|
||||||
|
|
||||||
|
infoResp := madmin.InfoAccessKeyResp{
|
||||||
|
AccessKey: accessKey,
|
||||||
|
InfoServiceAccountResp: madmin.InfoServiceAccountResp{
|
||||||
|
ParentUser: targetCred.ParentUser,
|
||||||
|
Name: targetCred.Name,
|
||||||
|
Description: targetCred.Description,
|
||||||
|
AccountStatus: targetCred.Status,
|
||||||
|
ImpliedPolicy: impliedPolicy,
|
||||||
|
Policy: string(policyJSON),
|
||||||
|
Expiration: expiration,
|
||||||
|
},
|
||||||
|
|
||||||
|
UserType: userType,
|
||||||
|
UserProvider: userProvider,
|
||||||
|
}
|
||||||
|
|
||||||
|
populateProviderInfoFromClaims(targetCred.Claims, userProvider, &infoResp)
|
||||||
|
|
||||||
|
data, err := json.Marshal(infoResp)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptedData, err := madmin.EncryptData(cred.SecretKey, data)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeSuccessResponseJSON(w, encryptedData)
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
allPoliciesFile = "policies.json"
|
allPoliciesFile = "policies.json"
|
||||||
allUsersFile = "users.json"
|
allUsersFile = "users.json"
|
||||||
|
@ -246,6 +246,7 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
|
|||||||
|
|
||||||
// Access key (service account/STS) operations
|
// Access key (service account/STS) operations
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/list-access-keys-bulk").HandlerFunc(adminMiddleware(adminAPI.ListAccessKeysBulk)).Queries("listType", "{listType:.*}")
|
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/list-access-keys-bulk").HandlerFunc(adminMiddleware(adminAPI.ListAccessKeysBulk)).Queries("listType", "{listType:.*}")
|
||||||
|
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/info-access-key").HandlerFunc(adminMiddleware(adminAPI.InfoAccessKey)).Queries("accessKey", "{accessKey:.*}")
|
||||||
|
|
||||||
// Info policy IAM latest
|
// Info policy IAM latest
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/info-canned-policy").HandlerFunc(adminMiddleware(adminAPI.InfoCannedPolicy)).Queries("name", "{name:.*}")
|
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/info-canned-policy").HandlerFunc(adminMiddleware(adminAPI.InfoCannedPolicy)).Queries("name", "{name:.*}")
|
||||||
@ -312,6 +313,11 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
|
|||||||
// LDAP IAM operations
|
// LDAP IAM operations
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/idp/ldap/policy-entities").HandlerFunc(adminMiddleware(adminAPI.ListLDAPPolicyMappingEntities))
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/idp/ldap/policy-entities").HandlerFunc(adminMiddleware(adminAPI.ListLDAPPolicyMappingEntities))
|
||||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/idp/ldap/policy/{operation}").HandlerFunc(adminMiddleware(adminAPI.AttachDetachPolicyLDAP))
|
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/idp/ldap/policy/{operation}").HandlerFunc(adminMiddleware(adminAPI.AttachDetachPolicyLDAP))
|
||||||
|
|
||||||
|
// OpenID specific service accounts ops
|
||||||
|
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/idp/openid/list-access-keys-bulk").
|
||||||
|
HandlerFunc(adminMiddleware(adminAPI.ListAccessKeysOpenIDBulk)).Queries("listType", "{listType:.*}")
|
||||||
|
|
||||||
// -- END IAM APIs --
|
// -- END IAM APIs --
|
||||||
|
|
||||||
// GetBucketQuotaConfig
|
// GetBucketQuotaConfig
|
||||||
|
@ -215,6 +215,8 @@ const (
|
|||||||
ErrExcessData
|
ErrExcessData
|
||||||
ErrPolicyInvalidName
|
ErrPolicyInvalidName
|
||||||
ErrNoTokenRevokeType
|
ErrNoTokenRevokeType
|
||||||
|
ErrAdminOpenIDNotEnabled
|
||||||
|
ErrAdminNoSuchAccessKey
|
||||||
// Add new error codes here.
|
// Add new error codes here.
|
||||||
|
|
||||||
// SSE-S3/SSE-KMS related API errors
|
// SSE-S3/SSE-KMS related API errors
|
||||||
@ -568,6 +570,11 @@ var errorCodes = errorCodeMap{
|
|||||||
Description: "Policy name may not contain comma",
|
Description: "Policy name may not contain comma",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
|
ErrAdminOpenIDNotEnabled: {
|
||||||
|
Code: "OpenIDNotEnabled",
|
||||||
|
Description: "No enabled OpenID Connect identity providers",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
ErrPolicyTooLarge: {
|
ErrPolicyTooLarge: {
|
||||||
Code: "PolicyTooLarge",
|
Code: "PolicyTooLarge",
|
||||||
Description: "Policy exceeds the maximum allowed document size.",
|
Description: "Policy exceeds the maximum allowed document size.",
|
||||||
@ -1270,6 +1277,11 @@ var errorCodes = errorCodeMap{
|
|||||||
Description: "No token revoke type specified and one could not be inferred from the request",
|
Description: "No token revoke type specified and one could not be inferred from the request",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
|
ErrAdminNoSuchAccessKey: {
|
||||||
|
Code: "XMinioAdminNoSuchAccessKey",
|
||||||
|
Description: "The specified access key does not exist.",
|
||||||
|
HTTPStatusCode: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
|
||||||
// S3 extensions.
|
// S3 extensions.
|
||||||
ErrContentSHA256Mismatch: {
|
ErrContentSHA256Mismatch: {
|
||||||
@ -2167,6 +2179,8 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
|||||||
apiErr = ErrAdminNoSuchUserLDAPWarn
|
apiErr = ErrAdminNoSuchUserLDAPWarn
|
||||||
case errNoSuchServiceAccount:
|
case errNoSuchServiceAccount:
|
||||||
apiErr = ErrAdminServiceAccountNotFound
|
apiErr = ErrAdminServiceAccountNotFound
|
||||||
|
case errNoSuchAccessKey:
|
||||||
|
apiErr = ErrAdminNoSuchAccessKey
|
||||||
case errNoSuchGroup:
|
case errNoSuchGroup:
|
||||||
apiErr = ErrAdminNoSuchGroup
|
apiErr = ErrAdminNoSuchGroup
|
||||||
case errGroupNotEmpty:
|
case errGroupNotEmpty:
|
||||||
|
File diff suppressed because one or more lines are too long
@ -2777,6 +2777,31 @@ func (store *IAMStoreSys) ListSTSAccounts(ctx context.Context, accessKey string)
|
|||||||
return stsAccounts, nil
|
return stsAccounts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListAccessKeys - lists all access keys (sts/service accounts)
|
||||||
|
func (store *IAMStoreSys) ListAccessKeys(ctx context.Context) ([]auth.Credentials, error) {
|
||||||
|
cache := store.rlock()
|
||||||
|
defer store.runlock()
|
||||||
|
|
||||||
|
accessKeys := store.getSTSAndServiceAccounts(cache)
|
||||||
|
for i, accessKey := range accessKeys {
|
||||||
|
accessKeys[i].SecretKey = ""
|
||||||
|
if accessKey.IsTemp() {
|
||||||
|
secret, err := getTokenSigningKey()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
claims, err := getClaimsFromTokenWithSecret(accessKey.SessionToken, secret)
|
||||||
|
if err != nil {
|
||||||
|
continue // ignore invalid session tokens
|
||||||
|
}
|
||||||
|
accessKeys[i].Claims = claims.MapClaims
|
||||||
|
}
|
||||||
|
accessKeys[i].SessionToken = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessKeys, nil
|
||||||
|
}
|
||||||
|
|
||||||
// AddUser - adds/updates long term user account to storage.
|
// AddUser - adds/updates long term user account to storage.
|
||||||
func (store *IAMStoreSys) AddUser(ctx context.Context, accessKey string, ureq madmin.AddOrUpdateUserReq) (updatedAt time.Time, err error) {
|
func (store *IAMStoreSys) AddUser(ctx context.Context, accessKey string, ureq madmin.AddOrUpdateUserReq) (updatedAt time.Time, err error) {
|
||||||
cache := store.lock()
|
cache := store.lock()
|
||||||
@ -2839,6 +2864,10 @@ func (store *IAMStoreSys) GetSTSAndServiceAccounts() []auth.Credentials {
|
|||||||
cache := store.rlock()
|
cache := store.rlock()
|
||||||
defer store.runlock()
|
defer store.runlock()
|
||||||
|
|
||||||
|
return store.getSTSAndServiceAccounts(cache)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (store *IAMStoreSys) getSTSAndServiceAccounts(cache *iamCache) []auth.Credentials {
|
||||||
var res []auth.Credentials
|
var res []auth.Credentials
|
||||||
for _, u := range cache.iamUsersMap {
|
for _, u := range cache.iamUsersMap {
|
||||||
cred := u.Credentials
|
cred := u.Credentials
|
||||||
|
14
cmd/iam.go
14
cmd/iam.go
@ -1246,6 +1246,20 @@ func (sys *IAMSys) ListSTSAccounts(ctx context.Context, accessKey string) ([]aut
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListAllAccessKeys - lists all access keys (sts/service accounts)
|
||||||
|
func (sys *IAMSys) ListAllAccessKeys(ctx context.Context) ([]auth.Credentials, error) {
|
||||||
|
if !sys.Initialized() {
|
||||||
|
return nil, errServerNotInitialized
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-sys.configLoaded:
|
||||||
|
return sys.store.ListAccessKeys(ctx)
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GetServiceAccount - wrapper method to get information about a service account
|
// GetServiceAccount - wrapper method to get information about a service account
|
||||||
func (sys *IAMSys) GetServiceAccount(ctx context.Context, accessKey string) (auth.Credentials, *policy.Policy, error) {
|
func (sys *IAMSys) GetServiceAccount(ctx context.Context, accessKey string) (auth.Credentials, *policy.Policy, error) {
|
||||||
sa, embeddedPolicy, err := sys.getServiceAccount(ctx, accessKey)
|
sa, embeddedPolicy, err := sys.getServiceAccount(ctx, accessKey)
|
||||||
|
@ -75,6 +75,9 @@ var errNoSuchServiceAccount = errors.New("Specified service account does not exi
|
|||||||
// error returned when temporary account is not found
|
// error returned when temporary account is not found
|
||||||
var errNoSuchTempAccount = errors.New("Specified temporary account does not exist")
|
var errNoSuchTempAccount = errors.New("Specified temporary account does not exist")
|
||||||
|
|
||||||
|
// error returned when access key is not found
|
||||||
|
var errNoSuchAccessKey = errors.New("Specified access key does not exist")
|
||||||
|
|
||||||
// error returned in IAM subsystem when an account doesn't exist.
|
// error returned in IAM subsystem when an account doesn't exist.
|
||||||
var errNoSuchAccount = errors.New("Specified account does not exist")
|
var errNoSuchAccount = errors.New("Specified account does not exist")
|
||||||
|
|
||||||
|
@ -19,8 +19,10 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/minio/madmin-go/v3"
|
"github.com/minio/madmin-go/v3"
|
||||||
|
"github.com/minio/minio/internal/auth"
|
||||||
)
|
)
|
||||||
|
|
||||||
// getUserWithProvider - returns the appropriate internal username based on the user provider.
|
// getUserWithProvider - returns the appropriate internal username based on the user provider.
|
||||||
@ -55,3 +57,85 @@ func getUserWithProvider(ctx context.Context, userProvider, user string, validat
|
|||||||
return "", errIAMActionNotAllowed
|
return "", errIAMActionNotAllowed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// guessUserProvider - guesses the user provider based on the access key and claims.
|
||||||
|
func guessUserProvider(credentials auth.Credentials) string {
|
||||||
|
if !credentials.IsServiceAccount() && !credentials.IsTemp() {
|
||||||
|
return madmin.BuiltinProvider // regular users are always internal
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := credentials.Claims
|
||||||
|
if _, ok := claims[ldapUser]; ok {
|
||||||
|
return madmin.LDAPProvider // ldap users
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := claims[subClaim]; ok {
|
||||||
|
providerPrefix, _, found := strings.Cut(credentials.ParentUser, getKeySeparator())
|
||||||
|
if found {
|
||||||
|
return providerPrefix // this is true for certificate and custom providers
|
||||||
|
}
|
||||||
|
return madmin.OpenIDProvider // openid users are already hashed, so no separator
|
||||||
|
}
|
||||||
|
|
||||||
|
return madmin.BuiltinProvider // default to internal
|
||||||
|
}
|
||||||
|
|
||||||
|
// getProviderInfoFromClaims - returns the provider info from the claims.
|
||||||
|
func populateProviderInfoFromClaims(claims map[string]interface{}, provider string, resp *madmin.InfoAccessKeyResp) {
|
||||||
|
resp.UserProvider = provider
|
||||||
|
switch provider {
|
||||||
|
case madmin.LDAPProvider:
|
||||||
|
resp.LDAPSpecificInfo = getLDAPInfoFromClaims(claims)
|
||||||
|
case madmin.OpenIDProvider:
|
||||||
|
resp.OpenIDSpecificInfo = getOpenIDInfoFromClaims(claims)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOpenIDCfgNameFromClaims(claims map[string]interface{}) (string, bool) {
|
||||||
|
roleArn := claims[roleArnClaim]
|
||||||
|
|
||||||
|
s := globalServerConfig.Clone()
|
||||||
|
configs, err := globalIAMSys.OpenIDConfig.GetConfigList(s)
|
||||||
|
if err != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
for _, cfg := range configs {
|
||||||
|
if cfg.RoleARN == roleArn {
|
||||||
|
return cfg.Name, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOpenIDInfoFromClaims(claims map[string]interface{}) madmin.OpenIDSpecificAccessKeyInfo {
|
||||||
|
info := madmin.OpenIDSpecificAccessKeyInfo{}
|
||||||
|
|
||||||
|
cfgName, ok := getOpenIDCfgNameFromClaims(claims)
|
||||||
|
if !ok {
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
info.ConfigName = cfgName
|
||||||
|
if displayNameClaim := globalIAMSys.OpenIDConfig.GetUserReadableClaim(cfgName); displayNameClaim != "" {
|
||||||
|
name, _ := claims[displayNameClaim].(string)
|
||||||
|
info.DisplayName = name
|
||||||
|
info.DisplayNameClaim = displayNameClaim
|
||||||
|
}
|
||||||
|
if idClaim := globalIAMSys.OpenIDConfig.GetUserIDClaim(cfgName); idClaim != "" {
|
||||||
|
id, _ := claims[idClaim].(string)
|
||||||
|
info.UserID = id
|
||||||
|
info.UserIDClaim = idClaim
|
||||||
|
}
|
||||||
|
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLDAPInfoFromClaims(claims map[string]interface{}) madmin.LDAPSpecificAccessKeyInfo {
|
||||||
|
info := madmin.LDAPSpecificAccessKeyInfo{}
|
||||||
|
|
||||||
|
if name, ok := claims[ldapUser].(string); ok {
|
||||||
|
info.Username = name
|
||||||
|
}
|
||||||
|
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
2
go.mod
2
go.mod
@ -4,6 +4,8 @@ go 1.24.0
|
|||||||
|
|
||||||
toolchain go1.24.2
|
toolchain go1.24.2
|
||||||
|
|
||||||
|
replace github.com/minio/madmin-go/v3 => github.com/taran-p/madmin-go/v3 v3.0.55-0.20250325221636-f5498832320f
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go/storage v1.46.0
|
cloud.google.com/go/storage v1.46.0
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0
|
||||||
|
4
go.sum
4
go.sum
@ -436,8 +436,6 @@ github.com/minio/kms-go/kes v0.3.1 h1:K3sPFAvFbJx33XlCTUBnQo8JRmSZyDvT6T2/MQ2iC3
|
|||||||
github.com/minio/kms-go/kes v0.3.1/go.mod h1:Q9Ct0KUAuN9dH0hSVa0eva45Jg99cahbZpPxeqR9rOQ=
|
github.com/minio/kms-go/kes v0.3.1/go.mod h1:Q9Ct0KUAuN9dH0hSVa0eva45Jg99cahbZpPxeqR9rOQ=
|
||||||
github.com/minio/kms-go/kms v0.4.0 h1:cLPZceEp+05xHotVBaeFJrgL7JcXM4lBy6PU0idkE7I=
|
github.com/minio/kms-go/kms v0.4.0 h1:cLPZceEp+05xHotVBaeFJrgL7JcXM4lBy6PU0idkE7I=
|
||||||
github.com/minio/kms-go/kms v0.4.0/go.mod h1:q12CehiIy2qgBnDKq6Q7wmPi2PHSyRVug5DKp0HAVeE=
|
github.com/minio/kms-go/kms v0.4.0/go.mod h1:q12CehiIy2qgBnDKq6Q7wmPi2PHSyRVug5DKp0HAVeE=
|
||||||
github.com/minio/madmin-go/v3 v3.0.102 h1:bqy15D6d9uQOh/3B/sLfzRtWSJgZeuKnAI5VRDhvRQw=
|
|
||||||
github.com/minio/madmin-go/v3 v3.0.102/go.mod h1:pMLdj9OtN0CANNs5tdm6opvOlDFfj0WhbztboZAjRWE=
|
|
||||||
github.com/minio/mc v0.0.0-20250312172924-c1d5d4cbb4ca h1:Zeu+Gbsw/yoqJofAFaU3zbIVr51j9LULUrQqKFLQnGA=
|
github.com/minio/mc v0.0.0-20250312172924-c1d5d4cbb4ca h1:Zeu+Gbsw/yoqJofAFaU3zbIVr51j9LULUrQqKFLQnGA=
|
||||||
github.com/minio/mc v0.0.0-20250312172924-c1d5d4cbb4ca/go.mod h1:h5UQZ+5Qfq6XV81E4iZSgStPZ6Hy+gMuHMkLkjq4Gys=
|
github.com/minio/mc v0.0.0-20250312172924-c1d5d4cbb4ca/go.mod h1:h5UQZ+5Qfq6XV81E4iZSgStPZ6Hy+gMuHMkLkjq4Gys=
|
||||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||||
@ -612,6 +610,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
|||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/taran-p/madmin-go/v3 v3.0.55-0.20250325221636-f5498832320f h1:U8MMUkE2W8zVMMxpI6AwIf0PacwEYdBP1LR30FWNlhk=
|
||||||
|
github.com/taran-p/madmin-go/v3 v3.0.55-0.20250325221636-f5498832320f/go.mod h1:pMLdj9OtN0CANNs5tdm6opvOlDFfj0WhbztboZAjRWE=
|
||||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
|
@ -43,13 +43,15 @@ import (
|
|||||||
|
|
||||||
// OpenID keys and envs.
|
// OpenID keys and envs.
|
||||||
const (
|
const (
|
||||||
ClientID = "client_id"
|
ClientID = "client_id"
|
||||||
ClientSecret = "client_secret"
|
ClientSecret = "client_secret"
|
||||||
ConfigURL = "config_url"
|
ConfigURL = "config_url"
|
||||||
ClaimName = "claim_name"
|
ClaimName = "claim_name"
|
||||||
ClaimUserinfo = "claim_userinfo"
|
ClaimUserinfo = "claim_userinfo"
|
||||||
RolePolicy = "role_policy"
|
RolePolicy = "role_policy"
|
||||||
DisplayName = "display_name"
|
DisplayName = "display_name"
|
||||||
|
UserReadableClaim = "user_readable_claim"
|
||||||
|
UserIDClaim = "user_id_claim"
|
||||||
|
|
||||||
Scopes = "scopes"
|
Scopes = "scopes"
|
||||||
RedirectURI = "redirect_uri"
|
RedirectURI = "redirect_uri"
|
||||||
@ -130,6 +132,14 @@ var (
|
|||||||
Key: KeyCloakAdminURL,
|
Key: KeyCloakAdminURL,
|
||||||
Value: "",
|
Value: "",
|
||||||
},
|
},
|
||||||
|
config.KV{
|
||||||
|
Key: UserReadableClaim,
|
||||||
|
Value: "",
|
||||||
|
},
|
||||||
|
config.KV{
|
||||||
|
Key: UserIDClaim,
|
||||||
|
Value: "",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -628,3 +638,25 @@ func GetDefaultExpiration(dsecs string) (time.Duration, error) {
|
|||||||
|
|
||||||
return defaultExpiryDuration, nil
|
return defaultExpiryDuration, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUserReadableClaim returns the human readable claim name for the given
|
||||||
|
// configuration name.
|
||||||
|
func (r Config) GetUserReadableClaim(cfgName string) string {
|
||||||
|
pCfg, ok := r.ProviderCfgs[cfgName]
|
||||||
|
if ok {
|
||||||
|
return pCfg.UserReadableClaim
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserIDClaim returns the user ID claim for the given configuration name, or "sub" if not set.
|
||||||
|
func (r Config) GetUserIDClaim(cfgName string) string {
|
||||||
|
pCfg, ok := r.ProviderCfgs[cfgName]
|
||||||
|
if ok {
|
||||||
|
if pCfg.UserIDClaim != "" {
|
||||||
|
return pCfg.UserIDClaim
|
||||||
|
}
|
||||||
|
return "sub"
|
||||||
|
}
|
||||||
|
return "" // an incorrect config should be handled outside this function
|
||||||
|
}
|
||||||
|
@ -48,6 +48,8 @@ type providerCfg struct {
|
|||||||
ClientID string
|
ClientID string
|
||||||
ClientSecret string
|
ClientSecret string
|
||||||
RolePolicy string
|
RolePolicy string
|
||||||
|
UserReadableClaim string
|
||||||
|
UserIDClaim string
|
||||||
|
|
||||||
roleArn arn.ARN
|
roleArn arn.ARN
|
||||||
provider provider.Provider
|
provider provider.Provider
|
||||||
@ -64,6 +66,8 @@ func newProviderCfgFromConfig(getCfgVal func(cfgName string) string) providerCfg
|
|||||||
ClientID: getCfgVal(ClientID),
|
ClientID: getCfgVal(ClientID),
|
||||||
ClientSecret: getCfgVal(ClientSecret),
|
ClientSecret: getCfgVal(ClientSecret),
|
||||||
RolePolicy: getCfgVal(RolePolicy),
|
RolePolicy: getCfgVal(RolePolicy),
|
||||||
|
UserReadableClaim: getCfgVal(UserReadableClaim),
|
||||||
|
UserIDClaim: getCfgVal(UserIDClaim),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user