Add support for federation on browser (#6891)

This commit is contained in:
Harshavardhana 2018-12-19 05:13:47 -08:00 committed by Nitish Tiwari
parent 2aeb3fbe86
commit d1e41695fe
6 changed files with 365 additions and 99 deletions

View File

@ -19,7 +19,6 @@ package cmd
import (
"bufio"
"context"
"fmt"
"net"
"net/http"
"strings"
@ -627,11 +626,52 @@ type bucketForwardingHandler struct {
func (f bucketForwardingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if globalDNSConfig == nil || globalDomainName == "" ||
guessIsBrowserReq(r) || guessIsHealthCheckReq(r) ||
guessIsMetricsReq(r) || guessIsRPCReq(r) || isAdminReq(r) {
guessIsHealthCheckReq(r) || guessIsMetricsReq(r) ||
guessIsRPCReq(r) || isAdminReq(r) {
f.handler.ServeHTTP(w, r)
return
}
// For browser requests, when federation is setup we need to
// specifically handle download and upload for browser requests.
if guessIsBrowserReq(r) && globalDNSConfig != nil && globalDomainName != "" {
var bucket, _ string
switch r.Method {
case http.MethodPut:
if getRequestAuthType(r) == authTypeJWT {
bucket, _ = urlPath2BucketObjectName(strings.TrimPrefix(r.URL.Path, minioReservedBucketPath+"/upload"))
}
case http.MethodGet:
if t := r.URL.Query().Get("token"); t != "" {
bucket, _ = urlPath2BucketObjectName(strings.TrimPrefix(r.URL.Path, minioReservedBucketPath+"/download"))
} else if getRequestAuthType(r) != authTypeJWT && !strings.HasPrefix(r.URL.Path, minioReservedBucketPath) {
bucket, _ = urlPath2BucketObjectName(r.URL.Path)
}
}
if bucket != "" {
sr, err := globalDNSConfig.Get(bucket)
if err != nil {
if err == dns.ErrNoEntriesFound {
writeErrorResponse(w, ErrNoSuchBucket, r.URL, guessIsBrowserReq(r))
} else {
writeErrorResponse(w, toAPIErrorCode(context.Background(), err), r.URL, guessIsBrowserReq(r))
}
return
}
if globalDomainIPs.Intersection(set.CreateStringSet(getHostsSlice(sr)...)).IsEmpty() {
r.URL.Scheme = "http"
if globalIsSSL {
r.URL.Scheme = "https"
}
r.URL.Host = getHostFromSrv(sr)
f.fwd.ServeHTTP(w, r)
return
}
}
f.handler.ServeHTTP(w, r)
return
}
bucket, object := urlPath2BucketObjectName(r.URL.Path)
// ListBucket requests should be handled at current endpoint as
// all buckets data can be fetched from here.
@ -663,12 +703,11 @@ func (f bucketForwardingHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
return
}
if globalDomainIPs.Intersection(set.CreateStringSet(getHostsSlice(sr)...)).IsEmpty() {
host, port := getRandomHostPort(sr)
r.URL.Scheme = "http"
if globalIsSSL {
r.URL.Scheme = "https"
}
r.URL.Host = fmt.Sprintf("%s:%d", host, port)
r.URL.Host = getHostFromSrv(sr)
f.fwd.ServeHTTP(w, r)
return
}

View File

@ -183,6 +183,13 @@ func getReqAccessCred(r *http.Request, region string) (cred auth.Credentials) {
if cred.AccessKey == "" {
cred, _, _ = getReqAccessKeyV2(r)
}
if cred.AccessKey == "" {
claims, owner, _ := webRequestAuthenticate(r)
if owner {
return globalServerConfig.GetCredential()
}
cred, _ = globalIAMSys.GetUser(claims.Subject)
}
return cred
}
@ -194,6 +201,7 @@ func extractReqParams(r *http.Request) map[string]string {
region := globalServerConfig.GetRegion()
cred := getReqAccessCred(r, region)
// Success.
return map[string]string{
"region": region,

View File

@ -23,6 +23,7 @@ import (
"fmt"
"io"
"math/rand"
"net"
"net/http"
"path"
"runtime"
@ -299,11 +300,11 @@ func getHostsSlice(records []dns.SrvRecord) []string {
return hosts
}
// returns a random host (and corresponding port) from a slice of DNS records
func getRandomHostPort(records []dns.SrvRecord) (string, int) {
// returns a host (and corresponding port) from a slice of DNS records
func getHostFromSrv(records []dns.SrvRecord) string {
rand.Seed(time.Now().Unix())
srvRecord := records[rand.Intn(len(records))]
return srvRecord.Host, srvRecord.Port
return net.JoinHostPort(srvRecord.Host, fmt.Sprintf("%d", srvRecord.Port))
}
// IsCompressed returns true if the object is marked as compressed.

View File

@ -668,11 +668,25 @@ func getCpObjMetadataFromHeader(ctx context.Context, r *http.Request, userMeta m
// Returns a minio-go Client configured to access remote host described by destDNSRecord
// Applicable only in a federated deployment
var getRemoteInstanceClient = func(r *http.Request, host string, port int) (*miniogo.Core, error) {
// In a federated deployment, all the instances share config files and hence expected to have same
// credentials, make sure to send the same credentials for which the request came in.
var getRemoteInstanceClient = func(r *http.Request, host string) (*miniogo.Core, error) {
cred := getReqAccessCred(r, globalServerConfig.GetRegion())
return miniogo.NewCore(net.JoinHostPort(host, strconv.Itoa(port)), cred.AccessKey, cred.SecretKey, globalIsSSL)
// In a federated deployment, all the instances share config files
// and hence expected to have same credentials.
core, err := miniogo.NewCore(host, cred.AccessKey, cred.SecretKey, globalIsSSL)
if err != nil {
return nil, err
}
core.SetCustomTransport(NewCustomHTTPTransport())
return core, nil
}
// Check if the bucket is on a remote site, this code only gets executed when federation is enabled.
var isRemoteCallRequired = func(ctx context.Context, bucket string, objAPI ObjectLayer) bool {
if globalDNSConfig == nil {
return false
}
_, err := objAPI.GetBucketInfo(ctx, bucket)
return err == toObjectErr(errVolumeNotFound, bucket)
}
// CopyObjectHandler - Copy Object
@ -802,17 +816,6 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
srcInfo.metadataOnly = true
}
// Checks if a remote putobject call is needed for CopyObject operation
// 1. If source and destination bucket names are same, it means no call needed to etcd to get destination info
// 2. If destination bucket doesn't exist locally, only then a etcd call is needed
var isRemoteCallRequired = func(ctx context.Context, src, dst string, objAPI ObjectLayer) bool {
if src == dst {
return false
}
_, berr := objAPI.GetBucketInfo(ctx, dst)
return berr == toObjectErr(errVolumeNotFound, dst)
}
var reader io.Reader
var length = srcInfo.Size
@ -827,10 +830,27 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
length = actualSize
}
// Check if the destination bucket is on a remote site, this code only gets executed
// when federation is enabled, ie when globalDNSConfig is non 'nil'.
//
// This function is similar to isRemoteCallRequired but specifically for COPY object API
// if destination and source are same we do not need to check for destnation bucket
// to exist locally.
var isRemoteCopyRequired = func(ctx context.Context, srcBucket, dstBucket string, objAPI ObjectLayer) bool {
if globalDNSConfig == nil {
return false
}
if srcBucket == dstBucket {
return false
}
_, err := objAPI.GetBucketInfo(ctx, dstBucket)
return err == toObjectErr(errVolumeNotFound, dstBucket)
}
var compressMetadata map[string]string
// No need to compress for remote etcd calls
// Pass the decompressed stream to such calls.
isCompressed := objectAPI.IsCompressionSupported() && isCompressible(r.Header, srcObject) && !isRemoteCallRequired(ctx, srcBucket, dstBucket, objectAPI)
isCompressed := objectAPI.IsCompressionSupported() && isCompressible(r.Header, srcObject) && !isRemoteCopyRequired(ctx, srcBucket, dstBucket, objectAPI)
if isCompressed {
compressMetadata = make(map[string]string, 2)
// Preserving the compression metadata.
@ -1009,16 +1029,16 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
var objInfo ObjectInfo
if isRemoteCallRequired(ctx, srcBucket, dstBucket, objectAPI) {
if globalDNSConfig == nil {
writeErrorResponse(w, ErrNoSuchBucket, r.URL, guessIsBrowserReq(r))
if isRemoteCopyRequired(ctx, srcBucket, dstBucket, objectAPI) {
var dstRecords []dns.SrvRecord
dstRecords, err = globalDNSConfig.Get(dstBucket)
if err != nil {
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
var dstRecords []dns.SrvRecord
if dstRecords, err = globalDNSConfig.Get(dstBucket); err == nil {
// Send PutObject request to appropriate instance (in federated deployment)
host, port := getRandomHostPort(dstRecords)
client, rerr := getRemoteInstanceClient(r, host, port)
client, rerr := getRemoteInstanceClient(r, getHostFromSrv(dstRecords))
if rerr != nil {
writeErrorResponse(w, toAPIErrorCode(ctx, rerr), r.URL, guessIsBrowserReq(r))
return
@ -1031,7 +1051,6 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
}
objInfo.ETag = remoteObjInfo.ETag
objInfo.ModTime = remoteObjInfo.LastModified
}
} else {
// Copy source object to destination, if source and destination
// object is same then only metadata is updated.

View File

@ -99,11 +99,14 @@ func (web *webAPIHandlers) ServerInfo(r *http.Request, args *WebGenericArgs, rep
reply.MinioGlobalInfo = getGlobalInfo()
// If ENV creds are not set and incoming user is not owner
// disable changing credentials.
// TODO: fix this in future and allow changing user credentials.
v, ok := reply.MinioGlobalInfo["isEnvCreds"].(bool)
if ok && !v {
reply.MinioGlobalInfo["isEnvCreds"] = !owner
}
// if etcd is set disallow changing credentials through UI
if globalEtcdClient != nil {
reply.MinioGlobalInfo["isEnvCreds"] = true
}
reply.MinioMemory = mem
reply.MinioPlatform = platform
@ -148,7 +151,7 @@ func (web *webAPIHandlers) MakeBucket(r *http.Request, args *MakeBucketArgs, rep
if authErr != nil {
return toJSONError(authErr)
}
// TODO: Allow MakeBucket in future.
if !owner {
return toJSONError(errAccessDenied)
}
@ -201,11 +204,33 @@ func (web *webAPIHandlers) DeleteBucket(r *http.Request, args *RemoveBucketArgs,
if authErr != nil {
return toJSONError(authErr)
}
// TODO: Allow DeleteBucket in future.
if !owner {
return toJSONError(errAccessDenied)
}
reply.UIVersion = browser.UIVersion
if isRemoteCallRequired(context.Background(), args.BucketName, objectAPI) {
sr, err := globalDNSConfig.Get(args.BucketName)
if err != nil {
if err == dns.ErrNoEntriesFound {
return toJSONError(BucketNotFound{
Bucket: args.BucketName,
}, args.BucketName)
}
return toJSONError(err, args.BucketName)
}
core, err := getRemoteInstanceClient(r, getHostFromSrv(sr))
if err != nil {
return toJSONError(err, args.BucketName)
}
if err = core.RemoveBucket(args.BucketName); err != nil {
return toJSONError(err, args.BucketName)
}
return nil
}
ctx := context.Background()
deleteBucket := objectAPI.DeleteBucket
@ -229,7 +254,6 @@ func (web *webAPIHandlers) DeleteBucket(r *http.Request, args *RemoveBucketArgs,
}
}
reply.UIVersion = browser.UIVersion
return nil
}
@ -353,6 +377,42 @@ func (web *webAPIHandlers) ListObjects(r *http.Request, args *ListObjectsArgs, r
listObjects = web.CacheAPI().ListObjects
}
if isRemoteCallRequired(context.Background(), args.BucketName, objectAPI) {
sr, err := globalDNSConfig.Get(args.BucketName)
if err != nil {
if err == dns.ErrNoEntriesFound {
return toJSONError(BucketNotFound{
Bucket: args.BucketName,
}, args.BucketName)
}
return toJSONError(err, args.BucketName)
}
core, err := getRemoteInstanceClient(r, getHostFromSrv(sr))
if err != nil {
return toJSONError(err, args.BucketName)
}
result, err := core.ListObjects(args.BucketName, args.Prefix, args.Marker, slashSeparator, 1000)
if err != nil {
return toJSONError(err, args.BucketName)
}
reply.NextMarker = result.NextMarker
reply.IsTruncated = result.IsTruncated
for _, obj := range result.Contents {
reply.Objects = append(reply.Objects, WebObjectInfo{
Key: obj.Key,
LastModified: obj.LastModified,
Size: obj.Size,
ContentType: obj.ContentType,
})
}
for _, p := range result.CommonPrefixes {
reply.Objects = append(reply.Objects, WebObjectInfo{
Key: p.Prefix,
})
}
return nil
}
claims, owner, authErr := webRequestAuthenticate(r)
if authErr != nil {
if authErr == errNoAuthToken {
@ -493,6 +553,40 @@ func (web *webAPIHandlers) RemoveObject(r *http.Request, args *RemoveObjectArgs,
return toJSONError(errInvalidArgument)
}
reply.UIVersion = browser.UIVersion
if isRemoteCallRequired(context.Background(), args.BucketName, objectAPI) {
sr, err := globalDNSConfig.Get(args.BucketName)
if err != nil {
if err == dns.ErrNoEntriesFound {
return toJSONError(BucketNotFound{
Bucket: args.BucketName,
}, args.BucketName)
}
return toJSONError(err, args.BucketName)
}
core, err := getRemoteInstanceClient(r, getHostFromSrv(sr))
if err != nil {
return toJSONError(err, args.BucketName)
}
objectsCh := make(chan string)
// Send object names that are needed to be removed to objectsCh
go func() {
defer close(objectsCh)
for _, objectName := range args.Objects {
objectsCh <- objectName
}
}()
for resp := range core.RemoveObjects(args.BucketName, objectsCh) {
if resp.Err != nil {
return toJSONError(resp.Err, args.BucketName, resp.ObjectName)
}
}
return nil
}
var err error
next:
for _, objectName := range args.Objects {
@ -516,7 +610,7 @@ next:
return toJSONError(errAccessDenied)
}
if err = deleteObject(nil, objectAPI, web.CacheAPI(), args.BucketName, objectName, r); err != nil {
if err = deleteObject(context.Background(), objectAPI, web.CacheAPI(), args.BucketName, objectName, r); err != nil {
break next
}
continue
@ -543,7 +637,7 @@ next:
}
marker = lo.NextMarker
for _, obj := range lo.Objects {
err = deleteObject(nil, objectAPI, web.CacheAPI(), args.BucketName, obj.Name, r)
err = deleteObject(context.Background(), objectAPI, web.CacheAPI(), args.BucketName, obj.Name, r)
if err != nil {
break next
}
@ -559,7 +653,6 @@ next:
return toJSONError(err, args.BucketName, "")
}
reply.UIVersion = browser.UIVersion
return nil
}
@ -633,8 +726,7 @@ func (web *webAPIHandlers) SetAuth(r *http.Request, args *SetAuthArgs, reply *Se
}
// If creds are set through ENV disallow changing credentials.
// TODO: Multi-user credentials also cannot be changed from browser.
if globalIsEnvCreds || globalWORMEnabled || !owner {
if globalIsEnvCreds || globalWORMEnabled || !owner || globalEtcdClient != nil {
return toJSONError(errChangeCredNotAllowed)
}
@ -858,6 +950,7 @@ func (web *webAPIHandlers) Upload(w http.ResponseWriter, r *http.Request) {
pReader = NewPutObjReader(rawReader, hashReader, objectEncryptionKey)
}
}
// Ensure that metadata does not contain sensitive information
crypto.RemoveSensitiveEntries(metadata)
@ -1306,6 +1399,35 @@ func (web *webAPIHandlers) GetBucketPolicy(r *http.Request, args *GetBucketPolic
return toJSONError(errAccessDenied)
}
var policyInfo = &miniogopolicy.BucketAccessPolicy{Version: "2012-10-17"}
if isRemoteCallRequired(context.Background(), args.BucketName, objectAPI) {
sr, err := globalDNSConfig.Get(args.BucketName)
if err != nil {
if err == dns.ErrNoEntriesFound {
return toJSONError(BucketNotFound{
Bucket: args.BucketName,
}, args.BucketName)
}
return toJSONError(err, args.BucketName)
}
client, rerr := getRemoteInstanceClient(r, getHostFromSrv(sr))
if rerr != nil {
return toJSONError(rerr, args.BucketName)
}
policyStr, err := client.GetBucketPolicy(args.BucketName)
if err != nil {
return toJSONError(rerr, args.BucketName)
}
bucketPolicy, err := policy.ParseConfig(strings.NewReader(policyStr), args.BucketName)
if err != nil {
return toJSONError(rerr, args.BucketName)
}
policyInfo, err = PolicyToBucketAccessPolicy(bucketPolicy)
if err != nil {
// This should not happen.
return toJSONError(err, args.BucketName)
}
} else {
bucketPolicy, err := objectAPI.GetBucketPolicy(context.Background(), args.BucketName)
if err != nil {
if _, ok := err.(BucketPolicyNotFound); !ok {
@ -1314,11 +1436,12 @@ func (web *webAPIHandlers) GetBucketPolicy(r *http.Request, args *GetBucketPolic
return err
}
policyInfo, err := PolicyToBucketAccessPolicy(bucketPolicy)
policyInfo, err = PolicyToBucketAccessPolicy(bucketPolicy)
if err != nil {
// This should not happen.
return toJSONError(err, args.BucketName)
}
}
reply.UIVersion = browser.UIVersion
reply.Policy = miniogopolicy.GetPolicy(policyInfo.Statements, args.BucketName, args.Prefix)
@ -1359,18 +1482,43 @@ func (web *webAPIHandlers) ListAllBucketPolicies(r *http.Request, args *ListAllB
return toJSONError(errAccessDenied)
}
var policyInfo = new(miniogopolicy.BucketAccessPolicy)
if isRemoteCallRequired(context.Background(), args.BucketName, objectAPI) {
sr, err := globalDNSConfig.Get(args.BucketName)
if err != nil {
if err == dns.ErrNoEntriesFound {
return toJSONError(BucketNotFound{
Bucket: args.BucketName,
}, args.BucketName)
}
return toJSONError(err, args.BucketName)
}
core, rerr := getRemoteInstanceClient(r, getHostFromSrv(sr))
if rerr != nil {
return toJSONError(rerr, args.BucketName)
}
var policyStr string
policyStr, err = core.Client.GetBucketPolicy(args.BucketName)
if err != nil {
return toJSONError(err, args.BucketName)
}
if policyStr != "" {
if err = json.Unmarshal([]byte(policyStr), policyInfo); err != nil {
return toJSONError(err, args.BucketName)
}
}
} else {
bucketPolicy, err := objectAPI.GetBucketPolicy(context.Background(), args.BucketName)
if err != nil {
if _, ok := err.(BucketPolicyNotFound); !ok {
return toJSONError(err, args.BucketName)
}
}
policyInfo, err := PolicyToBucketAccessPolicy(bucketPolicy)
policyInfo, err = PolicyToBucketAccessPolicy(bucketPolicy)
if err != nil {
// This should not happen.
return toJSONError(err, args.BucketName)
}
}
reply.UIVersion = browser.UIVersion
for prefix, policy := range miniogopolicy.GetPolicies(policyInfo.Statements, args.BucketName, "") {
@ -1419,13 +1567,64 @@ func (web *webAPIHandlers) SetBucketPolicy(r *http.Request, args *SetBucketPolic
ctx := context.Background()
if isRemoteCallRequired(context.Background(), args.BucketName, objectAPI) {
sr, err := globalDNSConfig.Get(args.BucketName)
if err != nil {
if err == dns.ErrNoEntriesFound {
return toJSONError(BucketNotFound{
Bucket: args.BucketName,
}, args.BucketName)
}
return toJSONError(err, args.BucketName)
}
core, rerr := getRemoteInstanceClient(r, getHostFromSrv(sr))
if rerr != nil {
return toJSONError(rerr, args.BucketName)
}
var policyStr string
// Use the abstracted API instead of core, such that
// NoSuchBucketPolicy errors are automatically handled.
policyStr, err = core.Client.GetBucketPolicy(args.BucketName)
if err != nil {
return toJSONError(err, args.BucketName)
}
var policyInfo = &miniogopolicy.BucketAccessPolicy{Version: "2012-10-17"}
if policyStr != "" {
if err = json.Unmarshal([]byte(policyStr), policyInfo); err != nil {
return toJSONError(err, args.BucketName)
}
}
policyInfo.Statements = miniogopolicy.SetPolicy(policyInfo.Statements, policyType, args.BucketName, args.Prefix)
if len(policyInfo.Statements) == 0 {
if err = core.SetBucketPolicy(args.BucketName, ""); err != nil {
return toJSONError(err, args.BucketName)
}
return nil
}
bucketPolicy, err := BucketAccessPolicyToPolicy(policyInfo)
if err != nil {
// This should not happen.
return toJSONError(err, args.BucketName)
}
policyData, err := json.Marshal(bucketPolicy)
if err != nil {
return toJSONError(err, args.BucketName)
}
if err = core.SetBucketPolicy(args.BucketName, string(policyData)); err != nil {
return toJSONError(err, args.BucketName)
}
} else {
bucketPolicy, err := objectAPI.GetBucketPolicy(ctx, args.BucketName)
if err != nil {
if _, ok := err.(BucketPolicyNotFound); !ok {
return toJSONError(err, args.BucketName)
}
}
policyInfo, err := PolicyToBucketAccessPolicy(bucketPolicy)
if err != nil {
// This should not happen.
@ -1433,7 +1632,6 @@ func (web *webAPIHandlers) SetBucketPolicy(r *http.Request, args *SetBucketPolic
}
policyInfo.Statements = miniogopolicy.SetPolicy(policyInfo.Statements, policyType, args.BucketName, args.Prefix)
if len(policyInfo.Statements) == 0 {
if err = objectAPI.DeleteBucketPolicy(ctx, args.BucketName); err != nil {
return toJSONError(err, args.BucketName)
@ -1456,6 +1654,7 @@ func (web *webAPIHandlers) SetBucketPolicy(r *http.Request, args *SetBucketPolic
globalPolicySys.Set(args.BucketName, *bucketPolicy)
globalNotificationSys.SetBucketPolicy(ctx, args.BucketName, bucketPolicy)
}
return nil
}

View File

@ -89,7 +89,7 @@ func registerWebRouter(router *mux.Router) error {
// These methods use short-expiry tokens in the URLs. These tokens may unintentionally
// be logged, so a new one must be generated for each request.
webBrowserRouter.Methods("GET").Path("/download/{bucket}/{object:.+}").Queries("token", "{token:.*}").HandlerFunc(web.Download)
webBrowserRouter.Methods("GET").Path("/download/{bucket}/{object:.+}").Queries("token", "{token:.*}").HandlerFunc(httpTraceHdrs(web.Download))
webBrowserRouter.Methods("POST").Path("/zip").Queries("token", "{token:.*}").HandlerFunc(httpTraceHdrs(web.DownloadZip))
// Add compression for assets.