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 }