github.com/minio/madmin-go/v2@v2.2.1/idp-commands.go (about)

     1  //
     2  // Copyright (c) 2015-2022 MinIO, Inc.
     3  //
     4  // This file is part of MinIO Object Storage stack
     5  //
     6  // This program is free software: you can redistribute it and/or modify
     7  // it under the terms of the GNU Affero General Public License as
     8  // published by the Free Software Foundation, either version 3 of the
     9  // License, or (at your option) any later version.
    10  //
    11  // This program is distributed in the hope that it will be useful,
    12  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    13  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    14  // GNU Affero General Public License for more details.
    15  //
    16  // You should have received a copy of the GNU Affero General Public License
    17  // along with this program. If not, see <http://www.gnu.org/licenses/>.
    18  //
    19  
    20  package madmin
    21  
    22  import (
    23  	"context"
    24  	"encoding/json"
    25  	"errors"
    26  	"fmt"
    27  	"net/http"
    28  	"net/url"
    29  	"strings"
    30  	"time"
    31  
    32  	"github.com/minio/minio-go/v7/pkg/set"
    33  )
    34  
    35  // AddOrUpdateIDPConfig - creates a new or updates an existing IDP
    36  // configuration on the server.
    37  func (adm *AdminClient) AddOrUpdateIDPConfig(ctx context.Context, cfgType, cfgName, cfgData string, update bool) (restart bool, err error) {
    38  	encBytes, err := EncryptData(adm.getSecretKey(), []byte(cfgData))
    39  	if err != nil {
    40  		return false, err
    41  	}
    42  
    43  	method := http.MethodPut
    44  	if update {
    45  		method = http.MethodPost
    46  	}
    47  
    48  	if cfgName == "" {
    49  		cfgName = Default
    50  	}
    51  
    52  	h := make(http.Header, 1)
    53  	h.Add("Content-Type", "application/octet-stream")
    54  	reqData := requestData{
    55  		customHeaders: h,
    56  		relPath:       strings.Join([]string{adminAPIPrefix, "idp-config", cfgType, cfgName}, "/"),
    57  		content:       encBytes,
    58  	}
    59  
    60  	resp, err := adm.executeMethod(ctx, method, reqData)
    61  	defer closeResponse(resp)
    62  	if err != nil {
    63  		return false, err
    64  	}
    65  
    66  	// FIXME: Remove support for this older API in 2023-04 (about 6 months).
    67  	//
    68  	// Attempt to fall back to older IDP API.
    69  	if resp.StatusCode == http.StatusUpgradeRequired {
    70  		// close old response
    71  		closeResponse(resp)
    72  
    73  		// Fallback is needed for `mc admin idp set myminio openid ...` only, as
    74  		// this was the only released API supported in the older version.
    75  
    76  		queryParams := make(url.Values, 2)
    77  		queryParams.Set("type", cfgType)
    78  		queryParams.Set("name", cfgName)
    79  		reqData := requestData{
    80  			customHeaders: h,
    81  			relPath:       adminAPIPrefix + "/idp-config",
    82  			queryValues:   queryParams,
    83  			content:       encBytes,
    84  		}
    85  		resp, err = adm.executeMethod(ctx, http.MethodPut, reqData)
    86  		defer closeResponse(resp)
    87  		if err != nil {
    88  			return false, err
    89  		}
    90  	}
    91  
    92  	if resp.StatusCode != http.StatusOK {
    93  		return false, httpRespToErrorResponse(resp)
    94  	}
    95  
    96  	return resp.Header.Get(ConfigAppliedHeader) != ConfigAppliedTrue, nil
    97  }
    98  
    99  // IDPCfgInfo represents a single configuration or related parameter
   100  type IDPCfgInfo struct {
   101  	Key   string `json:"key"`
   102  	Value string `json:"value"`
   103  	IsCfg bool   `json:"isCfg"`
   104  	IsEnv bool   `json:"isEnv"` // relevant only when isCfg=true
   105  }
   106  
   107  // IDPConfig contains IDP configuration information returned by server.
   108  type IDPConfig struct {
   109  	Type string       `json:"type"`
   110  	Name string       `json:"name,omitempty"`
   111  	Info []IDPCfgInfo `json:"info"`
   112  }
   113  
   114  // Constants for IDP configuration types.
   115  const (
   116  	OpenidIDPCfg string = "openid"
   117  	LDAPIDPCfg   string = "ldap"
   118  )
   119  
   120  // ValidIDPConfigTypes - set of valid IDP configs.
   121  var ValidIDPConfigTypes = set.CreateStringSet(OpenidIDPCfg, LDAPIDPCfg)
   122  
   123  // GetIDPConfig - fetch IDP config from server.
   124  func (adm *AdminClient) GetIDPConfig(ctx context.Context, cfgType, cfgName string) (c IDPConfig, err error) {
   125  	if !ValidIDPConfigTypes.Contains(cfgType) {
   126  		return c, fmt.Errorf("Invalid config type: %s", cfgType)
   127  	}
   128  
   129  	if cfgName == "" {
   130  		cfgName = Default
   131  	}
   132  
   133  	reqData := requestData{
   134  		relPath: strings.Join([]string{adminAPIPrefix, "idp-config", cfgType, cfgName}, "/"),
   135  	}
   136  
   137  	resp, err := adm.executeMethod(ctx, http.MethodGet, reqData)
   138  	defer closeResponse(resp)
   139  	if err != nil {
   140  		return c, err
   141  	}
   142  
   143  	// FIXME: Remove support for this older API in 2023-04 (about 6 months).
   144  	//
   145  	// Attempt to fall back to older IDP API.
   146  	if resp.StatusCode == http.StatusUpgradeRequired {
   147  		// close old response
   148  		closeResponse(resp)
   149  
   150  		queryParams := make(url.Values, 2)
   151  		queryParams.Set("type", cfgType)
   152  		queryParams.Set("name", cfgName)
   153  		reqData := requestData{
   154  			relPath:     adminAPIPrefix + "/idp-config",
   155  			queryValues: queryParams,
   156  		}
   157  		resp, err = adm.executeMethod(ctx, http.MethodGet, reqData)
   158  		defer closeResponse(resp)
   159  		if err != nil {
   160  			return c, err
   161  		}
   162  	}
   163  
   164  	if resp.StatusCode != http.StatusOK {
   165  		return c, httpRespToErrorResponse(resp)
   166  	}
   167  
   168  	content, err := DecryptData(adm.getSecretKey(), resp.Body)
   169  	if err != nil {
   170  		return c, err
   171  	}
   172  
   173  	err = json.Unmarshal(content, &c)
   174  	return c, err
   175  }
   176  
   177  // IDPListItem - represents an item in the List IDPs call.
   178  type IDPListItem struct {
   179  	Type    string `json:"type"`
   180  	Name    string `json:"name"`
   181  	Enabled bool   `json:"enabled"`
   182  	RoleARN string `json:"roleARN,omitempty"`
   183  }
   184  
   185  // ListIDPConfig - list IDP configuration on the server.
   186  func (adm *AdminClient) ListIDPConfig(ctx context.Context, cfgType string) ([]IDPListItem, error) {
   187  	if !ValidIDPConfigTypes.Contains(cfgType) {
   188  		return nil, fmt.Errorf("Invalid config type: %s", cfgType)
   189  	}
   190  
   191  	reqData := requestData{
   192  		relPath: strings.Join([]string{adminAPIPrefix, "idp-config", cfgType}, "/"),
   193  	}
   194  
   195  	resp, err := adm.executeMethod(ctx, http.MethodGet, reqData)
   196  	defer closeResponse(resp)
   197  	if err != nil {
   198  		return nil, err
   199  	}
   200  
   201  	// FIXME: Remove support for this older API in 2023-04 (about 6 months).
   202  	//
   203  	// Attempt to fall back to older IDP API.
   204  	if resp.StatusCode == http.StatusUpgradeRequired {
   205  		// close old response
   206  		closeResponse(resp)
   207  
   208  		queryParams := make(url.Values, 2)
   209  		queryParams.Set("type", cfgType)
   210  		reqData := requestData{
   211  			relPath:     adminAPIPrefix + "/idp-config",
   212  			queryValues: queryParams,
   213  		}
   214  		resp, err = adm.executeMethod(ctx, http.MethodGet, reqData)
   215  		defer closeResponse(resp)
   216  		if err != nil {
   217  			return nil, err
   218  		}
   219  	}
   220  
   221  	if resp.StatusCode != http.StatusOK {
   222  		return nil, httpRespToErrorResponse(resp)
   223  	}
   224  
   225  	content, err := DecryptData(adm.getSecretKey(), resp.Body)
   226  	if err != nil {
   227  		return nil, err
   228  	}
   229  
   230  	var lst []IDPListItem
   231  	err = json.Unmarshal(content, &lst)
   232  	return lst, err
   233  }
   234  
   235  // DeleteIDPConfig - delete an IDP configuration on the server.
   236  func (adm *AdminClient) DeleteIDPConfig(ctx context.Context, cfgType, cfgName string) (restart bool, err error) {
   237  	if cfgName == "" {
   238  		cfgName = Default
   239  	}
   240  	reqData := requestData{
   241  		relPath: strings.Join([]string{adminAPIPrefix, "idp-config", cfgType, cfgName}, "/"),
   242  	}
   243  
   244  	resp, err := adm.executeMethod(ctx, http.MethodDelete, reqData)
   245  	defer closeResponse(resp)
   246  	if err != nil {
   247  		return false, err
   248  	}
   249  
   250  	// FIXME: Remove support for this older API in 2023-04 (about 6 months).
   251  	//
   252  	// Attempt to fall back to older IDP API.
   253  	if resp.StatusCode == http.StatusUpgradeRequired {
   254  		// close old response
   255  		closeResponse(resp)
   256  
   257  		queryParams := make(url.Values, 2)
   258  		queryParams.Set("type", cfgType)
   259  		queryParams.Set("name", cfgName)
   260  		reqData := requestData{
   261  			relPath:     adminAPIPrefix + "/idp-config",
   262  			queryValues: queryParams,
   263  		}
   264  		resp, err = adm.executeMethod(ctx, http.MethodDelete, reqData)
   265  		defer closeResponse(resp)
   266  		if err != nil {
   267  			return false, err
   268  		}
   269  	}
   270  
   271  	if resp.StatusCode != http.StatusOK {
   272  		return false, httpRespToErrorResponse(resp)
   273  	}
   274  
   275  	return resp.Header.Get(ConfigAppliedHeader) != ConfigAppliedTrue, nil
   276  }
   277  
   278  // PolicyEntitiesResult - contains response to a policy entities query.
   279  type PolicyEntitiesResult struct {
   280  	Timestamp      time.Time             `json:"timestamp"`
   281  	UserMappings   []UserPolicyEntities  `json:"userMappings,omitempty"`
   282  	GroupMappings  []GroupPolicyEntities `json:"groupMappings,omitempty"`
   283  	PolicyMappings []PolicyEntities      `json:"policyMappings,omitempty"`
   284  }
   285  
   286  // UserPolicyEntities - user -> policies mapping
   287  type UserPolicyEntities struct {
   288  	User     string   `json:"user"`
   289  	Policies []string `json:"policies"`
   290  }
   291  
   292  // GroupPolicyEntities - group -> policies mapping
   293  type GroupPolicyEntities struct {
   294  	Group    string   `json:"group"`
   295  	Policies []string `json:"policies"`
   296  }
   297  
   298  // PolicyEntities - policy -> user+group mapping
   299  type PolicyEntities struct {
   300  	Policy string   `json:"policy"`
   301  	Users  []string `json:"users"`
   302  	Groups []string `json:"groups"`
   303  }
   304  
   305  // PolicyEntitiesQuery - contains request info for policy entities query.
   306  type PolicyEntitiesQuery struct {
   307  	Users  []string
   308  	Groups []string
   309  	Policy []string
   310  }
   311  
   312  // GetLDAPPolicyEntities - returns LDAP policy entities.
   313  func (adm *AdminClient) GetLDAPPolicyEntities(ctx context.Context,
   314  	q PolicyEntitiesQuery,
   315  ) (r PolicyEntitiesResult, err error) {
   316  	params := make(url.Values)
   317  	params["user"] = q.Users
   318  	params["group"] = q.Groups
   319  	params["policy"] = q.Policy
   320  
   321  	reqData := requestData{
   322  		relPath:     adminAPIPrefix + "/idp/ldap/policy-entities",
   323  		queryValues: params,
   324  	}
   325  
   326  	resp, err := adm.executeMethod(ctx, http.MethodGet, reqData)
   327  	defer closeResponse(resp)
   328  	if err != nil {
   329  		return r, err
   330  	}
   331  
   332  	if resp.StatusCode != http.StatusOK {
   333  		return r, httpRespToErrorResponse(resp)
   334  	}
   335  
   336  	content, err := DecryptData(adm.getSecretKey(), resp.Body)
   337  	if err != nil {
   338  		return r, err
   339  	}
   340  
   341  	err = json.Unmarshal(content, &r)
   342  	return r, err
   343  }
   344  
   345  // PolicyAssociationResp - result of a policy association request.
   346  type PolicyAssociationResp struct {
   347  	PoliciesAttached []string `json:"policiesAttached,omitempty"`
   348  	PoliciesDetached []string `json:"policiesDetached,omitempty"`
   349  
   350  	UpdatedAt time.Time `json:"updatedAt"`
   351  }
   352  
   353  // PolicyAssociationReq - request to attach/detach policies from/to a user or
   354  // group.
   355  type PolicyAssociationReq struct {
   356  	Policies []string `json:"policies"`
   357  
   358  	// Exactly one of the following must be non-empty in a valid request.
   359  	User  string `json:"user,omitempty"`
   360  	Group string `json:"group,omitempty"`
   361  }
   362  
   363  // IsValid validates the object and returns a reason for when it is not.
   364  func (p PolicyAssociationReq) IsValid() error {
   365  	if len(p.Policies) == 0 {
   366  		return errors.New("no policy names were given")
   367  	}
   368  	for _, p := range p.Policies {
   369  		if p == "" {
   370  			return errors.New("an empty policy name was given")
   371  		}
   372  	}
   373  
   374  	if p.User == "" && p.Group == "" {
   375  		return errors.New("no user or group association was given")
   376  	}
   377  
   378  	if p.User != "" && p.Group != "" {
   379  		return errors.New("either a group or a user association must be given, not both")
   380  	}
   381  
   382  	return nil
   383  }
   384  
   385  // AttachPolicyLDAP - client call to attach policies for LDAP.
   386  func (adm *AdminClient) AttachPolicyLDAP(ctx context.Context, par PolicyAssociationReq) (PolicyAssociationResp, error) {
   387  	return adm.attachOrDetachPolicyLDAP(ctx, true, par)
   388  }
   389  
   390  // DetachPolicyLDAP - client call to detach policies for LDAP.
   391  func (adm *AdminClient) DetachPolicyLDAP(ctx context.Context, par PolicyAssociationReq) (PolicyAssociationResp, error) {
   392  	return adm.attachOrDetachPolicyLDAP(ctx, false, par)
   393  }
   394  
   395  func (adm *AdminClient) attachOrDetachPolicyLDAP(ctx context.Context, isAttach bool,
   396  	par PolicyAssociationReq,
   397  ) (PolicyAssociationResp, error) {
   398  	plainBytes, err := json.Marshal(par)
   399  	if err != nil {
   400  		return PolicyAssociationResp{}, err
   401  	}
   402  
   403  	encBytes, err := EncryptData(adm.getSecretKey(), plainBytes)
   404  	if err != nil {
   405  		return PolicyAssociationResp{}, err
   406  	}
   407  
   408  	suffix := "detach"
   409  	if isAttach {
   410  		suffix = "attach"
   411  	}
   412  	h := make(http.Header, 1)
   413  	h.Add("Content-Type", "application/octet-stream")
   414  	reqData := requestData{
   415  		customHeaders: h,
   416  		relPath:       adminAPIPrefix + "/idp/ldap/policy/" + suffix,
   417  		content:       encBytes,
   418  	}
   419  
   420  	resp, err := adm.executeMethod(ctx, http.MethodPost, reqData)
   421  	defer closeResponse(resp)
   422  	if err != nil {
   423  		return PolicyAssociationResp{}, err
   424  	}
   425  
   426  	if resp.StatusCode != http.StatusOK {
   427  		return PolicyAssociationResp{}, httpRespToErrorResponse(resp)
   428  	}
   429  
   430  	content, err := DecryptData(adm.getSecretKey(), resp.Body)
   431  	if err != nil {
   432  		return PolicyAssociationResp{}, err
   433  	}
   434  
   435  	r := PolicyAssociationResp{}
   436  	err = json.Unmarshal(content, &r)
   437  	return r, err
   438  }