Add 'ocs/' from commit '7ca52baa61c4370a1bad0e2f74e85073798bdde9'

git-subtree-dir: ocs
git-subtree-mainline: b274cac8c2
git-subtree-split: 7ca52baa61
This commit is contained in:
A.Unger
2020-09-18 12:53:21 +02:00
77 changed files with 7917 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
package svc
import (
"net/http"
"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, response.DataRender(&data.ConfigData{
Version: "1.7", // TODO get from env
Website: "ocis", // TODO get from env
Host: "", // TODO get from FRONTEND config
Contact: "", // TODO get from env
SSL: "true", // TODO get from env
}))
}
+171
View File
@@ -0,0 +1,171 @@
package data
import (
"encoding/xml"
)
// ocsBool implements the xml/json Marshaler interface. The OCS API inconsistency require us to parse boolean values
// as native booleans for json requests but "truthy" 0/1 values for xml requests.
type ocsBool bool
func (c *ocsBool) MarshalJSON() ([]byte, error) {
if *c {
return []byte("true"), nil
}
return []byte("false"), nil
}
func (c ocsBool) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
if c {
return e.EncodeElement("1", start)
}
return e.EncodeElement("0", start)
}
// CapabilitiesData TODO document
type CapabilitiesData struct {
Capabilities *Capabilities `json:"capabilities" xml:"capabilities"`
Version *Version `json:"version" xml:"version"`
}
// Capabilities groups several capability aspects
type Capabilities struct {
Core *CapabilitiesCore `json:"core" xml:"core"`
Checksums *CapabilitiesChecksums `json:"checksums" xml:"checksums"`
Files *CapabilitiesFiles `json:"files" xml:"files" mapstructure:"files"`
Dav *CapabilitiesDav `json:"dav" xml:"dav"`
FilesSharing *CapabilitiesFilesSharing `json:"files_sharing" xml:"files_sharing" mapstructure:"files_sharing"`
Notifications *CapabilitiesNotifications `json:"notifications" xml:"notifications"`
}
// 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" mapstructure:"status"`
SupportURLSigning ocsBool `json:"support-url-signing,omitempty" xml:"support-url-signing,omitempty" mapstructure:"support-url-signing"`
}
// Status holds basic status information
type Status struct {
Installed ocsBool `json:"installed" xml:"installed"`
Maintenance ocsBool `json:"maintenance" xml:"maintenance"`
NeedsDBUpgrade ocsBool `json:"needsDbUpgrade" xml:"needsDbUpgrade"`
Version string `json:"version" xml:"version"`
VersionString string `json:"versionstring" xml:"versionstring"`
Edition string `json:"edition" xml:"edition"`
ProductName string `json:"productname" xml:"productname"`
Hostname string `json:"hostname,omitempty" xml:"hostname,omitempty"`
}
// CapabilitiesChecksums holds available hashes
type CapabilitiesChecksums struct {
SupportedTypes []string `json:"supportedTypes" xml:"supportedTypes>element" mapstructure:"supported_types"`
PreferredUploadType string `json:"preferredUploadType" xml:"preferredUploadType" mapstructure:"preferred_upload_type"`
}
// CapabilitiesFilesTusSupport TODO this must be a summary of storages
type CapabilitiesFilesTusSupport struct {
Version string `json:"version" xml:"version"`
Resumable string `json:"resumable" xml:"resumable"`
Extension string `json:"extension" xml:"extension"`
MaxChunkSize int `json:"max_chunk_size" xml:"max_chunk_size" mapstructure:"max_chunk_size"`
HTTPMethodOverride string `json:"http_method_override" xml:"http_method_override" mapstructure:"http_method_override"`
}
// CapabilitiesFiles TODO this is storage specific, not global. What effect do these options have on the clients?
type CapabilitiesFiles struct {
PrivateLinks ocsBool `json:"privateLinks" xml:"privateLinks" mapstructure:"private_links"`
BigFileChunking ocsBool `json:"bigfilechunking" xml:"bigfilechunking"`
Undelete ocsBool `json:"undelete" xml:"undelete"`
Versioning ocsBool `json:"versioning" xml:"versioning"`
BlacklistedFiles []string `json:"blacklisted_files" xml:"blacklisted_files>element" mapstructure:"blacklisted_files"`
TusSupport *CapabilitiesFilesTusSupport `json:"tus_support" xml:"tus_support" mapstructure:"tus_support"`
}
// CapabilitiesDav holds dav endpoint config
type CapabilitiesDav struct {
Chunking string `json:"chunking" xml:"chunking"`
Trashbin string `json:"trashbin" xml:"trashbin"`
Reports []string `json:"reports" xml:"reports>element" mapstructure:"reports"`
ChunkingParallelUploadDisabled bool `json:"chunkingParallelUploadDisabled" xml:"chunkingParallelUploadDisabled"`
}
// CapabilitiesFilesSharing TODO document
type CapabilitiesFilesSharing struct {
APIEnabled ocsBool `json:"api_enabled" xml:"api_enabled" mapstructure:"api_enabled"`
Resharing ocsBool `json:"resharing" xml:"resharing"`
GroupSharing ocsBool `json:"group_sharing" xml:"group_sharing" mapstructure:"group_sharing"`
AutoAcceptShare ocsBool `json:"auto_accept_share" xml:"auto_accept_share" mapstructure:"auto_accept_share"`
ShareWithGroupMembersOnly ocsBool `json:"share_with_group_members_only" xml:"share_with_group_members_only" mapstructure:"share_with_group_members_only"`
ShareWithMembershipGroupsOnly ocsBool `json:"share_with_membership_groups_only" xml:"share_with_membership_groups_only" mapstructure:"share_with_membership_groups_only"`
SearchMinLength int `json:"search_min_length" xml:"search_min_length" mapstructure:"search_min_length"`
DefaultPermissions int `json:"default_permissions" xml:"default_permissions" mapstructure:"default_permissions"`
UserEnumeration *CapabilitiesFilesSharingUserEnumeration `json:"user_enumeration" xml:"user_enumeration" mapstructure:"user_enumeration"`
Federation *CapabilitiesFilesSharingFederation `json:"federation" xml:"federation"`
Public *CapabilitiesFilesSharingPublic `json:"public" xml:"public"`
User *CapabilitiesFilesSharingUser `json:"user" xml:"user"`
}
// CapabilitiesFilesSharingPublic TODO document
type CapabilitiesFilesSharingPublic struct {
Enabled ocsBool `json:"enabled" xml:"enabled"`
SendMail ocsBool `json:"send_mail" xml:"send_mail" mapstructure:"send_mail"`
SocialShare ocsBool `json:"social_share" xml:"social_share" mapstructure:"social_share"`
Upload ocsBool `json:"upload" xml:"upload"`
Multiple ocsBool `json:"multiple" xml:"multiple"`
SupportsUploadOnly ocsBool `json:"supports_upload_only" xml:"supports_upload_only" mapstructure:"supports_upload_only"`
Password *CapabilitiesFilesSharingPublicPassword `json:"password" xml:"password"`
ExpireDate *CapabilitiesFilesSharingPublicExpireDate `json:"expire_date" xml:"expire_date" mapstructure:"expire_date"`
}
// CapabilitiesFilesSharingPublicPassword TODO document
type CapabilitiesFilesSharingPublicPassword struct {
EnforcedFor *CapabilitiesFilesSharingPublicPasswordEnforcedFor `json:"enforced_for" xml:"enforced_for" mapstructure:"enforced_for"`
Enforced ocsBool `json:"enforced" xml:"enforced"`
}
// CapabilitiesFilesSharingPublicPasswordEnforcedFor TODO document
type CapabilitiesFilesSharingPublicPasswordEnforcedFor struct {
ReadOnly ocsBool `json:"read_only" xml:"read_only,omitempty" mapstructure:"read_only"`
ReadWrite ocsBool `json:"read_write" xml:"read_write,omitempty" mapstructure:"read_write"`
UploadOnly ocsBool `json:"upload_only" xml:"upload_only,omitempty" mapstructure:"upload_only"`
}
// CapabilitiesFilesSharingPublicExpireDate TODO document
type CapabilitiesFilesSharingPublicExpireDate struct {
Enabled ocsBool `json:"enabled" xml:"enabled"`
}
// CapabilitiesFilesSharingUser TODO document
type CapabilitiesFilesSharingUser struct {
SendMail ocsBool `json:"send_mail" xml:"send_mail" mapstructure:"send_mail"`
}
// CapabilitiesFilesSharingUserEnumeration TODO document
type CapabilitiesFilesSharingUserEnumeration struct {
Enabled ocsBool `json:"enabled" xml:"enabled"`
GroupMembersOnly ocsBool `json:"group_members_only" xml:"group_members_only" mapstructure:"group_members_only"`
}
// CapabilitiesFilesSharingFederation holds outgoing and incoming flags
type CapabilitiesFilesSharingFederation struct {
Outgoing ocsBool `json:"outgoing" xml:"outgoing"`
Incoming ocsBool `json:"incoming" xml:"incoming"`
}
// CapabilitiesNotifications holds a list of notification endpoints
type CapabilitiesNotifications struct {
Endpoints []string `json:"ocs-endpoints" xml:"ocs-endpoints>element" mapstructure:"endpoints"`
}
// Version holds version information
type Version struct {
Major int `json:"major" xml:"major"`
Minor int `json:"minor" xml:"minor"`
Micro int `json:"micro" xml:"micro"` // = patch level
String string `json:"string" xml:"string"`
Edition string `json:"edition" xml:"edition"`
}
+10
View File
@@ -0,0 +1,10 @@
package data
// ConfigData holds basic config
type ConfigData struct {
Version string `json:"version" xml:"version"`
Website string `json:"website" xml:"website"`
Host string `json:"host" xml:"host"`
Contact string `json:"contact" xml:"contact"`
SSL string `json:"ssl" xml:"ssl"`
}
+6
View File
@@ -0,0 +1,6 @@
package data
// Groups holds group ids for the groups listing
type Groups struct {
Groups []string `json:"groups" xml:"groups>element"`
}
+34
View File
@@ -0,0 +1,34 @@
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 with code 100
var MetaOK = Meta{Status: "ok", StatusCode: 100, Message: "OK"}
// MetaFailure is a failure response with code 101
var MetaFailure = Meta{Status: "", StatusCode: 101, Message: "Failure"}
// MetaInvalidInput is an error response with code 102
var MetaInvalidInput = Meta{Status: "", StatusCode: 102, Message: "Invalid Input"}
// 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"}
+35
View File
@@ -0,0 +1,35 @@
package data
// Users holds user ids for the user listing
type Users struct {
Users []string `json:"users" xml:"users>element"`
}
// 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
Enabled string `json:"enabled" xml:"enabled"`
UserID string `json:"id" xml:"id"`
Username string `json:"username" xml:"username"`
DisplayName string `json:"display-name" xml:"display-name"`
LegacyDisplayName string `json:"displayname" xml:"displayname"`
Email string `json:"email" xml:"email"`
Quota *Quota `json:"quota" xml:"quota"`
UIDNumber int64 `json:"uidnumber" xml:"uidnumber"`
GIDNumber int64 `json:"gidnumber" xml:"gidnumber"`
}
// Quota holds quota information
type Quota struct {
Free int64 `json:"free" xml:"free"`
Used int64 `json:"used" xml:"used"`
Total int64 `json:"total" xml:"total"`
Relative float32 `json:"relative" xml:"relative"`
Definition string `json:"definition" xml:"definition"`
}
// SigningKey holds the Payload for a GetSigningKey response
type SigningKey struct {
User string `json:"user" xml:"user"`
SigningKey string `json:"signing-key" xml:"signing-key"`
}
+177
View File
@@ -0,0 +1,177 @@
package svc
import (
"fmt"
"net/http"
"github.com/go-chi/chi"
"github.com/go-chi/render"
merrors "github.com/micro/go-micro/v2/errors"
accounts "github.com/owncloud/ocis-accounts/pkg/proto/v0"
"github.com/owncloud/ocis-ocs/pkg/service/v0/data"
"github.com/owncloud/ocis-ocs/pkg/service/v0/response"
)
// ListUserGroups lists a users groups
func (o Ocs) ListUserGroups(w http.ResponseWriter, r *http.Request) {
userid := chi.URLParam(r, "userid")
account, err := o.getAccountService().GetAccount(r.Context(), &accounts.GetAccountRequest{Id: userid})
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
render.Render(w, r, response.ErrRender(data.MetaNotFound.StatusCode, "The requested user could not be found"))
} else {
render.Render(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("userid", userid).Msg("could not get list of user groups")
return
}
groups := []string{}
for i := range account.MemberOf {
groups = append(groups, account.MemberOf[i].Id)
}
o.logger.Error().Err(err).Int("count", len(groups)).Str("userid", userid).Msg("listing groups for user")
render.Render(w, r, response.DataRender(&data.Groups{Groups: groups}))
}
// AddToGroup adds a user to a group
func (o Ocs) AddToGroup(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
userid := chi.URLParam(r, "userid")
groupid := r.PostForm.Get("groupid")
if groupid == "" {
render.Render(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, "empty group assignment: unspecified group"))
return
}
_, err := o.getGroupsService().AddMember(r.Context(), &accounts.AddMemberRequest{
AccountId: userid,
GroupId: groupid,
})
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
render.Render(w, r, response.ErrRender(data.MetaNotFound.StatusCode, "The requested user could not be found"))
} else {
render.Render(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("userid", userid).Str("groupid", groupid).Msg("could not add user to group")
return
}
o.logger.Debug().Str("userid", userid).Str("groupid", groupid).Msg("added user to group")
render.Render(w, r, response.DataRender(struct{}{}))
}
// RemoveFromGroup removes a user from a group
func (o Ocs) RemoveFromGroup(w http.ResponseWriter, r *http.Request) {
userid := chi.URLParam(r, "userid")
groupid := r.URL.Query().Get("groupid")
_, err := o.getGroupsService().RemoveMember(r.Context(), &accounts.RemoveMemberRequest{
AccountId: userid,
GroupId: groupid,
})
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
render.Render(w, r, response.ErrRender(data.MetaNotFound.StatusCode, "The requested user could not be found"))
} else {
render.Render(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("userid", userid).Str("groupid", groupid).Msg("could not remove user from group")
return
}
o.logger.Debug().Str("userid", userid).Str("groupid", groupid).Msg("removed user from group")
render.Render(w, r, response.DataRender(struct{}{}))
}
// ListGroups lists all groups
func (o Ocs) ListGroups(w http.ResponseWriter, r *http.Request) {
search := r.URL.Query().Get("search")
query := ""
if search != "" {
query = fmt.Sprintf("id eq '%s' or on_premises_sam_account_name eq '%s'", escapeValue(search), escapeValue(search))
}
res, err := o.getGroupsService().ListGroups(r.Context(), &accounts.ListGroupsRequest{
Query: query,
})
if err != nil {
o.logger.Err(err).Msg("could not list users")
render.Render(w, r, response.ErrRender(data.MetaServerError.StatusCode, "could not list users"))
return
}
groups := []string{}
for i := range res.Groups {
groups = append(groups, res.Groups[i].Id)
}
render.Render(w, r, response.DataRender(&data.Groups{Groups: groups}))
}
// AddGroup adds a group
func (o Ocs) AddGroup(w http.ResponseWriter, r *http.Request) {
render.Render(w, r, response.ErrRender(data.MetaUnknownError.StatusCode, "not implemented"))
}
// DeleteGroup deletes a group
func (o Ocs) DeleteGroup(w http.ResponseWriter, r *http.Request) {
groupid := chi.URLParam(r, "groupid")
_, err := o.getGroupsService().DeleteGroup(r.Context(), &accounts.DeleteGroupRequest{
Id: groupid,
})
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
render.Render(w, r, response.ErrRender(data.MetaNotFound.StatusCode, "The requested group could not be found"))
} else {
render.Render(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("groupid", groupid).Msg("could not remove group")
return
}
o.logger.Debug().Str("groupid", groupid).Msg("removed group")
render.Render(w, r, response.DataRender(struct{}{}))
}
// GetGroupMembers lists all members of a group
func (o Ocs) GetGroupMembers(w http.ResponseWriter, r *http.Request) {
groupid := chi.URLParam(r, "groupid")
res, err := o.getGroupsService().ListMembers(r.Context(), &accounts.ListMembersRequest{Id: groupid})
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
render.Render(w, r, response.ErrRender(data.MetaNotFound.StatusCode, "The requested group could not be found"))
} else {
render.Render(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("groupid", groupid).Msg("could not get list of members")
return
}
members := []string{}
for i := range res.Members {
members = append(members, res.Members[i].Id)
}
o.logger.Error().Err(err).Int("count", len(members)).Str("groupid", groupid).Msg("listing group members")
render.Render(w, r, response.DataRender(&data.Users{Users: members}))
}
+30
View File
@@ -0,0 +1,30 @@
package svc
import (
"net/http"
"github.com/owncloud/ocis-ocs/pkg/metrics"
)
// NewInstrument returns a service that instruments metrics.
func NewInstrument(next Service, metrics *metrics.Metrics) Service {
return instrument{
next: next,
metrics: metrics,
}
}
type instrument struct {
next Service
metrics *metrics.Metrics
}
// ServeHTTP implements the Service interface.
func (i instrument) ServeHTTP(w http.ResponseWriter, r *http.Request) {
i.next.ServeHTTP(w, r)
}
// GetConfig implements the Service interface.
func (i instrument) GetConfig(w http.ResponseWriter, r *http.Request) {
i.next.GetConfig(w, r)
}
+30
View File
@@ -0,0 +1,30 @@
package svc
import (
"net/http"
"github.com/owncloud/ocis-pkg/v2/log"
)
// NewLogging returns a service that logs messages.
func NewLogging(next Service, logger log.Logger) Service {
return logging{
next: next,
logger: logger,
}
}
type logging struct {
next Service
logger log.Logger
}
// ServeHTTP implements the Service interface.
func (l logging) ServeHTTP(w http.ResponseWriter, r *http.Request) {
l.next.ServeHTTP(w, r)
}
// GetConfig implements the Service interface.
func (l logging) GetConfig(w http.ResponseWriter, r *http.Request) {
l.next.GetConfig(w, r)
}
+50
View File
@@ -0,0 +1,50 @@
package svc
import (
"net/http"
"github.com/owncloud/ocis-ocs/pkg/config"
"github.com/owncloud/ocis-pkg/v2/log"
)
// Option defines a single option function.
type Option func(o *Options)
// Options defines the available options for this package.
type Options struct {
Logger log.Logger
Config *config.Config
Middleware []func(http.Handler) http.Handler
}
// newOptions initializes the available default options.
func newOptions(opts ...Option) Options {
opt := Options{}
for _, o := range opts {
o(&opt)
}
return opt
}
// Logger provides a function to set the logger option.
func Logger(val log.Logger) Option {
return func(o *Options) {
o.Logger = val
}
}
// Config provides a function to set the config option.
func Config(val *config.Config) Option {
return func(o *Options) {
o.Config = val
}
}
// Middleware provides a function to set the middleware option.
func Middleware(val ...func(http.Handler) http.Handler) Option {
return func(o *Options) {
o.Middleware = val
}
}
+114
View File
@@ -0,0 +1,114 @@
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
type Response struct {
OCS *Payload `json:"ocs" xml:"ocs"`
}
var (
elementStartElement = xml.StartElement{Name: xml.Name{Local: "element"}}
metaStartElement = xml.StartElement{Name: xml.Name{Local: "meta"}}
ocsName = xml.Name{Local: "ocs"}
dataName = xml.Name{Local: "data"}
)
// Payload combines response metadata and data
type Payload struct {
Meta data.Meta `json:"meta" xml:"meta"`
Data interface{} `json:"data,omitempty" xml:"data,omitempty"`
}
// MarshalXML handles ocs specific wrapping of array members in 'element' tags for the data
func (rsp Response) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
// first the easy part
// use ocs as the surrounding tag
start.Name = ocsName
if err = e.EncodeToken(start); err != nil {
return
}
// encode the meta tag
if err = e.EncodeElement(rsp.OCS.Meta, metaStartElement); err != nil {
return
}
// we need to use reflection to determine if p.Data is an array or a slice
rt := reflect.TypeOf(rsp.OCS.Data)
if rt != nil && (rt.Kind() == reflect.Array || rt.Kind() == reflect.Slice) {
// this is how to wrap the data elements in their own <element> tag
v := reflect.ValueOf(rsp.OCS.Data)
if err = e.EncodeToken(xml.StartElement{Name: dataName}); err != nil {
return
}
for i := 0; i < v.Len(); i++ {
if err = e.EncodeElement(v.Index(i).Interface(), elementStartElement); err != nil {
return
}
}
if err = e.EncodeToken(xml.EndElement{Name: dataName}); err != nil {
return
}
} else if err = e.EncodeElement(rsp.OCS.Data, xml.StartElement{Name: dataName}); err != nil {
return
}
// write the closing <ocs> tag
if err = e.EncodeToken(xml.EndElement{Name: start.Name}); err != nil {
return
}
return
}
// Render sets the status code of the http response, taking the ocs version into account
func (rsp *Response) Render(w http.ResponseWriter, r *http.Request) error {
version := APIVersion(r.Context())
m := statusCodeMapper(version)
statusCode := m(rsp.OCS.Meta)
render.Status(r, statusCode)
if version == ocsVersion2 && statusCode == http.StatusOK {
rsp.OCS.Meta.StatusCode = statusCode
}
return nil
}
// DataRender creates an OK Payload for the given data
func DataRender(d interface{}) render.Renderer {
return &Response{
&Payload{
Meta: data.MetaOK,
Data: d,
},
}
}
// ErrRender creates an Error Paylod with the given OCS error code and message
// The httpcode will be determined using the API version stored in the context
func ErrRender(c int, m string) render.Renderer {
return &Response{
&Payload{
Meta: data.Meta{Status: "error", StatusCode: c, Message: m},
},
}
}
func statusCodeMapper(version string) func(data.Meta) int {
var mapper func(data.Meta) int
switch version {
case ocsVersion1:
mapper = OcsV1StatusCodes
case ocsVersion2:
mapper = OcsV2StatusCodes
default:
mapper = defaultStatusCodeMapper
}
return mapper
}
+84
View File
@@ -0,0 +1,84 @@
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
const (
apiVersionKey key = iota
ocsVersion1 = "1"
ocsVersion2 = "2"
)
var (
defaultStatusCodeMapper = OcsV2StatusCodes
)
// 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 ""
}
// OcsV1StatusCodes returns the http status codes for the OCS API v1.
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 data.Meta) int {
sc := meta.StatusCode
switch sc {
case data.MetaNotFound.StatusCode:
return http.StatusNotFound
case data.MetaUnknownError.StatusCode:
fallthrough
case data.MetaServerError.StatusCode:
return http.StatusInternalServerError
case data.MetaUnauthorized.StatusCode:
return http.StatusUnauthorized
case 100:
meta.StatusCode = http.StatusOK
return http.StatusOK
}
// any 2xx, 4xx and 5xx will be used as is
if sc >= 200 && sc < 600 {
return sc
}
// any error codes > 100 are treated as client errors
if sc > 100 && sc < 200 {
return http.StatusBadRequest
}
// TODO change this status code?
return http.StatusOK
}
// 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 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(data.MetaBadRequest.StatusCode, "unknown ocs api version"))
return
}
w.Header().Set("Ocs-Api-Version", version)
// store version in context so handlers can access it
ctx := context.WithValue(r.Context(), apiVersionKey, version)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
+110
View File
@@ -0,0 +1,110 @@
package svc
import (
"net/http"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"github.com/go-chi/render"
"github.com/micro/go-micro/v2/client/grpc"
accounts "github.com/owncloud/ocis-accounts/pkg/proto/v0"
"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"
)
var defaultClient = grpc.NewClient()
// Service defines the extension handlers.
type Service interface {
ServeHTTP(http.ResponseWriter, *http.Request)
GetConfig(http.ResponseWriter, *http.Request)
}
// NewService returns a service implementation for Service.
func NewService(opts ...Option) Service {
options := newOptions(opts...)
m := chi.NewMux()
m.Use(options.Middleware...)
svc := Ocs{
config: options.Config,
mux: m,
logger: options.Logger,
}
m.Route(options.Config.HTTP.Root, func(r chi.Router) {
r.NotFound(svc.NotFound)
r.Use(middleware.StripSlashes)
r.Use(ocsm.AccessToken(
ocsm.Logger(options.Logger),
ocsm.TokenManagerConfig(options.Config.TokenManager),
))
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(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) {
r.Route("/capabilities", func(r chi.Router) {})
r.Route("/user", func(r chi.Router) {
r.Get("/", svc.GetUser)
r.Get("/signing-key", svc.GetSigningKey)
})
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("/{userid}/groups", func(r chi.Router) {
r.Get("/", svc.ListUserGroups)
r.Post("/", svc.AddToGroup)
r.Delete("/", svc.RemoveFromGroup)
})
})
r.Route("/groups", func(r chi.Router) {
r.Get("/", svc.ListGroups)
r.Post("/", svc.AddGroup)
r.Delete("/{groupid}", svc.DeleteGroup)
r.Get("/{groupid}", svc.GetGroupMembers)
})
})
r.Route("/config", func(r chi.Router) {
r.Get("/", svc.GetConfig)
})
})
})
return svc
}
// Ocs defines implements the business logic for Service.
type Ocs struct {
config *config.Config
logger log.Logger
mux *chi.Mux
}
// ServeHTTP implements the Service interface.
func (o Ocs) ServeHTTP(w http.ResponseWriter, r *http.Request) {
o.mux.ServeHTTP(w, r)
}
// NotFound uses ErrRender to always return a proper OCS payload
func (o Ocs) NotFound(w http.ResponseWriter, r *http.Request) {
render.Render(w, r, response.ErrRender(data.MetaNotFound.StatusCode, "not found"))
}
func (o Ocs) getAccountService() accounts.AccountsService {
return accounts.NewAccountsService("com.owncloud.api.accounts", defaultClient)
}
func (o Ocs) getGroupsService() accounts.GroupsService {
return accounts.NewGroupsService("com.owncloud.api.accounts", defaultClient)
}
+28
View File
@@ -0,0 +1,28 @@
package svc
import (
"net/http"
"github.com/owncloud/ocis-pkg/v2/middleware"
)
// NewTracing returns a service that instruments traces.
func NewTracing(next Service) Service {
return tracing{
next: next,
}
}
type tracing struct {
next Service
}
// ServeHTTP implements the Service interface.
func (t tracing) ServeHTTP(w http.ResponseWriter, r *http.Request) {
middleware.Trace(t.next).ServeHTTP(w, r)
}
// GetConfig implements the Service interface.
func (t tracing) GetConfig(w http.ResponseWriter, r *http.Request) {
t.next.GetConfig(w, r)
}
+370
View File
@@ -0,0 +1,370 @@
package svc
import (
"crypto/rand"
"encoding/hex"
"fmt"
"net/http"
"strconv"
"strings"
"github.com/cs3org/reva/pkg/user"
"github.com/go-chi/chi"
"github.com/go-chi/render"
"google.golang.org/protobuf/types/known/fieldmaskpb"
"github.com/micro/go-micro/v2/client/grpc"
merrors "github.com/micro/go-micro/v2/errors"
accounts "github.com/owncloud/ocis-accounts/pkg/proto/v0"
"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) {
// TODO this endpoint needs authentication using the roles and permissions
userid := chi.URLParam(r, "userid")
if userid == "" {
u, ok := user.ContextGetUser(r.Context())
if !ok || u.Id == nil || u.Id.OpaqueId == "" {
render.Render(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, "missing user in context"))
return
}
userid = u.Id.OpaqueId
}
account, err := o.getAccountService().GetAccount(r.Context(), &accounts.GetAccountRequest{
Id: userid,
})
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
render.Render(w, r, response.ErrRender(data.MetaNotFound.StatusCode, "The requested user could not be found"))
} else {
render.Render(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("userid", userid).Msg("could not get user")
return
}
// remove password from log if it is set
if account.PasswordProfile != nil {
account.PasswordProfile.Password = ""
}
o.logger.Debug().Interface("account", account).Msg("got user")
// mimic the oc10 bool as string for the user enabled property
var enabled string
if account.AccountEnabled {
enabled = "true"
} else {
enabled = "false"
}
render.Render(w, r, response.DataRender(&data.User{
UserID: account.Id, // TODO userid vs username! implications for clients if we return the userid here? -> implement graph ASAP?
Username: account.PreferredName,
DisplayName: account.DisplayName,
LegacyDisplayName: account.DisplayName,
Email: account.Mail,
UIDNumber: account.UidNumber,
GIDNumber: account.GidNumber,
Enabled: enabled,
// FIXME onlyfor users/{userid} endpoint (not /user)
// TODO query storage registry for free space? of home storage, maybe...
Quota: &data.Quota{
Free: 2840756224000,
Used: 5059416668,
Total: 2845815640668,
Relative: 0.18,
Definition: "default",
},
}))
}
// AddUser creates a new user account
func (o Ocs) AddUser(w http.ResponseWriter, r *http.Request) {
// TODO this endpoint needs authentication using the roles and permissions
userid := r.PostFormValue("userid")
password := r.PostFormValue("password")
username := r.PostFormValue("username")
displayname := r.PostFormValue("displayname")
email := r.PostFormValue("email")
uid := r.PostFormValue("uidnumber")
gid := r.PostFormValue("gidnumber")
var uidNumber, gidNumber int64
var err error
if uid != "" {
uidNumber, err = strconv.ParseInt(uid, 10, 64)
if err != nil {
render.Render(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, "Cannot use the uidnumber provided"))
o.logger.Error().Err(err).Str("userid", userid).Msg("Cannot use the uidnumber provided")
return
}
}
if gid != "" {
gidNumber, err = strconv.ParseInt(gid, 10, 64)
if err != nil {
render.Render(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, "Cannot use the gidnumber provided"))
o.logger.Error().Err(err).Str("userid", userid).Msg("Cannot use the gidnumber provided")
return
}
}
// fallbacks
/* TODO decide if we want to make these fallbacks. Keep in mind:
- ocis requires a username and email
- the username should really be different from the userid
if username == "" {
username = userid
}
if displayname == "" {
displayname = username
}
*/
newAccount := &accounts.Account{
DisplayName: displayname,
PreferredName: username,
OnPremisesSamAccountName: username,
PasswordProfile: &accounts.PasswordProfile{
Password: password,
},
Id: userid,
Mail: email,
AccountEnabled: true,
}
if uidNumber != 0 {
newAccount.UidNumber = uidNumber
}
if gidNumber != 0 {
newAccount.GidNumber = gidNumber
}
account, err := o.getAccountService().CreateAccount(r.Context(), &accounts.CreateAccountRequest{
Account: newAccount,
})
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusBadRequest {
render.Render(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, merr.Detail))
} else {
render.Render(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("userid", userid).Msg("could not add user")
// TODO check error if account already existed
return
}
// remove password from log if it is set
if account.PasswordProfile != nil {
account.PasswordProfile.Password = ""
}
o.logger.Debug().Interface("account", account).Msg("added user")
// mimic the oc10 bool as string for the user enabled property
var enabled string
if account.AccountEnabled {
enabled = "true"
} else {
enabled = "false"
}
render.Render(w, r, response.DataRender(&data.User{
UserID: account.Id,
Username: account.PreferredName,
DisplayName: account.DisplayName,
LegacyDisplayName: account.DisplayName,
Email: account.Mail,
UIDNumber: account.UidNumber,
GIDNumber: account.UidNumber,
Enabled: enabled,
}))
}
// EditUser creates a new user account
func (o Ocs) EditUser(w http.ResponseWriter, r *http.Request) {
// TODO this endpoint needs authentication
req := accounts.UpdateAccountRequest{
Account: &accounts.Account{
Id: chi.URLParam(r, "userid"),
},
}
key := r.PostFormValue("key")
value := r.PostFormValue("value")
switch key {
case "email":
req.Account.Mail = value
req.UpdateMask = &fieldmaskpb.FieldMask{Paths: []string{"Mail"}}
case "username":
req.Account.PreferredName = value
req.Account.OnPremisesSamAccountName = value
req.UpdateMask = &fieldmaskpb.FieldMask{Paths: []string{"PreferredName", "OnPremisesSamAccountName"}}
case "password":
req.Account.PasswordProfile = &accounts.PasswordProfile{
Password: value,
}
req.UpdateMask = &fieldmaskpb.FieldMask{Paths: []string{"PasswordProfile.Password"}}
case "displayname", "display":
req.Account.DisplayName = value
req.UpdateMask = &fieldmaskpb.FieldMask{Paths: []string{"DisplayName"}}
default:
// https://github.com/owncloud/core/blob/24b7fa1d2604a208582055309a5638dbd9bda1d1/apps/provisioning_api/lib/Users.php#L321
render.Render(w, r, response.ErrRender(103, "unknown key '"+key+"'"))
return
}
account, err := o.getAccountService().UpdateAccount(r.Context(), &req)
if err != nil {
merr := merrors.FromError(err)
switch merr.Code {
case http.StatusNotFound:
render.Render(w, r, response.ErrRender(data.MetaNotFound.StatusCode, "The requested user could not be found"))
case http.StatusBadRequest:
render.Render(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, merr.Detail))
default:
render.Render(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("userid", req.Account.Id).Msg("could not edit user")
return
}
// remove password from log if it is set
if account.PasswordProfile != nil {
account.PasswordProfile.Password = ""
}
o.logger.Debug().Interface("account", account).Msg("updated user")
render.Render(w, r, response.DataRender(struct{}{}))
}
// DeleteUser deletes a user
func (o Ocs) DeleteUser(w http.ResponseWriter, r *http.Request) {
req := accounts.DeleteAccountRequest{
Id: chi.URLParam(r, "userid"),
}
_, err := o.getAccountService().DeleteAccount(r.Context(), &req)
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
render.Render(w, r, response.ErrRender(data.MetaNotFound.StatusCode, "The requested user could not be found"))
} else {
render.Render(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("userid", req.Id).Msg("could not delete user")
return
}
o.logger.Debug().Str("userid", req.Id).Msg("deleted user")
render.Render(w, r, response.DataRender(struct{}{}))
}
// 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
}
// use the user's UUID
userID := u.Id.OpaqueId
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: userID,
})
if err == nil && len(res.Records) > 0 {
render.Render(w, r, response.DataRender(&data.SigningKey{
User: userID,
SigningKey: string(res.Records[0].Value),
}))
return
}
if err != nil {
e := merrors.Parse(err.Error())
if e.Code == http.StatusNotFound {
// not found is ok, so we can continue and generate the key on the fly
} else {
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 {
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: userID,
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: userID,
SigningKey: signingKey,
}))
}
// ListUsers lists the users
func (o Ocs) ListUsers(w http.ResponseWriter, r *http.Request) {
search := r.URL.Query().Get("search")
query := ""
if search != "" {
query = fmt.Sprintf("id eq '%s' or on_premises_sam_account_name eq '%s'", escapeValue(search), escapeValue(search))
}
res, err := o.getAccountService().ListAccounts(r.Context(), &accounts.ListAccountsRequest{
Query: query,
})
if err != nil {
o.logger.Err(err).Msg("could not list users")
render.Render(w, r, response.ErrRender(data.MetaServerError.StatusCode, "could not list users"))
return
}
users := []string{}
for i := range res.Accounts {
users = append(users, res.Accounts[i].Id)
}
render.Render(w, r, response.DataRender(&data.Users{Users: users}))
}
// escapeValue escapes all special characters in the value
func escapeValue(value string) string {
return strings.ReplaceAll(value, "'", "''")
}