diff --git a/pkg/service/v0/config.go b/pkg/service/v0/config.go index cd2615c30b..58f6bfec1d 100644 --- a/pkg/service/v0/config.go +++ b/pkg/service/v0/config.go @@ -5,11 +5,12 @@ import ( "github.com/go-chi/render" "github.com/owncloud/ocis-ocs/pkg/service/v0/data" + "github.com/owncloud/ocis-ocs/pkg/service/v0/response" ) // GetConfig renders the ocs config endpoint func (o Ocs) GetConfig(w http.ResponseWriter, r *http.Request) { - render.Render(w, r, DataRender(&data.ConfigData{ + render.Render(w, r, response.DataRender(&data.ConfigData{ Version: "1.7", // TODO get from env Website: "ocis", // TODO get from env Host: "", // TODO get from FRONTEND config diff --git a/pkg/service/v0/data/capabilities.go b/pkg/service/v0/data/capabilities.go index d5aa9eba2c..e567df8453 100644 --- a/pkg/service/v0/data/capabilities.go +++ b/pkg/service/v0/data/capabilities.go @@ -42,9 +42,10 @@ type Capabilities struct { // CapabilitiesCore holds webdav config type CapabilitiesCore struct { - PollInterval int `json:"pollinterval" xml:"pollinterval" mapstructure:"poll_interval"` - WebdavRoot string `json:"webdav-root,omitempty" xml:"webdav-root,omitempty" mapstructure:"webdav_root"` - Status *Status `json:"status" xml:"status"` + PollInterval int `json:"pollinterval" xml:"pollinterval" mapstructure:"poll_interval"` + WebdavRoot string `json:"webdav-root,omitempty" xml:"webdav-root,omitempty" mapstructure:"webdav_root"` + Status *Status `json:"status" xml:"status" mapstructure:"status"` + SupportURLSigning ocsBool `json:"support-url-signing,omitempty" xml:"support-url-signing,omitempty" mapstructure:"support-url-signing"` } // Status holds basic status information diff --git a/pkg/service/v0/data/meta.go b/pkg/service/v0/data/meta.go new file mode 100644 index 0000000000..66acd70e53 --- /dev/null +++ b/pkg/service/v0/data/meta.go @@ -0,0 +1,28 @@ +package data + +// Meta holds response metadata +type Meta struct { + Status string `json:"status" xml:"status"` + StatusCode int `json:"statuscode" xml:"statuscode"` + Message string `json:"message" xml:"message"` + TotalItems string `json:"totalitems,omitempty" xml:"totalitems,omitempty"` + ItemsPerPage string `json:"itemsperpage,omitempty" xml:"itemsperpage,omitempty"` +} + +// MetaOK is the default ok response +var MetaOK = Meta{Status: "ok", StatusCode: 100, Message: "OK"} + +// MetaBadRequest is used for unknown errors +var MetaBadRequest = Meta{Status: "error", StatusCode: 400, Message: "Bad Request"} + +// MetaServerError is returned on server errors +var MetaServerError = Meta{Status: "error", StatusCode: 996, Message: "Server Error"} + +// MetaUnauthorized is returned on unauthorized requests +var MetaUnauthorized = Meta{Status: "error", StatusCode: 997, Message: "Unauthorised"} + +// MetaNotFound is returned when trying to access not existing resources +var MetaNotFound = Meta{Status: "error", StatusCode: 998, Message: "Not Found"} + +// MetaUnknownError is used for unknown errors +var MetaUnknownError = Meta{Status: "error", StatusCode: 999, Message: "Unknown Error"} diff --git a/pkg/service/v0/response.go b/pkg/service/v0/response/response.go similarity index 90% rename from pkg/service/v0/response.go rename to pkg/service/v0/response/response.go index f2863735b3..c8f148f017 100644 --- a/pkg/service/v0/response.go +++ b/pkg/service/v0/response/response.go @@ -1,4 +1,4 @@ -package svc +package response import ( "encoding/xml" @@ -6,6 +6,7 @@ import ( "reflect" "github.com/go-chi/render" + "github.com/owncloud/ocis-ocs/pkg/service/v0/data" ) // Response is the top level response structure @@ -23,7 +24,7 @@ var ( // Payload combines response metadata and data type Payload struct { XMLName struct{} `json:"-" xml:"ocs"` - Meta Meta `json:"meta" xml:"meta"` + Meta data.Meta `json:"meta" xml:"meta"` Data interface{} `json:"data,omitempty" xml:"data,omitempty"` } @@ -83,7 +84,7 @@ func (p *Payload) Render(w http.ResponseWriter, r *http.Request) error { // DataRender creates an OK Payload for the given data func DataRender(d interface{}) render.Renderer { return &Payload{ - Meta: MetaOK, + Meta: data.MetaOK, Data: d, } } @@ -92,12 +93,12 @@ func DataRender(d interface{}) render.Renderer { // The httpcode will be determined using the API version stored in the context func ErrRender(c int, m string) render.Renderer { return &Payload{ - Meta: Meta{Status: "error", StatusCode: c, Message: m}, + Meta: data.Meta{Status: "error", StatusCode: c, Message: m}, } } -func statusCodeMapper(version string) func(Meta) int { - var mapper func(Meta) int +func statusCodeMapper(version string) func(data.Meta) int { + var mapper func(data.Meta) int switch version { case ocsVersion1: mapper = OcsV1StatusCodes diff --git a/pkg/service/v0/version.go b/pkg/service/v0/response/version.go similarity index 51% rename from pkg/service/v0/version.go rename to pkg/service/v0/response/version.go index e052a4dd13..8a14818c96 100644 --- a/pkg/service/v0/version.go +++ b/pkg/service/v0/response/version.go @@ -1,4 +1,4 @@ -package svc +package response import ( "context" @@ -6,6 +6,7 @@ import ( "github.com/go-chi/chi" "github.com/go-chi/render" + "github.com/owncloud/ocis-ocs/pkg/service/v0/data" ) type key int @@ -20,49 +21,31 @@ var ( defaultStatusCodeMapper = OcsV2StatusCodes ) -// Meta holds response metadata -type Meta struct { - Status string `json:"status" xml:"status"` - StatusCode int `json:"statuscode" xml:"statuscode"` - Message string `json:"message" xml:"message"` - TotalItems string `json:"totalitems,omitempty" xml:"totalitems,omitempty"` - ItemsPerPage string `json:"itemsperpage,omitempty" xml:"itemsperpage,omitempty"` +// APIVersion retrieves the api version from the context. +func APIVersion(ctx context.Context) string { + value := ctx.Value(apiVersionKey) + if value != nil { + return value.(string) + } + return "" } -// MetaOK is the default ok response -var MetaOK = Meta{Status: "ok", StatusCode: 100, Message: "OK"} - -// MetaBadRequest is used for unknown errors -var MetaBadRequest = Meta{Status: "error", StatusCode: 400, Message: "Bad Request"} - -// MetaServerError is returned on server errors -var MetaServerError = Meta{Status: "error", StatusCode: 996, Message: "Server Error"} - -// MetaUnauthorized is returned on unauthorized requests -var MetaUnauthorized = Meta{Status: "error", StatusCode: 997, Message: "Unauthorised"} - -// MetaNotFound is returned when trying to access not existing resources -var MetaNotFound = Meta{Status: "error", StatusCode: 998, Message: "Not Found"} - -// MetaUnknownError is used for unknown errors -var MetaUnknownError = Meta{Status: "error", StatusCode: 999, Message: "Unknown Error"} - // OcsV1StatusCodes returns the http status codes for the OCS API v1. -func OcsV1StatusCodes(meta Meta) int { +func OcsV1StatusCodes(meta data.Meta) int { return http.StatusOK } // OcsV2StatusCodes maps the OCS codes to http status codes for the ocs API v2. -func OcsV2StatusCodes(meta Meta) int { +func OcsV2StatusCodes(meta data.Meta) int { sc := meta.StatusCode switch sc { - case MetaNotFound.StatusCode: + case data.MetaNotFound.StatusCode: return http.StatusNotFound - case MetaUnknownError.StatusCode: + case data.MetaUnknownError.StatusCode: fallthrough - case MetaServerError.StatusCode: + case data.MetaServerError.StatusCode: return http.StatusInternalServerError - case MetaUnauthorized.StatusCode: + case data.MetaUnauthorized.StatusCode: return http.StatusUnauthorized case 100: meta.StatusCode = http.StatusOK @@ -82,23 +65,14 @@ func OcsV2StatusCodes(meta Meta) int { return http.StatusOK } -// APIVersion retrieves the api version from the context. -func APIVersion(ctx context.Context) string { - value := ctx.Value(apiVersionKey) - if value != nil { - return value.(string) - } - return "" -} - // VersionCtx middleware is used to determine the response mapper from // the URL parameters passed through as the request. In case // the Version is unknown, we stop here and return a 404. -func (g Ocs) VersionCtx(next http.Handler) http.Handler { +func VersionCtx(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { version := chi.URLParam(r, "version") if version == "" { - render.Render(w, r, ErrRender(MetaBadRequest.StatusCode, "unknown ocs api version")) + render.Render(w, r, ErrRender(data.MetaBadRequest.StatusCode, "unknown ocs api version")) return } w.Header().Set("Ocs-Api-Version", version) diff --git a/pkg/service/v0/service.go b/pkg/service/v0/service.go index 52d379e556..68df0a0501 100644 --- a/pkg/service/v0/service.go +++ b/pkg/service/v0/service.go @@ -1,22 +1,17 @@ package svc import ( - "crypto/rand" - "encoding/hex" "net/http" - "github.com/cs3org/reva/pkg/user" "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" "github.com/go-chi/render" - "github.com/micro/go-micro/v2/client/grpc" - merrors "github.com/micro/go-micro/v2/errors" "github.com/owncloud/ocis-ocs/pkg/config" ocsm "github.com/owncloud/ocis-ocs/pkg/middleware" "github.com/owncloud/ocis-ocs/pkg/service/v0/data" + "github.com/owncloud/ocis-ocs/pkg/service/v0/response" "github.com/owncloud/ocis-pkg/v2/log" - storepb "github.com/owncloud/ocis-store/pkg/proto/v0" ) // Service defines the extension handlers. @@ -47,7 +42,7 @@ func NewService(opts ...Option) Service { )) r.Use(ocsm.OCSFormatCtx) // updates request Accept header according to format=(json|xml) query parameter r.Route("/v{version:(1|2)}.php", func(r chi.Router) { - r.Use(svc.VersionCtx) // stores version in context + r.Use(response.VersionCtx) // stores version in context r.Route("/apps/files_sharing/api/v1", func(r chi.Router) {}) r.Route("/apps/notifications/api/v1", func(r chi.Router) {}) r.Route("/cloud", func(r chi.Router) { @@ -58,6 +53,7 @@ func NewService(opts ...Option) Service { }) r.Route("/users", func(r chi.Router) { r.Get("/", svc.ListUsers) + r.Get("/{userid}", svc.GetUser) }) }) r.Route("/config", func(r chi.Router) { @@ -83,95 +79,5 @@ func (o Ocs) ServeHTTP(w http.ResponseWriter, r *http.Request) { // NotFound uses ErrRender to always return a proper OCS payload func (o Ocs) NotFound(w http.ResponseWriter, r *http.Request) { - render.Render(w, r, ErrRender(MetaUnknownError.StatusCode, "please check the syntax. API specifications are here: http://www.freedesktop.org/wiki/Specifications/open-collaboration-services")) -} - -// GetUser returns the currently logged in user -func (o Ocs) GetUser(w http.ResponseWriter, r *http.Request) { - u, ok := user.ContextGetUser(r.Context()) - if !ok { - render.Render(w, r, ErrRender(MetaBadRequest.StatusCode, "missing user in context")) - return - } - - render.Render(w, r, DataRender(&data.User{ - ID: u.Username, // TODO userid vs username! implications for clients if we return the userid here? -> implement graph ASAP? - DisplayName: u.DisplayName, - Email: u.Mail, - })) -} - -// GetSigningKey returns the signing key for the current user. It will create it on the fly if it does not exist -// The signing key is part of the user settings and is used by the proxy to authenticate requests -// Currently, the username is used as the OC-Credential -func (o Ocs) GetSigningKey(w http.ResponseWriter, r *http.Request) { - u, ok := user.ContextGetUser(r.Context()) - if !ok { - o.logger.Error().Msg("missing user in context") - render.Render(w, r, ErrRender(MetaBadRequest.StatusCode, "missing user in context")) - return - } - c := storepb.NewStoreService("com.owncloud.api.store", grpc.NewClient()) - res, err := c.Read(r.Context(), &storepb.ReadRequest{ - Options: &storepb.ReadOptions{ - Database: "proxy", - Table: "signing-keys", - }, - Key: u.Username, - }) - if err == nil && len(res.Records) > 0 { - render.Render(w, r, DataRender(&data.SigningKey{ - User: u.Username, - SigningKey: string(res.Records[0].Value), - })) - return - } - if err != nil { - e := merrors.Parse(err.Error()) - if e.Code == http.StatusNotFound { - o.logger.Debug().Str("username", u.Username).Msg("signing key not found") - // not found is ok, so we can continue and generate the key on the fly - } else { - o.logger.Err(err).Msg("error reading from store") - render.Render(w, r, ErrRender(MetaServerError.StatusCode, "error reading from store")) - return - } - } - - // try creating it - key := make([]byte, 64) - _, err = rand.Read(key[:]) - if err != nil { - o.logger.Error().Err(err).Msg("could not generate signing key") - render.Render(w, r, ErrRender(MetaServerError.StatusCode, "could not generate signing key")) - return - } - signingKey := hex.EncodeToString(key) - - _, err = c.Write(r.Context(), &storepb.WriteRequest{ - Options: &storepb.WriteOptions{ - Database: "proxy", - Table: "signing-keys", - }, - Record: &storepb.Record{ - Key: u.Username, - Value: []byte(signingKey), - // TODO Expiry? - }, - }) - if err != nil { - o.logger.Error().Err(err).Msg("error writing key") - render.Render(w, r, ErrRender(MetaServerError.StatusCode, "could not persist signing key")) - return - } - - render.Render(w, r, DataRender(&data.SigningKey{ - User: u.Username, - SigningKey: signingKey, - })) -} - -// ListUsers lists the users -func (o Ocs) ListUsers(w http.ResponseWriter, r *http.Request) { - render.Render(w, r, ErrRender(MetaUnknownError.StatusCode, "please check the syntax. API specifications are here: http://www.freedesktop.org/wiki/Specifications/open-collaboration-services")) + render.Render(w, r, response.ErrRender(data.MetaUnknownError.StatusCode, "please check the syntax. API specifications are here: http://www.freedesktop.org/wiki/Specifications/open-collaboration-services")) } diff --git a/pkg/service/v0/users.go b/pkg/service/v0/users.go new file mode 100644 index 0000000000..21a2efcf99 --- /dev/null +++ b/pkg/service/v0/users.go @@ -0,0 +1,118 @@ +package svc + +import ( + "crypto/rand" + "encoding/hex" + "net/http" + + "github.com/cs3org/reva/pkg/user" + "github.com/go-chi/chi" + "github.com/go-chi/render" + + "github.com/micro/go-micro/v2/client/grpc" + merrors "github.com/micro/go-micro/v2/errors" + "github.com/owncloud/ocis-ocs/pkg/service/v0/data" + "github.com/owncloud/ocis-ocs/pkg/service/v0/response" + storepb "github.com/owncloud/ocis-store/pkg/proto/v0" +) + +// GetUser returns the currently logged in user +func (o Ocs) GetUser(w http.ResponseWriter, r *http.Request) { + + userid := chi.URLParam(r, "userid") + + if userid == "" { + u, ok := user.ContextGetUser(r.Context()) // TODO HERE + if !ok { + render.Render(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, "missing user in context")) + return + } + } + /* + + accSvcID := "com.owncloud.api.accounts" // TODO query the registry and filter using labels + accSvc := accounts.NewAccountsService(accSvcID, grpc.NewClient()) + resp, err := accSvc.ListAccounts(c.Context, &accounts.ListAccountsRequest{}) + */ + render.Render(w, r, response.DataRender(&data.User{ + ID: u.Username, // TODO userid vs username! implications for clients if we return the userid here? -> implement graph ASAP? + DisplayName: u.DisplayName, + Email: u.Mail, + })) +} + +// GetSigningKey returns the signing key for the current user. It will create it on the fly if it does not exist +// The signing key is part of the user settings and is used by the proxy to authenticate requests +// Currently, the username is used as the OC-Credential +func (o Ocs) GetSigningKey(w http.ResponseWriter, r *http.Request) { + u, ok := user.ContextGetUser(r.Context()) + if !ok { + //o.logger.Error().Msg("missing user in context") + render.Render(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, "missing user in context")) + return + } + c := storepb.NewStoreService("com.owncloud.api.store", grpc.NewClient()) + res, err := c.Read(r.Context(), &storepb.ReadRequest{ + Options: &storepb.ReadOptions{ + Database: "proxy", + Table: "signing-keys", + }, + Key: u.Username, + }) + if err == nil && len(res.Records) > 0 { + render.Render(w, r, response.DataRender(&data.SigningKey{ + User: u.Username, + SigningKey: string(res.Records[0].Value), + })) + return + } + if err != nil { + e := merrors.Parse(err.Error()) + if e.Code == http.StatusNotFound { + //o.logger.Debug().Str("username", u.Username).Msg("signing key not found") + // not found is ok, so we can continue and generate the key on the fly + } else { + //o.logger.Err(err).Msg("error reading from store") + render.Render(w, r, response.ErrRender(data.MetaServerError.StatusCode, "error reading from store")) + return + } + } + + // try creating it + key := make([]byte, 64) + _, err = rand.Read(key[:]) + if err != nil { + //o.logger.Error().Err(err).Msg("could not generate signing key") + render.Render(w, r, response.ErrRender(data.MetaServerError.StatusCode, "could not generate signing key")) + return + } + signingKey := hex.EncodeToString(key) + + _, err = c.Write(r.Context(), &storepb.WriteRequest{ + Options: &storepb.WriteOptions{ + Database: "proxy", + Table: "signing-keys", + }, + Record: &storepb.Record{ + Key: u.Username, + Value: []byte(signingKey), + // TODO Expiry? + }, + }) + if err != nil { + //o.logger.Error().Err(err).Msg("error writing key") + render.Render(w, r, response.ErrRender(data.MetaServerError.StatusCode, "could not persist signing key")) + return + } + + render.Render(w, r, response.DataRender(&data.SigningKey{ + User: u.Username, + SigningKey: signingKey, + })) +} + +// ListUsers lists the users +func (o Ocs) ListUsers(w http.ResponseWriter, r *http.Request) { + + render.Render(w, r, response.ErrRender(data.MetaUnknownError.StatusCode, "please check the syntax. API specifications are here: http://www.freedesktop.org/wiki/Specifications/open-collaboration-services")) +}