mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-18 12:08:32 +02:00
* s3tables: scope OIDC-derived account IDs to the token issuer Table ownership keyed off the bare sub claim, so two identity providers issuing the same sub collided on one account - a caller could act on tables owned by a different provider's user. OIDC bearer tokens now carry the same issuer-namespaced (sub, iss) hash STS sessions already expose as aws:userid, and getAccountID prefers it over the raw claims. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * s3tables: keep legacy owners working and scrub client-subject claims - OIDC bearer tokens without a subject are rejected, and a token-supplied aws:userid claim is always overwritten with the verified (sub, iss) hash, so a forged claim can no longer stand in for the principal. - Owners written before issuer scoping hold a bare subject; permission checks now re-anchor that subject under the caller's issuer, so the same (sub, iss) pair keeps its ownership while scoped owners remain issuer-isolated. The token issuer is asserted into claims alongside sub and carried through PolicyContext. * s3tables: anchor legacy subjects to the configured issuer set * s3: enumerate trusted issuers from registered providers too * s3: fail closed when the OIDC provider store cannot be listed trustedOIDCIssuers kept the registered issuers when ListOIDCProviders failed, presenting a partial set as complete. A deployment trusting one registered issuer A plus a stored issuer B would, on a listing error, report only A — the single-issuer shape under which a legacy bare subject re-anchors — letting a caller from A claim a stored subject that belongs to B and gain owner access. With no provider store the registered providers are provably complete, so keep returning them; with a store, a failed listing means the real set is unknown, so return none. --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
192 lines
6.2 KiB
Go
192 lines
6.2 KiB
Go
package s3tables
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
)
|
|
|
|
// handleCreateTableBucket creates a new table bucket
|
|
func (h *S3TablesHandler) handleCreateTableBucket(w http.ResponseWriter, r *http.Request, filerClient FilerClient) error {
|
|
var req CreateTableBucketRequest
|
|
if err := h.readRequestBody(r, &req); err != nil {
|
|
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
|
|
return err
|
|
}
|
|
|
|
// Validate bucket name
|
|
if err := validateBucketName(req.Name); err != nil {
|
|
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
|
|
return err
|
|
}
|
|
|
|
// A bucket is a catalog, and a catalog serves one protocol. Saying which one
|
|
// at creation is what lets everything downstream - the endpoint the UI
|
|
// shows, the tables the bucket accepts - be answered without opening a table.
|
|
bucketFormat := FormatIceberg
|
|
if req.Format != "" {
|
|
normalized, ok := NormalizeFormat(req.Format)
|
|
if !ok {
|
|
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, fmt.Sprintf("unsupported format %q", req.Format))
|
|
return fmt.Errorf("invalid format")
|
|
}
|
|
bucketFormat = normalized
|
|
}
|
|
|
|
principal := h.getAccountID(r)
|
|
identityActions := getIdentityActions(r)
|
|
identityPolicyNames := getIdentityPolicyNames(r)
|
|
useIAM := h.shouldUseIAM(r, identityActions, identityPolicyNames)
|
|
useLegacy := !useIAM
|
|
if useIAM {
|
|
allowed, err := h.authorizeIAMAction(r, identityPolicyNames, "CreateTableBucket", h.generateTableBucketARN(principal, req.Name), h.generateS3BucketARN(req.Name))
|
|
if err != nil {
|
|
h.writeError(w, http.StatusForbidden, ErrCodeAccessDenied, "not authorized to create table buckets")
|
|
return NewAuthError("CreateTableBucket", principal, "not authorized to create table buckets")
|
|
}
|
|
if !allowed {
|
|
if h.defaultAllow && len(identityActions) == 0 && len(identityPolicyNames) == 0 {
|
|
useLegacy = true
|
|
} else {
|
|
h.writeError(w, http.StatusForbidden, ErrCodeAccessDenied, "not authorized to create table buckets")
|
|
return NewAuthError("CreateTableBucket", principal, "not authorized to create table buckets")
|
|
}
|
|
}
|
|
}
|
|
if useLegacy {
|
|
owner := h.accountID
|
|
if owner == "" {
|
|
owner = DefaultAccountID
|
|
}
|
|
if !CheckPermissionWithContext("CreateTableBucket", principal, owner, "", "", &PolicyContext{
|
|
TrustedIssuers: h.trustedIssuers(r),
|
|
IdentityActions: identityActions,
|
|
DefaultAllow: h.defaultAllowFor(r),
|
|
}) {
|
|
h.writeError(w, http.StatusForbidden, ErrCodeAccessDenied, "not authorized to create table buckets")
|
|
return NewAuthError("CreateTableBucket", principal, "not authorized to create table buckets")
|
|
}
|
|
}
|
|
|
|
bucketPath := GetTableBucketPath(req.Name)
|
|
|
|
// Check if bucket already exists and ensure no conflict with object store buckets
|
|
tableBucketExists := false
|
|
s3BucketExists := false
|
|
bucketsPath := s3_constants.DefaultBucketsPath
|
|
err := filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
|
resp, err := client.GetFilerConfiguration(r.Context(), &filer_pb.GetFilerConfigurationRequest{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if resp.DirBuckets != "" {
|
|
bucketsPath = resp.DirBuckets
|
|
}
|
|
entryResp, err := filer_pb.LookupEntry(r.Context(), client, &filer_pb.LookupDirectoryEntryRequest{
|
|
Directory: bucketsPath,
|
|
Name: req.Name,
|
|
})
|
|
if err != nil {
|
|
if !errors.Is(err, filer_pb.ErrNotFound) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
if entryResp != nil && entryResp.Entry != nil {
|
|
if IsTableBucketEntry(entryResp.Entry) {
|
|
tableBucketExists = true
|
|
} else {
|
|
s3BucketExists = true
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
glog.Errorf("S3Tables: failed to check bucket existence: %v", err)
|
|
h.writeError(w, http.StatusInternalServerError, ErrCodeInternalError, "failed to check bucket existence")
|
|
return err
|
|
}
|
|
|
|
if tableBucketExists {
|
|
h.writeError(w, http.StatusConflict, ErrCodeBucketAlreadyExists, fmt.Sprintf("table bucket %s already exists", req.Name))
|
|
return fmt.Errorf("bucket already exists")
|
|
}
|
|
if s3BucketExists {
|
|
h.writeError(w, http.StatusConflict, ErrCodeBucketAlreadyExists, fmt.Sprintf("bucket %s already exists", req.Name))
|
|
return fmt.Errorf("bucket already exists")
|
|
}
|
|
|
|
// Create the bucket directory and set metadata as extended attributes
|
|
now := time.Now()
|
|
metadata := &tableBucketMetadata{
|
|
Name: req.Name,
|
|
CreatedAt: now,
|
|
OwnerAccountID: principal,
|
|
Format: bucketFormat,
|
|
}
|
|
|
|
metadataBytes, err := json.Marshal(metadata)
|
|
if err != nil {
|
|
glog.Errorf("S3Tables: failed to marshal metadata: %v", err)
|
|
h.writeError(w, http.StatusInternalServerError, ErrCodeInternalError, "failed to marshal metadata")
|
|
return err
|
|
}
|
|
|
|
err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
|
// Ensure root tables directory exists
|
|
if !h.entryExists(r.Context(), client, TablesPath) {
|
|
if err := h.createDirectory(r.Context(), client, TablesPath); err != nil {
|
|
return fmt.Errorf("failed to create root tables directory: %w", err)
|
|
}
|
|
}
|
|
|
|
// Create bucket directory
|
|
if err := h.createDirectory(r.Context(), client, bucketPath); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Mark as a table bucket
|
|
if err := h.setExtendedAttribute(r.Context(), client, bucketPath, ExtendedKeyTableBucket, []byte("true")); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Set metadata as extended attribute
|
|
if err := h.setExtendedAttribute(r.Context(), client, bucketPath, ExtendedKeyMetadata, metadataBytes); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Set tags if provided
|
|
if len(req.Tags) > 0 {
|
|
tagsBytes, err := json.Marshal(req.Tags)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal tags: %w", err)
|
|
}
|
|
if err := h.setExtendedAttribute(r.Context(), client, bucketPath, ExtendedKeyTags, tagsBytes); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
glog.Errorf("S3Tables: failed to create table bucket %s: %v", req.Name, err)
|
|
h.writeError(w, http.StatusInternalServerError, ErrCodeInternalError, "failed to create table bucket")
|
|
return err
|
|
}
|
|
|
|
resp := &CreateTableBucketResponse{
|
|
ARN: h.generateTableBucketARN(metadata.OwnerAccountID, req.Name),
|
|
}
|
|
|
|
h.writeJSON(w, http.StatusOK, resp)
|
|
return nil
|
|
}
|