Add external IDP management Admin API for OpenID (#15152)

This commit is contained in:
Aditya Manthramurthy 2022-07-05 18:18:04 -07:00 committed by GitHub
parent ac055b09e9
commit af9bc7ea7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 616 additions and 116 deletions

View File

@ -0,0 +1,321 @@
// Copyright (c) 2015-2022 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 (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"github.com/gorilla/mux"
"github.com/minio/madmin-go"
"github.com/minio/minio-go/v7/pkg/set"
"github.com/minio/minio/internal/config"
"github.com/minio/minio/internal/config/identity/openid"
"github.com/minio/minio/internal/logger"
iampolicy "github.com/minio/pkg/iam/policy"
)
// List of implemented ID config types.
var idCfgTypes = set.CreateStringSet("openid")
// SetIdentityProviderCfg:
//
// PUT <admin-prefix>/id-cfg?type=openid&name=dex1
func (a adminAPIHandlers) SetIdentityProviderCfg(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "SetIdentityCfg")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.ConfigUpdateAdminAction)
if objectAPI == nil {
return
}
if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 {
// More than maxConfigSize bytes were available
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigTooLarge), r.URL)
return
}
password := cred.SecretKey
reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
if err != nil {
logger.LogIf(ctx, err, logger.Application)
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL)
return
}
cfgType := mux.Vars(r)["type"]
if !idCfgTypes.Contains(cfgType) {
// TODO: change this to invalid type error when implementation
// is complete.
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
return
}
var cfgDataBuilder strings.Builder
switch cfgType {
case "openid":
fmt.Fprintf(&cfgDataBuilder, "identity_openid")
}
// Ensure body content type is opaque.
contentType := r.Header.Get("Content-Type")
if contentType != "application/octet-stream" {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrBadRequest), r.URL)
return
}
// Subsystem configuration name could be empty.
cfgName := mux.Vars(r)["name"]
if cfgName != "" {
fmt.Fprintf(&cfgDataBuilder, "%s%s", config.SubSystemSeparator, cfgName)
}
fmt.Fprintf(&cfgDataBuilder, "%s%s", config.KvSpaceSeparator, string(reqBytes))
cfgData := cfgDataBuilder.String()
subSys, _, _, err := config.GetSubSys(cfgData)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
cfg, err := readServerConfig(ctx, objectAPI)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
dynamic, err := cfg.ReadConfig(strings.NewReader(cfgData))
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
// IDP config is not dynamic. Sanity check.
if dynamic {
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
return
}
if err = validateConfig(cfg, subSys); err != nil {
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL)
return
}
// Update the actual server config on disk.
if err = saveServerConfig(ctx, objectAPI, cfg); err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
// Write to the config input KV to history.
if err = saveServerConfigHistory(ctx, objectAPI, []byte(cfgData)); err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
writeSuccessResponseHeadersOnly(w)
}
// GetIdentityProviderCfg:
//
// GET <admin-prefix>/id-cfg?type=openid&name=dex_test
//
// GetIdentityProviderCfg returns a list of configured IDPs on the server if
// name is empty. If name is non-empty, returns the configuration details for
// the IDP of the given type and configuration name. The configuration name for
// the default ("un-named") configuration target is `_`.
func (a adminAPIHandlers) GetIdentityProviderCfg(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "GetIdentityProviderCfg")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.ConfigUpdateAdminAction)
if objectAPI == nil {
return
}
cfgType := mux.Vars(r)["type"]
cfgName := r.Form.Get("name")
password := cred.SecretKey
if !idCfgTypes.Contains(cfgType) {
// TODO: change this to invalid type error when implementation
// is complete.
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
return
}
// If no cfgName is provided, we list.
if cfgName == "" {
a.listIdentityProviders(ctx, w, r, cfgType, password)
return
}
cfg := globalServerConfig.Clone()
cfgInfos, err := globalOpenIDConfig.GetConfigInfo(cfg, cfgName)
if err != nil {
if err == openid.ErrProviderConfigNotFound {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminNoSuchConfigTarget), r.URL)
return
}
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
res := madmin.IDPConfig{
Type: cfgType,
Name: cfgName,
Info: cfgInfos,
}
data, err := json.Marshal(res)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
econfigData, err := madmin.EncryptData(password, data)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
writeSuccessResponseJSON(w, econfigData)
}
func (a adminAPIHandlers) listIdentityProviders(ctx context.Context, w http.ResponseWriter, r *http.Request, cfgType, password string) {
// var subSys string
switch cfgType {
case "openid":
// subSys = config.IdentityOpenIDSubSys
default:
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
return
}
cfg := globalServerConfig.Clone()
cfgList, err := globalOpenIDConfig.GetConfigList(cfg)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
data, err := json.Marshal(cfgList)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
econfigData, err := madmin.EncryptData(password, data)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
writeSuccessResponseJSON(w, econfigData)
}
// DeleteIdentityProviderCfg:
//
// DELETE <admin-prefix>/id-cfg?type=openid&name=dex_test
func (a adminAPIHandlers) DeleteIdentityProviderCfg(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "DeleteIdentityProviderCfg")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ConfigUpdateAdminAction)
if objectAPI == nil {
return
}
cfgType := mux.Vars(r)["type"]
cfgName := mux.Vars(r)["name"]
if !idCfgTypes.Contains(cfgType) {
// TODO: change this to invalid type error when implementation
// is complete.
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
return
}
cfg := globalServerConfig.Clone()
cfgInfos, err := globalOpenIDConfig.GetConfigInfo(cfg, cfgName)
if err != nil {
if err == openid.ErrProviderConfigNotFound {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminNoSuchConfigTarget), r.URL)
return
}
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
hasEnv := false
for _, ci := range cfgInfos {
if ci.IsCfg && ci.IsEnv {
hasEnv = true
break
}
}
if hasEnv {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigEnvOverridden), r.URL)
return
}
var subSys string
switch cfgType {
case "openid":
subSys = config.IdentityOpenIDSubSys
default:
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
return
}
cfg, err = readServerConfig(ctx, objectAPI)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
if err = cfg.DelKVS(fmt.Sprintf("%s:%s", subSys, cfgName)); err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
if err = validateConfig(cfg, subSys); err != nil {
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL)
return
}
if err = saveServerConfig(ctx, objectAPI, cfg); err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
dynamic := config.SubSystemsDynamic.Contains(subSys)
if dynamic {
applyDynamic(ctx, objectAPI, cfg, subSys, r, w)
}
}

View File

@ -178,6 +178,13 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
// Import IAM info
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/import-iam").HandlerFunc(httpTraceHdrs(adminAPI.ImportIAM))
// IDentity Provider configuration APIs
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/idp-config").HandlerFunc(gz(httpTraceHdrs(adminAPI.SetIdentityProviderCfg))).Queries("type", "{type:.*}").Queries("name", "{name:.*}")
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/idp-config").HandlerFunc(gz(httpTraceHdrs(adminAPI.GetIdentityProviderCfg))).Queries("type", "{type:.*}")
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/idp-config").HandlerFunc(gz(httpTraceHdrs(adminAPI.DeleteIdentityProviderCfg))).Queries("type", "{type:.*}").Queries("name", "{name:.*}")
// -- END IAM APIs --
// GetBucketQuotaConfig
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/get-bucket-quota").HandlerFunc(
gz(httpTraceHdrs(adminAPI.GetBucketQuotaConfigHandler))).Queries("bucket", "{bucket:.*}")

View File

@ -267,6 +267,8 @@ const (
ErrAdminConfigNoQuorum
ErrAdminConfigTooLarge
ErrAdminConfigBadJSON
ErrAdminNoSuchConfigTarget
ErrAdminConfigEnvOverridden
ErrAdminConfigDuplicateKeys
ErrAdminCredentialsMismatch
ErrInsecureClientRequest
@ -1240,11 +1242,21 @@ var errorCodes = errorCodeMap{
maxEConfigJSONSize),
HTTPStatusCode: http.StatusBadRequest,
},
ErrAdminNoSuchConfigTarget: {
Code: "XMinioAdminNoSuchConfigTarget",
Description: "No such named configuration target exists",
HTTPStatusCode: http.StatusBadRequest,
},
ErrAdminConfigBadJSON: {
Code: "XMinioAdminConfigBadJSON",
Description: "JSON configuration provided is of incorrect format",
HTTPStatusCode: http.StatusBadRequest,
},
ErrAdminConfigEnvOverridden: {
Code: "XMinioAdminConfigEnvOverridden",
Description: "Unable to update config via Admin API due to environment variable override",
HTTPStatusCode: http.StatusBadRequest,
},
ErrAdminConfigDuplicateKeys: {
Code: "XMinioAdminConfigDuplicateKeys",
Description: "JSON configuration provided has objects with duplicate keys",

File diff suppressed because one or more lines are too long

View File

@ -1100,6 +1100,10 @@ func (c Config) ResolveConfigParam(subSys, target, cfgParam string) (value strin
}
defValue, isFound := defKVS.Lookup(cfgParam)
// Comments usually are absent from `defKVS`, so we handle it specially.
if cfgParam == Comment {
defValue, isFound = "", true
}
if !isFound {
return
}
@ -1134,3 +1138,54 @@ func (c Config) ResolveConfigParam(subSys, target, cfgParam string) (value strin
cs = ValueSourceDef
return
}
// KVSrc represents a configuration parameter key and value along with the
// source of the value.
type KVSrc struct {
Key string
Value string
Src ValueSource
}
// GetResolvedConfigParams returns all applicable config parameters with their
// value sources.
func (c Config) GetResolvedConfigParams(subSys, target string) ([]KVSrc, error) {
// Initially only support OpenID
if !resolvableSubsystems.Contains(subSys) {
return nil, fmt.Errorf("unsupported subsystem: %s", subSys)
}
// Check if config param requested is valid.
defKVS, ok := DefaultKVS[subSys]
if !ok {
return nil, fmt.Errorf("unknown subsystem: %s", subSys)
}
r := make([]KVSrc, 0, len(defKVS)+1)
for _, kv := range defKVS {
v, vs := c.ResolveConfigParam(subSys, target, kv.Key)
// Fix `vs` when default.
if v == kv.Value {
vs = ValueSourceDef
}
r = append(r, KVSrc{
Key: kv.Key,
Value: v,
Src: vs,
})
}
// Add the comment key as well if non-empty.
v, vs := c.ResolveConfigParam(subSys, target, Comment)
if vs != ValueSourceDef {
r = append(r, KVSrc{
Key: Comment,
Value: v,
Src: vs,
})
}
return r, nil
}

View File

@ -52,6 +52,7 @@ var (
config.HelpKV{
Key: ClientSecret,
Description: `secret for the unique public identifier for apps` + defaultHelpPostfix(ClientSecret),
Sensitive: true,
Type: "string",
},
config.HelpKV{

View File

@ -24,6 +24,7 @@ import (
"errors"
"io"
"net/http"
"sort"
"strconv"
"strings"
"sync"
@ -391,6 +392,107 @@ func LookupConfig(s config.Config, transport http.RoundTripper, closeRespFn func
return c, nil
}
// ErrProviderConfigNotFound - represents a non-existing provider error.
var ErrProviderConfigNotFound = errors.New("provider configuration not found")
// GetConfigInfo - returns configuration and related info for the given IDP
// provider.
func (r *Config) GetConfigInfo(s config.Config, cfgName string) ([]madmin.IDPCfgInfo, error) {
openIDConfigs, err := s.GetAvailableTargets(config.IdentityOpenIDSubSys)
if err != nil {
return nil, err
}
present := false
for _, cfg := range openIDConfigs {
if cfg == cfgName {
present = true
break
}
}
if !present {
return nil, ErrProviderConfigNotFound
}
kvsrcs, err := s.GetResolvedConfigParams(config.IdentityOpenIDSubSys, cfgName)
if err != nil {
return nil, err
}
res := make([]madmin.IDPCfgInfo, 0, len(kvsrcs)+1)
for _, kvsrc := range kvsrcs {
// skip default values.
if kvsrc.Src == config.ValueSourceDef {
if kvsrc.Key != madmin.EnableKey {
continue
}
// set an explicit on/off from live configuration.
kvsrc.Value = "off"
if _, ok := r.ProviderCfgs[cfgName]; ok {
if r.Enabled {
kvsrc.Value = "on"
}
}
}
res = append(res, madmin.IDPCfgInfo{
Key: kvsrc.Key,
Value: kvsrc.Value,
IsCfg: true,
IsEnv: kvsrc.Src == config.ValueSourceEnv,
})
}
if provCfg, exists := r.ProviderCfgs[cfgName]; exists && provCfg.RolePolicy != "" {
// Append roleARN
res = append(res, madmin.IDPCfgInfo{
Key: "roleARN",
Value: provCfg.roleArn.String(),
IsCfg: false,
})
}
// sort the structs by the key
sort.Slice(res, func(i, j int) bool {
return res[i].Key < res[j].Key
})
return res, nil
}
// GetConfigList - list openID configurations
func (r *Config) GetConfigList(s config.Config) ([]madmin.IDPListItem, error) {
openIDConfigs, err := s.GetAvailableTargets(config.IdentityOpenIDSubSys)
if err != nil {
return nil, err
}
var res []madmin.IDPListItem
for _, cfg := range openIDConfigs {
pcfg, ok := r.ProviderCfgs[cfg]
if !ok {
res = append(res, madmin.IDPListItem{
Type: "openid",
Name: cfg,
Enabled: false,
})
} else {
var roleARN string
if pcfg.RolePolicy != "" {
roleARN = pcfg.roleArn.String()
}
res = append(res, madmin.IDPListItem{
Type: "openid",
Name: cfg,
Enabled: r.Enabled,
RoleARN: roleARN,
})
}
}
return res, nil
}
// Enabled returns if configURL is enabled.
func Enabled(kvs config.KVS) bool {
return kvs.Get(ConfigURL) != ""