mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-19 13:00:45 +02:00
* s3api: fail closed when S3 Tables signature verification fails * s3api: avoid nil Account dereference in S3 Tables auth log * iceberg: return auth error instead of falling back to DefaultAllow * lance: return auth error instead of falling back to DefaultAllow * s3api: stop trusting client-supplied s3-account-id The header is set by the server after successful authentication; scrub inbound values alongside the other internal headers, and apply the same admin guard to the header fallback branch of getAccountID that the identity branch already has. * test: cover table-catalog auth wrappers and principal resolution * test: configure anonymous identity where catalog clients do not sign * s3api: scrub s3-account-id after signature verification
186 lines
7.5 KiB
Go
186 lines
7.5 KiB
Go
package lance
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3tables"
|
|
)
|
|
|
|
// FilerClient provides access to the filer for storage operations.
|
|
type FilerClient interface {
|
|
WithFilerClient(streamingMode bool, fn func(client filer_pb.SeaweedFilerClient) error) error
|
|
}
|
|
|
|
type S3Authenticator interface {
|
|
AuthenticateRequest(r *http.Request) (string, interface{}, s3err.ErrorCode)
|
|
DefaultAllow() bool
|
|
}
|
|
|
|
// VendedCredentials are short-lived S3 credentials scoped to one table.
|
|
type VendedCredentials struct {
|
|
AccessKeyID string
|
|
SecretAccessKey string
|
|
SessionToken string
|
|
Expiration time.Time
|
|
}
|
|
|
|
// CredentialVendor mints credentials limited to a single table's prefix for a
|
|
// caller the catalog has already authenticated and authorized. A nil result with
|
|
// no error means the deployment has vending switched off.
|
|
type CredentialVendor interface {
|
|
VendTableCredentials(ctx context.Context, principal, bucket, prefix string) (*VendedCredentials, error)
|
|
}
|
|
|
|
// Server implements the Lance Namespace REST spec.
|
|
type Server struct {
|
|
filerClient FilerClient
|
|
tablesManager *s3tables.Manager
|
|
authenticator S3Authenticator
|
|
credentialVendor CredentialVendor
|
|
s3Endpoint string
|
|
s3Region string
|
|
}
|
|
|
|
// NewServer creates a Lance namespace server over the given filer.
|
|
func NewServer(filerClient FilerClient, authenticator S3Authenticator) *Server {
|
|
manager := s3tables.NewManager()
|
|
// Mirror the S3 port: fall open by default only when the gateway itself is
|
|
// open, so an authenticated caller still passes the normal permission check.
|
|
if authenticator != nil {
|
|
manager.SetDefaultAllow(authenticator.DefaultAllow())
|
|
}
|
|
return &Server{
|
|
filerClient: filerClient,
|
|
tablesManager: manager,
|
|
authenticator: authenticator,
|
|
}
|
|
}
|
|
|
|
// SetCredentialVendor enables storage_options credential vending for clients
|
|
// that ask for it with vend_credentials.
|
|
func (s *Server) SetCredentialVendor(vendor CredentialVendor) {
|
|
s.credentialVendor = vendor
|
|
}
|
|
|
|
// SetS3Endpoint configures the S3 endpoint advertised in storage_options so a
|
|
// client can reach the dataset without separately discovering the S3 address.
|
|
func (s *Server) SetS3Endpoint(endpoint string) {
|
|
s.s3Endpoint = endpoint
|
|
}
|
|
|
|
// SetS3Region configures the region advertised in storage_options.
|
|
func (s *Server) SetS3Region(region string) {
|
|
s.s3Region = region
|
|
}
|
|
|
|
// RegisterRoutes registers the Lance Namespace REST routes.
|
|
//
|
|
// The spec puts the identifier in the path rather than the body so a reverse
|
|
// proxy can route and authorize without deserializing the request.
|
|
func (s *Server) RegisterRoutes(router *mux.Router) {
|
|
router.Use(loggingMiddleware)
|
|
|
|
router.HandleFunc("/v1/namespace/{id}/create", s.Auth(s.handleCreateNamespace)).Methods(http.MethodPost)
|
|
router.HandleFunc("/v1/namespace/{id}/list", s.Auth(s.handleListNamespaces)).Methods(http.MethodGet)
|
|
router.HandleFunc("/v1/namespace/{id}/describe", s.Auth(s.handleDescribeNamespace)).Methods(http.MethodPost)
|
|
router.HandleFunc("/v1/namespace/{id}/drop", s.Auth(s.handleDropNamespace)).Methods(http.MethodPost)
|
|
router.HandleFunc("/v1/namespace/{id}/exists", s.Auth(s.handleNamespaceExists)).Methods(http.MethodPost)
|
|
router.HandleFunc("/v1/namespace/{id}/table/list", s.Auth(s.handleListTables)).Methods(http.MethodGet)
|
|
|
|
router.HandleFunc("/v1/table", s.Auth(s.handleListAllTables)).Methods(http.MethodGet)
|
|
router.HandleFunc("/v1/table/{id}/declare", s.Auth(s.handleDeclareTable)).Methods(http.MethodPost)
|
|
router.HandleFunc("/v1/table/{id}/describe", s.Auth(s.handleDescribeTable)).Methods(http.MethodPost)
|
|
router.HandleFunc("/v1/table/{id}/exists", s.Auth(s.handleTableExists)).Methods(http.MethodPost)
|
|
router.HandleFunc("/v1/table/{id}/register", s.Auth(s.handleRegisterTable)).Methods(http.MethodPost)
|
|
router.HandleFunc("/v1/table/{id}/deregister", s.Auth(s.handleDeregisterTable)).Methods(http.MethodPost)
|
|
router.HandleFunc("/v1/table/{id}/drop", s.Auth(s.handleDropTable)).Methods(http.MethodPost)
|
|
router.HandleFunc("/v1/table/{id}/rename", s.Auth(s.handleRenameTable)).Methods(http.MethodPost)
|
|
|
|
// The data plane needs Lance format support that does not exist in Go. Say
|
|
// so with the spec's own code instead of returning a bare 404.
|
|
for _, action := range []string{"create", "insert", "merge_insert", "update", "delete",
|
|
"query", "count_rows", "explain_plan", "analyze_plan", "restore",
|
|
"add_columns", "alter_columns", "drop_columns", "backfill_column",
|
|
"create_index", "create_scalar_index", "stats", "schema_metadata/update"} {
|
|
router.HandleFunc("/v1/table/{id}/"+action, s.Auth(s.handleUnsupported)).Methods(http.MethodPost)
|
|
}
|
|
// Version ops exist in the spec for stores that cannot order commits
|
|
// themselves. Ours can: a Lance commit is a put-if-not-exists, and this S3
|
|
// evaluates that precondition at the object's owner filer under a per-path
|
|
// lock, so the dataset keeps its own version history and the catalog stays
|
|
// out of the commit path.
|
|
for _, action := range []string{
|
|
"version/create", "version/list", "version/describe", "version/delete",
|
|
"index/list", "tags/list", "tags/version", "tags/create", "tags/delete", "tags/update",
|
|
"branches/list", "branches/create", "branches/delete"} {
|
|
router.HandleFunc("/v1/table/{id}/"+action, s.Auth(s.handleUnsupported)).Methods(http.MethodPost)
|
|
}
|
|
|
|
router.PathPrefix("/").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
glog.V(2).Infof("lance: no route for %s %s", r.Method, r.RequestURI)
|
|
writeError(w, r, http.StatusNotFound, codeUnsupported, "no such operation")
|
|
})
|
|
|
|
glog.V(2).Infof("Registered Lance Namespace routes")
|
|
}
|
|
|
|
func (s *Server) handleUnsupported(w http.ResponseWriter, r *http.Request) {
|
|
writeError(w, r, http.StatusNotImplemented, codeUnsupported,
|
|
"this namespace records table metadata only; run data operations through a Lance client against the table location")
|
|
}
|
|
|
|
func loggingMiddleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
glog.V(2).Infof("lance request: %s %s from %s", r.Method, r.RequestURI, r.RemoteAddr)
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
// Auth authenticates the caller and puts the identity in the request context.
|
|
// The Lance spec maps identity onto the same headers the S3 authenticator
|
|
// already understands, so SigV4 and bearer tokens both keep working.
|
|
func (s *Server) Auth(handler http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if s.authenticator == nil {
|
|
writeError(w, r, http.StatusUnauthorized, codeUnauthenticated, "authentication required")
|
|
return
|
|
}
|
|
|
|
identityName, identity, errCode := s.authenticator.AuthenticateRequest(r)
|
|
if errCode != s3err.ErrNone {
|
|
apiErr := s3err.GetAPIError(errCode)
|
|
code := codeInternal
|
|
switch apiErr.HTTPStatusCode {
|
|
case http.StatusForbidden:
|
|
code = codePermissionDenied
|
|
case http.StatusUnauthorized:
|
|
code = codeUnauthenticated
|
|
case http.StatusBadRequest:
|
|
code = codeInvalidInput
|
|
}
|
|
writeError(w, r, apiErr.HTTPStatusCode, code, apiErr.Description)
|
|
return
|
|
}
|
|
|
|
if identityName != "" || identity != nil {
|
|
ctx := r.Context()
|
|
if identityName != "" {
|
|
ctx = s3_constants.SetIdentityNameInContext(ctx, identityName)
|
|
}
|
|
if identity != nil {
|
|
ctx = s3_constants.SetIdentityInContext(ctx, identity)
|
|
}
|
|
r = r.WithContext(ctx)
|
|
}
|
|
|
|
handler(w, r)
|
|
}
|
|
}
|