Skip to content
This repository has been archived by the owner on Jan 27, 2021. It is now read-only.

User provisioning api #23

Merged
merged 7 commits into from
Jul 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelog/unreleased/user-provisioning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Enhancement: Basic Support for the User Provisioning API

We added support for a basic set of API calls for the user provisioning API. [Reference](https://doc.owncloud.com/server/admin_manual/configuration/user/user_provisioning_api.html)

https://github.com/owncloud/ocis-ocs/pull/23
9 changes: 3 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,19 @@ require (
github.com/cs3org/reva v0.1.0
github.com/go-chi/chi v4.1.2+incompatible
github.com/go-chi/render v1.0.1
github.com/go-test/deep v1.0.6 // indirect
github.com/gogo/protobuf v1.3.1 // indirect
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect
github.com/mattn/go-colorable v0.1.2 // indirect
github.com/micro/cli/v2 v2.1.2
github.com/micro/go-micro/v2 v2.6.0
github.com/oklog/run v1.1.0
github.com/openzipkin/zipkin-go v0.2.2
github.com/owncloud/ocis-pkg/v2 v2.2.2-0.20200527082518-5641fa4a4c8c
github.com/owncloud/ocis-accounts v0.1.2-0.20200727195215-6816703df41d
github.com/owncloud/ocis-pkg/v2 v2.2.2-0.20200602070144-cd0620668170
github.com/owncloud/ocis-store v0.0.0-20200716140351-f9670592fb7b
github.com/prometheus/client_golang v1.7.0 // indirect
github.com/restic/calens v0.2.0
github.com/rs/zerolog v1.19.0 // indirect
github.com/spf13/afero v1.2.2 // indirect
github.com/spf13/viper v1.7.0
go.opencensus.io v0.22.4
google.golang.org/protobuf v1.25.0
gopkg.in/square/go-jose.v2 v2.4.0 // indirect
gopkg.in/yaml.v2 v2.2.8 // indirect
)
Expand Down
296 changes: 296 additions & 0 deletions go.sum

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion pkg/service/v0/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions pkg/service/v0/data/capabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 28 additions & 0 deletions pkg/service/v0/data/meta.go
Original file line number Diff line number Diff line change
@@ -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"}
6 changes: 4 additions & 2 deletions pkg/service/v0/data/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package data
// User holds the payload for a GetUser response
type User struct {
// TODO needs better naming, clarify if we need a userid, a username or both
ID string `json:"id" xml:"id"`
DisplayName string `json:"display-name" xml:"display-name"`
UserID string `json:"userid" xml:"userid"`
Username string `json:"username" xml:"username"`
DisplayName string `json:"displayname" xml:"displayname"`
Email string `json:"email" xml:"email"`
Enabled bool `json:"enabled" xml:"enabled"`
}

// SigningKey holds the Payload for a GetSigningKey response
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package svc
package response

import (
"encoding/xml"
"net/http"
"reflect"

"github.com/go-chi/render"
"github.com/owncloud/ocis-ocs/pkg/service/v0/data"
)

// Response is the top level response structure
Expand All @@ -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"`
}

Expand Down Expand Up @@ -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,
}
}
Expand All @@ -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
Expand Down
60 changes: 17 additions & 43 deletions pkg/service/v0/version.go → pkg/service/v0/response/version.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package svc
package response

import (
"context"
"net/http"

"github.com/go-chi/chi"
"github.com/go-chi/render"
"github.com/owncloud/ocis-ocs/pkg/service/v0/data"
)

type key int
Expand All @@ -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
Expand All @@ -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)
Expand Down
105 changes: 7 additions & 98 deletions pkg/service/v0/service.go
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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) {
Expand All @@ -58,6 +53,10 @@ func NewService(opts ...Option) Service {
})
r.Route("/users", func(r chi.Router) {
r.Get("/", svc.ListUsers)
r.Post("/", svc.AddUser)
r.Get("/{userid}", svc.GetUser)
r.Put("/{userid}", svc.EditUser)
r.Delete("/{userid}", svc.DeleteUser)
})
})
r.Route("/config", func(r chi.Router) {
Expand All @@ -83,95 +82,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"))
}
Loading