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 { if !s.authenticator.DefaultAllow() { 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 } glog.V(2).Infof("lance: authentication failed (%v) but the gateway is open, proceeding", errCode) } 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) } }