router_utils.go

Copyright 2020, 2021, 2022 Red Hat, Inc

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

 http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

package
httputils

Documentation in literate-programming-style is available at: https://redhatinsights.github.io/insights-operator-utils/packages/http/router_utils.html


import
(
"encoding/json"
"errors"
"fmt"
"net/http"
"regexp"
"strconv"
"strings"
ctypes
"github.com/RedHatInsights/insights-results-types"
"github.com/google/uuid"
"github.com/gorilla/mux"
"github.com/rs/zerolog/log"
"github.com/RedHatInsights/insights-operator-utils/types"
)
var
(

RuleIDValidator points to a Regexp expression that matches any string that has alphanumeric characters separated by at least one dot (".")

	
RuleIDValidator
=
regexp
.
MustCompile
(
`^[a-zA-Z_0-9.]+$`
)

RuleSelectorValidator points to a Regexp expression that matches any string that has alphanumeric characters separated by at least one dot (".") before a vertical line ("|"), followed by only characters, numbers, or underscores ("_")

	
RuleSelectorValidator
=
regexp
.
MustCompile
(
`[a-zA-Z_0-9]+\.[a-zA-Z_0-9.]+\|[a-zA-Z_0-9]+$`
)
)

GetRouterParam retrieves parameter from URL like /organization/{org_id}

func
GetRouterParam
(
request
*
http
.
Request
,
paramName
string
)
(
string
,
error
)
{
value
,
found
:=
mux
.
Vars
(
request
)
[
paramName
]
if
!
found
{
return
""
,
&
types
.
RouterMissingParamError
{
ParamName
:
paramName
}
}
return
value
,
nil
}

GetRouterPositiveIntParam retrieves parameter from URL like /organization/{org_id} and check it for being valid and positive integer, otherwise returns error

func
GetRouterPositiveIntParam
(
request
*
http
.
Request
,
paramName
string
)
(
uint64
,
error
)
{
value
,
err
:=
GetRouterParam
(
request
,
paramName
)
if
err
!=
nil
{
return
0
,
err
}
uintValue
,
err
:=
strconv
.
ParseUint
(
value
,
10
,
64
)
if
err
!=
nil
{
return
0
,
&
types
.
RouterParsingError
{
ParamName
:
paramName
,
ParamValue
:
value
,
ErrString
:
"unsigned integer expected"
,
}
}
if
uintValue
==
0
{
return
0
,
&
types
.
RouterParsingError
{
ParamName
:
paramName
,
ParamValue
:
value
,
ErrString
:
"positive value expected"
,
}
}
return
uintValue
,
nil
}

ReadClusterName retrieves cluster name from request if it's not possible, it writes http error to the writer and returns false

func
ReadClusterName
(
writer
http
.
ResponseWriter
,
request
*
http
.
Request
)
(
ctypes
.
ClusterName
,
bool
)
{
clusterName
,
err
:=
GetRouterParam
(
request
,
"cluster"
)
if
err
!=
nil
{
handleClusterNameError
(
writer
,
err
)
return
""
,
false
}
validatedClusterName
,
err
:=
ValidateClusterName
(
clusterName
)
if
err
!=
nil
{
handleClusterNameError
(
writer
,
err
)
return
""
,
false
}
return
validatedClusterName
,
true
}

ReadRuleID retrieves rule id from request's url or writes an error to writer. The function returns a rule id and a bool indicating if it was successful.

func
ReadRuleID
(
writer
http
.
ResponseWriter
,
request
*
http
.
Request
)
(
ctypes
.
RuleID
,
bool
)
{
ruleID
,
err
:=
GetRouterParam
(
request
,
"rule_id"
)
if
err
!=
nil
{
const
message
=
"unable to get rule id"
log
.
Warn
(
)
.
Err
(
err
)
.
Msg
(
message
)
types
.
HandleServerError
(
writer
,
err
)
return
ctypes
.
RuleID
(
"0"
)
,
false
}
isRuleIDValid
:=
RuleIDValidator
.
MatchString
(
ruleID
)
if
!
isRuleIDValid
{
err
=
fmt
.
Errorf
(
"invalid rule ID, it must contain only from latin characters, number, underscores or dots"
)
log
.
Warn
(
)
.
Err
(
err
)
types
.
HandleServerError
(
writer
,
&
types
.
RouterParsingError
{
ParamName
:
"rule_id"
,
ParamValue
:
ruleID
,
ErrString
:
err
.
Error
(
)
,
}
)
return
ctypes
.
RuleID
(
"0"
)
,
false
}
return
ctypes
.
RuleID
(
ruleID
)
,
true
}

ReadErrorKey retrieves error key from request's url or writes an error to writer. The function returns an error key and a bool indicating if it was successful.

func
ReadErrorKey
(
writer
http
.
ResponseWriter
,
request
*
http
.
Request
)
(
ctypes
.
ErrorKey
,
bool
)
{
errorKey
,
err
:=
GetRouterParam
(
request
,
"error_key"
)
if
err
!=
nil
{
const
message
=
"unable to get error_key"
log
.
Warn
(
)
.
Err
(
err
)
.
Msg
(
message
)
types
.
HandleServerError
(
writer
,
err
)
return
ctypes
.
ErrorKey
(
"0"
)
,
false
}
return
ctypes
.
ErrorKey
(
errorKey
)
,
true
}

ReadRuleSelector retrieves the rule selector (ruleid|errorkey) from request's url or writes an error to writer. The function returns the selector and a bool indicating if it was successful.

func
ReadRuleSelector
(
writer
http
.
ResponseWriter
,
request
*
http
.
Request
)
(
ctypes
.
RuleSelector
,
bool
)
{
ruleSelector
,
err
:=
GetRouterParam
(
request
,
"rule_selector"
)
if
err
!=
nil
{
const
message
=
"Unable to get rule selector from request"
log
.
Warn
(
)
.
Err
(
err
)
.
Msg
(
message
)
types
.
HandleServerError
(
writer
,
err
)
return
""
,
false
}
isRuleSelectorValid
:=
RuleSelectorValidator
.
MatchString
(
ruleSelector
)
if
!
isRuleSelectorValid
{
errMsg
:=
"Param rule_selector is not a valid rule selector (plugin_name|error_key)"
log
.
Warn
(
)
.
Msg
(
errMsg
)
types
.
HandleServerError
(
writer
,
&
types
.
RouterParsingError
{
ParamName
:
"rule_selector"
,
ParamValue
:
ruleSelector
,
ErrString
:
errMsg
,
}
)
return
""
,
false
}
return
ctypes
.
RuleSelector
(
ruleSelector
)
,
true
}

ReadAndTrimRuleSelector retrieves the rule selector (ruleid|errorkey) from request's url or writes an error to writer. The function returns the selector WITHOUT '.report' and a bool indicating if retrieval was successful.

func
ReadAndTrimRuleSelector
(
writer
http
.
ResponseWriter
,
request
*
http
.
Request
)
(
ctypes
.
RuleSelector
,
bool
)
{
selector
,
success
:=
ReadRuleSelector
(
writer
,
request
)
if
!
success
{
return
""
,
false
}
return
ctypes
.
RuleSelector
(
strings
.
ReplaceAll
(
string
(
selector
)
,
".report|"
,
"|"
)
)
,
success
}

ReadOrganizationID retrieves organization id from request if it's not possible, it writes http error to the writer and returns false

func
ReadOrganizationID
(
writer
http
.
ResponseWriter
,
request
*
http
.
Request
,
auth
bool
)
(
ctypes
.
OrgID
,
bool
)
{
organizationID
,
err
:=
GetRouterPositiveIntParam
(
request
,
"organization"
)
if
err
!=
nil
{
HandleOrgIDError
(
writer
,
err
)
return
0
,
false
}
orgID
,
err
:=
types
.
Uint64ToUint32
(
organizationID
)
if
err
!=
nil
{
HandleOrgIDError
(
writer
,
err
)
return
0
,
false
}
successful
:=
CheckPermissions
(
writer
,
request
,
ctypes
.
OrgID
(
orgID
)
,
auth
)
return
ctypes
.
OrgID
(
orgID
)
,
successful
}

ReadClusterNames does the same as readClusterName, except for multiple clusters.

func
ReadClusterNames
(
writer
http
.
ResponseWriter
,
request
*
http
.
Request
)
(
[
]
ctypes
.
ClusterName
,
bool
)
{
clusterNamesParam
,
err
:=
GetRouterParam
(
request
,
"clusters"
)
if
err
!=
nil
{
message
:=
fmt
.
Sprintf
(
"Cluster names are not provided %v"
,
err
.
Error
(
)
)
log
.
Warn
(
)
.
Msg
(
message
)
types
.
HandleServerError
(
writer
,
err
)
return
[
]
ctypes
.
ClusterName
{
}
,
false
}
clusterNamesConverted
:=
make
(
[
]
ctypes
.
ClusterName
,
0
)
for
_
,
clusterName
:=
range
SplitRequestParamArray
(
clusterNamesParam
)
{
convertedName
,
err
:=
ValidateClusterName
(
clusterName
)
if
err
!=
nil
{
types
.
HandleServerError
(
writer
,
err
)
return
[
]
ctypes
.
ClusterName
{
}
,
false
}
clusterNamesConverted
=
append
(
clusterNamesConverted
,
convertedName
)
}
return
clusterNamesConverted
,
true
}

parseAndValidateOrgID parses and validates a single organization ID string.

func
parseAndValidateOrgID
(
writer
http
.
ResponseWriter
,
orgStr
string
)
(
ctypes
.
OrgID
,
bool
)
{
v
,
err
:=
strconv
.
ParseUint
(
orgStr
,
10
,
64
)
if
err
!=
nil
{
handleOrgIDParsingError
(
writer
,
orgStr
,
"integer array expected"
)
return
0
,
false
}
orgInt
,
err
:=
types
.
Uint64ToUint32
(
v
)
if
err
!=
nil
{
handleOrgIDParsingError
(
writer
,
orgStr
,
"integer array expected"
)
return
0
,
false
}
return
ctypes
.
OrgID
(
orgInt
)
,
true
}

handleOrgIDParsingError handles the error for parsing organization IDs.

func
handleOrgIDParsingError
(
writer
http
.
ResponseWriter
,
orgStr
,
errString
string
)
{
types
.
HandleServerError
(
writer
,
&
types
.
RouterParsingError
{
ParamName
:
"organizations"
,
ParamValue
:
orgStr
,
ErrString
:
errString
,
}
)
}

ReadOrganizationIDs does the same as readOrganizationID, except for multiple organizations.

func
ReadOrganizationIDs
(
writer
http
.
ResponseWriter
,
request
*
http
.
Request
)
(
[
]
ctypes
.
OrgID
,
bool
)
{
organizationsParam
,
err
:=
GetRouterParam
(
request
,
"organizations"
)
if
err
!=
nil
{
HandleOrgIDError
(
writer
,
err
)
return
[
]
ctypes
.
OrgID
{
}
,
false
}
organizationsConverted
:=
make
(
[
]
ctypes
.
OrgID
,
0
)
for
_
,
orgStr
:=
range
SplitRequestParamArray
(
organizationsParam
)
{
orgID
,
ok
:=
parseAndValidateOrgID
(
writer
,
orgStr
)
if
!
ok
{
return
[
]
ctypes
.
OrgID
{
}
,
false
}
organizationsConverted
=
append
(
organizationsConverted
,
orgID
)
}
return
organizationsConverted
,
true
}

HandleOrgIDError logs org id error and writes corresponding http response

func
HandleOrgIDError
(
writer
http
.
ResponseWriter
,
err
error
)
{
log
.
Warn
(
)
.
Err
(
err
)
.
Msg
(
"error getting organization ID from request"
)
types
.
HandleServerError
(
writer
,
err
)
}

CheckPermissions checks whether user with a provided token(from request) can access current organization and handled the error on negative result by logging the error and writing a corresponding http response

func
CheckPermissions
(
writer
http
.
ResponseWriter
,
request
*
http
.
Request
,
orgID
ctypes
.
OrgID
,
auth
bool
)
bool
{
identityContext
:=
request
.
Context
(
)
.
Value
(
ctypes
.
ContextKeyUser
)
if
identityContext
!=
nil
&&
auth
{
identity
:=
identityContext
.
(
ctypes
.
Identity
)
if
identity
.
OrgID
!=
orgID
{
message
:=
fmt
.
Sprintf
(
"you have no permissions to get or change info about the organization "
+
"with ID %d; you can access info about organization with ID %d"
,
orgID
,
identity
.
OrgID
)
log
.
Warn
(
)
.
Msg
(
message
)
types
.
HandleServerError
(
writer
,
&
types
.
ForbiddenError
{
ErrString
:
message
}
)
return
false
}
}
return
true
}

ValidateClusterName checks that the cluster name is a valid UUID. Converted cluster name is returned if everything is okay, otherwise an error is returned.

func
ValidateClusterName
(
clusterName
string
)
(
ctypes
.
ClusterName
,
error
)
{
if
_
,
err
:=
uuid
.
Parse
(
clusterName
)
;
err
!=
nil
{
message
:=
fmt
.
Sprintf
(
"invalid cluster name: '%s'. Error: %s"
,
clusterName
,
err
.
Error
(
)
)
log
.
Warn
(
)
.
Err
(
err
)
.
Msg
(
message
)
return
""
,
&
types
.
RouterParsingError
{
ParamName
:
"cluster"
,
ParamValue
:
clusterName
,
ErrString
:
err
.
Error
(
)
,
}
}
return
ctypes
.
ClusterName
(
clusterName
)
,
nil
}
func
handleClusterNameError
(
writer
http
.
ResponseWriter
,
err
error
)
{
log
.
Warn
(
)
.
Msg
(
err
.
Error
(
)
)

query parameter 'cluster' can't be found in request, which might be caused by issue in Gorilla mux (not on client side), but let's assume it won't :)

	
types
.
HandleServerError
(
writer
,
err
)
}

SplitRequestParamArray takes a single HTTP request parameter and splits it into a slice of strings. This assumes that the parameter is a comma-separated array.

func
SplitRequestParamArray
(
arrayParam
string
)
[
]
string
{
return
strings
.
Split
(
arrayParam
,
","
)
}

ReadClusterListFromPath retrieves list of clusters from request's path if it's not possible, it writes http error to the writer and returns false

func
ReadClusterListFromPath
(
writer
http
.
ResponseWriter
,
request
*
http
.
Request
)
(
[
]
string
,
bool
)
{
rawClusterList
,
err
:=
GetRouterParam
(
request
,
"cluster_list"
)
if
err
!=
nil
{
types
.
HandleServerError
(
writer
,
err
)
return
[
]
string
{
}
,
false
}

basic check that should not happen in reality (because of Gorilla mux checks)

	
if
rawClusterList
==
""
{
types
.
HandleServerError
(
writer
,
errors
.
New
(
"cluster list is empty"
)
)
return
[
]
string
{
}
,
false
}

split the list into items

	
clusterList
:=
strings
.
Split
(
rawClusterList
,
","
)

everything seems ok -> return list of clusters

	
return
clusterList
,
true
}

ReadClusterListFromBody retrieves list of clusters from request's body if it's not possible, it writes http error to the writer and returns false

func
ReadClusterListFromBody
(
writer
http
.
ResponseWriter
,
request
*
http
.
Request
)
(
[
]
string
,
bool
)
{
var
clusterList
ctypes
.
ClusterListInRequest

check if there's any body provided in the request sent by client

	
if
request
.
ContentLength
<=
0
{
err
:=
&
types
.
NoBodyError
{
}
types
.
HandleServerError
(
writer
,
err
)
return
[
]
string
{
}
,
false
}

try to read cluster list from request parameter

	
err
:=
json
.
NewDecoder
(
request
.
Body
)
.
Decode
(
&
clusterList
)
if
err
!=
nil
{
types
.
HandleServerError
(
writer
,
err
)
return
[
]
string
{
}
,
false
}

everything seems ok -> return list of clusters

	
return
clusterList
.
Clusters
,
true
}