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  }