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 }