github.com/mmatczuk/gohan@v0.0.0-20170206152520-30e45d9bdb69/server/middleware/middleware.go (about)

     1  // Copyright (C) 2015 NTT Innovation Institute, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //    http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
    12  // implied.
    13  // See the License for the specific language governing permissions and
    14  // limitations under the License.
    15  
    16  package middleware
    17  
    18  import (
    19  	"bytes"
    20  	"encoding/json"
    21  	"io/ioutil"
    22  	"net/http"
    23  	"regexp"
    24  	"strings"
    25  	"time"
    26  
    27  	"github.com/cloudwan/gohan/schema"
    28  	"github.com/go-martini/martini"
    29  	"github.com/rackspace/gophercloud"
    30  )
    31  
    32  const webuiPATH = "/webui/"
    33  
    34  type responseHijacker struct {
    35  	martini.ResponseWriter
    36  	Response *bytes.Buffer
    37  }
    38  
    39  func newResponseHijacker(rw martini.ResponseWriter) *responseHijacker {
    40  	return &responseHijacker{rw, bytes.NewBuffer(nil)}
    41  }
    42  
    43  func (rh *responseHijacker) Write(b []byte) (int, error) {
    44  	rh.Response.Write(b)
    45  	return rh.ResponseWriter.Write(b)
    46  }
    47  
    48  //Logging logs requests and responses
    49  func Logging() martini.Handler {
    50  	return func(res http.ResponseWriter, req *http.Request, c martini.Context) {
    51  		if strings.HasPrefix(req.URL.Path, webuiPATH) {
    52  			c.Next()
    53  			return
    54  		}
    55  		start := time.Now()
    56  
    57  		addr := req.Header.Get("X-Real-IP")
    58  		if addr == "" {
    59  			addr = req.Header.Get("X-Forwarded-For")
    60  			if addr == "" {
    61  				addr = req.RemoteAddr
    62  			}
    63  		}
    64  
    65  		reqData, _ := ioutil.ReadAll(req.Body)
    66  		buff := ioutil.NopCloser(bytes.NewBuffer(reqData))
    67  		req.Body = buff
    68  
    69  		log.Info("Started %s %s for client %s data: %s",
    70  			req.Method, req.URL.String(), addr, string(reqData))
    71  		log.Debug("Request headers: %v", filterHeaders(req.Header))
    72  		log.Debug("Request body: %s", string(reqData))
    73  
    74  		rw := res.(martini.ResponseWriter)
    75  		rh := newResponseHijacker(rw)
    76  		c.MapTo(rh, (*http.ResponseWriter)(nil))
    77  		c.MapTo(rh, (*martini.ResponseWriter)(nil))
    78  
    79  		c.Next()
    80  
    81  		response, _ := ioutil.ReadAll(rh.Response)
    82  		log.Debug("Response headers: %v", rh.Header())
    83  		log.Debug("Response body: %s", string(response))
    84  		log.Info("Completed %v %s in %v", rw.Status(), http.StatusText(rw.Status()), time.Since(start))
    85  	}
    86  }
    87  
    88  func filterHeaders(headers http.Header) http.Header {
    89  	filtered := http.Header{}
    90  	for k, v := range headers {
    91  		if k == "X-Auth-Token" {
    92  			filtered[k] = []string{"***"}
    93  			continue
    94  		}
    95  		filtered[k] = v
    96  	}
    97  	return filtered
    98  }
    99  
   100  //IdentityService for user authentication & authorization
   101  type IdentityService interface {
   102  	GetTenantID(string) (string, error)
   103  	GetTenantName(string) (string, error)
   104  	VerifyToken(string) (schema.Authorization, error)
   105  	GetServiceAuthorization() (schema.Authorization, error)
   106  	GetClient() *gophercloud.ServiceClient
   107  }
   108  
   109  //NobodyResourceService contains a definition of nobody resources (that do not require authorization)
   110  type NobodyResourceService interface {
   111  	VerifyResourcePath(string) bool
   112  }
   113  
   114  type DefaultNobodyResourceService struct {
   115  	resourcePathRegexes []*regexp.Regexp
   116  }
   117  
   118  func (nrs *DefaultNobodyResourceService) VerifyResourcePath(resourcePath string) bool {
   119  	for _, regex := range nrs.resourcePathRegexes {
   120  		if regex.MatchString(resourcePath) {
   121  			return true
   122  		}
   123  	}
   124  	return false
   125  }
   126  
   127  func NewNobodyResourceService(nobodyResourcePathRegexes []*regexp.Regexp) NobodyResourceService {
   128  	return &DefaultNobodyResourceService{resourcePathRegexes: nobodyResourcePathRegexes}
   129  }
   130  
   131  //NoIdentityService for disabled auth
   132  type NoIdentityService struct {
   133  }
   134  
   135  //GetTenantID returns always admin
   136  func (i *NoIdentityService) GetTenantID(string) (string, error) {
   137  	return "admin", nil
   138  }
   139  
   140  //GetTenantName returns always admin
   141  func (i *NoIdentityService) GetTenantName(string) (string, error) {
   142  	return "admin", nil
   143  }
   144  
   145  //VerifyToken returns always authorization for admin
   146  func (i *NoIdentityService) VerifyToken(string) (schema.Authorization, error) {
   147  	return schema.NewAuthorization("admin", "admin", "admin_token", []string{"admin"}, nil), nil
   148  }
   149  
   150  //GetServiceAuthorization returns always authorization for admin
   151  func (i *NoIdentityService) GetServiceAuthorization() (schema.Authorization, error) {
   152  	return schema.NewAuthorization("admin", "admin", "admin_token", []string{"admin"}, nil), nil
   153  }
   154  
   155  //GetClient returns always nil
   156  func (i *NoIdentityService) GetClient() *gophercloud.ServiceClient {
   157  	return nil
   158  }
   159  
   160  //NobodyIdentityService for nobody auth
   161  type NobodyIdentityService struct {
   162  }
   163  
   164  //GetTenantID returns always nobody
   165  func (i *NobodyIdentityService) GetTenantID(string) (string, error) {
   166  	return "nobody", nil
   167  }
   168  
   169  //GetTenantName returns always nobody
   170  func (i *NobodyIdentityService) GetTenantName(string) (string, error) {
   171  	return "nobody", nil
   172  }
   173  
   174  //VerifyToken returns always authorization for nobody
   175  func (i *NobodyIdentityService) VerifyToken(string) (schema.Authorization, error) {
   176  	return schema.NewAuthorization("nobody", "nobody", "nobody_token", []string{"Nobody"}, nil), nil
   177  }
   178  
   179  //GetServiceAuthorization returns always authorization for nobody
   180  func (i *NobodyIdentityService) GetServiceAuthorization() (schema.Authorization, error) {
   181  	return schema.NewAuthorization("nobody", "nobody", "nobody_token", []string{"Nobody"}, nil), nil
   182  }
   183  
   184  //GetClient returns always nil
   185  func (i *NobodyIdentityService) GetClient() *gophercloud.ServiceClient {
   186  	return nil
   187  }
   188  
   189  //HTTPJSONError helper for returning JSON errors
   190  func HTTPJSONError(res http.ResponseWriter, err string, code int) {
   191  	errorMessage := ""
   192  	if code == http.StatusInternalServerError {
   193  		log.Error(err)
   194  	} else {
   195  		errorMessage = err
   196  		log.Notice(err)
   197  	}
   198  	response := map[string]interface{}{"error": errorMessage}
   199  	responseJSON, _ := json.Marshal(response)
   200  	http.Error(res, string(responseJSON), code)
   201  }
   202  
   203  //Authentication authenticates user using keystone
   204  func Authentication() martini.Handler {
   205  	return func(res http.ResponseWriter, req *http.Request, identityService IdentityService, nobodyResourceService NobodyResourceService, c martini.Context) {
   206  		if req.Method == "OPTIONS" {
   207  			c.Next()
   208  			return
   209  		}
   210  		//TODO(nati) make this configurable
   211  		if strings.HasPrefix(req.URL.Path, webuiPATH) {
   212  			c.Next()
   213  			return
   214  		}
   215  
   216  		if req.URL.Path == "/" || req.URL.Path == "/webui" {
   217  			http.Redirect(res, req, webuiPATH, http.StatusTemporaryRedirect)
   218  			return
   219  		}
   220  
   221  		if req.URL.Path == "/v2.0/tokens" {
   222  			c.Next()
   223  			return
   224  		}
   225  
   226  		authToken := req.Header.Get("X-Auth-Token")
   227  
   228  		var targetIdentityService IdentityService
   229  
   230  		if authToken == "" {
   231  			if nobodyResourceService.VerifyResourcePath(req.URL.Path) {
   232  				targetIdentityService = &NobodyIdentityService{}
   233  			} else {
   234  				HTTPJSONError(res, "No X-Auth-Token", http.StatusUnauthorized)
   235  				return
   236  			}
   237  		} else {
   238  			targetIdentityService = identityService
   239  		}
   240  
   241  		auth, err := targetIdentityService.VerifyToken(authToken)
   242  
   243  		if err != nil {
   244  			HTTPJSONError(res, err.Error(), http.StatusUnauthorized)
   245  			return
   246  		}
   247  
   248  		c.Map(auth)
   249  		c.Next()
   250  	}
   251  }
   252  
   253  //Context type
   254  type Context map[string]interface{}
   255  
   256  //WithContext injects new empty context object
   257  func WithContext() martini.Handler {
   258  	return func(c martini.Context) {
   259  		c.Map(Context{})
   260  	}
   261  }
   262  
   263  //Authorization checks user permissions against policy
   264  func Authorization(action string) martini.Handler {
   265  	return func(res http.ResponseWriter, req *http.Request, auth schema.Authorization, context Context) {
   266  		context["tenant_id"] = auth.TenantID()
   267  		context["tenant_name"] = auth.TenantName()
   268  		context["auth_token"] = auth.AuthToken()
   269  		context["catalog"] = auth.Catalog()
   270  		context["auth"] = auth
   271  	}
   272  }
   273  
   274  // JSONURLs strips ".json" suffixes added to URLs
   275  func JSONURLs() martini.Handler {
   276  	return func(res http.ResponseWriter, req *http.Request, c martini.Context) {
   277  		if !strings.Contains(req.URL.Path, "gohan") && !strings.Contains(req.URL.Path, "webui") {
   278  			req.URL.Path = strings.TrimSuffix(req.URL.Path, ".json")
   279  		}
   280  		c.Next()
   281  	}
   282  }