mirror of
https://github.com/fleetdm/fleet
synced 2026-05-20 15:38:39 +00:00
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #37244 Goal: Make common_mysql package independent of domain packages so it can be reused by future bounded contexts. Changes made: 1. List options decoupling The AppendListOptionsToSQL functions previously required fleet.ListOptions directly. Now common_mysql defines its own interface that describes what a list options type must provide (page number, per-page limit, sort order, etc.). The fleet.ListOptions type implements this interface through new getter methods. This lets any bounded context use the SQL helpers without importing the fleet package. 2. Error types moved Database-specific error types like IsDuplicate and IsChildForeignKeyError were moved from fleet package to common_mysql where they belong. A new http/errors.go file was created for the HTTP-specific error helpers that remain in the platform layer. 3. Configuration restructuring MySQL configuration types and functions were moved to common_mysql/config.go, reducing coupling between packages. 4. Architecture tests added A new arch_test.go file enforces that common_mysql doesn't import domain packages like fleet, preventing future regressions. # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added cursor-based pagination support for list queries with improved sorting capabilities including secondary order keys. * **Bug Fixes** * Improved database connection initialization with separate connection management and error handling. * **Refactor** * Consolidated error handling interfaces and decoupled configuration structures for better modularity. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
378 lines
9.8 KiB
Go
378 lines
9.8 KiB
Go
package http
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"reflect"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// ErrWithInternal defines an interface for errors that have an internal message
|
|
// that should only be logged, not returned to the client.
|
|
type ErrWithInternal interface {
|
|
error
|
|
// Internal returns the error string that must only be logged internally,
|
|
// not returned to the client.
|
|
Internal() string
|
|
}
|
|
|
|
// ErrWithLogFields defines an interface for errors that have additional log fields.
|
|
type ErrWithLogFields interface {
|
|
error
|
|
// LogFields returns the additional log fields to add, which should come in
|
|
// key, value pairs (as used in go-kit log).
|
|
LogFields() []any
|
|
}
|
|
|
|
// ErrorUUIDer defines an interface for errors that have a UUID for tracking.
|
|
type ErrorUUIDer interface {
|
|
// UUID returns the error's UUID.
|
|
UUID() string
|
|
}
|
|
|
|
// ErrorWithUUID can be embedded in error types to implement ErrorUUIDer.
|
|
type ErrorWithUUID struct {
|
|
uuid string
|
|
}
|
|
|
|
var _ ErrorUUIDer = (*ErrorWithUUID)(nil)
|
|
|
|
// UUID implements the ErrorUUIDer interface.
|
|
func (e *ErrorWithUUID) UUID() string {
|
|
if e.uuid == "" {
|
|
u, err := uuid.NewRandom()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
e.uuid = u.String()
|
|
}
|
|
return e.uuid
|
|
}
|
|
|
|
// BadRequestError is the error returned when the request is invalid.
|
|
type BadRequestError struct {
|
|
Message string
|
|
InternalErr error
|
|
|
|
ErrorWithUUID
|
|
}
|
|
|
|
// Error returns the error message.
|
|
func (e *BadRequestError) Error() string {
|
|
return e.Message
|
|
}
|
|
|
|
// BadRequestError implements the interface required by the server/service package logic
|
|
// to determine the status code to return to the client.
|
|
func (e *BadRequestError) BadRequestError() []map[string]string {
|
|
return nil
|
|
}
|
|
|
|
// Internal implements the ErrWithInternal interface.
|
|
func (e BadRequestError) Internal() string {
|
|
if e.InternalErr != nil {
|
|
return e.InternalErr.Error()
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// UserMessageError is an error that wraps another error with a user-friendly message.
|
|
type UserMessageError struct {
|
|
error
|
|
statusCode int
|
|
|
|
ErrorWithUUID
|
|
}
|
|
|
|
// NewUserMessageError creates a UserMessageError that will translate the
|
|
// error message of err to a user-friendly form. If statusCode is > 0, it
|
|
// will be used as the HTTP status code for the error, otherwise it defaults
|
|
// to http.StatusUnprocessableEntity (422).
|
|
func NewUserMessageError(err error, statusCode int) *UserMessageError {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
return &UserMessageError{
|
|
error: err,
|
|
statusCode: statusCode,
|
|
}
|
|
}
|
|
|
|
// StatusCode returns the HTTP status code for this error.
|
|
func (e UserMessageError) StatusCode() int {
|
|
if e.statusCode > 0 {
|
|
return e.statusCode
|
|
}
|
|
return http.StatusUnprocessableEntity
|
|
}
|
|
|
|
var rxJSONUnknownField = regexp.MustCompile(`^json: unknown field "(.+)"$`)
|
|
|
|
// IsJSONUnknownFieldError returns true if err is a JSON unknown field error.
|
|
// There is no exported type or value for this error, so we have to match the
|
|
// error message.
|
|
func IsJSONUnknownFieldError(err error) bool {
|
|
return rxJSONUnknownField.MatchString(err.Error())
|
|
}
|
|
|
|
// GetJSONUnknownField returns the unknown field name from a JSON unknown field error.
|
|
func GetJSONUnknownField(err error) *string {
|
|
errCause := Cause(err)
|
|
if IsJSONUnknownFieldError(errCause) {
|
|
substr := rxJSONUnknownField.FindStringSubmatch(errCause.Error())
|
|
return &substr[1]
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// UserMessage implements the user-friendly translation of the error if its
|
|
// root cause is one of the supported types, otherwise it returns the error
|
|
// message.
|
|
func (e UserMessageError) UserMessage() string {
|
|
cause := Cause(e.error)
|
|
switch cause := cause.(type) {
|
|
case *json.UnmarshalTypeError:
|
|
var sb strings.Builder
|
|
curType := cause.Type
|
|
for curType.Kind() == reflect.Slice || curType.Kind() == reflect.Array {
|
|
sb.WriteString("array of ")
|
|
curType = curType.Elem()
|
|
}
|
|
sb.WriteString(curType.Name())
|
|
if curType != cause.Type {
|
|
// it was an array
|
|
sb.WriteString("s")
|
|
}
|
|
|
|
return fmt.Sprintf("invalid value type at '%s': expected %s but got %s", cause.Field, sb.String(), cause.Value)
|
|
|
|
default:
|
|
// there's no specific error type for the strict json mode
|
|
// (DisallowUnknownFields), so resort to message-matching.
|
|
if matches := rxJSONUnknownField.FindStringSubmatch(cause.Error()); matches != nil {
|
|
return fmt.Sprintf("unsupported key provided: %q", matches[1])
|
|
}
|
|
return e.Error()
|
|
}
|
|
}
|
|
|
|
// Cause returns the root error in err's chain.
|
|
func Cause(err error) error {
|
|
for {
|
|
uerr := errors.Unwrap(err)
|
|
if uerr == nil {
|
|
return err
|
|
}
|
|
err = uerr
|
|
}
|
|
}
|
|
|
|
// ErrWithRetryAfter is an interface for errors that should set a specific HTTP
|
|
// Header Retry-After value (see
|
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After)
|
|
type ErrWithRetryAfter interface {
|
|
error
|
|
// RetryAfter returns the number of seconds to wait before retry.
|
|
RetryAfter() int
|
|
}
|
|
|
|
// ForeignKeyError is an interface for errors caused by foreign key constraint violations.
|
|
type ForeignKeyError interface {
|
|
error
|
|
IsForeignKey() bool
|
|
}
|
|
|
|
// IsForeignKey returns true if err is a foreign key constraint violation.
|
|
func IsForeignKey(err error) bool {
|
|
var fke ForeignKeyError
|
|
if errors.As(err, &fke) {
|
|
return fke.IsForeignKey()
|
|
}
|
|
return false
|
|
}
|
|
|
|
// NotFoundError is an interface for errors when a resource cannot be found.
|
|
type NotFoundError interface {
|
|
error
|
|
IsNotFound() bool
|
|
}
|
|
|
|
// IsNotFound returns true if err is a not-found error.
|
|
func IsNotFound(err error) bool {
|
|
var nfe NotFoundError
|
|
if errors.As(err, &nfe) {
|
|
return nfe.IsNotFound()
|
|
}
|
|
return false
|
|
}
|
|
|
|
// AlreadyExistsError is an interface for errors when a resource already exists.
|
|
type AlreadyExistsError interface {
|
|
error
|
|
IsExists() bool
|
|
}
|
|
|
|
// Error is a generic error type with a code and message.
|
|
type Error struct {
|
|
Code int `json:"code,omitempty"`
|
|
Message string `json:"message,omitempty"`
|
|
|
|
ErrorWithUUID
|
|
}
|
|
|
|
// Error returns the error message.
|
|
func (e *Error) Error() string {
|
|
return e.Message
|
|
}
|
|
|
|
// ErrWithIsClientError is an interface for errors that explicitly specify
|
|
// whether they are client errors or not. By default, errors are treated as
|
|
// server errors.
|
|
type ErrWithIsClientError interface {
|
|
error
|
|
IsClientError() bool
|
|
}
|
|
|
|
// AuthFailedError is returned when authentication fails.
|
|
type AuthFailedError struct {
|
|
// internal is the reason that should only be logged internally
|
|
internal string
|
|
|
|
ErrorWithUUID
|
|
}
|
|
|
|
// NewAuthFailedError creates a new AuthFailedError.
|
|
func NewAuthFailedError(internal string) *AuthFailedError {
|
|
return &AuthFailedError{internal: internal}
|
|
}
|
|
|
|
// Error implements the error interface.
|
|
func (e AuthFailedError) Error() string {
|
|
return "Authentication failed"
|
|
}
|
|
|
|
// Internal implements ErrWithInternal.
|
|
func (e AuthFailedError) Internal() string {
|
|
return e.internal
|
|
}
|
|
|
|
// StatusCode implements kithttp.StatusCoder.
|
|
func (e AuthFailedError) StatusCode() int {
|
|
return http.StatusUnauthorized
|
|
}
|
|
|
|
// AuthRequiredError is returned when authentication is required.
|
|
type AuthRequiredError struct {
|
|
// internal is the reason that should only be logged internally
|
|
internal string
|
|
|
|
ErrorWithUUID
|
|
}
|
|
|
|
// NewAuthRequiredError creates a new AuthRequiredError.
|
|
func NewAuthRequiredError(internal string) *AuthRequiredError {
|
|
return &AuthRequiredError{internal: internal}
|
|
}
|
|
|
|
// Error implements the error interface.
|
|
func (e AuthRequiredError) Error() string {
|
|
return "Authentication required"
|
|
}
|
|
|
|
// Internal implements ErrWithInternal.
|
|
func (e AuthRequiredError) Internal() string {
|
|
return e.internal
|
|
}
|
|
|
|
// StatusCode implements kithttp.StatusCoder.
|
|
func (e AuthRequiredError) StatusCode() int {
|
|
return http.StatusUnauthorized
|
|
}
|
|
|
|
// AuthHeaderRequiredError is returned when an authorization header is required.
|
|
type AuthHeaderRequiredError struct {
|
|
// internal is the reason that should only be logged internally
|
|
internal string
|
|
|
|
ErrorWithUUID
|
|
}
|
|
|
|
// NewAuthHeaderRequiredError creates a new AuthHeaderRequiredError.
|
|
func NewAuthHeaderRequiredError(internal string) *AuthHeaderRequiredError {
|
|
return &AuthHeaderRequiredError{
|
|
internal: internal,
|
|
}
|
|
}
|
|
|
|
// Error implements the error interface.
|
|
func (e AuthHeaderRequiredError) Error() string {
|
|
return "Authorization header required"
|
|
}
|
|
|
|
// Internal implements ErrWithInternal.
|
|
func (e AuthHeaderRequiredError) Internal() string {
|
|
return e.internal
|
|
}
|
|
|
|
// StatusCode implements kithttp.StatusCoder.
|
|
func (e AuthHeaderRequiredError) StatusCode() int {
|
|
return http.StatusUnauthorized
|
|
}
|
|
|
|
// ErrPasswordResetRequired is returned when a password reset is required.
|
|
var ErrPasswordResetRequired = &passwordResetRequiredError{}
|
|
|
|
type passwordResetRequiredError struct {
|
|
ErrorWithUUID
|
|
}
|
|
|
|
// Error implements the error interface.
|
|
func (e passwordResetRequiredError) Error() string {
|
|
return "password reset required"
|
|
}
|
|
|
|
// StatusCode implements kithttp.StatusCoder.
|
|
func (e passwordResetRequiredError) StatusCode() int {
|
|
return http.StatusUnauthorized
|
|
}
|
|
|
|
// ForbiddenErrorMessage is the error message that should be returned to
|
|
// clients when an action is forbidden. It is intentionally vague to prevent
|
|
// disclosing information that a client should not have access to.
|
|
const ForbiddenErrorMessage = "forbidden"
|
|
|
|
// CheckMissing is the error to return when no authorization check was performed
|
|
// by the service.
|
|
type CheckMissing struct {
|
|
response any
|
|
|
|
ErrorWithUUID
|
|
}
|
|
|
|
// CheckMissingWithResponse creates a new error indicating the authorization
|
|
// check was missed, and including the response for further analysis by the error
|
|
// encoder.
|
|
func CheckMissingWithResponse(response any) *CheckMissing {
|
|
return &CheckMissing{response: response}
|
|
}
|
|
|
|
// Error implements the error interface.
|
|
func (e *CheckMissing) Error() string {
|
|
return ForbiddenErrorMessage
|
|
}
|
|
|
|
// Internal implements the ErrWithInternal interface.
|
|
func (e *CheckMissing) Internal() string {
|
|
return "Missing authorization check"
|
|
}
|
|
|
|
// Response returns the response that was generated before the authorization
|
|
// check was found to be missing.
|
|
func (e *CheckMissing) Response() any {
|
|
return e.response
|
|
}
|