github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/admin-handlers-idp-config.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  	"context"
    22  	"encoding/json"
    23  	"errors"
    24  	"fmt"
    25  	"io"
    26  	"net/http"
    27  	"strings"
    28  
    29  	"github.com/minio/madmin-go/v3"
    30  	"github.com/minio/minio-go/v7/pkg/set"
    31  	"github.com/minio/minio/internal/config"
    32  	cfgldap "github.com/minio/minio/internal/config/identity/ldap"
    33  	"github.com/minio/minio/internal/config/identity/openid"
    34  	"github.com/minio/minio/internal/logger"
    35  	"github.com/minio/mux"
    36  	"github.com/minio/pkg/v2/ldap"
    37  	"github.com/minio/pkg/v2/policy"
    38  )
    39  
    40  func addOrUpdateIDPHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, isUpdate bool) {
    41  	objectAPI, cred := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
    42  	if objectAPI == nil {
    43  		return
    44  	}
    45  
    46  	if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 {
    47  		// More than maxConfigSize bytes were available
    48  		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigTooLarge), r.URL)
    49  		return
    50  	}
    51  
    52  	// Ensure body content type is opaque to ensure that request body has not
    53  	// been interpreted as form data.
    54  	contentType := r.Header.Get("Content-Type")
    55  	if contentType != "application/octet-stream" {
    56  		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrBadRequest), r.URL)
    57  		return
    58  	}
    59  
    60  	password := cred.SecretKey
    61  	reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
    62  	if err != nil {
    63  		logger.LogIf(ctx, err, logger.ErrorKind)
    64  		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL)
    65  		return
    66  	}
    67  
    68  	idpCfgType := mux.Vars(r)["type"]
    69  	if !madmin.ValidIDPConfigTypes.Contains(idpCfgType) {
    70  		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigInvalidIDPType), r.URL)
    71  		return
    72  	}
    73  
    74  	var subSys string
    75  	switch idpCfgType {
    76  	case madmin.OpenidIDPCfg:
    77  		subSys = madmin.IdentityOpenIDSubSys
    78  	case madmin.LDAPIDPCfg:
    79  		subSys = madmin.IdentityLDAPSubSys
    80  	}
    81  
    82  	cfgName := mux.Vars(r)["name"]
    83  	cfgTarget := madmin.Default
    84  	if cfgName != "" {
    85  		cfgTarget = cfgName
    86  		if idpCfgType == madmin.LDAPIDPCfg && cfgName != madmin.Default {
    87  			// LDAP does not support multiple configurations. So cfgName must be
    88  			// empty or `madmin.Default`.
    89  			writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigLDAPNonDefaultConfigName), r.URL)
    90  			return
    91  		}
    92  	}
    93  
    94  	// Check that this is a valid Create vs Update API call.
    95  	s := globalServerConfig.Clone()
    96  	if apiErrCode := handleCreateUpdateValidation(s, subSys, cfgTarget, isUpdate); apiErrCode != ErrNone {
    97  		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(apiErrCode), r.URL)
    98  		return
    99  	}
   100  
   101  	cfgData := ""
   102  	{
   103  		tgtSuffix := ""
   104  		if cfgTarget != madmin.Default {
   105  			tgtSuffix = config.SubSystemSeparator + cfgTarget
   106  		}
   107  		cfgData = subSys + tgtSuffix + config.KvSpaceSeparator + string(reqBytes)
   108  	}
   109  
   110  	cfg, err := readServerConfig(ctx, objectAPI, nil)
   111  	if err != nil {
   112  		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
   113  		return
   114  	}
   115  
   116  	dynamic, err := cfg.ReadConfig(strings.NewReader(cfgData))
   117  	if err != nil {
   118  		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
   119  		return
   120  	}
   121  
   122  	// IDP config is not dynamic. Sanity check.
   123  	if dynamic {
   124  		writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), "", r.URL)
   125  		return
   126  	}
   127  
   128  	if err = validateConfig(ctx, cfg, subSys); err != nil {
   129  
   130  		var validationErr ldap.Validation
   131  		if errors.As(err, &validationErr) {
   132  			// If we got an LDAP validation error, we need to send appropriate
   133  			// error message back to client (likely mc).
   134  			writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigLDAPValidation),
   135  				validationErr.FormatError(), r.URL)
   136  			return
   137  		}
   138  
   139  		writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL)
   140  		return
   141  	}
   142  
   143  	// Update the actual server config on disk.
   144  	if err = saveServerConfig(ctx, objectAPI, cfg); err != nil {
   145  		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
   146  		return
   147  	}
   148  
   149  	// Write to the config input KV to history.
   150  	if err = saveServerConfigHistory(ctx, objectAPI, []byte(cfgData)); err != nil {
   151  		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
   152  		return
   153  	}
   154  
   155  	writeSuccessResponseHeadersOnly(w)
   156  }
   157  
   158  func handleCreateUpdateValidation(s config.Config, subSys, cfgTarget string, isUpdate bool) APIErrorCode {
   159  	if cfgTarget != madmin.Default {
   160  		// This cannot give an error at this point.
   161  		subSysTargets, _ := s.GetAvailableTargets(subSys)
   162  		subSysTargetsSet := set.CreateStringSet(subSysTargets...)
   163  		if isUpdate && !subSysTargetsSet.Contains(cfgTarget) {
   164  			return ErrAdminConfigIDPCfgNameDoesNotExist
   165  		}
   166  		if !isUpdate && subSysTargetsSet.Contains(cfgTarget) {
   167  			return ErrAdminConfigIDPCfgNameAlreadyExists
   168  		}
   169  
   170  		return ErrNone
   171  	}
   172  
   173  	// For the default configuration name, since it will always be an available
   174  	// target, we need to check if a configuration value has been set previously
   175  	// to figure out if this is a valid create or update API call.
   176  
   177  	// This cannot really error (FIXME: improve the type for GetConfigInfo)
   178  	var cfgInfos []madmin.IDPCfgInfo
   179  	switch subSys {
   180  	case madmin.IdentityOpenIDSubSys:
   181  		cfgInfos, _ = globalIAMSys.OpenIDConfig.GetConfigInfo(s, cfgTarget)
   182  	case madmin.IdentityLDAPSubSys:
   183  		cfgInfos, _ = globalIAMSys.LDAPConfig.GetConfigInfo(s, cfgTarget)
   184  	}
   185  
   186  	if len(cfgInfos) > 0 && !isUpdate {
   187  		return ErrAdminConfigIDPCfgNameAlreadyExists
   188  	}
   189  	if len(cfgInfos) == 0 && isUpdate {
   190  		return ErrAdminConfigIDPCfgNameDoesNotExist
   191  	}
   192  	return ErrNone
   193  }
   194  
   195  // AddIdentityProviderCfg: adds a new IDP config for openid/ldap.
   196  //
   197  // PUT <admin-prefix>/idp-cfg/openid/dex1 -> create named config `dex1`
   198  //
   199  // PUT <admin-prefix>/idp-cfg/openid/_ -> create (default) named config `_`
   200  func (a adminAPIHandlers) AddIdentityProviderCfg(w http.ResponseWriter, r *http.Request) {
   201  	ctx := r.Context()
   202  
   203  	addOrUpdateIDPHandler(ctx, w, r, false)
   204  }
   205  
   206  // UpdateIdentityProviderCfg: updates an existing IDP config for openid/ldap.
   207  //
   208  // POST <admin-prefix>/idp-cfg/openid/dex1 -> update named config `dex1`
   209  //
   210  // POST <admin-prefix>/idp-cfg/openid/_ -> update (default) named config `_`
   211  func (a adminAPIHandlers) UpdateIdentityProviderCfg(w http.ResponseWriter, r *http.Request) {
   212  	ctx := r.Context()
   213  
   214  	addOrUpdateIDPHandler(ctx, w, r, true)
   215  }
   216  
   217  // ListIdentityProviderCfg:
   218  //
   219  // GET <admin-prefix>/idp-cfg/openid -> lists openid provider configs.
   220  func (a adminAPIHandlers) ListIdentityProviderCfg(w http.ResponseWriter, r *http.Request) {
   221  	ctx := r.Context()
   222  
   223  	objectAPI, cred := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
   224  	if objectAPI == nil {
   225  		return
   226  	}
   227  	password := cred.SecretKey
   228  
   229  	idpCfgType := mux.Vars(r)["type"]
   230  	if !madmin.ValidIDPConfigTypes.Contains(idpCfgType) {
   231  		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigInvalidIDPType), r.URL)
   232  		return
   233  	}
   234  
   235  	var cfgList []madmin.IDPListItem
   236  	var err error
   237  	switch idpCfgType {
   238  	case madmin.OpenidIDPCfg:
   239  		cfg := globalServerConfig.Clone()
   240  		cfgList, err = globalIAMSys.OpenIDConfig.GetConfigList(cfg)
   241  	case madmin.LDAPIDPCfg:
   242  		cfg := globalServerConfig.Clone()
   243  		cfgList, err = globalIAMSys.LDAPConfig.GetConfigList(cfg)
   244  
   245  	default:
   246  		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
   247  		return
   248  	}
   249  
   250  	if err != nil {
   251  		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
   252  		return
   253  	}
   254  
   255  	data, err := json.Marshal(cfgList)
   256  	if err != nil {
   257  		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
   258  		return
   259  	}
   260  
   261  	econfigData, err := madmin.EncryptData(password, data)
   262  	if err != nil {
   263  		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
   264  		return
   265  	}
   266  
   267  	writeSuccessResponseJSON(w, econfigData)
   268  }
   269  
   270  // GetIdentityProviderCfg:
   271  //
   272  // GET <admin-prefix>/idp-cfg/openid/dex_test
   273  func (a adminAPIHandlers) GetIdentityProviderCfg(w http.ResponseWriter, r *http.Request) {
   274  	ctx := r.Context()
   275  
   276  	objectAPI, cred := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
   277  	if objectAPI == nil {
   278  		return
   279  	}
   280  
   281  	idpCfgType := mux.Vars(r)["type"]
   282  	cfgName := mux.Vars(r)["name"]
   283  	password := cred.SecretKey
   284  
   285  	if !madmin.ValidIDPConfigTypes.Contains(idpCfgType) {
   286  		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigInvalidIDPType), r.URL)
   287  		return
   288  	}
   289  
   290  	cfg := globalServerConfig.Clone()
   291  	var cfgInfos []madmin.IDPCfgInfo
   292  	var err error
   293  	switch idpCfgType {
   294  	case madmin.OpenidIDPCfg:
   295  		cfgInfos, err = globalIAMSys.OpenIDConfig.GetConfigInfo(cfg, cfgName)
   296  	case madmin.LDAPIDPCfg:
   297  		cfgInfos, err = globalIAMSys.LDAPConfig.GetConfigInfo(cfg, cfgName)
   298  	}
   299  	if err != nil {
   300  		if errors.Is(err, openid.ErrProviderConfigNotFound) || errors.Is(err, cfgldap.ErrProviderConfigNotFound) {
   301  			writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminNoSuchConfigTarget), r.URL)
   302  			return
   303  		}
   304  
   305  		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
   306  		return
   307  	}
   308  
   309  	res := madmin.IDPConfig{
   310  		Type: idpCfgType,
   311  		Name: cfgName,
   312  		Info: cfgInfos,
   313  	}
   314  	data, err := json.Marshal(res)
   315  	if err != nil {
   316  		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
   317  		return
   318  	}
   319  
   320  	econfigData, err := madmin.EncryptData(password, data)
   321  	if err != nil {
   322  		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
   323  		return
   324  	}
   325  
   326  	writeSuccessResponseJSON(w, econfigData)
   327  }
   328  
   329  // DeleteIdentityProviderCfg:
   330  //
   331  // DELETE <admin-prefix>/idp-cfg/openid/dex_test
   332  func (a adminAPIHandlers) DeleteIdentityProviderCfg(w http.ResponseWriter, r *http.Request) {
   333  	ctx := r.Context()
   334  
   335  	objectAPI, _ := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
   336  	if objectAPI == nil {
   337  		return
   338  	}
   339  
   340  	idpCfgType := mux.Vars(r)["type"]
   341  	cfgName := mux.Vars(r)["name"]
   342  	if !madmin.ValidIDPConfigTypes.Contains(idpCfgType) {
   343  		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigInvalidIDPType), r.URL)
   344  		return
   345  	}
   346  
   347  	cfgCopy := globalServerConfig.Clone()
   348  	var subSys string
   349  	switch idpCfgType {
   350  	case madmin.OpenidIDPCfg:
   351  		subSys = config.IdentityOpenIDSubSys
   352  		cfgInfos, err := globalIAMSys.OpenIDConfig.GetConfigInfo(cfgCopy, cfgName)
   353  		if err != nil {
   354  			if errors.Is(err, openid.ErrProviderConfigNotFound) {
   355  				writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminNoSuchConfigTarget), r.URL)
   356  				return
   357  			}
   358  
   359  			writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
   360  			return
   361  		}
   362  
   363  		hasEnv := false
   364  		for _, ci := range cfgInfos {
   365  			if ci.IsCfg && ci.IsEnv {
   366  				hasEnv = true
   367  				break
   368  			}
   369  		}
   370  
   371  		if hasEnv {
   372  			writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigEnvOverridden), r.URL)
   373  			return
   374  		}
   375  	case madmin.LDAPIDPCfg:
   376  		subSys = config.IdentityLDAPSubSys
   377  		cfgInfos, err := globalIAMSys.LDAPConfig.GetConfigInfo(cfgCopy, cfgName)
   378  		if err != nil {
   379  			if errors.Is(err, openid.ErrProviderConfigNotFound) {
   380  				writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminNoSuchConfigTarget), r.URL)
   381  				return
   382  			}
   383  
   384  			writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
   385  			return
   386  		}
   387  
   388  		hasEnv := false
   389  		for _, ci := range cfgInfos {
   390  			if ci.IsCfg && ci.IsEnv {
   391  				hasEnv = true
   392  				break
   393  			}
   394  		}
   395  
   396  		if hasEnv {
   397  			writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigEnvOverridden), r.URL)
   398  			return
   399  		}
   400  	default:
   401  		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
   402  		return
   403  	}
   404  
   405  	cfg, err := readServerConfig(ctx, objectAPI, nil)
   406  	if err != nil {
   407  		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
   408  		return
   409  	}
   410  
   411  	cfgKey := fmt.Sprintf("%s:%s", subSys, cfgName)
   412  	if cfgName == madmin.Default {
   413  		cfgKey = subSys
   414  	}
   415  	if err = cfg.DelKVS(cfgKey); err != nil {
   416  		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
   417  		return
   418  	}
   419  	if err = validateConfig(ctx, cfg, subSys); err != nil {
   420  
   421  		var validationErr ldap.Validation
   422  		if errors.As(err, &validationErr) {
   423  			// If we got an LDAP validation error, we need to send appropriate
   424  			// error message back to client (likely mc).
   425  			writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigLDAPValidation),
   426  				validationErr.FormatError(), r.URL)
   427  			return
   428  		}
   429  
   430  		writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL)
   431  		return
   432  	}
   433  	if err = saveServerConfig(ctx, objectAPI, cfg); err != nil {
   434  		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
   435  		return
   436  	}
   437  
   438  	dynamic := config.SubSystemsDynamic.Contains(subSys)
   439  	if dynamic {
   440  		applyDynamic(ctx, objectAPI, cfg, subSys, r, w)
   441  	}
   442  }