initial user provisioning

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
This commit is contained in:
Jörn Friedrich Dreyer
2020-07-28 14:38:48 +02:00
parent 5ebcf1b0fe
commit b4aca3c5fd
7 changed files with 180 additions and 151 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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"}

View File

@@ -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

View File

@@ -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)

View File

@@ -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"))
}

118
pkg/service/v0/users.go Normal file
View File

@@ -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"))
}