mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-06 04:09:40 -06:00
enhancement: add graph beta listPermissions endpoint (#7753)
* enhancement: add graph beta listPermissions endpoint besides the new api endpoint it includes several utilities to simplify the graph api development. * resolve drive and item id from the request path * generic pointer and value utilities * space root detection * update GetDriveAndItemIDParam signature to return a error * move errorcode package * enhancement: add generic error code handling * fix: rebase
This commit is contained in:
62
services/graph/pkg/errorcode/cs3.go
Normal file
62
services/graph/pkg/errorcode/cs3.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package errorcode
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
)
|
||||
|
||||
// FromCS3Status converts a CS3 status code into a corresponding local Error representation.
|
||||
//
|
||||
// It evaluates the provided CS3 status code and returns an equivalent graph Error.
|
||||
// If the CS3 status code does not have a direct equivalent within the app,
|
||||
// or is ignored, a general purpose Error is returned.
|
||||
//
|
||||
// This function is particularly useful when dealing with CS3 responses,
|
||||
// and a unified error handling within the application is necessary.
|
||||
func FromCS3Status(status *cs3rpc.Status, ignore ...cs3rpc.Code) *Error {
|
||||
err := &Error{errorCode: GeneralException, msg: "unspecified error has occurred"}
|
||||
|
||||
if status != nil {
|
||||
err.msg = status.GetMessage()
|
||||
}
|
||||
|
||||
code := status.GetCode()
|
||||
switch {
|
||||
case slices.Contains(ignore, status.GetCode()):
|
||||
fallthrough
|
||||
case code == cs3rpc.Code_CODE_OK:
|
||||
err = nil
|
||||
case code == cs3rpc.Code_CODE_NOT_FOUND:
|
||||
err.errorCode = ItemNotFound
|
||||
case code == cs3rpc.Code_CODE_PERMISSION_DENIED:
|
||||
err.errorCode = AccessDenied
|
||||
case code == cs3rpc.Code_CODE_UNAUTHENTICATED:
|
||||
err.errorCode = Unauthenticated
|
||||
case code == cs3rpc.Code_CODE_INVALID_ARGUMENT:
|
||||
err.errorCode = InvalidRequest
|
||||
case code == cs3rpc.Code_CODE_ALREADY_EXISTS:
|
||||
err.errorCode = NameAlreadyExists
|
||||
case code == cs3rpc.Code_CODE_FAILED_PRECONDITION:
|
||||
err.errorCode = PreconditionFailed
|
||||
case code == cs3rpc.Code_CODE_UNIMPLEMENTED:
|
||||
err.errorCode = NotSupported
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// FromStat transforms a *provider.StatResponse object and an error into an *Error.
|
||||
//
|
||||
// It takes a stat of type *provider.StatResponse, an error, and a variadic parameter of type cs3rpc.Code.
|
||||
// If the error is not nil, it creates an Error object with the error message and a GeneralException code.
|
||||
// If the error is nil, it invokes the FromCS3Status function with the StatResponse Status and the ignore codes.
|
||||
func FromStat(stat *provider.StatResponse, err error, ignore ...cs3rpc.Code) *Error {
|
||||
switch {
|
||||
case err != nil:
|
||||
return &Error{msg: err.Error(), errorCode: GeneralException}
|
||||
default:
|
||||
return FromCS3Status(stat.GetStatus(), ignore...)
|
||||
}
|
||||
}
|
||||
67
services/graph/pkg/errorcode/cs3_test.go
Normal file
67
services/graph/pkg/errorcode/cs3_test.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package errorcode_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/conversions"
|
||||
"github.com/owncloud/ocis/v2/services/graph/pkg/errorcode"
|
||||
)
|
||||
|
||||
func TestFromCS3Status(t *testing.T) {
|
||||
var tests = []struct {
|
||||
status *cs3rpc.Status
|
||||
ignore []cs3rpc.Code
|
||||
result *errorcode.Error
|
||||
}{
|
||||
{nil, nil, conversions.ToPointer(errorcode.New(errorcode.GeneralException, "unspecified error has occurred"))},
|
||||
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_OK}, nil, nil},
|
||||
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_NOT_FOUND}, []cs3rpc.Code{cs3rpc.Code_CODE_NOT_FOUND}, nil},
|
||||
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_PERMISSION_DENIED}, []cs3rpc.Code{cs3rpc.Code_CODE_NOT_FOUND, cs3rpc.Code_CODE_PERMISSION_DENIED}, nil},
|
||||
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_NOT_FOUND, Message: "msg"}, nil, conversions.ToPointer(errorcode.New(errorcode.ItemNotFound, "msg"))},
|
||||
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_PERMISSION_DENIED, Message: "msg"}, nil, conversions.ToPointer(errorcode.New(errorcode.AccessDenied, "msg"))},
|
||||
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_UNAUTHENTICATED, Message: "msg"}, nil, conversions.ToPointer(errorcode.New(errorcode.Unauthenticated, "msg"))},
|
||||
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_INVALID_ARGUMENT, Message: "msg"}, nil, conversions.ToPointer(errorcode.New(errorcode.InvalidRequest, "msg"))},
|
||||
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_ALREADY_EXISTS, Message: "msg"}, nil, conversions.ToPointer(errorcode.New(errorcode.NameAlreadyExists, "msg"))},
|
||||
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_FAILED_PRECONDITION, Message: "msg"}, nil, conversions.ToPointer(errorcode.New(errorcode.PreconditionFailed, "msg"))},
|
||||
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_UNIMPLEMENTED, Message: "msg"}, nil, conversions.ToPointer(errorcode.New(errorcode.NotSupported, "msg"))},
|
||||
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_INVALID, Message: "msg"}, nil, conversions.ToPointer(errorcode.New(errorcode.GeneralException, "msg"))},
|
||||
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_CANCELLED, Message: "msg"}, nil, conversions.ToPointer(errorcode.New(errorcode.GeneralException, "msg"))},
|
||||
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_UNKNOWN, Message: "msg"}, nil, conversions.ToPointer(errorcode.New(errorcode.GeneralException, "msg"))},
|
||||
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_RESOURCE_EXHAUSTED, Message: "msg"}, nil, conversions.ToPointer(errorcode.New(errorcode.GeneralException, "msg"))},
|
||||
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_ABORTED, Message: "msg"}, nil, conversions.ToPointer(errorcode.New(errorcode.GeneralException, "msg"))},
|
||||
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_OUT_OF_RANGE, Message: "msg"}, nil, conversions.ToPointer(errorcode.New(errorcode.GeneralException, "msg"))},
|
||||
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_INTERNAL, Message: "msg"}, nil, conversions.ToPointer(errorcode.New(errorcode.GeneralException, "msg"))},
|
||||
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_UNAVAILABLE, Message: "msg"}, nil, conversions.ToPointer(errorcode.New(errorcode.GeneralException, "msg"))},
|
||||
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_REDIRECTION, Message: "msg"}, nil, conversions.ToPointer(errorcode.New(errorcode.GeneralException, "msg"))},
|
||||
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_INSUFFICIENT_STORAGE, Message: "msg"}, nil, conversions.ToPointer(errorcode.New(errorcode.GeneralException, "msg"))},
|
||||
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_LOCKED, Message: "msg"}, nil, conversions.ToPointer(errorcode.New(errorcode.GeneralException, "msg"))},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
if output := errorcode.FromCS3Status(test.status, test.ignore...); !reflect.DeepEqual(output, test.result) {
|
||||
t.Error("Test Failed: {} expected, recieved: {}", test.result, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromStat(t *testing.T) {
|
||||
var tests = []struct {
|
||||
stat *provider.StatResponse
|
||||
err error
|
||||
result *errorcode.Error
|
||||
}{
|
||||
{nil, errors.New("some error"), conversions.ToPointer(errorcode.New(errorcode.GeneralException, "some error"))},
|
||||
{&provider.StatResponse{Status: &cs3rpc.Status{Code: cs3rpc.Code_CODE_OK}}, nil, nil},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
if output := errorcode.FromStat(test.stat, test.err); !reflect.DeepEqual(output, test.result) {
|
||||
t.Error("Test Failed: {} expected, recieved: {}", test.result, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
165
services/graph/pkg/errorcode/errorcode.go
Normal file
165
services/graph/pkg/errorcode/errorcode.go
Normal file
@@ -0,0 +1,165 @@
|
||||
// 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/owncloud/libre-graph-api-go"
|
||||
)
|
||||
|
||||
// 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
|
||||
|
||||
// Error defines a custom error struct, containing and MS Graph error code an a textual error message
|
||||
type Error struct {
|
||||
errorCode ErrorCode
|
||||
msg string
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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
|
||||
// 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",
|
||||
"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 an 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]interface{}{
|
||||
"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 an 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
|
||||
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
|
||||
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 return the concatenation of the error string and optinal message
|
||||
func (e Error) Error() string {
|
||||
errString := errorCodes[e.errorCode]
|
||||
if e.msg != "" {
|
||||
errString += ": " + e.msg
|
||||
}
|
||||
return errString
|
||||
}
|
||||
|
||||
// RenderError render the Graph Error based on a code or default one
|
||||
func RenderError(w http.ResponseWriter, r *http.Request, err error) {
|
||||
var errcode Error
|
||||
if errors.As(err, &errcode) {
|
||||
errcode.Render(w, r)
|
||||
} else {
|
||||
GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user