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 }