mirror of
https://github.com/pgsty/minio.git
synced 2026-11-10 01:31:52 +03:00
fix(batch): pause transient authorization failures with owned retries
Signed-off-by: Feng Ruohang <rh@vonng.com>
This commit is contained in:
@@ -19,7 +19,9 @@ package plugin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
@@ -180,6 +182,14 @@ func New(args Args) *AuthZPlugin {
|
||||
|
||||
// IsAllowed - checks given policy args is allowed to continue the REST API.
|
||||
func (o *AuthZPlugin) IsAllowed(args policy.Args) (bool, error) {
|
||||
return o.IsAllowedContext(context.Background(), args)
|
||||
}
|
||||
|
||||
// IsAllowedContext requires an explicit decision from a successful response.
|
||||
// The timeout also bounds a server that accepts a connection but never replies.
|
||||
func (o *AuthZPlugin) IsAllowedContext(ctx context.Context, args policy.Args) (bool, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, time.Minute)
|
||||
defer cancel()
|
||||
if o == nil {
|
||||
return false, nil
|
||||
}
|
||||
@@ -193,7 +203,7 @@ func (o *AuthZPlugin) IsAllowed(args policy.Args) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, o.args.URL.String(), bytes.NewReader(inputBytes))
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, o.args.URL.String(), bytes.NewReader(inputBytes))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -209,37 +219,32 @@ func (o *AuthZPlugin) IsAllowed(args policy.Args) (bool, error) {
|
||||
}
|
||||
defer o.args.CloseRespFn(resp.Body)
|
||||
|
||||
// Read the body to be saved later.
|
||||
opaRespBytes, err := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return false, fmt.Errorf("authorization plugin returned HTTP %d", resp.StatusCode)
|
||||
}
|
||||
// Bound input and reject trailing data as well as absent/null decisions.
|
||||
data, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20+1))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Handle large OPA responses when OPA URL is of
|
||||
// form http://localhost:8181/v1/data/httpapi/authz
|
||||
type opaResultAllow struct {
|
||||
Result struct {
|
||||
Allow bool `json:"allow"`
|
||||
} `json:"result"`
|
||||
if len(data) > 1<<20 {
|
||||
return false, fmt.Errorf("authorization plugin response is too large")
|
||||
}
|
||||
|
||||
// Handle simpler OPA responses when OPA URL is of
|
||||
// form http://localhost:8181/v1/data/httpapi/authz/allow
|
||||
type opaResult struct {
|
||||
Result bool `json:"result"`
|
||||
var result struct {
|
||||
Result json.RawMessage `json:"result"`
|
||||
}
|
||||
|
||||
respBody := bytes.NewReader(opaRespBytes)
|
||||
|
||||
var result opaResult
|
||||
if err = json.NewDecoder(respBody).Decode(&result); err != nil {
|
||||
respBody.Seek(0, 0)
|
||||
var resultAllow opaResultAllow
|
||||
if err = json.NewDecoder(respBody).Decode(&resultAllow); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return resultAllow.Result.Allow, nil
|
||||
if err := json.Unmarshal(data, &result); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return result.Result, nil
|
||||
var allowed *bool
|
||||
if err := json.Unmarshal(result.Result, &allowed); err == nil && allowed != nil {
|
||||
return *allowed, nil
|
||||
}
|
||||
var nested struct {
|
||||
Allow *bool `json:"allow"`
|
||||
}
|
||||
if err := json.Unmarshal(result.Result, &nested); err == nil && nested.Allow != nil {
|
||||
return *nested.Allow, nil
|
||||
}
|
||||
return false, fmt.Errorf("authorization plugin response has no boolean decision")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) 2026 PGSTY
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/pgsty/silo-pkg/v3/net"
|
||||
"github.com/pgsty/silo-pkg/v3/policy"
|
||||
)
|
||||
|
||||
func TestAuthZPluginExplicitDecision(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
body string
|
||||
status int
|
||||
allow, fail bool
|
||||
}{
|
||||
{`{"result":true}`, 200, true, false}, {`{"result":false}`, 200, false, false},
|
||||
{`{"result":{"allow":true}}`, 200, true, false}, {`{"result":{"allow":false}}`, 200, false, false},
|
||||
{`{"result":true}`, 503, false, true}, {`{"result":true}`, 403, false, true},
|
||||
{`{}`, 200, false, true}, {`{"error":"unavailable"}`, 200, false, true}, {`{"result":null}`, 200, false, true},
|
||||
{`{"result":{}}`, 200, false, true}, {`{"result":{"allow":null}}`, 200, false, true}, {`{"result":"true"}`, 200, false, true},
|
||||
{`{"result":true} garbage`, 200, false, true},
|
||||
} {
|
||||
t.Run(tc.body+http.StatusText(tc.status), func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(tc.status); io.WriteString(w, tc.body) }))
|
||||
defer server.Close()
|
||||
u, err := net.ParseHTTPURL(server.URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
plugin := New(Args{URL: u, CloseRespFn: func(r io.ReadCloser) { r.Close() }})
|
||||
allowed, err := plugin.IsAllowedContext(t.Context(), policy.Args{})
|
||||
if allowed != tc.allow || (err != nil) != tc.fail {
|
||||
t.Fatalf("allow=%v err=%v", allowed, err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
if allowed, err := plugin.IsAllowedContext(ctx, policy.Args{}); allowed || err == nil {
|
||||
t.Fatal("canceled evaluation did not stop")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user