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:
Florian Schade
2023-11-28 17:06:04 +01:00
committed by GitHub
parent eb6ec1311a
commit ad06a192d8
52 changed files with 946 additions and 220 deletions

View 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...)
}
}

View 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)
}
}
}

View 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())
}
}