mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-21 05:24:28 +02:00
* s3tables: share the UpdateTable authorization gate Extract the validation, load, and permission check from handleUpdateTable into authorizeUpdateTable so the table an update was authorized against is the same snapshot the mutation writes to. Manager gains AuthorizeUpdateTable, mirroring AuthorizeCreateTable, so callers can ask the question without updating anything. * s3tables: share the UpdateView authorization gate Extract the lookup, policy load, and permission check from handleUpdateView into authorizeUpdateView, mirroring authorizeUpdateTable, and expose it as Manager.AuthorizeUpdateView for callers that need the answer without the update. * s3api: scope vended table credentials to read-only on request VendTableCredentials takes a readOnly flag and tablePrefixSessionPolicy emits only s3:GetObject for the table prefix when it is set, keeping the conditioned ListBucket and unconditioned GetBucketLocation statements that reads still need. * iceberg: vend read-only credentials when the caller may not write LoadTable and friends vend storage credentials only for the authority the caller holds: the vend now checks UpdateTable/UpdateView permission and a caller that may only read the catalog entry gets a read-only session. Paths that just passed a write gate (stage-create, view commit) keep the full session. * lance: vend read-only credentials when the caller may not write storage_options vends the same read-only session as the Iceberg catalog when the caller lacks UpdateTable on the table. * lance: keep write credentials for callers authorized to declare A Lance commit is a direct S3 write that never revisits the catalog, so a caller that just passed CreateTable still gets a writable session; the UpdateTable gate applies to describe, the read path. * iceberg, lance: log a failed write check instead of only reading it as denied
177 lines
5.5 KiB
Go
177 lines
5.5 KiB
Go
package s3tables
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
)
|
|
|
|
// Manager provides reusable S3 Tables operations for shell/admin without HTTP routing.
|
|
type Manager struct {
|
|
handler *S3TablesHandler
|
|
}
|
|
|
|
// NewManager creates a new Manager.
|
|
func NewManager() *Manager {
|
|
m := &Manager{handler: NewS3TablesHandler()}
|
|
// Default to allowing access when IAM is not configured
|
|
m.handler.SetDefaultAllow(true)
|
|
return m
|
|
}
|
|
|
|
// SetRegion sets the AWS region for ARN generation.
|
|
func (m *Manager) SetRegion(region string) {
|
|
m.handler.SetRegion(region)
|
|
}
|
|
|
|
// SetAccountID sets the AWS account ID for ARN generation.
|
|
func (m *Manager) SetAccountID(accountID string) {
|
|
m.handler.SetAccountID(accountID)
|
|
}
|
|
|
|
// SetDefaultAllow sets whether to allow access by default.
|
|
func (m *Manager) SetDefaultAllow(allow bool) {
|
|
m.handler.SetDefaultAllow(allow)
|
|
}
|
|
|
|
// SetTrusted lets trusted local tooling (shell, admin console) bypass authorization.
|
|
func (m *Manager) SetTrusted(trusted bool) {
|
|
m.handler.SetTrusted(trusted)
|
|
}
|
|
|
|
// Execute runs an S3 Tables operation and decodes the response into resp (if provided).
|
|
func (m *Manager) Execute(ctx context.Context, filerClient FilerClient, operation string, req interface{}, resp interface{}, identity string) error {
|
|
body, err := json.Marshal(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
httpReq, err := newManagerRequest(ctx, operation, body, identity)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
recorder := httptest.NewRecorder()
|
|
m.handler.HandleRequest(recorder, httpReq, filerClient)
|
|
return decodeS3TablesHTTPResponse(recorder, resp)
|
|
}
|
|
|
|
// AuthorizeCreateTable checks that identity may create the table the request
|
|
// describes, without creating anything. A deferred create (Iceberg
|
|
// stage-create) writes into the table bucket long before it registers the
|
|
// table, so it passes this gate first.
|
|
func (m *Manager) AuthorizeCreateTable(ctx context.Context, filerClient FilerClient, req *CreateTableRequest, identity string) error {
|
|
httpReq, err := newManagerRequest(ctx, "CreateTable", nil, identity)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
recorder := httptest.NewRecorder()
|
|
_, authErr := m.handler.authorizeCreateTable(recorder, httpReq, filerClient, req.TableBucketARN, req.Namespace, req.Name, req.Tags)
|
|
if authErr == nil {
|
|
return nil
|
|
}
|
|
if decoded := decodeS3TablesHTTPResponse(recorder, nil); decoded != nil {
|
|
return decoded
|
|
}
|
|
return authErr
|
|
}
|
|
|
|
// AuthorizeUpdateTable checks that identity may update the table the request
|
|
// describes, without updating anything. Credential vending gates on the same
|
|
// permission a commit needs.
|
|
func (m *Manager) AuthorizeUpdateTable(ctx context.Context, filerClient FilerClient, req *UpdateTableRequest, identity string) error {
|
|
httpReq, err := newManagerRequest(ctx, "UpdateTable", nil, identity)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
recorder := httptest.NewRecorder()
|
|
_, authErr := m.handler.authorizeUpdateTable(recorder, httpReq, filerClient, req)
|
|
if authErr == nil {
|
|
return nil
|
|
}
|
|
if decoded := decodeS3TablesHTTPResponse(recorder, nil); decoded != nil {
|
|
return decoded
|
|
}
|
|
return authErr
|
|
}
|
|
|
|
// AuthorizeUpdateView checks that identity may update the view the request
|
|
// describes, without updating anything.
|
|
func (m *Manager) AuthorizeUpdateView(ctx context.Context, filerClient FilerClient, req *UpdateViewRequest, identity string) error {
|
|
httpReq, err := newManagerRequest(ctx, "UpdateView", nil, identity)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
recorder := httptest.NewRecorder()
|
|
_, authErr := m.handler.authorizeUpdateView(recorder, httpReq, filerClient, req)
|
|
if authErr == nil {
|
|
return nil
|
|
}
|
|
if decoded := decodeS3TablesHTTPResponse(recorder, nil); decoded != nil {
|
|
return decoded
|
|
}
|
|
return authErr
|
|
}
|
|
|
|
func newManagerRequest(ctx context.Context, operation string, body []byte, identity string) (*http.Request, error) {
|
|
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, "/", bytes.NewReader(body))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
httpReq.Header.Set("Content-Type", "application/x-amz-json-1.1")
|
|
httpReq.Header.Set("X-Amz-Target", "S3Tables."+operation)
|
|
if identity != "" {
|
|
httpReq.Header.Set(s3_constants.AmzAccountId, identity)
|
|
httpReq = httpReq.WithContext(s3_constants.SetIdentityNameInContext(httpReq.Context(), identity))
|
|
}
|
|
return httpReq, nil
|
|
}
|
|
|
|
func decodeS3TablesHTTPResponse(recorder *httptest.ResponseRecorder, resp interface{}) error {
|
|
result := recorder.Result()
|
|
defer result.Body.Close()
|
|
data, err := io.ReadAll(result.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if result.StatusCode >= http.StatusBadRequest {
|
|
var errResp S3TablesError
|
|
if len(data) > 0 {
|
|
if jsonErr := json.Unmarshal(data, &errResp); jsonErr == nil && (errResp.Type != "" || errResp.Message != "") {
|
|
return &errResp
|
|
}
|
|
}
|
|
return &S3TablesError{Type: ErrCodeInternalError, Message: string(bytes.TrimSpace(data))}
|
|
}
|
|
if resp == nil || len(data) == 0 {
|
|
return nil
|
|
}
|
|
if err := json.Unmarshal(data, resp); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ManagerClient adapts a SeaweedFilerClient to the FilerClient interface.
|
|
type ManagerClient struct {
|
|
client filer_pb.SeaweedFilerClient
|
|
}
|
|
|
|
// NewManagerClient wraps a filer client.
|
|
func NewManagerClient(client filer_pb.SeaweedFilerClient) *ManagerClient {
|
|
return &ManagerClient{client: client}
|
|
}
|
|
|
|
// WithFilerClient implements FilerClient.
|
|
func (m *ManagerClient) WithFilerClient(streamingMode bool, fn func(client filer_pb.SeaweedFilerClient) error) error {
|
|
if m.client == nil {
|
|
return errors.New("nil filer client")
|
|
}
|
|
return fn(m.client)
|
|
}
|