Skip to content

Commit 16033bc

Browse files
authored
Generic HTTP Errors (#6)
Siphon these off sos they can be used across all server components.
1 parent 2ecaa71 commit 16033bc

File tree

2 files changed

+274
-2
lines changed

2 files changed

+274
-2
lines changed

charts/core/Chart.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ description: A Helm chart for deploying Unikorn Core
44

55
type: application
66

7-
version: v0.1.5
8-
appVersion: v0.1.5
7+
version: v0.1.6
8+
appVersion: v0.1.6
99

1010
icon: https://assets.unikorn-cloud.org/images/logos/dark-on-light/icon.svg

pkg/server/errors/errors.go

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
/*
2+
Copyright 2022-2024 EscherCloud.
3+
Copyright 2024 the Unikorn Authors.
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
package errors
19+
20+
import (
21+
"encoding/json"
22+
"errors"
23+
"net/http"
24+
25+
"sigs.k8s.io/controller-runtime/pkg/log"
26+
)
27+
28+
// OAuth2ErrorType defines our core error type based on oauth2.
29+
type OAuth2ErrorType string
30+
31+
const (
32+
AccessDenied OAuth2ErrorType = "access_denied"
33+
Conflict OAuth2ErrorType = "conflict"
34+
Forbidden OAuth2ErrorType = "forbidden"
35+
InvalidClient OAuth2ErrorType = "invalid_client"
36+
InvalidGrant OAuth2ErrorType = "invalid_grant"
37+
InvalidRequest OAuth2ErrorType = "invalid_request"
38+
InvalidScope OAuth2ErrorType = "invalid_scope"
39+
MethodNotAllowed OAuth2ErrorType = "method_not_allowed"
40+
NotFound OAuth2ErrorType = "not_found"
41+
ServerError OAuth2ErrorType = "server_error"
42+
TemporarilyUnavailable OAuth2ErrorType = "temporarily_unavailable"
43+
UnauthorizedClient OAuth2ErrorType = "unauthorized_client"
44+
UnsupportedGrantType OAuth2ErrorType = "unsupported_grant_type"
45+
UnsupportedMediaType OAuth2ErrorType = "unsupported_media_type"
46+
UnsupportedResponseType OAuth2ErrorType = "unsupported_response_type"
47+
)
48+
49+
// OAuth2Error is the type sent on the wire on error.
50+
type OAuth2Error struct {
51+
// Error defines the error type.
52+
Error OAuth2ErrorType `json:"error"`
53+
// Description is a verbose description of the error. This should be
54+
// informative to the end user, not a bunch of debugging nonsense. We
55+
// keep that in telemetry dats.
56+
//nolint:tagliatelle
57+
Description string `json:"error_description"`
58+
}
59+
60+
var (
61+
// ErrRequest is raised for all handler errors.
62+
ErrRequest = errors.New("request error")
63+
)
64+
65+
// Error wraps ErrRequest with more contextual information that is used to
66+
// propagate and create suitable responses.
67+
type Error struct {
68+
// status is the HTTP error code.
69+
status int
70+
71+
// code us the terse error code to return to the client.
72+
code OAuth2ErrorType
73+
74+
// description is a verbose description to log/return to the user.
75+
description string
76+
77+
// err is set when the originator was an error. This is only used
78+
// for logging so as not to leak server internals to the client.
79+
err error
80+
81+
// values are arbitrary key value pairs for logging.
82+
values []interface{}
83+
}
84+
85+
// newError returns a new HTTP error.
86+
func newError(status int, code OAuth2ErrorType, description string) *Error {
87+
return &Error{
88+
status: status,
89+
code: code,
90+
description: description,
91+
}
92+
}
93+
94+
// WithError augments the error with an error from a library.
95+
func (e *Error) WithError(err error) *Error {
96+
e.err = err
97+
98+
return e
99+
}
100+
101+
// WithValues augments the error with a set of K/V pairs.
102+
// Values should not use the "error" key as that's implicitly defined
103+
// by WithError and could collide.
104+
func (e *Error) WithValues(values ...interface{}) *Error {
105+
e.values = values
106+
107+
return e
108+
}
109+
110+
// Unwrap implements Go 1.13 errors.
111+
func (e *Error) Unwrap() error {
112+
return ErrRequest
113+
}
114+
115+
// Error implements the error interface.
116+
func (e *Error) Error() string {
117+
return e.description
118+
}
119+
120+
// Write returns the error code and description to the client.
121+
func (e *Error) Write(w http.ResponseWriter, r *http.Request) {
122+
// Log out any detail from the error that shouldn't be
123+
// reported to the client. Do it before things can error
124+
// and return.
125+
log := log.FromContext(r.Context())
126+
127+
var details []interface{}
128+
129+
if e.description != "" {
130+
details = append(details, "detail", e.description)
131+
}
132+
133+
if e.err != nil {
134+
details = append(details, "error", e.err)
135+
}
136+
137+
if e.values != nil {
138+
details = append(details, e.values...)
139+
}
140+
141+
log.Info("error detail", details...)
142+
143+
// Emit the response to the client.
144+
w.Header().Add("Cache-Control", "no-cache")
145+
w.Header().Add("Content-Type", "application/json")
146+
w.WriteHeader(e.status)
147+
148+
// Emit the response body.
149+
ge := &OAuth2Error{
150+
Error: e.code,
151+
Description: e.description,
152+
}
153+
154+
body, err := json.Marshal(ge)
155+
if err != nil {
156+
log.Error(err, "failed to marshal error response")
157+
158+
return
159+
}
160+
161+
if _, err := w.Write(body); err != nil {
162+
log.Error(err, "failed to wirte error response")
163+
164+
return
165+
}
166+
}
167+
168+
// HTTPForbidden is raised when a user isn't permitted to do something by RBAC.
169+
func HTTPForbidden(description string) *Error {
170+
return newError(http.StatusForbidden, Forbidden, description)
171+
}
172+
173+
// HTTPNotFound is raised when the requested resource doesn't exist.
174+
func HTTPNotFound() *Error {
175+
return newError(http.StatusNotFound, NotFound, "resource not found")
176+
}
177+
178+
// IsHTTPNotFound interrogates the error type.
179+
func IsHTTPNotFound(err error) bool {
180+
httpError := &Error{}
181+
182+
if ok := errors.As(err, &httpError); !ok {
183+
return false
184+
}
185+
186+
if httpError.status != http.StatusNotFound {
187+
return false
188+
}
189+
190+
return true
191+
}
192+
193+
// HTTPMethodNotAllowed is raised when the method is not supported.
194+
func HTTPMethodNotAllowed() *Error {
195+
return newError(http.StatusMethodNotAllowed, MethodNotAllowed, "the requested method was not allowed")
196+
}
197+
198+
// HTTPConflict is raised when a request conflicts with another resource.
199+
func HTTPConflict() *Error {
200+
return newError(http.StatusConflict, Conflict, "the requested resource already exists")
201+
}
202+
203+
// OAuth2InvalidRequest indicates a client error.
204+
func OAuth2InvalidRequest(description string) *Error {
205+
return newError(http.StatusBadRequest, InvalidRequest, description)
206+
}
207+
208+
// OAuth2UnauthorizedClient indicates the client is not authorized to perform the
209+
// requested operation.
210+
func OAuth2UnauthorizedClient(description string) *Error {
211+
return newError(http.StatusBadRequest, UnauthorizedClient, description)
212+
}
213+
214+
// OAuth2UnsupportedGrantType is raised when the requested grant is not supported.
215+
func OAuth2UnsupportedGrantType(description string) *Error {
216+
return newError(http.StatusBadRequest, UnsupportedGrantType, description)
217+
}
218+
219+
// OAuth2InvalidGrant is raised when the requested grant is unknown.
220+
func OAuth2InvalidGrant(description string) *Error {
221+
return newError(http.StatusBadRequest, InvalidGrant, description)
222+
}
223+
224+
// OAuth2InvalidClient is raised when the client ID is not known.
225+
func OAuth2InvalidClient(description string) *Error {
226+
return newError(http.StatusBadRequest, InvalidClient, description)
227+
}
228+
229+
// OAuth2AccessDenied tells the client the authentication failed e.g.
230+
// username/password are wrong, or a token has expired and needs reauthentication.
231+
func OAuth2AccessDenied(description string) *Error {
232+
return newError(http.StatusUnauthorized, AccessDenied, description)
233+
}
234+
235+
// OAuth2ServerError tells the client we are at fault, this should never be seen
236+
// in production. If so then our testing needs to improve.
237+
func OAuth2ServerError(description string) *Error {
238+
return newError(http.StatusInternalServerError, ServerError, description)
239+
}
240+
241+
// OAuth2InvalidScope tells the client it doesn't have the necessary scope
242+
// to access the resource.
243+
func OAuth2InvalidScope(description string) *Error {
244+
return newError(http.StatusUnauthorized, InvalidScope, description)
245+
}
246+
247+
// toError is a handy unwrapper to get a HTTP error from a generic one.
248+
func toError(err error) *Error {
249+
var httpErr *Error
250+
251+
if !errors.As(err, &httpErr) {
252+
return nil
253+
}
254+
255+
return httpErr
256+
}
257+
258+
// HandleError is the top level error handler that should be called from all
259+
// path handlers on error.
260+
func HandleError(w http.ResponseWriter, r *http.Request, err error) {
261+
log := log.FromContext(r.Context())
262+
263+
if httpError := toError(err); httpError != nil {
264+
httpError.Write(w, r)
265+
266+
return
267+
}
268+
269+
log.Error(err, "unhandled error")
270+
271+
OAuth2ServerError("unhandled error").Write(w, r)
272+
}

0 commit comments

Comments
 (0)