github.com/companieshouse/lfp-pay-api@v0.0.0-20230203133422-0ca455cd79f9/interceptors/payable_resource_authentication.go (about)

     1  package interceptors
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net/http"
     7  	"strings"
     8  
     9  	"github.com/companieshouse/chs.go/authentication"
    10  	"github.com/companieshouse/chs.go/log"
    11  	"github.com/companieshouse/lfp-pay-api-core/models"
    12  	"github.com/companieshouse/lfp-pay-api/config"
    13  	"github.com/companieshouse/lfp-pay-api/service"
    14  	"github.com/companieshouse/lfp-pay-api/utils"
    15  	"github.com/gorilla/mux"
    16  )
    17  
    18  // PayableAuthenticationInterceptor contains the payable_resource service used in the interceptor
    19  type PayableAuthenticationInterceptor struct {
    20  	Service service.PayableResourceService
    21  }
    22  
    23  // PayableAuthenticationIntercept checks that the user is authenticated for the payable_resource
    24  func (payableAuthInterceptor *PayableAuthenticationInterceptor) PayableAuthenticationIntercept(next http.Handler) http.Handler {
    25  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    26  		companyNumber, payableID, identityType, err := preCheckRequest(w, r)
    27  		if err {
    28  			return
    29  		}
    30  
    31  		authorisedUser := ""
    32  
    33  		if identityType == authentication.Oauth2IdentityType {
    34  			// Get user details from context, passed in by UserAuthenticationInterceptor
    35  			userDetails, ok := r.Context().Value(authentication.ContextKeyUserDetails).(authentication.AuthUserDetails)
    36  			if !ok {
    37  				log.ErrorR(r, fmt.Errorf("PayableAuthenticationInterceptor error: invalid AuthUserDetails from UserAuthenticationInterceptor"))
    38  				w.WriteHeader(http.StatusInternalServerError)
    39  				return
    40  			}
    41  
    42  			// Get user details from request
    43  			authorisedUser = userDetails.ID
    44  			if authorisedUser == "" {
    45  				log.ErrorR(r, fmt.Errorf("PayableAuthenticationInterceptor unauthorised: no authorised identity"))
    46  				w.WriteHeader(http.StatusUnauthorized)
    47  				return
    48  			}
    49  		}
    50  
    51  		payableResource, ret := writeHeader(w, r, payableAuthInterceptor,
    52  			companyNumber, payableID)
    53  		if ret {
    54  			return
    55  		}
    56  
    57  		// Store payable_resource in context to use later in the handler
    58  		ctx := context.WithValue(r.Context(), config.PayableResource, payableResource)
    59  
    60  		// Set up variables that are used to determine authorisation below
    61  		isGetRequest := http.MethodGet == r.Method
    62  		authUserIsPayableResourceCreator := authorisedUser == payableResource.CreatedBy.ID
    63  		authUserHasPenaltyLookupRole := authentication.IsRoleAuthorised(r, utils.AdminPenaltyLookupRole)
    64  		isAPIKeyRequest := identityType == authentication.APIKeyIdentityType
    65  		apiKeyHasElevatedPrivileges := authentication.IsKeyElevatedPrivilegesAuthorised(r)
    66  
    67  		// Set up debug map for logging at each exit point
    68  		debugMap := log.Data{
    69  			"company_number":                             companyNumber,
    70  			"payable_resource_id":                        payableID,
    71  			"auth_user_is_payable_resource_creator":      authUserIsPayableResourceCreator,
    72  			"auth_user_has_payable_resource_lookup_role": authUserHasPenaltyLookupRole,
    73  			"api_key_has_elevated_privileges":            apiKeyHasElevatedPrivileges,
    74  			"request_method":                             r.Method,
    75  		}
    76  
    77  		booleans := booleanParameters{authUserIsPayableResourceCreator,
    78  			authUserHasPenaltyLookupRole, isGetRequest,
    79  			isAPIKeyRequest, apiKeyHasElevatedPrivileges}
    80  
    81  		checkAllowedThrough(w, r, debugMap, next, ctx, booleans)
    82  	})
    83  }
    84  
    85  func preCheckRequest(w http.ResponseWriter, r *http.Request) (string, string, string, bool) {
    86  	// Check for a company_number and payable_id in request
    87  	vars := mux.Vars(r)
    88  	companyNumber := strings.ToUpper(vars["company_number"])
    89  	if companyNumber == "" {
    90  		log.InfoR(r, "PayableAuthenticationInterceptor error: no company_number")
    91  		w.WriteHeader(http.StatusBadRequest)
    92  		return "", "", "", true
    93  	}
    94  	payableID := vars["payable_id"]
    95  	if payableID == "" {
    96  		log.InfoR(r, "PayableAuthenticationInterceptor error: no payable_id")
    97  		w.WriteHeader(http.StatusBadRequest)
    98  		return "", "", "", true
    99  	}
   100  
   101  	// Get identity type from request
   102  	identityType := authentication.GetAuthorisedIdentityType(r)
   103  	if isUnauthorizedIdentityType(identityType) {
   104  		log.InfoR(r, "PayableAuthenticationInterceptor unauthorised: not oauth2 or API key identity type")
   105  		w.WriteHeader(http.StatusUnauthorized)
   106  		return "", "", "", true
   107  	}
   108  	return companyNumber, payableID, identityType, false
   109  }
   110  
   111  func checkAllowedThrough(w http.ResponseWriter,
   112  	r *http.Request,
   113  	debugMap log.Data,
   114  	next http.Handler,
   115  	ctx context.Context,
   116  	b booleanParameters) {
   117  	// Now that we have the payable resource data and authorized user there are
   118  	// multiple cases that can be allowed through:
   119  	switch {
   120  	case b.authUserIsPayableResourceCreator:
   121  		// 1) Authorized user created the payable_resource
   122  		log.InfoR(r, "PayableAuthenticationInterceptor authorised as creator", debugMap)
   123  		// Call the next handler
   124  		next.ServeHTTP(w, r.WithContext(ctx))
   125  	case isAuthorizedGetRequest(b.authUserHasPenaltyLookupRole, b.isGetRequest):
   126  		// 2) Authorized user has permission to lookup any payable_resource and
   127  		// request is a GET i.e. to see payable_resource data but not modify/delete
   128  		log.InfoR(r, "PayableAuthenticationInterceptor authorised as admin penalty lookup role on GET", debugMap)
   129  		// Call the next handler
   130  		next.ServeHTTP(w, r.WithContext(ctx))
   131  	case isAuthorizedApiKeyRequest(b.isAPIKeyRequest, b.apiKeyHasElevatedPrivileges):
   132  		// 3) Authorized API key with elevated privileges is an internal API key
   133  		// that we trust
   134  		log.InfoR(r, "PayableAuthenticationInterceptor authorised as api key elevated user", debugMap)
   135  		// Call the next handler
   136  		next.ServeHTTP(w, r.WithContext(ctx))
   137  	default:
   138  		// If none of the above conditions above are met then the request is
   139  		// unauthorized
   140  		w.WriteHeader(http.StatusUnauthorized)
   141  		log.InfoR(r, "PayableAuthenticationInterceptor unauthorised", debugMap)
   142  	}
   143  }
   144  
   145  func writeHeader(w http.ResponseWriter,
   146  	r *http.Request,
   147  	payableAuthInterceptor *PayableAuthenticationInterceptor,
   148  	companyNumber string,
   149  	payableID string) (*models.PayableResource, bool) {
   150  	// Get the payable resource from the ID in request
   151  	payableResource, responseType, err := payableAuthInterceptor.Service.GetPayableResource(r, companyNumber, payableID)
   152  	if err != nil {
   153  		log.ErrorR(r, fmt.Errorf("PayableAuthenticationInterceptor error when retrieving payable_resource: [%v]", err), log.Data{"service_response_type": responseType.String()})
   154  		switch responseType {
   155  		case service.Forbidden:
   156  			w.WriteHeader(http.StatusForbidden)
   157  			return nil, true
   158  		default:
   159  			w.WriteHeader(http.StatusInternalServerError)
   160  			return nil, true
   161  		}
   162  	}
   163  
   164  	if responseType == service.NotFound {
   165  		log.InfoR(r, "PayableAuthenticationInterceptor not found", log.Data{"payable_id": payableID, "company_number": companyNumber})
   166  		w.WriteHeader(http.StatusNotFound)
   167  		return nil, true
   168  	}
   169  
   170  	if responseType != service.Success {
   171  		log.ErrorR(r, fmt.Errorf("PayableAuthenticationInterceptor error when retrieving payable_resource. Status: [%s]", responseType.String()))
   172  		w.WriteHeader(http.StatusInternalServerError)
   173  		return nil, true
   174  	}
   175  	return payableResource, false
   176  }
   177  
   178  func isUnauthorizedIdentityType(identityType string) bool {
   179  	return !(identityType == authentication.Oauth2IdentityType ||
   180  		identityType == authentication.APIKeyIdentityType)
   181  }
   182  
   183  func isAuthorizedApiKeyRequest(isAPIKeyRequest bool,
   184  	apiKeyHasElevatedPrivileges bool) bool {
   185  	return isAPIKeyRequest && apiKeyHasElevatedPrivileges
   186  }
   187  
   188  func isAuthorizedGetRequest(authUserHasPenaltyLookupRole bool,
   189  	isGetRequest bool) bool {
   190  	return authUserHasPenaltyLookupRole && isGetRequest
   191  }
   192  
   193  type booleanParameters struct {
   194  	authUserIsPayableResourceCreator bool
   195  	authUserHasPenaltyLookupRole     bool
   196  	isGetRequest                     bool
   197  	isAPIKeyRequest                  bool
   198  	apiKeyHasElevatedPrivileges      bool
   199  }