mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-09-12 13:49:13 -04:00
Introducing gowrap as a build-time tool to generate interface delegate structs from templates: * added as a 'make go-generate' target in services/graph, * added as a build-time dependency in .bingo/ Introduce an LDAP client abstraction interface to be able to wrap the go-ldap client API with metrics transparently (and possibly hooks and such in the future), in order to use delegation patterns to measure the time LDAP (client) operations take to finish, as well as to track their results (success, failure, not-found). Has two implementations that are generated using gowrap: * a go-ldap adapter implementation that directly delegates to a go-ldap connection * a time measuring and metrics collecting implementation that delegates to another LdapClient The metrics collecting one is disabled by default, can be enabled with GRAPH_LDAP_METRICS_DISABLE=false It collects durations of outbound LDAP client operations into a histogram, as well as the number of concurrent outbound LDAP operations in a gauge (via an atomic int and a gauge function, as that performs best). Add an HTTP middleware that measures how long Graph HTTP API requests take, storing taken time into a histogram along with labels for * method, * path pattern (from the chi routes), * Graph API version prefix, * Graph API resource name, * and the resulting status code. It also tracks the number of concurrent inbound Graph API HTTP requests using a gauge (also using an atomic int and a gauge function). Disabled by default, can be enabled with GRAPH_HTTP_METRICS_DISABLE=false Add Backend and EducationBackend delegate implementations that measure execution time on the level of the higher API call operations there (CreateUser, DeleteUser, ..., CreateSchool, ...), generated using gowrap. Disabled by default, can be enabled with GRAPH_IDENTITY_BACKEND_METRICS_DISABLE=false Also added a small k6 script to produce some read-only load on the Graph API, for a casual test of the metrics, as well as k6 in mise.toml. Make an internal changes to how singular LDAP entry searches work in the LDAP identity backends: * check whether searches for a singular entry returns more than one result, in which case a new error TooManyResults is returned, instead of leaving that undetected, blindly taking the first result, and potentially risking data inconsistencies Improve the loggers in identity backends by adding attributes for their request targets (Reva gateway address or LDAP URI, respectively). Also add a "backend" attribute for all Graph API logs (set to "ldap" or "cs3"), to help debug potential issues, and remove them from all the logger debug calls at the beginning of each LDAP-related function as those should really be part of the logger and set beforehand. The LDAP identity backend logger also has two new attributes to help debugging with logs: * write (bool): whether write operations are enabled * refint (bool): whether refint is enabled or not Also adds a dedicated counter metric for user password change operations. Minor campfire improvements: * add a constructor func for the CS3 backend * add a constructor func for the LDAP backend * in the LDAP identity backend, in searchLDAPEntryByFilter (used by all search/get public functions), errors that occur when performing LDAP SEARCH operations were blindly mapped to a ItemNotFound error, instead of being analyzed as it could be caused by a technical error * in the requireadmin middleware, add debug logging to explain why a request is denied * when an LDAP password change fails because the user entry was not found in LDAP, we now have a log message that tracks that
225 lines
6.7 KiB
Go
225 lines
6.7 KiB
Go
// Package errorcode allows to deal with graph error codes
|
|
package errorcode
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5/middleware"
|
|
"github.com/go-chi/render"
|
|
libregraph "github.com/opencloud-eu/libre-graph-api-go"
|
|
)
|
|
|
|
// Error defines a custom error struct, containing and MS Graph error code and a textual error message
|
|
type Error struct {
|
|
errorCode ErrorCode
|
|
msg string
|
|
origin ErrorOrigin
|
|
}
|
|
|
|
// ErrorOrigin gives information about where the error originated
|
|
type ErrorOrigin int
|
|
|
|
const (
|
|
// ErrorOriginUnknown is the default error source
|
|
// and indicates that the error does not have any information about its origin
|
|
ErrorOriginUnknown ErrorOrigin = iota
|
|
|
|
// ErrorOriginCS3 indicates that the error originated from a CS3 service
|
|
ErrorOriginCS3
|
|
)
|
|
|
|
// ErrorCode defines code as used in MS Graph - see https://docs.microsoft.com/en-us/graph/errors?context=graph%2Fapi%2F1.0&view=graph-rest-1.0
|
|
type ErrorCode int
|
|
|
|
// List taken from https://github.com/microsoft/microsoft-graph-docs-1/blob/main/concepts/errors.md#code-property
|
|
const (
|
|
// AccessDenied defines the error if the caller doesn't have permission to perform the action.
|
|
AccessDenied ErrorCode = iota
|
|
// ActivityLimitReached defines the error if the app or user has been throttled.
|
|
ActivityLimitReached
|
|
// GeneralException defines the error if an unspecified error has occurred.
|
|
GeneralException
|
|
// InvalidAuthenticationToken defines the error if the access token is missing
|
|
InvalidAuthenticationToken
|
|
// InvalidRange defines the error if the specified byte range is invalid or unavailable.
|
|
InvalidRange
|
|
// InvalidRequest defines the error if the request is malformed or incorrect.
|
|
InvalidRequest
|
|
// ItemNotFound defines the error if the resource could not be found.
|
|
ItemNotFound
|
|
// TooManyResults defines the error if multiple results are found for a unique resource.
|
|
TooManyResults
|
|
// MalwareDetected defines the error if malware was detected in the requested resource.
|
|
MalwareDetected
|
|
// NameAlreadyExists defines the error if the specified item name already exists.
|
|
NameAlreadyExists
|
|
// NotAllowed defines the error if the action is not allowed by the system.
|
|
NotAllowed
|
|
// NotSupported defines the error if the request is not supported by the system.
|
|
NotSupported
|
|
// ResourceModified defines the error if the resource being updated has changed since the caller last read it, usually an eTag mismatch.
|
|
ResourceModified
|
|
// ResyncRequired defines the error if the delta token is no longer valid, and the app must reset the sync state.
|
|
ResyncRequired
|
|
// ServiceNotAvailable defines the error if the service is not available. Try the request again after a delay. There may be a Retry-After header.
|
|
ServiceNotAvailable
|
|
// SyncStateNotFound defines the error when the sync state generation is not found. The delta token is expired and data must be synchronized again.
|
|
SyncStateNotFound
|
|
// QuotaLimitReached the user has reached their quota limit.
|
|
QuotaLimitReached
|
|
// Unauthenticated the caller is not authenticated.
|
|
Unauthenticated
|
|
// PreconditionFailed the request cannot be made and this error response is sent back
|
|
PreconditionFailed
|
|
// ItemIsLocked The item is locked by another process. Try again later.
|
|
ItemIsLocked
|
|
)
|
|
|
|
var errorCodes = [...]string{
|
|
"accessDenied",
|
|
"activityLimitReached",
|
|
"generalException",
|
|
"InvalidAuthenticationToken",
|
|
"invalidRange",
|
|
"invalidRequest",
|
|
"itemNotFound",
|
|
"tooManyResults",
|
|
"malwareDetected",
|
|
"nameAlreadyExists",
|
|
"notAllowed",
|
|
"notSupported",
|
|
"resourceModified",
|
|
"resyncRequired",
|
|
"serviceNotAvailable",
|
|
"syncStateNotFound",
|
|
"quotaLimitReached",
|
|
"unauthenticated",
|
|
"preconditionFailed",
|
|
"itemIsLocked",
|
|
}
|
|
|
|
// New constructs a new errorcode.Error
|
|
func New(e ErrorCode, msg string) Error {
|
|
return Error{
|
|
errorCode: e,
|
|
msg: msg,
|
|
}
|
|
}
|
|
|
|
// Render writes a Graph ErrorCode object to the response writer
|
|
func (e ErrorCode) Render(w http.ResponseWriter, r *http.Request, status int, msg string) {
|
|
render.Status(r, status)
|
|
render.JSON(w, r, e.CreateOdataError(r.Context(), msg))
|
|
}
|
|
|
|
// CreateOdataError creates and populates a Graph ErrorCode object
|
|
func (e ErrorCode) CreateOdataError(ctx context.Context, msg string) *libregraph.OdataError {
|
|
innererror := map[string]any{
|
|
"date": time.Now().UTC().Format(time.RFC3339),
|
|
}
|
|
|
|
innererror["request-id"] = middleware.GetReqID(ctx)
|
|
return &libregraph.OdataError{
|
|
Error: libregraph.OdataErrorMain{
|
|
Code: e.String(),
|
|
Message: msg,
|
|
Innererror: innererror,
|
|
},
|
|
}
|
|
}
|
|
|
|
// Render writes a Graph Error object to the response writer
|
|
func (e Error) Render(w http.ResponseWriter, r *http.Request) {
|
|
var status int
|
|
switch e.errorCode {
|
|
case AccessDenied:
|
|
status = http.StatusForbidden
|
|
case NotSupported:
|
|
status = http.StatusNotImplemented
|
|
case InvalidRange:
|
|
status = http.StatusRequestedRangeNotSatisfiable
|
|
case InvalidRequest:
|
|
status = http.StatusBadRequest
|
|
case ItemNotFound:
|
|
status = http.StatusNotFound
|
|
case NameAlreadyExists:
|
|
status = http.StatusConflict
|
|
case NotAllowed:
|
|
status = http.StatusMethodNotAllowed
|
|
case ItemIsLocked:
|
|
status = http.StatusLocked
|
|
case PreconditionFailed:
|
|
status = http.StatusPreconditionFailed
|
|
default:
|
|
status = http.StatusInternalServerError
|
|
}
|
|
e.errorCode.Render(w, r, status, e.msg)
|
|
}
|
|
|
|
// String returns the string corresponding to the ErrorCode
|
|
func (e ErrorCode) String() string {
|
|
return errorCodes[e]
|
|
}
|
|
|
|
// Error returns the concatenation of the error string and optional message
|
|
func (e Error) Error() string {
|
|
errString := errorCodes[e.errorCode]
|
|
if e.msg != "" {
|
|
errString += ": " + e.msg
|
|
}
|
|
return errString
|
|
}
|
|
|
|
func (e Error) GetCode() ErrorCode {
|
|
return e.errorCode
|
|
}
|
|
|
|
// GetOrigin returns the source of the error
|
|
func (e Error) GetOrigin() ErrorOrigin {
|
|
return e.origin
|
|
}
|
|
|
|
// WithOrigin returns a new Error with the provided origin
|
|
func (e Error) WithOrigin(o ErrorOrigin) Error {
|
|
e.origin = o
|
|
return e
|
|
}
|
|
|
|
// RenderError render the Graph Error based on a code or default one
|
|
func RenderError(w http.ResponseWriter, r *http.Request, err error) {
|
|
e, ok := ToError(err)
|
|
if !ok {
|
|
GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
e.Render(w, r)
|
|
}
|
|
|
|
// ToError checks if the error is of type Error and returns it,
|
|
// the second parameter indicates if the error conversion was successful
|
|
func ToError(err error) (Error, bool) {
|
|
var e Error
|
|
if errors.As(err, &e) {
|
|
return e, true
|
|
}
|
|
|
|
return Error{}, false
|
|
}
|
|
|
|
// Returns true if the error is of type Error and has an ErrorCode that matches
|
|
// the one specified as the second parameter, and false if not.
|
|
func IsErrorCode(err error, code ErrorCode) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
if e, ok := ToError(err); ok {
|
|
return e.errorCode == code
|
|
} else {
|
|
return false
|
|
}
|
|
}
|