mirror of
https://github.com/minio/minio.git
synced 2025-11-10 05:59:43 -05:00
fix: progagation of service accounts for site replication (#14054)
- Only non-root-owned service accounts are replicated for now. - Add integration tests for OIDC with site replication
This commit is contained in:
committed by
GitHub
parent
47e4a36d7e
commit
0a224654c2
@@ -743,14 +743,8 @@ func (a adminAPIHandlers) UpdateServiceAccount(w http.ResponseWriter, r *http.Re
|
||||
return
|
||||
}
|
||||
|
||||
// Call site replication hook. Only LDAP accounts are supported for
|
||||
// replication operations.
|
||||
svcAccClaims, err := globalIAMSys.GetClaimsForSvcAcc(ctx, accessKey)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
if _, isLDAPAccount := svcAccClaims[ldapUserN]; isLDAPAccount {
|
||||
// Call site replication hook - non-root user accounts are replicated.
|
||||
if svcAccount.ParentUser != globalActiveCred.AccessKey {
|
||||
err = globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
||||
Type: madmin.SRIAMItemSvcAcc,
|
||||
SvcAccChange: &madmin.SRSvcAccChange{
|
||||
@@ -767,6 +761,7 @@ func (a adminAPIHandlers) UpdateServiceAccount(w http.ResponseWriter, r *http.Re
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
writeSuccessNoContent(w)
|
||||
}
|
||||
|
||||
@@ -990,22 +985,14 @@ func (a adminAPIHandlers) DeleteServiceAccount(w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
}
|
||||
|
||||
// Save svc acc claims before deletion (for site replication hook).
|
||||
svcAccClaims, err := globalIAMSys.GetClaimsForSvcAcc(ctx, serviceAccount)
|
||||
if err != nil && err != errNoSuchServiceAccount {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
err = globalIAMSys.DeleteServiceAccount(ctx, serviceAccount, true)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Call site replication hook. Only LDAP accounts are supported for
|
||||
// replication operations.
|
||||
if _, isLDAPAccount := svcAccClaims[ldapUserN]; isLDAPAccount {
|
||||
// Call site replication hook - non-root user accounts are replicated.
|
||||
if svcAccount.ParentUser != globalActiveCred.AccessKey {
|
||||
err = globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
||||
Type: madmin.SRIAMItemSvcAcc,
|
||||
SvcAccChange: &madmin.SRSvcAccChange{
|
||||
|
||||
@@ -20,19 +20,15 @@ package cmd
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc"
|
||||
"github.com/minio/madmin-go"
|
||||
minio "github.com/minio/minio-go/v7"
|
||||
cr "github.com/minio/minio-go/v7/pkg/credentials"
|
||||
"github.com/minio/minio-go/v7/pkg/set"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func runAllIAMSTSTests(suite *TestSuiteIAM, c *check) {
|
||||
@@ -742,7 +738,7 @@ func (s *TestSuiteIAM) TestOpenIDSTS(c *check) {
|
||||
}
|
||||
|
||||
// Generate web identity STS token by interacting with OpenID IDP.
|
||||
token, err := mockTestUserInteraction(ctx, testProvider, "dillon@example.io", "dillon")
|
||||
token, err := MockOpenIDTestUserInteraction(ctx, testAppParams, "dillon@example.io", "dillon")
|
||||
if err != nil {
|
||||
c.Fatalf("mock user err: %v", err)
|
||||
}
|
||||
@@ -817,7 +813,7 @@ func (s *TestSuiteIAM) TestOpenIDSTSAddUser(c *check) {
|
||||
}
|
||||
|
||||
// Generate web identity STS token by interacting with OpenID IDP.
|
||||
token, err := mockTestUserInteraction(ctx, testProvider, "dillon@example.io", "dillon")
|
||||
token, err := MockOpenIDTestUserInteraction(ctx, testAppParams, "dillon@example.io", "dillon")
|
||||
if err != nil {
|
||||
c.Fatalf("mock user err: %v", err)
|
||||
}
|
||||
@@ -907,7 +903,7 @@ func (s *TestSuiteIAM) TestOpenIDServiceAcc(c *check) {
|
||||
}
|
||||
|
||||
// Generate web identity STS token by interacting with OpenID IDP.
|
||||
token, err := mockTestUserInteraction(ctx, testProvider, "dillon@example.io", "dillon")
|
||||
token, err := MockOpenIDTestUserInteraction(ctx, testAppParams, "dillon@example.io", "dillon")
|
||||
if err != nil {
|
||||
c.Fatalf("mock user err: %v", err)
|
||||
}
|
||||
@@ -985,132 +981,11 @@ func (s *TestSuiteIAM) TestOpenIDServiceAcc(c *check) {
|
||||
c.assertSvcAccDeletion(ctx, s, userAdmClient, value.AccessKeyID, bucket)
|
||||
}
|
||||
|
||||
type providerParams struct {
|
||||
clientID, clientSecret, providerURL, redirectURL string
|
||||
}
|
||||
|
||||
var testProvider = providerParams{
|
||||
clientID: "minio-client-app",
|
||||
clientSecret: "minio-client-app-secret",
|
||||
providerURL: "http://127.0.0.1:5556/dex",
|
||||
redirectURL: "http://127.0.0.1:10000/oauth_callback",
|
||||
}
|
||||
|
||||
// mockTestUserInteraction - tries to login to dex using provided credentials.
|
||||
// It performs the user's browser interaction to login and retrieves the auth
|
||||
// code from dex and exchanges it for a JWT.
|
||||
func mockTestUserInteraction(ctx context.Context, pro providerParams, username, password string) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
provider, err := oidc.NewProvider(ctx, pro.providerURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to create provider: %v", err)
|
||||
}
|
||||
|
||||
// Configure an OpenID Connect aware OAuth2 client.
|
||||
oauth2Config := oauth2.Config{
|
||||
ClientID: pro.clientID,
|
||||
ClientSecret: pro.clientSecret,
|
||||
RedirectURL: pro.redirectURL,
|
||||
|
||||
// Discovery returns the OAuth2 endpoints.
|
||||
Endpoint: provider.Endpoint(),
|
||||
|
||||
// "openid" is a required scope for OpenID Connect flows.
|
||||
Scopes: []string{oidc.ScopeOpenID, "groups"},
|
||||
}
|
||||
|
||||
state := "xxx"
|
||||
authCodeURL := oauth2Config.AuthCodeURL(state)
|
||||
// fmt.Printf("authcodeurl: %s\n", authCodeURL)
|
||||
|
||||
var lastReq *http.Request
|
||||
checkRedirect := func(req *http.Request, via []*http.Request) error {
|
||||
// fmt.Printf("CheckRedirect:\n")
|
||||
// fmt.Printf("Upcoming: %s %#v\n", req.URL.String(), req)
|
||||
// for _, c := range via {
|
||||
// fmt.Printf("Sofar: %s %#v\n", c.URL.String(), c)
|
||||
// }
|
||||
// Save the last request in a redirect chain.
|
||||
lastReq = req
|
||||
// We do not follow redirect back to client application.
|
||||
if req.URL.Path == "/oauth_callback" {
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
dexClient := http.Client{
|
||||
CheckRedirect: checkRedirect,
|
||||
}
|
||||
|
||||
u, err := url.Parse(authCodeURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("url parse err: %v", err)
|
||||
}
|
||||
|
||||
// Start the user auth flow. This page would present the login with
|
||||
// email or LDAP option.
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("new request err: %v", err)
|
||||
}
|
||||
_, err = dexClient.Do(req)
|
||||
// fmt.Printf("Do: %#v %#v\n", resp, err)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("auth url request err: %v", err)
|
||||
}
|
||||
|
||||
// Modify u to choose the ldap option
|
||||
u.Path += "/ldap"
|
||||
// fmt.Println(u)
|
||||
|
||||
// Pick the LDAP login option. This would return a form page after
|
||||
// following some redirects. `lastReq` would be the URL of the form
|
||||
// page, where we need to POST (submit) the form.
|
||||
req, err = http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("new request err (/ldap): %v", err)
|
||||
}
|
||||
_, err = dexClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("request err: %v", err)
|
||||
}
|
||||
|
||||
// Fill the login form with our test creds:
|
||||
// fmt.Printf("login form url: %s\n", lastReq.URL.String())
|
||||
formData := url.Values{}
|
||||
formData.Set("login", username)
|
||||
formData.Set("password", password)
|
||||
req, err = http.NewRequestWithContext(ctx, http.MethodPost, lastReq.URL.String(), strings.NewReader(formData.Encode()))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("new request err (/login): %v", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
_, err = dexClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("post form err: %v", err)
|
||||
}
|
||||
// fmt.Printf("resp: %#v %#v\n", resp.StatusCode, resp.Header)
|
||||
// fmt.Printf("lastReq: %#v\n", lastReq.URL.String())
|
||||
|
||||
// On form submission, the last redirect response contains the auth
|
||||
// code, which we now have in `lastReq`. Exchange it for a JWT id_token.
|
||||
q := lastReq.URL.Query()
|
||||
code := q.Get("code")
|
||||
oauth2Token, err := oauth2Config.Exchange(ctx, code)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to exchange code for id token: %v", err)
|
||||
}
|
||||
|
||||
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("id_token not found!")
|
||||
}
|
||||
|
||||
// fmt.Printf("TOKEN: %s\n", rawIDToken)
|
||||
return rawIDToken, nil
|
||||
var testAppParams = OpenIDClientAppParams{
|
||||
ClientID: "minio-client-app",
|
||||
ClientSecret: "minio-client-app-secret",
|
||||
ProviderURL: "http://127.0.0.1:5556/dex",
|
||||
RedirectURL: "http://127.0.0.1:10000/oauth_callback",
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -1248,7 +1123,7 @@ func (s *TestSuiteIAM) TestOpenIDSTSWithRolePolicy(c *check) {
|
||||
}
|
||||
|
||||
// Generate web identity STS token by interacting with OpenID IDP.
|
||||
token, err := mockTestUserInteraction(ctx, testProvider, "dillon@example.io", "dillon")
|
||||
token, err := MockOpenIDTestUserInteraction(ctx, testAppParams, "dillon@example.io", "dillon")
|
||||
if err != nil {
|
||||
c.Fatalf("mock user err: %v", err)
|
||||
}
|
||||
@@ -1294,7 +1169,7 @@ func (s *TestSuiteIAM) TestOpenIDServiceAccWithRolePolicy(c *check) {
|
||||
}
|
||||
|
||||
// Generate web identity STS token by interacting with OpenID IDP.
|
||||
token, err := mockTestUserInteraction(ctx, testProvider, "dillon@example.io", "dillon")
|
||||
token, err := MockOpenIDTestUserInteraction(ctx, testAppParams, "dillon@example.io", "dillon")
|
||||
if err != nil {
|
||||
c.Fatalf("mock user err: %v", err)
|
||||
}
|
||||
|
||||
141
cmd/utils.go
141
cmd/utils.go
@@ -43,6 +43,7 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc"
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/madmin-go"
|
||||
@@ -58,6 +59,7 @@ import (
|
||||
"github.com/minio/minio/internal/rest"
|
||||
"github.com/minio/pkg/certs"
|
||||
"github.com/minio/pkg/env"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -1160,3 +1162,142 @@ func newTLSConfig(getCert certs.GetCertificateFunc) *tls.Config {
|
||||
}
|
||||
return tlsConfig
|
||||
}
|
||||
|
||||
/////////// Types and functions for OpenID IAM testing
|
||||
|
||||
// OpenIDClientAppParams - contains openID client application params, used in
|
||||
// testing.
|
||||
type OpenIDClientAppParams struct {
|
||||
ClientID, ClientSecret, ProviderURL, RedirectURL string
|
||||
}
|
||||
|
||||
// MockOpenIDTestUserInteraction - tries to login to dex using provided credentials.
|
||||
// It performs the user's browser interaction to login and retrieves the auth
|
||||
// code from dex and exchanges it for a JWT.
|
||||
func MockOpenIDTestUserInteraction(ctx context.Context, pro OpenIDClientAppParams, username, password string) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
provider, err := oidc.NewProvider(ctx, pro.ProviderURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to create provider: %v", err)
|
||||
}
|
||||
|
||||
// Configure an OpenID Connect aware OAuth2 client.
|
||||
oauth2Config := oauth2.Config{
|
||||
ClientID: pro.ClientID,
|
||||
ClientSecret: pro.ClientSecret,
|
||||
RedirectURL: pro.RedirectURL,
|
||||
|
||||
// Discovery returns the OAuth2 endpoints.
|
||||
Endpoint: provider.Endpoint(),
|
||||
|
||||
// "openid" is a required scope for OpenID Connect flows.
|
||||
Scopes: []string{oidc.ScopeOpenID, "groups"},
|
||||
}
|
||||
|
||||
state := fmt.Sprintf("x%dx", time.Now().Unix())
|
||||
authCodeURL := oauth2Config.AuthCodeURL(state)
|
||||
// fmt.Printf("authcodeurl: %s\n", authCodeURL)
|
||||
|
||||
var lastReq *http.Request
|
||||
checkRedirect := func(req *http.Request, via []*http.Request) error {
|
||||
// fmt.Printf("CheckRedirect:\n")
|
||||
// fmt.Printf("Upcoming: %s %s\n", req.Method, req.URL.String())
|
||||
// for i, c := range via {
|
||||
// fmt.Printf("Sofar %d: %s %s\n", i, c.Method, c.URL.String())
|
||||
// }
|
||||
// Save the last request in a redirect chain.
|
||||
lastReq = req
|
||||
// We do not follow redirect back to client application.
|
||||
if req.URL.Path == "/oauth_callback" {
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
dexClient := http.Client{
|
||||
CheckRedirect: checkRedirect,
|
||||
}
|
||||
|
||||
u, err := url.Parse(authCodeURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("url parse err: %v", err)
|
||||
}
|
||||
|
||||
// Start the user auth flow. This page would present the login with
|
||||
// email or LDAP option.
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("new request err: %v", err)
|
||||
}
|
||||
_, err = dexClient.Do(req)
|
||||
// fmt.Printf("Do: %#v %#v\n", resp, err)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("auth url request err: %v", err)
|
||||
}
|
||||
|
||||
// Modify u to choose the ldap option
|
||||
u.Path += "/ldap"
|
||||
// fmt.Println(u)
|
||||
|
||||
// Pick the LDAP login option. This would return a form page after
|
||||
// following some redirects. `lastReq` would be the URL of the form
|
||||
// page, where we need to POST (submit) the form.
|
||||
req, err = http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("new request err (/ldap): %v", err)
|
||||
}
|
||||
_, err = dexClient.Do(req)
|
||||
// fmt.Printf("Fetch LDAP login page: %#v %#v\n", resp, err)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("request err: %v", err)
|
||||
}
|
||||
// {
|
||||
// bodyBuf, err := ioutil.ReadAll(resp.Body)
|
||||
// if err != nil {
|
||||
// return "", fmt.Errorf("Error reading body: %v", err)
|
||||
// }
|
||||
// fmt.Printf("bodyBuf (for LDAP login page): %s\n", string(bodyBuf))
|
||||
// }
|
||||
|
||||
// Fill the login form with our test creds:
|
||||
// fmt.Printf("login form url: %s\n", lastReq.URL.String())
|
||||
formData := url.Values{}
|
||||
formData.Set("login", username)
|
||||
formData.Set("password", password)
|
||||
req, err = http.NewRequestWithContext(ctx, http.MethodPost, lastReq.URL.String(), strings.NewReader(formData.Encode()))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("new request err (/login): %v", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
_, err = dexClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("post form err: %v", err)
|
||||
}
|
||||
// fmt.Printf("resp: %#v %#v\n", resp.StatusCode, resp.Header)
|
||||
// bodyBuf, err := ioutil.ReadAll(resp.Body)
|
||||
// if err != nil {
|
||||
// return "", fmt.Errorf("Error reading body: %v", err)
|
||||
// }
|
||||
// fmt.Printf("resp body: %s\n", string(bodyBuf))
|
||||
// fmt.Printf("lastReq: %#v\n", lastReq.URL.String())
|
||||
|
||||
// On form submission, the last redirect response contains the auth
|
||||
// code, which we now have in `lastReq`. Exchange it for a JWT id_token.
|
||||
q := lastReq.URL.Query()
|
||||
// fmt.Printf("lastReq.URL: %#v q: %#v\n", lastReq.URL, q)
|
||||
code := q.Get("code")
|
||||
oauth2Token, err := oauth2Config.Exchange(ctx, code)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to exchange code for id token: %v", err)
|
||||
}
|
||||
|
||||
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("id_token not found!")
|
||||
}
|
||||
|
||||
// fmt.Printf("TOKEN: %s\n", rawIDToken)
|
||||
return rawIDToken, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user