ldap: Create services accounts for LDAP and STS temp accounts (#11808)

This commit is contained in:
Anis Elleuch 2021-04-15 06:51:14 +01:00 committed by GitHub
parent b70c298c27
commit 291d2793ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 731 additions and 178 deletions

View File

@ -471,18 +471,12 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
return
}
cred, _, owner, s3Err := validateAdminSignature(ctx, r, "")
cred, claims, owner, s3Err := validateAdminSignature(ctx, r, "")
if s3Err != ErrNone {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
return
}
// Disallow creating service accounts by root user.
if owner {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminAccountNotEligible), r.URL)
return
}
password := cred.SecretKey
reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
if err != nil {
@ -496,12 +490,55 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
return
}
parentUser := cred.AccessKey
if cred.ParentUser != "" {
parentUser = cred.ParentUser
// Disallow creating service accounts by root user.
if createReq.TargetUser == globalActiveCred.AccessKey {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminAccountNotEligible), r.URL)
return
}
newCred, err := globalIAMSys.NewServiceAccount(ctx, parentUser, cred.Groups, createReq.Policy)
var (
targetUser string
targetGroups []string
)
targetUser = createReq.TargetUser
// Need permission if we are creating a service acccount
// for a user <> to the request sender
if targetUser != "" && targetUser != cred.AccessKey {
if !globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: cred.AccessKey,
Action: iampolicy.CreateServiceAccountAdminAction,
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
IsOwner: owner,
Claims: claims,
}) {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
return
}
}
if globalLDAPConfig.Enabled && targetUser != "" {
// If LDAP enabled, service accounts need
// to be created only for LDAP users.
var err error
_, targetGroups, err = globalLDAPConfig.LookupUserDN(targetUser)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
} else {
if targetUser == "" {
targetUser = cred.AccessKey
}
if cred.ParentUser != "" {
targetUser = cred.ParentUser
}
targetGroups = cred.Groups
}
opts := newServiceAccountOpts{sessionPolicy: createReq.Policy, accessKey: createReq.AccessKey, secretKey: createReq.SecretKey}
newCred, err := globalIAMSys.NewServiceAccount(ctx, targetUser, targetGroups, opts)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
@ -537,6 +574,191 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
writeSuccessResponseJSON(w, encryptedData)
}
// UpdateServiceAccount - POST /minio/admin/v3/update-service-account
func (a adminAPIHandlers) UpdateServiceAccount(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "UpdateServiceAccount")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
// Get current object layer instance.
objectAPI := newObjectLayerFn()
if objectAPI == nil || globalNotificationSys == nil {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
return
}
cred, claims, owner, s3Err := validateAdminSignature(ctx, r, "")
if s3Err != ErrNone {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
return
}
accessKey := mux.Vars(r)["accessKey"]
if accessKey == "" {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
return
}
// Disallow editing service accounts by root user.
if owner {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminAccountNotEligible), r.URL)
return
}
svcAccount, _, err := globalIAMSys.GetServiceAccount(ctx, accessKey)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
if !globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: cred.AccessKey,
Action: iampolicy.UpdateServiceAccountAdminAction,
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
IsOwner: owner,
Claims: claims,
}) {
requestUser := cred.AccessKey
if cred.ParentUser != "" {
requestUser = cred.ParentUser
}
if requestUser != svcAccount.ParentUser {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
return
}
}
password := cred.SecretKey
reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
if err != nil {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
return
}
var updateReq madmin.UpdateServiceAccountReq
if err = json.Unmarshal(reqBytes, &updateReq); err != nil {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
return
}
opts := updateServiceAccountOpts{sessionPolicy: updateReq.NewPolicy, secretKey: updateReq.NewSecretKey, status: updateReq.NewStatus}
err = globalIAMSys.UpdateServiceAccount(ctx, accessKey, opts)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
// Notify all other Minio peers to reload user the service account
for _, nerr := range globalNotificationSys.LoadServiceAccount(accessKey) {
if nerr.Err != nil {
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
logger.LogIf(ctx, nerr.Err)
}
}
writeSuccessNoContent(w)
}
// InfoServiceAccount - GET /minio/admin/v3/info-service-account
func (a adminAPIHandlers) InfoServiceAccount(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "InfoServiceAccount")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
// Get current object layer instance.
objectAPI := newObjectLayerFn()
if objectAPI == nil || globalNotificationSys == nil {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
return
}
cred, claims, owner, s3Err := validateAdminSignature(ctx, r, "")
if s3Err != ErrNone {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
return
}
// Disallow creating service accounts by root user.
if owner {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminAccountNotEligible), r.URL)
return
}
accessKey := mux.Vars(r)["accessKey"]
if accessKey == "" {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
return
}
svcAccount, policy, err := globalIAMSys.GetServiceAccount(ctx, accessKey)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
if !globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: cred.AccessKey,
Action: iampolicy.ListServiceAccountsAdminAction,
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
IsOwner: owner,
Claims: claims,
}) {
requestUser := cred.AccessKey
if cred.ParentUser != "" {
requestUser = cred.ParentUser
}
if requestUser != svcAccount.ParentUser {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
return
}
}
var svcAccountPolicy iampolicy.Policy
impliedPolicy := policy == nil
// If policy is empty, check for policy of the parent user
if !impliedPolicy {
svcAccountPolicy.Merge(*policy)
} else {
policiesNames, err := globalIAMSys.PolicyDBGet(svcAccount.AccessKey, false)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
svcAccountPolicy.Merge(globalIAMSys.GetCombinedPolicy(policiesNames...))
}
policyJSON, err := json.Marshal(svcAccountPolicy)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
var infoResp = madmin.InfoServiceAccountResp{
ParentUser: svcAccount.ParentUser,
AccountStatus: svcAccount.Status,
ImpliedPolicy: impliedPolicy,
Policy: string(policyJSON),
}
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)
}
// ListServiceAccounts - GET /minio/admin/v3/list-service-accounts
func (a adminAPIHandlers) ListServiceAccounts(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "ListServiceAccounts")
@ -550,7 +772,7 @@ func (a adminAPIHandlers) ListServiceAccounts(w http.ResponseWriter, r *http.Req
return
}
cred, _, owner, s3Err := validateAdminSignature(ctx, r, "")
cred, claims, owner, s3Err := validateAdminSignature(ctx, r, "")
if s3Err != ErrNone {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
return
@ -562,19 +784,42 @@ func (a adminAPIHandlers) ListServiceAccounts(w http.ResponseWriter, r *http.Req
return
}
parentUser := cred.AccessKey
if cred.ParentUser != "" {
parentUser = cred.ParentUser
var targetAccount string
user := r.URL.Query().Get("user")
if user != "" {
if !globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: cred.AccessKey,
Action: iampolicy.ListServiceAccountsAdminAction,
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
IsOwner: owner,
Claims: claims,
}) {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
return
}
targetAccount = user
} else {
targetAccount = cred.AccessKey
if cred.ParentUser != "" {
targetAccount = cred.ParentUser
}
}
serviceAccounts, err := globalIAMSys.ListServiceAccounts(ctx, parentUser)
serviceAccounts, err := globalIAMSys.ListServiceAccounts(ctx, targetAccount)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
var serviceAccountsNames []string
for _, svc := range serviceAccounts {
serviceAccountsNames = append(serviceAccountsNames, svc.AccessKey)
}
var listResp = madmin.ListServiceAccountsResp{
Accounts: serviceAccounts,
Accounts: serviceAccountsNames,
}
data, err := json.Marshal(listResp)
@ -605,7 +850,7 @@ func (a adminAPIHandlers) DeleteServiceAccount(w http.ResponseWriter, r *http.Re
return
}
cred, _, owner, s3Err := validateAdminSignature(ctx, r, "")
cred, claims, owner, s3Err := validateAdminSignature(ctx, r, "")
if s3Err != ErrNone {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
return
@ -623,23 +868,32 @@ func (a adminAPIHandlers) DeleteServiceAccount(w http.ResponseWriter, r *http.Re
return
}
user, err := globalIAMSys.GetServiceAccountParent(ctx, serviceAccount)
svcAccount, _, err := globalIAMSys.GetServiceAccount(ctx, serviceAccount)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
parentUser := cred.AccessKey
if cred.ParentUser != "" {
parentUser = cred.ParentUser
}
adminPrivilege := globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: cred.AccessKey,
Action: iampolicy.RemoveServiceAccountAdminAction,
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
IsOwner: owner,
Claims: claims,
})
if parentUser != user || user == "" {
// The service account belongs to another user but return not
// found error to mitigate brute force attacks. or the
// serviceAccount doesn't exist.
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServiceAccountNotFound), r.URL)
return
if !adminPrivilege {
parentUser := cred.AccessKey
if cred.ParentUser != "" {
parentUser = cred.ParentUser
}
if parentUser != svcAccount.ParentUser {
// The service account belongs to another user but return not
// found error to mitigate brute force attacks. or the
// serviceAccount doesn't exist.
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminServiceAccountNotFound), r.URL)
return
}
}
err = globalIAMSys.DeleteServiceAccount(ctx, serviceAccount)

View File

@ -121,6 +121,8 @@ func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool)
// Service accounts ops
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/add-service-account").HandlerFunc(httpTraceHdrs(adminAPI.AddServiceAccount))
adminRouter.Methods(http.MethodPost).Path(adminVersion+"/update-service-account").HandlerFunc(httpTraceHdrs(adminAPI.UpdateServiceAccount)).Queries("accessKey", "{accessKey:.*}")
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/info-service-account").HandlerFunc(httpTraceHdrs(adminAPI.InfoServiceAccount)).Queries("accessKey", "{accessKey:.*}")
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:.*}")

View File

@ -367,7 +367,7 @@ const (
ErrAddUserInvalidArgument
ErrAdminAccountNotEligible
ErrAccountNotEligible
ErrServiceAccountNotFound
ErrAdminServiceAccountNotFound
ErrPostPolicyConditionInvalidFormat
)
@ -1754,7 +1754,7 @@ var errorCodes = errorCodeMap{
Description: "The account key is not eligible for this operation",
HTTPStatusCode: http.StatusForbidden,
},
ErrServiceAccountNotFound: {
ErrAdminServiceAccountNotFound: {
Code: "XMinioInvalidIAMCredentials",
Description: "The specified service account is not found",
HTTPStatusCode: http.StatusNotFound,
@ -1790,6 +1790,8 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
apiErr = ErrAdminInvalidArgument
case errNoSuchUser:
apiErr = ErrAdminNoSuchUser
case errNoSuchServiceAccount:
apiErr = ErrAdminServiceAccountNotFound
case errNoSuchGroup:
apiErr = ErrAdminNoSuchGroup
case errGroupNotEmpty:

File diff suppressed because one or more lines are too long

View File

@ -194,24 +194,21 @@ func mustGetClaimsFromToken(r *http.Request) map[string]interface{} {
// Fetch claims in the security token returned by the client.
func getClaimsFromToken(token string) (map[string]interface{}, error) {
claims := xjwt.NewMapClaims()
if token == "" {
claims := xjwt.NewMapClaims()
return claims.Map(), nil
}
stsTokenCallback := func(claims *xjwt.MapClaims) ([]byte, error) {
// JWT token for x-amz-security-token is signed with admin
// secret key, temporary credentials become invalid if
// server admin credentials change. This is done to ensure
// that clients cannot decode the token using the temp
// secret keys and generate an entirely new claim by essentially
// hijacking the policies. We need to make sure that this is
// based an admin credential such that token cannot be decoded
// on the client side and is treated like an opaque value.
return []byte(globalActiveCred.SecretKey), nil
}
if err := xjwt.ParseWithClaims(token, claims, stsTokenCallback); err != nil {
// JWT token for x-amz-security-token is signed with admin
// secret key, temporary credentials become invalid if
// server admin credentials change. This is done to ensure
// that clients cannot decode the token using the temp
// secret keys and generate an entirely new claim by essentially
// hijacking the policies. We need to make sure that this is
// based an admin credential such that token cannot be decoded
// on the client side and is treated like an opaque value.
claims, err := auth.ExtractClaims(token, globalActiveCred.SecretKey)
if err != nil {
return nil, errAuthentication
}

View File

@ -260,6 +260,67 @@ func (l *Config) lookupUserDN(conn *ldap.Conn, username string) (string, error)
return searchResult.Entries[0].DN, nil
}
func (l *Config) searchForUserGroups(conn *ldap.Conn, username, bindDN string) ([]string, error) {
// User groups lookup.
var groups []string
if l.GroupSearchFilter != "" {
for _, groupSearchBase := range l.GroupSearchBaseDistNames {
filter := strings.Replace(l.GroupSearchFilter, "%s", ldap.EscapeFilter(username), -1)
filter = strings.Replace(filter, "%d", ldap.EscapeFilter(bindDN), -1)
searchRequest := ldap.NewSearchRequest(
groupSearchBase,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
filter,
nil,
nil,
)
var newGroups []string
newGroups, err := getGroups(conn, searchRequest)
if err != nil {
errRet := fmt.Errorf("Error finding groups of %s: %v", bindDN, err)
return nil, errRet
}
groups = append(groups, newGroups...)
}
}
return groups, nil
}
// LookupUserDN searches for the full DN ang groups of a given username
func (l *Config) LookupUserDN(username string) (string, []string, error) {
if !l.isUsingLookupBind {
return "", nil, errors.New("current lookup mode does not support searching for User DN")
}
conn, err := l.Connect()
if err != nil {
return "", nil, err
}
defer conn.Close()
// Bind to the lookup user account
if err = l.lookupBind(conn); err != nil {
return "", nil, err
}
// Lookup user DN
bindDN, err := l.lookupUserDN(conn, username)
if err != nil {
errRet := fmt.Errorf("Unable to find user DN: %w", err)
return "", nil, errRet
}
groups, err := l.searchForUserGroups(conn, username, bindDN)
if err != nil {
return "", nil, err
}
return bindDN, groups, nil
}
// Bind - binds to ldap, searches LDAP and returns the distinguished name of the
// user and the list of groups.
func (l *Config) Bind(username, password string) (string, []string, error) {
@ -310,28 +371,9 @@ func (l *Config) Bind(username, password string) (string, []string, error) {
}
// User groups lookup.
var groups []string
if l.GroupSearchFilter != "" {
for _, groupSearchBase := range l.GroupSearchBaseDistNames {
filter := strings.Replace(l.GroupSearchFilter, "%s", ldap.EscapeFilter(username), -1)
filter = strings.Replace(filter, "%d", ldap.EscapeFilter(bindDN), -1)
searchRequest := ldap.NewSearchRequest(
groupSearchBase,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
filter,
nil,
nil,
)
var newGroups []string
newGroups, err = getGroups(conn, searchRequest)
if err != nil {
errRet := fmt.Errorf("Error finding groups of %s: %v", bindDN, err)
return "", nil, errRet
}
groups = append(groups, newGroups...)
}
groups, err := l.searchForUserGroups(conn, username, bindDN)
if err != nil {
return "", nil, err
}
return bindDN, groups, nil

View File

@ -1050,19 +1050,25 @@ func (sys *IAMSys) SetUserStatus(accessKey string, status madmin.AccountStatus)
return nil
}
type newServiceAccountOpts struct {
sessionPolicy *iampolicy.Policy
accessKey string
secretKey string
}
// NewServiceAccount - create a new service account
func (sys *IAMSys) NewServiceAccount(ctx context.Context, parentUser string, groups []string, sessionPolicy *iampolicy.Policy) (auth.Credentials, error) {
func (sys *IAMSys) NewServiceAccount(ctx context.Context, parentUser string, groups []string, opts newServiceAccountOpts) (auth.Credentials, error) {
if !sys.Initialized() {
return auth.Credentials{}, errServerNotInitialized
}
var policyBuf []byte
if sessionPolicy != nil {
err := sessionPolicy.Validate()
if opts.sessionPolicy != nil {
err := opts.sessionPolicy.Validate()
if err != nil {
return auth.Credentials{}, err
}
policyBuf, err = json.Marshal(sessionPolicy)
policyBuf, err = json.Marshal(opts.sessionPolicy)
if err != nil {
return auth.Credentials{}, err
}
@ -1115,13 +1121,22 @@ func (sys *IAMSys) NewServiceAccount(ctx context.Context, parentUser string, gro
m[iamPolicyClaimNameSA()] = "inherited-policy"
}
secret := globalActiveCred.SecretKey
cred, err := auth.GetNewCredentialsWithMetadata(m, secret)
var (
cred auth.Credentials
err error
)
if len(opts.accessKey) > 0 {
cred, err = auth.CreateNewCredentialsWithMetadata(opts.accessKey, opts.secretKey, m, globalActiveCred.SecretKey)
} else {
cred, err = auth.GetNewCredentialsWithMetadata(m, globalActiveCred.SecretKey)
}
if err != nil {
return auth.Credentials{}, err
}
cred.ParentUser = parentUser
cred.Groups = groups
cred.Status = string(madmin.AccountEnabled)
u := newUserIdentity(cred)
@ -1134,8 +1149,69 @@ func (sys *IAMSys) NewServiceAccount(ctx context.Context, parentUser string, gro
return cred, nil
}
type updateServiceAccountOpts struct {
sessionPolicy *iampolicy.Policy
secretKey string
status string
}
// UpdateServiceAccount - edit a service account
func (sys *IAMSys) UpdateServiceAccount(ctx context.Context, accessKey string, opts updateServiceAccountOpts) error {
if !sys.Initialized() {
return errServerNotInitialized
}
sys.store.lock()
defer sys.store.unlock()
cr, ok := sys.iamUsersMap[accessKey]
if !ok || !cr.IsServiceAccount() {
return errNoSuchServiceAccount
}
if opts.secretKey != "" {
cr.SecretKey = opts.secretKey
}
if opts.status != "" {
cr.Status = opts.status
}
if opts.sessionPolicy != nil {
m := make(map[string]interface{})
err := opts.sessionPolicy.Validate()
if err != nil {
return err
}
policyBuf, err := json.Marshal(opts.sessionPolicy)
if err != nil {
return err
}
if len(policyBuf) > 16*humanize.KiByte {
return fmt.Errorf("Session policy should not exceed 16 KiB characters")
}
m[iampolicy.SessionPolicyName] = base64.StdEncoding.EncodeToString(policyBuf)
m[iamPolicyClaimNameSA()] = "embedded-policy"
m[parentClaim] = cr.ParentUser
cr.SessionToken, err = auth.JWTSignWithAccessKey(accessKey, m, globalActiveCred.SecretKey)
if err != nil {
return err
}
}
u := newUserIdentity(cr)
if err := sys.store.saveUserIdentity(context.Background(), u.Credentials.AccessKey, srvAccUser, u); err != nil {
return err
}
sys.iamUsersMap[u.Credentials.AccessKey] = u.Credentials
return nil
}
// ListServiceAccounts - lists all services accounts associated to a specific user
func (sys *IAMSys) ListServiceAccounts(ctx context.Context, accessKey string) ([]string, error) {
func (sys *IAMSys) ListServiceAccounts(ctx context.Context, accessKey string) ([]auth.Credentials, error) {
if !sys.Initialized() {
return nil, errServerNotInitialized
}
@ -1145,30 +1221,53 @@ func (sys *IAMSys) ListServiceAccounts(ctx context.Context, accessKey string) ([
sys.store.rlock()
defer sys.store.runlock()
var serviceAccounts []string
for k, v := range sys.iamUsersMap {
var serviceAccounts []auth.Credentials
for _, v := range sys.iamUsersMap {
if v.IsServiceAccount() && v.ParentUser == accessKey {
serviceAccounts = append(serviceAccounts, k)
// Hide secret key & session key here
v.SecretKey = ""
v.SessionToken = ""
serviceAccounts = append(serviceAccounts, v)
}
}
return serviceAccounts, nil
}
// GetServiceAccountParent - gets information about a service account
func (sys *IAMSys) GetServiceAccountParent(ctx context.Context, accessKey string) (string, error) {
// GetServiceAccount - gets information about a service account
func (sys *IAMSys) GetServiceAccount(ctx context.Context, accessKey string) (auth.Credentials, *iampolicy.Policy, error) {
if !sys.Initialized() {
return "", errServerNotInitialized
return auth.Credentials{}, nil, errServerNotInitialized
}
sys.store.rlock()
defer sys.store.runlock()
sa, ok := sys.iamUsersMap[accessKey]
if ok && sa.IsServiceAccount() {
return sa.ParentUser, nil
if !ok || !sa.IsServiceAccount() {
return auth.Credentials{}, nil, errNoSuchServiceAccount
}
return "", nil
var embeddedPolicy *iampolicy.Policy
jwtClaims, err := auth.ExtractClaims(sa.SessionToken, globalActiveCred.SecretKey)
if err == nil {
pt, ptok := jwtClaims.Lookup(iamPolicyClaimNameSA())
sp, spok := jwtClaims.Lookup(iampolicy.SessionPolicyName)
if ptok && spok && pt == "embedded-policy" {
p, err := iampolicy.ParseConfig(bytes.NewReader([]byte(sp)))
if err == nil {
embeddedPolicy = &iampolicy.Policy{}
embeddedPolicy.Merge(*p)
}
}
}
// Hide secret & session keys
sa.SecretKey = ""
sa.SessionToken = ""
return sa, embeddedPolicy, nil
}
// DeleteServiceAccount - delete a service account

View File

@ -77,6 +77,9 @@ var errInvalidDecompressedSize = errors.New("Invalid Decompressed Size")
// error returned in IAM subsystem when user doesn't exist.
var errNoSuchUser = errors.New("Specified user does not exist")
// error returned when service account is not found
var errNoSuchServiceAccount = errors.New("Specified service account does not exist")
// error returned in IAM subsystem when groups doesn't exist.
var errNoSuchGroup = errors.New("Specified group does not exist")

View File

@ -28,6 +28,7 @@ import (
"time"
jwtgo "github.com/dgrijalva/jwt-go"
"github.com/minio/minio/cmd/jwt"
)
const (
@ -210,16 +211,33 @@ func GetNewCredentialsWithMetadata(m map[string]interface{}, tokenSecret string)
for i := 0; i < accessKeyMaxLen; i++ {
keyBytes[i] = alphaNumericTable[keyBytes[i]%alphaNumericTableLen]
}
cred.AccessKey = string(keyBytes)
accessKey := string(keyBytes)
// Generate secret key.
keyBytes, err = readBytes(secretKeyMaxLen)
if err != nil {
return cred, err
}
cred.SecretKey = strings.Replace(string([]byte(base64.StdEncoding.EncodeToString(keyBytes))[:secretKeyMaxLen]),
secretKey := strings.Replace(string([]byte(base64.StdEncoding.EncodeToString(keyBytes))[:secretKeyMaxLen]),
"/", "+", -1)
return CreateNewCredentialsWithMetadata(accessKey, secretKey, m, tokenSecret)
}
// CreateNewCredentialsWithMetadata - creates new credentials using the specified access & secret keys
// and generate a session token if a secret token is provided.
func CreateNewCredentialsWithMetadata(accessKey, secretKey string, m map[string]interface{}, tokenSecret string) (cred Credentials, err error) {
if len(accessKey) < accessKeyMinLen || len(accessKey) > accessKeyMaxLen {
return Credentials{}, fmt.Errorf("access key length should be between %d and %d", accessKeyMinLen, accessKeyMaxLen)
}
if len(secretKey) < secretKeyMinLen || len(secretKey) > secretKeyMaxLen {
return Credentials{}, fmt.Errorf("secret key length should be between %d and %d", secretKeyMinLen, secretKeyMaxLen)
}
cred.AccessKey = accessKey
cred.SecretKey = secretKey
cred.Status = AccountOn
if tokenSecret == "" {
@ -231,12 +249,9 @@ func GetNewCredentialsWithMetadata(m map[string]interface{}, tokenSecret string)
if err != nil {
return cred, err
}
m["accessKey"] = cred.AccessKey
jwt := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, jwtgo.MapClaims(m))
cred.Expiration = time.Unix(expiry, 0).UTC()
cred.SessionToken, err = jwt.SignedString([]byte(tokenSecret))
cred.SessionToken, err = JWTSignWithAccessKey(cred.AccessKey, m, tokenSecret)
if err != nil {
return cred, err
}
@ -244,6 +259,31 @@ func GetNewCredentialsWithMetadata(m map[string]interface{}, tokenSecret string)
return cred, nil
}
// JWTSignWithAccessKey - generates a session token.
func JWTSignWithAccessKey(accessKey string, m map[string]interface{}, tokenSecret string) (string, error) {
m["accessKey"] = accessKey
jwt := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, jwtgo.MapClaims(m))
return jwt.SignedString([]byte(tokenSecret))
}
// ExtractClaims extracts JWT claims from a security token using a secret key
func ExtractClaims(token, secretKey string) (*jwt.MapClaims, error) {
if token == "" || secretKey == "" {
return nil, errors.New("invalid argument")
}
claims := jwt.NewMapClaims()
stsTokenCallback := func(claims *jwt.MapClaims) ([]byte, error) {
return []byte(secretKey), nil
}
if err := jwt.ParseWithClaims(token, claims, stsTokenCallback); err != nil {
return nil, err
}
return claims, nil
}
// GetNewCredentials generates and returns new credential.
func GetNewCredentials() (cred Credentials, err error) {
return GetNewCredentialsWithMetadata(map[string]interface{}{}, "")

View File

@ -77,6 +77,17 @@ const (
// GetUserAdminAction - allows GET permission on user info
GetUserAdminAction = "admin:GetUser"
// Service account Actions
// CreateServiceAccountAdminAction - allow create a service account for a user
CreateServiceAccountAdminAction = "admin:CreateServiceAccount"
// UpdateServiceAccountAdminAction - allow updating a service account
UpdateServiceAccountAdminAction = "admin:UpdateServiceAccount"
// RemoveServiceAccountAdminAction - allow removing a service account
RemoveServiceAccountAdminAction = "admin:RemoveServiceAccount"
// ListServiceAccountsAdminAction - allow listing service accounts
ListServiceAccountsAdminAction = "admin:ListServiceAccounts"
// Group Actions
// AddUserToGroupAdminAction - allow adding user to group permission
@ -125,43 +136,47 @@ const (
// List of all supported admin actions.
var supportedAdminActions = map[AdminAction]struct{}{
HealAdminAction: {},
StorageInfoAdminAction: {},
DataUsageInfoAdminAction: {},
TopLocksAdminAction: {},
ProfilingAdminAction: {},
TraceAdminAction: {},
ConsoleLogAdminAction: {},
KMSKeyStatusAdminAction: {},
ServerInfoAdminAction: {},
HealthInfoAdminAction: {},
BandwidthMonitorAction: {},
ServerUpdateAdminAction: {},
ServiceRestartAdminAction: {},
ServiceStopAdminAction: {},
ConfigUpdateAdminAction: {},
CreateUserAdminAction: {},
DeleteUserAdminAction: {},
ListUsersAdminAction: {},
EnableUserAdminAction: {},
DisableUserAdminAction: {},
GetUserAdminAction: {},
AddUserToGroupAdminAction: {},
RemoveUserFromGroupAdminAction: {},
GetGroupAdminAction: {},
ListGroupsAdminAction: {},
EnableGroupAdminAction: {},
DisableGroupAdminAction: {},
CreatePolicyAdminAction: {},
DeletePolicyAdminAction: {},
GetPolicyAdminAction: {},
AttachPolicyAdminAction: {},
ListUserPoliciesAdminAction: {},
SetBucketQuotaAdminAction: {},
GetBucketQuotaAdminAction: {},
SetBucketTargetAction: {},
GetBucketTargetAction: {},
AllAdminActions: {},
HealAdminAction: {},
StorageInfoAdminAction: {},
DataUsageInfoAdminAction: {},
TopLocksAdminAction: {},
ProfilingAdminAction: {},
TraceAdminAction: {},
ConsoleLogAdminAction: {},
KMSKeyStatusAdminAction: {},
ServerInfoAdminAction: {},
HealthInfoAdminAction: {},
BandwidthMonitorAction: {},
ServerUpdateAdminAction: {},
ServiceRestartAdminAction: {},
ServiceStopAdminAction: {},
ConfigUpdateAdminAction: {},
CreateUserAdminAction: {},
DeleteUserAdminAction: {},
ListUsersAdminAction: {},
EnableUserAdminAction: {},
DisableUserAdminAction: {},
GetUserAdminAction: {},
AddUserToGroupAdminAction: {},
RemoveUserFromGroupAdminAction: {},
GetGroupAdminAction: {},
ListGroupsAdminAction: {},
EnableGroupAdminAction: {},
DisableGroupAdminAction: {},
CreateServiceAccountAdminAction: {},
UpdateServiceAccountAdminAction: {},
RemoveServiceAccountAdminAction: {},
ListServiceAccountsAdminAction: {},
CreatePolicyAdminAction: {},
DeletePolicyAdminAction: {},
GetPolicyAdminAction: {},
AttachPolicyAdminAction: {},
ListUserPoliciesAdminAction: {},
SetBucketQuotaAdminAction: {},
GetBucketQuotaAdminAction: {},
SetBucketTargetAction: {},
GetBucketTargetAction: {},
AllAdminActions: {},
}
// IsValid - checks if action is valid or not.
@ -172,40 +187,45 @@ func (action AdminAction) IsValid() bool {
// adminActionConditionKeyMap - holds mapping of supported condition key for an action.
var adminActionConditionKeyMap = map[Action]condition.KeySet{
AllAdminActions: condition.NewKeySet(condition.AllSupportedAdminKeys...),
HealAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
StorageInfoAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
ServerInfoAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
DataUsageInfoAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
HealthInfoAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
BandwidthMonitorAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
TopLocksAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
ProfilingAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
TraceAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
ConsoleLogAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
KMSKeyStatusAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
ServerUpdateAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
ServiceRestartAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
ServiceStopAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
ConfigUpdateAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
CreateUserAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
DeleteUserAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
ListUsersAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
EnableUserAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
DisableUserAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
GetUserAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
AddUserToGroupAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
RemoveUserFromGroupAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
ListGroupsAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
EnableGroupAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
DisableGroupAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
CreatePolicyAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
DeletePolicyAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
GetPolicyAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
AttachPolicyAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
ListUserPoliciesAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
SetBucketQuotaAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
GetBucketQuotaAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
SetBucketTargetAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
GetBucketTargetAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
AllAdminActions: condition.NewKeySet(condition.AllSupportedAdminKeys...),
HealAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
StorageInfoAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
ServerInfoAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
DataUsageInfoAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
HealthInfoAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
BandwidthMonitorAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
TopLocksAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
ProfilingAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
TraceAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
ConsoleLogAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
KMSKeyStatusAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
ServerUpdateAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
ServiceRestartAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
ServiceStopAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
ConfigUpdateAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
CreateUserAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
DeleteUserAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
ListUsersAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
EnableUserAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
DisableUserAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
GetUserAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
AddUserToGroupAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
RemoveUserFromGroupAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
ListGroupsAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
EnableGroupAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
DisableGroupAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
CreateServiceAccountAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
UpdateServiceAccountAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
RemoveServiceAccountAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
ListServiceAccountsAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
CreatePolicyAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
DeletePolicyAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
GetPolicyAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
AttachPolicyAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
ListUserPoliciesAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
SetBucketQuotaAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
GetBucketQuotaAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
SetBucketTargetAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
GetBucketTargetAction: condition.NewKeySet(condition.AllSupportedAdminKeys...),
}

View File

@ -56,14 +56,14 @@ func main() {
}
// Create a new service account
creds, err := madmClnt.AddServiceAccount(context.Background(), &p)
creds, err := madmClnt.AddServiceAccount(context.Background(), madmin.AddServiceAccountReq{Policy: &p})
if err != nil {
log.Fatalln(err)
}
fmt.Println(creds)
// List all services accounts
list, err := madmClnt.ListServiceAccounts(context.Background())
list, err := madmClnt.ListServiceAccounts(context.Background(), "")
if err != nil {
log.Fatalln(err)
}

View File

@ -266,9 +266,12 @@ func (adm *AdminClient) SetUserStatus(ctx context.Context, accessKey string, sta
return nil
}
// AddServiceAccountReq is the request body of the add service account admin call
// AddServiceAccountReq is the request options of the add service account admin call
type AddServiceAccountReq struct {
Policy *iampolicy.Policy `json:"policy,omitempty"`
Policy *iampolicy.Policy `json:"policy,omitempty"`
TargetUser string `json:"targetUser,omitempty"`
AccessKey string `json:"accessKey,omitempty"`
SecretKey string `json:"secretKey,omitempty"`
}
// AddServiceAccountResp is the response body of the add service account admin call
@ -278,16 +281,14 @@ type AddServiceAccountResp struct {
// AddServiceAccount - creates a new service account belonging to the user sending
// the request while restricting the service account permission by the given policy document.
func (adm *AdminClient) AddServiceAccount(ctx context.Context, policy *iampolicy.Policy) (auth.Credentials, error) {
if policy != nil {
if err := policy.Validate(); err != nil {
func (adm *AdminClient) AddServiceAccount(ctx context.Context, opts AddServiceAccountReq) (auth.Credentials, error) {
if opts.Policy != nil {
if err := opts.Policy.Validate(); err != nil {
return auth.Credentials{}, err
}
}
data, err := json.Marshal(AddServiceAccountReq{
Policy: policy,
})
data, err := json.Marshal(opts)
if err != nil {
return auth.Credentials{}, err
}
@ -325,15 +326,67 @@ func (adm *AdminClient) AddServiceAccount(ctx context.Context, policy *iampolicy
return serviceAccountResp.Credentials, nil
}
// UpdateServiceAccountReq is the request options of the edit service account admin call
type UpdateServiceAccountReq struct {
NewPolicy *iampolicy.Policy `json:"newPolicy,omitempty"`
NewSecretKey string `json:"newSecretKey,omitempty"`
NewStatus string `json:"newStatus,omityempty"`
}
// UpdateServiceAccount - edit an existing service account
func (adm *AdminClient) UpdateServiceAccount(ctx context.Context, accessKey string, opts UpdateServiceAccountReq) error {
if opts.NewPolicy != nil {
if err := opts.NewPolicy.Validate(); err != nil {
return err
}
}
data, err := json.Marshal(opts)
if err != nil {
return err
}
econfigBytes, err := EncryptData(adm.getSecretKey(), data)
if err != nil {
return err
}
queryValues := url.Values{}
queryValues.Set("accessKey", accessKey)
reqData := requestData{
relPath: adminAPIPrefix + "/update-service-account",
content: econfigBytes,
queryValues: queryValues,
}
// Execute POST on /minio/admin/v3/update-service-account to edit a service account
resp, err := adm.executeMethod(ctx, http.MethodPost, reqData)
defer closeResponse(resp)
if err != nil {
return err
}
if resp.StatusCode != http.StatusNoContent {
return httpRespToErrorResponse(resp)
}
return nil
}
// ListServiceAccountsResp is the response body of the list service accounts call
type ListServiceAccountsResp struct {
Accounts []string `json:"accounts"`
}
// ListServiceAccounts - list service accounts belonging to the specified user
func (adm *AdminClient) ListServiceAccounts(ctx context.Context) (ListServiceAccountsResp, error) {
func (adm *AdminClient) ListServiceAccounts(ctx context.Context, user string) (ListServiceAccountsResp, error) {
queryValues := url.Values{}
queryValues.Set("user", user)
reqData := requestData{
relPath: adminAPIPrefix + "/list-service-accounts",
relPath: adminAPIPrefix + "/list-service-accounts",
queryValues: queryValues,
}
// Execute GET on /minio/admin/v3/list-service-accounts
@ -359,6 +412,47 @@ func (adm *AdminClient) ListServiceAccounts(ctx context.Context) (ListServiceAcc
return listResp, nil
}
// InfoServiceAccountResp is the response body of the info service account call
type InfoServiceAccountResp struct {
ParentUser string `json:"parentUser"`
AccountStatus string `json:"accountStatus"`
ImpliedPolicy bool `json:"impliedPolicy"`
Policy string `json:"policy"`
}
// InfoServiceAccount - returns the info of service account belonging to the specified user
func (adm *AdminClient) InfoServiceAccount(ctx context.Context, accessKey string) (InfoServiceAccountResp, error) {
queryValues := url.Values{}
queryValues.Set("accessKey", accessKey)
reqData := requestData{
relPath: adminAPIPrefix + "/info-service-account",
queryValues: queryValues,
}
// Execute GET on /minio/admin/v3/info-service-account
resp, err := adm.executeMethod(ctx, http.MethodGet, reqData)
defer closeResponse(resp)
if err != nil {
return InfoServiceAccountResp{}, err
}
if resp.StatusCode != http.StatusOK {
return InfoServiceAccountResp{}, httpRespToErrorResponse(resp)
}
data, err := DecryptData(adm.getSecretKey(), resp.Body)
if err != nil {
return InfoServiceAccountResp{}, err
}
var infoResp InfoServiceAccountResp
if err = json.Unmarshal(data, &infoResp); err != nil {
return InfoServiceAccountResp{}, err
}
return infoResp, nil
}
// DeleteServiceAccount - delete a specified service account. The server will reject
// the request if the service account does not belong to the user initiating the request
func (adm *AdminClient) DeleteServiceAccount(ctx context.Context, serviceAccount string) error {