* test: add Trino Iceberg catalog integration test - Create test/s3/catalog_trino/trino_catalog_test.go with TestTrinoIcebergCatalog - Tests integration between Trino SQL engine and SeaweedFS Iceberg REST catalog - Starts weed mini with all services and Trino in Docker container - Validates Iceberg catalog schema creation and listing operations - Uses native S3 filesystem support in Trino with path-style access - Add workflow job to s3-tables-tests.yml for CI execution * fix: preserve AWS environment credentials when replacing S3 configuration When S3 configuration is loaded from filer/db, it replaces the identities list and inadvertently removes AWS_ACCESS_KEY_ID credentials that were added from environment variables. This caused auth to remain disabled even though valid credentials were present. Fix by preserving environment-based identities when replacing the configuration and re-adding them after the replacement. This ensures environment credentials persist across configuration reloads and properly enable authentication. * fix: use correct ServerAddress format with gRPC port encoding The admin server couldn't connect to master because the master address was missing the gRPC port information. Use pb.NewServerAddress() which properly encodes both HTTP and gRPC ports in the address string. Changes: - weed/command/mini.go: Use pb.NewServerAddress for master address in admin - test/s3/policy/policy_test.go: Store and use gRPC ports for master/filer addresses This fix applies to: 1. Admin server connection to master (mini.go) 2. Test shell commands that need master/filer addresses (policy_test.go) * move * move * fix: always include gRPC port in server address encoding The NewServerAddress() function was omitting the gRPC port from the address string when it matched the port+10000 convention. However, gRPC port allocation doesn't always follow this convention - when the calculated port is busy, an alternative port is allocated. This caused a bug where: 1. Master's gRPC port was allocated as 50661 (sequential, not port+10000) 2. Address was encoded as '192.168.1.66:50660' (gRPC port omitted) 3. Admin client called ToGrpcAddress() which assumed port+10000 offset 4. Admin tried to connect to 60660 but master was on 50661 → connection failed Fix: Always include explicit gRPC port in address format (host:httpPort.grpcPort) unless gRPC port is 0. This makes addresses unambiguous and works regardless of the port allocation strategy used. Impacts: All server-to-server gRPC connections now use properly formatted addresses. * test: fix Iceberg REST API readiness check The Iceberg REST API endpoints require authentication. When checked without credentials, the API returns 403 Forbidden (not 401 Unauthorized). The readiness check now accepts both auth error codes (401/403) as indicators that the service is up and ready, it just needs credentials. This fixes the 'Iceberg REST API did not become ready' test failure. * Fix AWS SigV4 signature verification for base64-encoded payload hashes AWS SigV4 canonical requests must use hex-encoded SHA256 hashes, but the X-Amz-Content-Sha256 header may be transmitted as base64. Changes: - Added normalizePayloadHash() function to convert base64 to hex - Call normalizePayloadHash() in extractV4AuthInfoFromHeader() - Added encoding/base64 import Fixes 403 Forbidden errors on POST requests to Iceberg REST API when clients send base64-encoded content hashes in the header. Impacted services: Iceberg REST API, S3Tables * Fix AWS SigV4 signature verification for base64-encoded payload hashes AWS SigV4 canonical requests must use hex-encoded SHA256 hashes, but the X-Amz-Content-Sha256 header may be transmitted as base64. Changes: - Added normalizePayloadHash() function to convert base64 to hex - Call normalizePayloadHash() in extractV4AuthInfoFromHeader() - Added encoding/base64 import - Removed unused fmt import Fixes 403 Forbidden errors on POST requests to Iceberg REST API when clients send base64-encoded content hashes in the header. Impacted services: Iceberg REST API, S3Tables * pass sigv4 * s3api: fix identity preservation and logging levels - Ensure environment-based identities are preserved during config replacement - Update accessKeyIdent and nameToIdentity maps correctly - Downgrade informational logs to V(2) to reduce noise * test: fix trino integration test and s3 policy test - Pin Trino image version to 479 - Fix port binding to 0.0.0.0 for Docker connectivity - Fix S3 policy test hang by correctly assigning MiniClusterCtx - Improve port finding robustness in policy tests * ci: pre-pull trino image to avoid timeouts - Pull trinodb/trino:479 after Docker setup - Ensure image is ready before integration tests start * iceberg: remove unused checkAuth and improve logging - Remove unused checkAuth method - Downgrade informational logs to V(2) - Ensure loggingMiddleware uses a status writer for accurate reported codes - Narrow catch-all route to avoid interfering with other subsystems * iceberg: fix build failure by removing unused s3api import * Update iceberg.go * use warehouse * Update trino_catalog_test.go
662 lines
23 KiB
Go
662 lines
23 KiB
Go
package s3api
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"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"
|
|
)
|
|
|
|
// S3TablesApiServer wraps the S3 Tables handler with S3ApiServer's filer access
|
|
type S3TablesApiServer struct {
|
|
s3a *S3ApiServer
|
|
handler *s3tables.S3TablesHandler
|
|
}
|
|
|
|
// NewS3TablesApiServer creates a new S3 Tables API server
|
|
func NewS3TablesApiServer(s3a *S3ApiServer) *S3TablesApiServer {
|
|
return &S3TablesApiServer{
|
|
s3a: s3a,
|
|
handler: s3tables.NewS3TablesHandler(),
|
|
}
|
|
}
|
|
|
|
// SetRegion sets the AWS region for ARN generation
|
|
func (st *S3TablesApiServer) SetRegion(region string) {
|
|
st.handler.SetRegion(region)
|
|
}
|
|
|
|
// SetAccountID sets the AWS account ID for ARN generation
|
|
func (st *S3TablesApiServer) SetAccountID(accountID string) {
|
|
st.handler.SetAccountID(accountID)
|
|
}
|
|
|
|
// S3TablesHandler handles S3 Tables API requests
|
|
func (st *S3TablesApiServer) S3TablesHandler(w http.ResponseWriter, r *http.Request) {
|
|
st.handler.HandleRequest(w, r, st)
|
|
}
|
|
|
|
// WithFilerClient implements the s3tables.FilerClient interface
|
|
func (st *S3TablesApiServer) WithFilerClient(streamingMode bool, fn func(filer_pb.SeaweedFilerClient) error) error {
|
|
return st.s3a.WithFilerClient(streamingMode, fn)
|
|
}
|
|
|
|
// registerS3TablesRoutes registers S3 Tables API routes
|
|
func (s3a *S3ApiServer) registerS3TablesRoutes(router *mux.Router) {
|
|
// Create S3 Tables handler
|
|
s3TablesApi := NewS3TablesApiServer(s3a)
|
|
|
|
// Regex for S3 Tables Bucket ARN
|
|
const tableBucketARNRegex = "arn:aws:s3tables:[^/:]*:[^/:]*:bucket/[^/]+"
|
|
|
|
// REST-style S3 Tables API routes (used by AWS CLI)
|
|
targetMatcher := func(r *http.Request, rm *mux.RouteMatch) bool {
|
|
return strings.HasPrefix(r.Header.Get("X-Amz-Target"), "S3Tables.")
|
|
}
|
|
router.Methods(http.MethodPost).Path("/").MatcherFunc(targetMatcher).
|
|
HandlerFunc(track(s3a.authenticateS3Tables(s3TablesApi.S3TablesHandler), "S3Tables-Target"))
|
|
router.Methods(http.MethodPut).Path("/buckets").
|
|
HandlerFunc(track(s3a.authenticateS3Tables(s3TablesApi.handleRestOperation("CreateTableBucket", buildCreateTableBucketRequest)), "S3Tables-CreateTableBucket"))
|
|
router.Methods(http.MethodGet).Path("/buckets").
|
|
HandlerFunc(track(s3a.authenticateS3Tables(s3TablesApi.handleRestOperation("ListTableBuckets", buildListTableBucketsRequest)), "S3Tables-ListTableBuckets"))
|
|
router.Methods(http.MethodGet).Path("/buckets/{tableBucketARN:" + tableBucketARNRegex + "}").
|
|
HandlerFunc(track(s3a.authenticateS3Tables(s3TablesApi.handleRestOperation("GetTableBucket", buildTableBucketArnRequest)), "S3Tables-GetTableBucket"))
|
|
router.Methods(http.MethodDelete).Path("/buckets/{tableBucketARN:" + tableBucketARNRegex + "}").
|
|
HandlerFunc(track(s3a.authenticateS3Tables(s3TablesApi.handleRestOperation("DeleteTableBucket", buildDeleteTableBucketRequest)), "S3Tables-DeleteTableBucket"))
|
|
router.Methods(http.MethodPut).Path("/buckets/{tableBucketARN:" + tableBucketARNRegex + "}/policy").
|
|
HandlerFunc(track(s3a.authenticateS3Tables(s3TablesApi.handleRestOperation("PutTableBucketPolicy", buildPutTableBucketPolicyRequest)), "S3Tables-PutTableBucketPolicy"))
|
|
router.Methods(http.MethodGet).Path("/buckets/{tableBucketARN:" + tableBucketARNRegex + "}/policy").
|
|
HandlerFunc(track(s3a.authenticateS3Tables(s3TablesApi.handleRestOperation("GetTableBucketPolicy", buildGetTableBucketPolicyRequest)), "S3Tables-GetTableBucketPolicy"))
|
|
router.Methods(http.MethodDelete).Path("/buckets/{tableBucketARN:" + tableBucketARNRegex + "}/policy").
|
|
HandlerFunc(track(s3a.authenticateS3Tables(s3TablesApi.handleRestOperation("DeleteTableBucketPolicy", buildDeleteTableBucketPolicyRequest)), "S3Tables-DeleteTableBucketPolicy"))
|
|
|
|
router.Methods(http.MethodPut).Path("/namespaces/{tableBucketARN:" + tableBucketARNRegex + "}").
|
|
HandlerFunc(track(s3a.authenticateS3Tables(s3TablesApi.handleRestOperation("CreateNamespace", buildCreateNamespaceRequest)), "S3Tables-CreateNamespace"))
|
|
router.Methods(http.MethodGet).Path("/namespaces/{tableBucketARN:" + tableBucketARNRegex + "}").
|
|
HandlerFunc(track(s3a.authenticateS3Tables(s3TablesApi.handleRestOperation("ListNamespaces", buildListNamespacesRequest)), "S3Tables-ListNamespaces"))
|
|
router.Methods(http.MethodGet).Path("/namespaces/{tableBucketARN:" + tableBucketARNRegex + "}/{namespace}").
|
|
HandlerFunc(track(s3a.authenticateS3Tables(s3TablesApi.handleRestOperation("GetNamespace", buildGetNamespaceRequest)), "S3Tables-GetNamespace"))
|
|
router.Methods(http.MethodDelete).Path("/namespaces/{tableBucketARN:" + tableBucketARNRegex + "}/{namespace}").
|
|
HandlerFunc(track(s3a.authenticateS3Tables(s3TablesApi.handleRestOperation("DeleteNamespace", buildDeleteNamespaceRequest)), "S3Tables-DeleteNamespace"))
|
|
|
|
router.Methods(http.MethodPut).Path("/tables/{tableBucketARN:" + tableBucketARNRegex + "}/{namespace}").
|
|
HandlerFunc(track(s3a.authenticateS3Tables(s3TablesApi.handleRestOperation("CreateTable", buildCreateTableRequest)), "S3Tables-CreateTable"))
|
|
router.Methods(http.MethodGet).Path("/tables/{tableBucketARN:" + tableBucketARNRegex + "}").
|
|
HandlerFunc(track(s3a.authenticateS3Tables(s3TablesApi.handleRestOperation("ListTables", buildListTablesRequest)), "S3Tables-ListTables"))
|
|
router.Methods(http.MethodDelete).Path("/tables/{tableBucketARN:" + tableBucketARNRegex + "}/{namespace}/{name}").
|
|
HandlerFunc(track(s3a.authenticateS3Tables(s3TablesApi.handleRestOperation("DeleteTable", buildDeleteTableRequest)), "S3Tables-DeleteTable"))
|
|
|
|
router.Methods(http.MethodPut).Path("/tables/{tableBucketARN:" + tableBucketARNRegex + "}/{namespace}/{name}/policy").
|
|
HandlerFunc(track(s3a.authenticateS3Tables(s3TablesApi.handleRestOperation("PutTablePolicy", buildPutTablePolicyRequest)), "S3Tables-PutTablePolicy"))
|
|
router.Methods(http.MethodGet).Path("/tables/{tableBucketARN:" + tableBucketARNRegex + "}/{namespace}/{name}/policy").
|
|
HandlerFunc(track(s3a.authenticateS3Tables(s3TablesApi.handleRestOperation("GetTablePolicy", buildGetTablePolicyRequest)), "S3Tables-GetTablePolicy"))
|
|
router.Methods(http.MethodDelete).Path("/tables/{tableBucketARN:" + tableBucketARNRegex + "}/{namespace}/{name}/policy").
|
|
HandlerFunc(track(s3a.authenticateS3Tables(s3TablesApi.handleRestOperation("DeleteTablePolicy", buildDeleteTablePolicyRequest)), "S3Tables-DeleteTablePolicy"))
|
|
|
|
router.Methods(http.MethodPost).Path("/tag/{resourceArn:arn:aws:s3tables:.*}").
|
|
HandlerFunc(track(s3a.authenticateS3Tables(s3TablesApi.handleRestOperation("TagResource", buildTagResourceRequest)), "S3Tables-TagResource"))
|
|
router.Methods(http.MethodGet).Path("/tag/{resourceArn:arn:aws:s3tables:.*}").
|
|
HandlerFunc(track(s3a.authenticateS3Tables(s3TablesApi.handleRestOperation("ListTagsForResource", buildListTagsForResourceRequest)), "S3Tables-ListTagsForResource"))
|
|
router.Methods(http.MethodDelete).Path("/tag/{resourceArn:arn:aws:s3tables:.*}").
|
|
HandlerFunc(track(s3a.authenticateS3Tables(s3TablesApi.handleRestOperation("UntagResource", buildUntagResourceRequest)), "S3Tables-UntagResource"))
|
|
|
|
router.Methods(http.MethodGet).Path("/get-table").
|
|
HandlerFunc(track(s3a.authenticateS3Tables(s3TablesApi.handleRestOperation("GetTable", buildGetTableRequest)), "S3Tables-GetTable"))
|
|
|
|
glog.V(1).Infof("S3 Tables API enabled")
|
|
}
|
|
|
|
type s3tablesRequestBuilder func(r *http.Request) (interface{}, error)
|
|
|
|
func (st *S3TablesApiServer) handleRestOperation(operation string, builder s3tablesRequestBuilder) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
payload, err := builder(r)
|
|
if err != nil {
|
|
writeS3TablesError(w, http.StatusBadRequest, s3tables.ErrCodeInvalidRequest, err.Error())
|
|
return
|
|
}
|
|
if err := setS3TablesRequestBody(r, payload); err != nil {
|
|
writeS3TablesError(w, http.StatusInternalServerError, s3tables.ErrCodeInternalError, err.Error())
|
|
return
|
|
}
|
|
r.Header.Set("X-Amz-Target", "S3Tables."+operation)
|
|
st.S3TablesHandler(w, r)
|
|
}
|
|
}
|
|
|
|
func setS3TablesRequestBody(r *http.Request, payload interface{}) error {
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.Body = io.NopCloser(bytes.NewReader(body))
|
|
r.ContentLength = int64(len(body))
|
|
r.Header.Set("Content-Type", "application/x-amz-json-1.1")
|
|
return nil
|
|
}
|
|
|
|
func readS3TablesJSONBody(r *http.Request, v interface{}) error {
|
|
if r.Body == nil {
|
|
return nil
|
|
}
|
|
defer r.Body.Close()
|
|
const maxRequestBodySize = 10 * 1024 * 1024
|
|
if r.ContentLength > maxRequestBodySize {
|
|
return fmt.Errorf("request body too large: exceeds maximum size of %d bytes", maxRequestBodySize)
|
|
}
|
|
limitedReader := io.LimitReader(r.Body, maxRequestBodySize+1)
|
|
body, err := io.ReadAll(limitedReader)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(body) > maxRequestBodySize {
|
|
return fmt.Errorf("request body too large: exceeds maximum size of %d bytes", maxRequestBodySize)
|
|
}
|
|
if len(bytes.TrimSpace(body)) == 0 {
|
|
return nil
|
|
}
|
|
return json.Unmarshal(body, v)
|
|
}
|
|
|
|
func writeS3TablesError(w http.ResponseWriter, status int, code, message string) {
|
|
w.Header().Set("Content-Type", "application/x-amz-json-1.1")
|
|
w.WriteHeader(status)
|
|
errorResponse := map[string]interface{}{
|
|
"__type": code,
|
|
"message": message,
|
|
}
|
|
if err := json.NewEncoder(w).Encode(errorResponse); err != nil {
|
|
glog.Errorf("failed to encode S3Tables error response (status=%d, code=%s, message=%q): %v", status, code, message, err)
|
|
}
|
|
}
|
|
|
|
func getDecodedPathParam(r *http.Request, name string) (string, error) {
|
|
value := mux.Vars(r)[name]
|
|
if value == "" {
|
|
return "", nil
|
|
}
|
|
decoded, err := url.PathUnescape(value)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if decoded == ".." || strings.Contains(decoded, "../") || strings.Contains(decoded, `..\`) || strings.Contains(decoded, "\x00") {
|
|
return "", fmt.Errorf("invalid path parameter %s", name)
|
|
}
|
|
return decoded, nil
|
|
}
|
|
|
|
func buildTableBucketRequestWithARN(r *http.Request, constructor func(string) interface{}) (interface{}, error) {
|
|
arn, err := getDecodedPathParam(r, "tableBucketARN")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if arn == "" {
|
|
return nil, fmt.Errorf("tableBucketARN is required")
|
|
}
|
|
if _, err := s3tables.ParseBucketNameFromARN(arn); err != nil {
|
|
return nil, err
|
|
}
|
|
return constructor(arn), nil
|
|
}
|
|
|
|
func parseOptionalIntParam(r *http.Request, name string) (int, error) {
|
|
value := r.URL.Query().Get(name)
|
|
if value == "" {
|
|
return 0, nil
|
|
}
|
|
parsed, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("%s must be an integer", name)
|
|
}
|
|
if parsed <= 0 {
|
|
return 0, fmt.Errorf("%s must be a positive integer", name)
|
|
}
|
|
return parsed, nil
|
|
}
|
|
|
|
func parseOptionalNamespace(r *http.Request, name string) []string {
|
|
value := r.URL.Query().Get(name)
|
|
if value == "" {
|
|
return nil
|
|
}
|
|
if _, err := s3tables.ValidateNamespace([]string{value}); err != nil {
|
|
glog.V(1).Infof("invalid namespace value for %s: %q: %v", name, value, err)
|
|
return nil
|
|
}
|
|
return []string{value}
|
|
}
|
|
|
|
// parseTagKeys handles tag key parsing from query parameters.
|
|
// If a single value contains commas, it is split into multiple keys (e.g., "key1,key2,key3").
|
|
// Otherwise, multiple query values are returned as-is.
|
|
func parseTagKeys(values []string) []string {
|
|
if len(values) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]string, 0, len(values))
|
|
for _, value := range values {
|
|
for _, part := range strings.Split(value, ",") {
|
|
part = strings.TrimSpace(part)
|
|
if part != "" {
|
|
out = append(out, part)
|
|
}
|
|
}
|
|
}
|
|
if len(out) == 0 {
|
|
return nil
|
|
}
|
|
return out
|
|
}
|
|
|
|
func buildCreateTableBucketRequest(r *http.Request) (interface{}, error) {
|
|
var req s3tables.CreateTableBucketRequest
|
|
if err := readS3TablesJSONBody(r, &req); err != nil {
|
|
return nil, err
|
|
}
|
|
return &req, nil
|
|
}
|
|
|
|
func buildListTableBucketsRequest(r *http.Request) (interface{}, error) {
|
|
maxBuckets, err := parseOptionalIntParam(r, "maxBuckets")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &s3tables.ListTableBucketsRequest{
|
|
Prefix: r.URL.Query().Get("prefix"),
|
|
ContinuationToken: r.URL.Query().Get("continuationToken"),
|
|
MaxBuckets: maxBuckets,
|
|
}, nil
|
|
}
|
|
|
|
func buildTableBucketArnRequest(r *http.Request) (interface{}, error) {
|
|
return buildTableBucketRequestWithARN(r, func(arn string) interface{} {
|
|
return &s3tables.GetTableBucketRequest{TableBucketARN: arn}
|
|
})
|
|
}
|
|
|
|
func buildDeleteTableBucketRequest(r *http.Request) (interface{}, error) {
|
|
return buildTableBucketRequestWithARN(r, func(arn string) interface{} {
|
|
return &s3tables.DeleteTableBucketRequest{TableBucketARN: arn}
|
|
})
|
|
}
|
|
|
|
func buildPutTableBucketPolicyRequest(r *http.Request) (interface{}, error) {
|
|
var req s3tables.PutTableBucketPolicyRequest
|
|
if err := readS3TablesJSONBody(r, &req); err != nil {
|
|
return nil, err
|
|
}
|
|
tableBucketARN, err := getDecodedPathParam(r, "tableBucketARN")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.TableBucketARN = tableBucketARN
|
|
return &req, nil
|
|
}
|
|
|
|
func buildGetTableBucketPolicyRequest(r *http.Request) (interface{}, error) {
|
|
return buildTableBucketRequestWithARN(r, func(arn string) interface{} {
|
|
return &s3tables.GetTableBucketPolicyRequest{TableBucketARN: arn}
|
|
})
|
|
}
|
|
|
|
func buildDeleteTableBucketPolicyRequest(r *http.Request) (interface{}, error) {
|
|
return buildTableBucketRequestWithARN(r, func(arn string) interface{} {
|
|
return &s3tables.DeleteTableBucketPolicyRequest{TableBucketARN: arn}
|
|
})
|
|
}
|
|
|
|
func buildCreateNamespaceRequest(r *http.Request) (interface{}, error) {
|
|
var req s3tables.CreateNamespaceRequest
|
|
if err := readS3TablesJSONBody(r, &req); err != nil {
|
|
return nil, err
|
|
}
|
|
tableBucketARN, err := getDecodedPathParam(r, "tableBucketARN")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.TableBucketARN = tableBucketARN
|
|
return &req, nil
|
|
}
|
|
|
|
func buildListNamespacesRequest(r *http.Request) (interface{}, error) {
|
|
tableBucketARN, err := getDecodedPathParam(r, "tableBucketARN")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
maxNamespaces, err := parseOptionalIntParam(r, "maxNamespaces")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &s3tables.ListNamespacesRequest{
|
|
TableBucketARN: tableBucketARN,
|
|
Prefix: r.URL.Query().Get("prefix"),
|
|
ContinuationToken: r.URL.Query().Get("continuationToken"),
|
|
MaxNamespaces: maxNamespaces,
|
|
}, nil
|
|
}
|
|
|
|
func buildGetNamespaceRequest(r *http.Request) (interface{}, error) {
|
|
tableBucketARN, err := getDecodedPathParam(r, "tableBucketARN")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
namespace, err := getDecodedPathParam(r, "namespace")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if namespace == "" {
|
|
return nil, fmt.Errorf("namespace is required")
|
|
}
|
|
if _, err := s3tables.ValidateNamespace([]string{namespace}); err != nil {
|
|
return nil, err
|
|
}
|
|
return &s3tables.GetNamespaceRequest{
|
|
TableBucketARN: tableBucketARN,
|
|
Namespace: []string{namespace},
|
|
}, nil
|
|
}
|
|
|
|
func buildDeleteNamespaceRequest(r *http.Request) (interface{}, error) {
|
|
tableBucketARN, err := getDecodedPathParam(r, "tableBucketARN")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
namespace, err := getDecodedPathParam(r, "namespace")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if namespace == "" {
|
|
return nil, fmt.Errorf("namespace is required")
|
|
}
|
|
if _, err := s3tables.ValidateNamespace([]string{namespace}); err != nil {
|
|
return nil, err
|
|
}
|
|
return &s3tables.DeleteNamespaceRequest{
|
|
TableBucketARN: tableBucketARN,
|
|
Namespace: []string{namespace},
|
|
}, nil
|
|
}
|
|
|
|
func buildCreateTableRequest(r *http.Request) (interface{}, error) {
|
|
var req s3tables.CreateTableRequest
|
|
if err := readS3TablesJSONBody(r, &req); err != nil {
|
|
return nil, err
|
|
}
|
|
tableBucketARN, err := getDecodedPathParam(r, "tableBucketARN")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
namespace, err := getDecodedPathParam(r, "namespace")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if namespace == "" {
|
|
return nil, fmt.Errorf("namespace is required")
|
|
}
|
|
if _, err := s3tables.ValidateNamespace([]string{namespace}); err != nil {
|
|
return nil, err
|
|
}
|
|
req.TableBucketARN = tableBucketARN
|
|
req.Namespace = []string{namespace}
|
|
return &req, nil
|
|
}
|
|
|
|
func buildListTablesRequest(r *http.Request) (interface{}, error) {
|
|
tableBucketARN, err := getDecodedPathParam(r, "tableBucketARN")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
maxTables, err := parseOptionalIntParam(r, "maxTables")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &s3tables.ListTablesRequest{
|
|
TableBucketARN: tableBucketARN,
|
|
Namespace: parseOptionalNamespace(r, "namespace"),
|
|
Prefix: r.URL.Query().Get("prefix"),
|
|
ContinuationToken: r.URL.Query().Get("continuationToken"),
|
|
MaxTables: maxTables,
|
|
}, nil
|
|
}
|
|
|
|
func buildGetTableRequest(r *http.Request) (interface{}, error) {
|
|
query := r.URL.Query()
|
|
tableARN := query.Get("tableArn")
|
|
req := &s3tables.GetTableRequest{
|
|
TableARN: tableARN,
|
|
}
|
|
if tableARN == "" {
|
|
req.TableBucketARN = query.Get("tableBucketARN")
|
|
req.Namespace = parseOptionalNamespace(r, "namespace")
|
|
req.Name = query.Get("name")
|
|
if req.TableBucketARN == "" || len(req.Namespace) == 0 || req.Name == "" {
|
|
return nil, fmt.Errorf("either tableArn or (tableBucketARN, namespace, name) must be provided")
|
|
}
|
|
}
|
|
return req, nil
|
|
}
|
|
|
|
func buildDeleteTableRequest(r *http.Request) (interface{}, error) {
|
|
tableBucketARN, err := getDecodedPathParam(r, "tableBucketARN")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
namespace, err := getDecodedPathParam(r, "namespace")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if namespace == "" {
|
|
return nil, fmt.Errorf("namespace is required")
|
|
}
|
|
if _, err := s3tables.ValidateNamespace([]string{namespace}); err != nil {
|
|
return nil, err
|
|
}
|
|
name, err := getDecodedPathParam(r, "name")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if name == "" {
|
|
return nil, fmt.Errorf("name is required")
|
|
}
|
|
if _, err := s3tables.ValidateTableName(name); err != nil {
|
|
return nil, err
|
|
}
|
|
return &s3tables.DeleteTableRequest{
|
|
TableBucketARN: tableBucketARN,
|
|
Namespace: []string{namespace},
|
|
Name: name,
|
|
VersionToken: r.URL.Query().Get("versionToken"),
|
|
}, nil
|
|
}
|
|
|
|
func buildPutTablePolicyRequest(r *http.Request) (interface{}, error) {
|
|
var req s3tables.PutTablePolicyRequest
|
|
if err := readS3TablesJSONBody(r, &req); err != nil {
|
|
return nil, err
|
|
}
|
|
tableBucketARN, err := getDecodedPathParam(r, "tableBucketARN")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
namespace, err := getDecodedPathParam(r, "namespace")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if namespace == "" {
|
|
return nil, fmt.Errorf("namespace is required")
|
|
}
|
|
if _, err := s3tables.ValidateNamespace([]string{namespace}); err != nil {
|
|
return nil, err
|
|
}
|
|
name, err := getDecodedPathParam(r, "name")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if name == "" {
|
|
return nil, fmt.Errorf("name is required")
|
|
}
|
|
if _, err := s3tables.ValidateTableName(name); err != nil {
|
|
return nil, err
|
|
}
|
|
req.TableBucketARN = tableBucketARN
|
|
req.Namespace = []string{namespace}
|
|
req.Name = name
|
|
return &req, nil
|
|
}
|
|
|
|
func buildGetTablePolicyRequest(r *http.Request) (interface{}, error) {
|
|
tableBucketARN, err := getDecodedPathParam(r, "tableBucketARN")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
namespace, err := getDecodedPathParam(r, "namespace")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if namespace == "" {
|
|
return nil, fmt.Errorf("namespace is required")
|
|
}
|
|
if _, err := s3tables.ValidateNamespace([]string{namespace}); err != nil {
|
|
return nil, err
|
|
}
|
|
name, err := getDecodedPathParam(r, "name")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if name == "" {
|
|
return nil, fmt.Errorf("name is required")
|
|
}
|
|
if _, err := s3tables.ValidateTableName(name); err != nil {
|
|
return nil, err
|
|
}
|
|
return &s3tables.GetTablePolicyRequest{
|
|
TableBucketARN: tableBucketARN,
|
|
Namespace: []string{namespace},
|
|
Name: name,
|
|
}, nil
|
|
}
|
|
|
|
func buildDeleteTablePolicyRequest(r *http.Request) (interface{}, error) {
|
|
tableBucketARN, err := getDecodedPathParam(r, "tableBucketARN")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
namespace, err := getDecodedPathParam(r, "namespace")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if namespace == "" {
|
|
return nil, fmt.Errorf("namespace is required")
|
|
}
|
|
if _, err := s3tables.ValidateNamespace([]string{namespace}); err != nil {
|
|
return nil, err
|
|
}
|
|
name, err := getDecodedPathParam(r, "name")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if name == "" {
|
|
return nil, fmt.Errorf("name is required")
|
|
}
|
|
if _, err := s3tables.ValidateTableName(name); err != nil {
|
|
return nil, err
|
|
}
|
|
return &s3tables.DeleteTablePolicyRequest{
|
|
TableBucketARN: tableBucketARN,
|
|
Namespace: []string{namespace},
|
|
Name: name,
|
|
}, nil
|
|
}
|
|
|
|
func buildTagResourceRequest(r *http.Request) (interface{}, error) {
|
|
var req s3tables.TagResourceRequest
|
|
if err := readS3TablesJSONBody(r, &req); err != nil {
|
|
return nil, err
|
|
}
|
|
resourceARN, err := getDecodedPathParam(r, "resourceArn")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resourceARN == "" {
|
|
return nil, fmt.Errorf("resourceArn is required")
|
|
}
|
|
req.ResourceARN = resourceARN
|
|
return &req, nil
|
|
}
|
|
|
|
func buildListTagsForResourceRequest(r *http.Request) (interface{}, error) {
|
|
resourceARN, err := getDecodedPathParam(r, "resourceArn")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resourceARN == "" {
|
|
return nil, fmt.Errorf("resourceArn is required")
|
|
}
|
|
return &s3tables.ListTagsForResourceRequest{
|
|
ResourceARN: resourceARN,
|
|
}, nil
|
|
}
|
|
|
|
func buildUntagResourceRequest(r *http.Request) (interface{}, error) {
|
|
resourceARN, err := getDecodedPathParam(r, "resourceArn")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resourceARN == "" {
|
|
return nil, fmt.Errorf("resourceArn is required")
|
|
}
|
|
tagKeys := parseTagKeys(r.URL.Query()["tagKeys"])
|
|
if len(tagKeys) == 0 {
|
|
return nil, fmt.Errorf("tagKeys is required for %s", resourceARN)
|
|
}
|
|
return &s3tables.UntagResourceRequest{
|
|
ResourceARN: resourceARN,
|
|
TagKeys: tagKeys,
|
|
}, nil
|
|
}
|
|
|
|
// authenticateS3Tables wraps the handler with IAM authentication using AuthSignatureOnly
|
|
// This authenticates the request but delegates authorization to the S3 Tables handler
|
|
// which performs granular permission checks based on the specific operation.
|
|
func (s3a *S3ApiServer) authenticateS3Tables(f http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
glog.V(2).Infof("S3Tables: authenticateS3Tables called, iam.isEnabled()=%t", s3a.iam.isEnabled())
|
|
if !s3a.iam.isEnabled() {
|
|
f(w, r)
|
|
return
|
|
}
|
|
|
|
// Use AuthSignatureOnly to authenticate the request without authorizing specific actions
|
|
identity, errCode := s3a.iam.AuthSignatureOnly(r)
|
|
if errCode != s3err.ErrNone {
|
|
glog.Errorf("S3Tables: AuthSignatureOnly failed: %v", errCode)
|
|
s3err.WriteErrorResponse(w, r, errCode)
|
|
return
|
|
}
|
|
|
|
// Store the authenticated identity in request context
|
|
if identity != nil && identity.Name != "" {
|
|
glog.V(2).Infof("S3Tables: authenticated identity Name=%s Account.Id=%s", identity.Name, identity.Account.Id)
|
|
ctx := s3_constants.SetIdentityNameInContext(r.Context(), identity.Name)
|
|
ctx = s3_constants.SetIdentityInContext(ctx, identity)
|
|
r = r.WithContext(ctx)
|
|
} else {
|
|
glog.V(2).Infof("S3Tables: authenticated identity is nil or empty name")
|
|
}
|
|
|
|
f(w, r)
|
|
}
|
|
}
|