refactor ocs

Signed-off-by: Christian Richter <crichter@owncloud.com>
This commit is contained in:
Christian Richter
2022-04-13 17:04:37 +02:00
parent 2153d22e61
commit 689ec4f266
54 changed files with 62 additions and 65 deletions
+2
View File
@@ -0,0 +1,2 @@
*
!bin/
+37
View File
@@ -0,0 +1,37 @@
SHELL := bash
NAME := ocs
include ../../.make/recursion.mk
############ tooling ############
ifneq (, $(shell which go 2> /dev/null)) # suppress `command not found warnings` for non go targets in CI
include ../../.bingo/Variables.mk
endif
############ go tooling ############
include ../../.make/go.mk
############ release ############
include ../../.make/release.mk
############ docs generate ############
include ../../.make/docs.mk
.PHONY: docs-generate
docs-generate: config-docs-generate
############ generate ############
include ../../.make/generate.mk
.PHONY: ci-go-generate
ci-go-generate: # CI runs ci-node-generate automatically before this target
.PHONY: ci-node-generate
ci-node-generate:
############ licenses ############
.PHONY: ci-node-check-licenses
ci-node-check-licenses:
.PHONY: ci-node-save-licenses
ci-node-save-licenses:
+14
View File
@@ -0,0 +1,14 @@
package main
import (
"os"
"github.com/owncloud/ocis/extensions/ocs/pkg/command"
"github.com/owncloud/ocis/extensions/ocs/pkg/config/defaults"
)
func main() {
if err := command.Execute(defaults.DefaultConfig()); err != nil {
os.Exit(1)
}
}
@@ -0,0 +1,19 @@
FROM amd64/alpine:latest
RUN apk update && \
apk upgrade && \
apk add ca-certificates mailcap && \
rm -rf /var/cache/apk/* && \
echo 'hosts: files dns' >| /etc/nsswitch.conf
LABEL maintainer="ownCloud GmbH <devops@owncloud.com>" \
org.label-schema.name="oCIS OCS" \
org.label-schema.vendor="ownCloud GmbH" \
org.label-schema.schema-version="1.0"
EXPOSE 9110 9114
ENTRYPOINT ["/usr/bin/ocis-ocs"]
CMD ["server"]
COPY bin/ocis-ocs /usr/bin/ocis-ocs
@@ -0,0 +1,19 @@
FROM arm32v6/alpine:latest
RUN apk update && \
apk upgrade && \
apk add ca-certificates mailcap && \
rm -rf /var/cache/apk/* && \
echo 'hosts: files dns' >| /etc/nsswitch.conf
LABEL maintainer="ownCloud GmbH <devops@owncloud.com>" \
org.label-schema.name="oCIS OCS" \
org.label-schema.vendor="ownCloud GmbH" \
org.label-schema.schema-version="1.0"
EXPOSE 9110 9114
ENTRYPOINT ["/usr/bin/ocis-ocs"]
CMD ["server"]
COPY bin/ocis-ocs /usr/bin/ocis-ocs
@@ -0,0 +1,19 @@
FROM arm64v8/alpine:latest
RUN apk update && \
apk upgrade && \
apk add ca-certificates mailcap && \
rm -rf /var/cache/apk/* && \
echo 'hosts: files dns' >| /etc/nsswitch.conf
LABEL maintainer="ownCloud GmbH <devops@owncloud.com>" \
org.label-schema.name="oCIS OCS" \
org.label-schema.vendor="ownCloud GmbH" \
org.label-schema.schema-version="1.0"
EXPOSE 9110 9114
ENTRYPOINT ["/usr/bin/ocis-ocs"]
CMD ["server"]
COPY bin/ocis-ocs /usr/bin/ocis-ocs
+22
View File
@@ -0,0 +1,22 @@
image: owncloud/ocis-ocs:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
{{#if build.tags}}
tags:
{{#each build.tags}}
- {{this}}
{{/each}}
{{/if}}
manifests:
- image: owncloud/ocis-ocs:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
platform:
architecture: amd64
os: linux
- image: owncloud/ocis-ocs:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
platform:
architecture: arm64
variant: v8
os: linux
- image: owncloud/ocis-ocs:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
platform:
architecture: arm
variant: v6
os: linux
+53
View File
@@ -0,0 +1,53 @@
package command
import (
"fmt"
"net/http"
"github.com/owncloud/ocis/extensions/ocs/pkg/config"
"github.com/owncloud/ocis/extensions/ocs/pkg/config/parser"
"github.com/owncloud/ocis/extensions/ocs/pkg/logging"
"github.com/urfave/cli/v2"
)
// Health is the entrypoint for the health command.
func Health(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "health",
Usage: "check health status",
Category: "info",
Before: func(c *cli.Context) error {
return parser.ParseConfig(cfg)
},
Action: func(c *cli.Context) error {
logger := logging.Configure(cfg.Service.Name, cfg.Log)
resp, err := http.Get(
fmt.Sprintf(
"http://%s/healthz",
cfg.Debug.Addr,
),
)
if err != nil {
logger.Fatal().
Err(err).
Msg("Failed to request health check")
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
logger.Fatal().
Int("code", resp.StatusCode).
Msg("Health seems to be in bad state")
}
logger.Debug().
Int("code", resp.StatusCode).
Msg("Health got a good state")
return nil
},
}
}
+64
View File
@@ -0,0 +1,64 @@
package command
import (
"context"
"os"
"github.com/owncloud/ocis/extensions/ocs/pkg/config"
"github.com/owncloud/ocis/ocis-pkg/clihelper"
ociscfg "github.com/owncloud/ocis/ocis-pkg/config"
"github.com/thejerf/suture/v4"
"github.com/urfave/cli/v2"
)
// GetCommands provides all commands for this service
func GetCommands(cfg *config.Config) cli.Commands {
return []*cli.Command{
// start this service
Server(cfg),
// interaction with this service
// infos about this service
Health(cfg),
Version(cfg),
}
}
// Execute is the entry point for the ocis-ocs command.
func Execute(cfg *config.Config) error {
app := clihelper.DefaultApp(&cli.App{
Name: "ocis-ocs",
Usage: "Serve OCS API for oCIS",
Commands: GetCommands(cfg),
})
cli.HelpFlag = &cli.BoolFlag{
Name: "help,h",
Usage: "Show the help",
}
return app.Run(os.Args)
}
// SutureService allows for the ocs command to be embedded and supervised by a suture supervisor tree.
type SutureService struct {
cfg *config.Config
}
// NewSutureService creates a new ocs.SutureService
func NewSutureService(cfg *ociscfg.Config) suture.Service {
cfg.OCS.Commons = cfg.Commons
return SutureService{
cfg: cfg.OCS,
}
}
func (s SutureService) Serve(ctx context.Context) error {
s.cfg.Context = ctx
if err := Execute(s.cfg); err != nil {
return err
}
return nil
}
+100
View File
@@ -0,0 +1,100 @@
package command
import (
"context"
"fmt"
"github.com/owncloud/ocis/extensions/ocs/pkg/config/parser"
"github.com/owncloud/ocis/extensions/ocs/pkg/logging"
"github.com/owncloud/ocis/extensions/ocs/pkg/tracing"
"github.com/owncloud/ocis/ocis-pkg/version"
"github.com/oklog/run"
"github.com/owncloud/ocis/extensions/ocs/pkg/config"
"github.com/owncloud/ocis/extensions/ocs/pkg/metrics"
"github.com/owncloud/ocis/extensions/ocs/pkg/server/debug"
"github.com/owncloud/ocis/extensions/ocs/pkg/server/http"
"github.com/urfave/cli/v2"
)
// Server is the entrypoint for the server command.
func Server(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "server",
Usage: fmt.Sprintf("start %s extension without runtime (unsupervised mode)", cfg.Service.Name),
Category: "server",
Before: func(c *cli.Context) error {
return parser.ParseConfig(cfg)
},
Action: func(c *cli.Context) error {
logger := logging.Configure(cfg.Service.Name, cfg.Log)
err := tracing.Configure(cfg)
if err != nil {
return err
}
var (
gr = run.Group{}
ctx, cancel = func() (context.Context, context.CancelFunc) {
if cfg.Context == nil {
return context.WithCancel(context.Background())
}
return context.WithCancel(cfg.Context)
}()
metrics = metrics.New()
)
defer cancel()
metrics.BuildInfo.WithLabelValues(version.String).Set(1)
{
server, err := http.Server(
http.Logger(logger),
http.Context(ctx),
http.Config(cfg),
http.Metrics(metrics),
)
if err != nil {
logger.Info().
Err(err).
Str("transport", "http").
Msg("Failed to initialize server")
return err
}
gr.Add(func() error {
return server.Run()
}, func(_ error) {
logger.Info().
Str("transport", "http").
Msg("Shutting down server")
cancel()
})
}
{
server, err := debug.Server(
debug.Logger(logger),
debug.Context(ctx),
debug.Config(cfg),
)
if err != nil {
logger.Info().Err(err).Str("transport", "debug").Msg("Failed to initialize server")
return err
}
gr.Add(server.ListenAndServe, func(_ error) {
_ = server.Shutdown(ctx)
cancel()
})
}
return gr.Run()
},
}
}
+50
View File
@@ -0,0 +1,50 @@
package command
import (
"fmt"
"os"
"github.com/owncloud/ocis/ocis-pkg/registry"
"github.com/owncloud/ocis/ocis-pkg/version"
tw "github.com/olekukonko/tablewriter"
"github.com/owncloud/ocis/extensions/ocs/pkg/config"
"github.com/urfave/cli/v2"
)
// Version prints the service versions of all running instances.
func Version(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "version",
Usage: "print the version of this binary and the running extension instances",
Category: "info",
Action: func(c *cli.Context) error {
fmt.Println("Version: " + version.String)
fmt.Printf("Compiled: %s\n", version.Compiled())
fmt.Println("")
reg := registry.GetRegistry()
services, err := reg.GetService(cfg.HTTP.Namespace + "." + cfg.Service.Name)
if err != nil {
fmt.Println(fmt.Errorf("could not get %s services from the registry: %v", cfg.Service.Name, err))
return err
}
if len(services) == 0 {
fmt.Println("No running " + cfg.Service.Name + " service found.")
return nil
}
table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Version", "Address", "Id"})
table.SetAutoFormatHeaders(false)
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})
}
}
table.Render()
return nil
},
}
}
+38
View File
@@ -0,0 +1,38 @@
package config
import (
"context"
"github.com/owncloud/ocis/ocis-pkg/shared"
)
// Config combines all available configuration parts.
type Config struct {
*shared.Commons `yaml:"-"`
Service Service `yaml:"-"`
Tracing *Tracing `yaml:"tracing"`
Log *Log `yaml:"log"`
Debug Debug `yaml:"debug"`
HTTP HTTP `yaml:"http"`
TokenManager TokenManager `yaml:"token_manager"`
Reva Reva `yaml:"reva"`
IdentityManagement IdentityManagement `yaml:"identity_management"`
AccountBackend string `yaml:"account_backend" env:"OCS_ACCOUNT_BACKEND_TYPE"`
StorageUsersDriver string `yaml:"storage_users_driver" env:"STORAGE_USERS_DRIVER;OCS_STORAGE_USERS_DRIVER"`
MachineAuthAPIKey string `yaml:"machine_auth_api_key" env:"OCIS_MACHINE_AUTH_API_KEY;OCS_MACHINE_AUTH_API_KEY"`
Context context.Context `yaml:"-"`
}
// IdentityManagement keeps track of the OIDC address. This is because Reva requisite of uniqueness for users
// is based in the combination of IDP hostname + UserID. For more information see:
// https://github.com/cs3org/reva/blob/4fd0229f13fae5bc9684556a82dbbd0eced65ef9/pkg/storage/utils/decomposedfs/node/node.go#L856-L865
type IdentityManagement struct {
Address string `yaml:"address" env:"OCIS_URL;OCS_IDM_ADDRESS"`
}
+9
View File
@@ -0,0 +1,9 @@
package config
// Debug defines the available debug configuration.
type Debug struct {
Addr string `yaml:"addr" env:"OCS_DEBUG_ADDR"`
Token string `yaml:"token" env:"OCS_DEBUG_TOKEN"`
Pprof bool `yaml:"pprof" env:"OCS_DEBUG_PPROF"`
Zpages bool `yaml:"zpages" env:"OCS_DEBUG_ZPAGES"`
}
@@ -0,0 +1,87 @@
package defaults
import (
"strings"
"github.com/owncloud/ocis/extensions/ocs/pkg/config"
)
func FullDefaultConfig() *config.Config {
cfg := DefaultConfig()
EnsureDefaults(cfg)
Sanitize(cfg)
return cfg
}
func DefaultConfig() *config.Config {
return &config.Config{
Debug: config.Debug{
Addr: "127.0.0.1:9114",
Token: "",
Pprof: false,
Zpages: false,
},
HTTP: config.HTTP{
Addr: "127.0.0.1:9110",
Root: "/ocs",
Namespace: "com.owncloud.web",
CORS: config.CORS{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Authorization", "Origin", "Content-Type", "Accept", "X-Requested-With"},
AllowCredentials: true,
},
},
Service: config.Service{
Name: "ocs",
},
TokenManager: config.TokenManager{
JWTSecret: "Pive-Fumkiu4",
},
AccountBackend: "accounts",
Reva: config.Reva{
Address: "127.0.0.1:9142",
},
StorageUsersDriver: "ocis",
MachineAuthAPIKey: "change-me-please",
IdentityManagement: config.IdentityManagement{
Address: "https://localhost:9200",
},
}
}
func EnsureDefaults(cfg *config.Config) {
// provide with defaults for shared logging, since we need a valid destination address for BindEnv.
if cfg.Log == nil && cfg.Commons != nil && cfg.Commons.Log != nil {
cfg.Log = &config.Log{
Level: cfg.Commons.Log.Level,
Pretty: cfg.Commons.Log.Pretty,
Color: cfg.Commons.Log.Color,
File: cfg.Commons.Log.File,
}
} else if cfg.Log == nil {
cfg.Log = &config.Log{}
}
// provide with defaults for shared tracing, since we need a valid destination address for BindEnv.
if cfg.Tracing == nil && cfg.Commons != nil && cfg.Commons.Tracing != nil {
cfg.Tracing = &config.Tracing{
Enabled: cfg.Commons.Tracing.Enabled,
Type: cfg.Commons.Tracing.Type,
Endpoint: cfg.Commons.Tracing.Endpoint,
Collector: cfg.Commons.Tracing.Collector,
}
} else if cfg.Tracing == nil {
cfg.Tracing = &config.Tracing{}
}
}
func Sanitize(cfg *config.Config) {
// sanitize config
if cfg.HTTP.Root != "/" {
cfg.HTTP.Root = strings.TrimSuffix(cfg.HTTP.Root, "/")
}
}
+17
View File
@@ -0,0 +1,17 @@
package config
// HTTP defines the available http configuration.
type HTTP struct {
Addr string `yaml:"addr" env:"OCS_HTTP_ADDR"`
Root string `yaml:"root" env:"OCS_HTTP_ROOT"`
Namespace string `yaml:"-"`
CORS CORS `yaml:"cors"`
}
// CORS defines the available cors configuration.
type CORS struct {
AllowedOrigins []string `yaml:"allowed_origins"`
AllowedMethods []string `yaml:"allowed_methods"`
AllowedHeaders []string `yaml:"allowed_headers"`
AllowCredentials bool `yaml:"allowed_credentials"`
}
+9
View File
@@ -0,0 +1,9 @@
package config
// Log defines the available log configuration.
type Log struct {
Level string `mapstructure:"level" env:"OCIS_LOG_LEVEL;OCS_LOG_LEVEL"`
Pretty bool `mapstructure:"pretty" env:"OCIS_LOG_PRETTY;OCS_LOG_PRETTY"`
Color bool `mapstructure:"color" env:"OCIS_LOG_COLOR;OCS_LOG_COLOR"`
File string `mapstructure:"file" env:"OCIS_LOG_FILE;OCS_LOG_FILE"`
}
+33
View File
@@ -0,0 +1,33 @@
package parser
import (
"errors"
"github.com/owncloud/ocis/extensions/ocs/pkg/config"
"github.com/owncloud/ocis/extensions/ocs/pkg/config/defaults"
ociscfg "github.com/owncloud/ocis/ocis-pkg/config"
"github.com/owncloud/ocis/ocis-pkg/config/envdecode"
)
// ParseConfig loads accounts configuration from known paths.
func ParseConfig(cfg *config.Config) error {
_, err := ociscfg.BindSourcesToStructs(cfg.Service.Name, cfg)
if err != nil {
return err
}
defaults.EnsureDefaults(cfg)
// load all env variables relevant to the config in the current context.
if err := envdecode.Decode(cfg); err != nil {
// no environment variable set for this config is an expected "error"
if !errors.Is(err, envdecode.ErrNoTargetFieldsAreSet) {
return err
}
}
defaults.Sanitize(cfg)
return nil
}
+11
View File
@@ -0,0 +1,11 @@
package config
// Reva defines all available REVA configuration.
type Reva struct {
Address string `yaml:"address" env:"REVA_GATEWAY"`
}
// TokenManager is the config for using the reva token manager
type TokenManager struct {
JWTSecret string `yaml:"jwt_secret" env:"OCIS_JWT_SECRET;OCS_JWT_SECRET"`
}
+6
View File
@@ -0,0 +1,6 @@
package config
// Service defines the available service configuration.
type Service struct {
Name string `yaml:"-"`
}
+9
View File
@@ -0,0 +1,9 @@
package config
// Tracing defines the available tracing configuration.
type Tracing struct {
Enabled bool `yaml:"enabled" env:"OCIS_TRACING_ENABLED;OCS_TRACING_ENABLED"`
Type string `yaml:"type" env:"OCIS_TRACING_TYPE;OCS_TRACING_TYPE"`
Endpoint string `yaml:"endpoint" env:"OCIS_TRACING_ENDPOINT;OCS_TRACING_ENDPOINT"`
Collector string `yaml:"collector" env:"OCIS_TRACING_COLLECTOR;OCS_TRACING_COLLECTOR"`
}
+17
View File
@@ -0,0 +1,17 @@
package logging
import (
"github.com/owncloud/ocis/extensions/ocs/pkg/config"
"github.com/owncloud/ocis/ocis-pkg/log"
)
// LoggerFromConfig initializes a service-specific logger instance.
func Configure(name string, cfg *config.Log) log.Logger {
return log.NewLogger(
log.Name(name),
log.Level(cfg.Level),
log.Pretty(cfg.Pretty),
log.Color(cfg.Color),
log.File(cfg.File),
)
}
+45
View File
@@ -0,0 +1,45 @@
package metrics
import "github.com/prometheus/client_golang/prometheus"
var (
// Namespace defines the namespace for the defines metrics.
Namespace = "ocis"
// Subsystem defines the subsystem for the defines metrics.
Subsystem = "ocs"
)
// Metrics defines the available metrics of this service.
type Metrics struct {
// Counter *prometheus.CounterVec
BuildInfo *prometheus.GaugeVec
}
// New initializes the available metrics.
func New() *Metrics {
m := &Metrics{
// Counter: prometheus.NewCounterVec(prometheus.CounterOpts{
// Namespace: Namespace,
// Subsystem: Subsystem,
// Name: "greet_total",
// Help: "How many greeting requests processed",
// }, []string{}),
BuildInfo: prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: Namespace,
Subsystem: Subsystem,
Name: "build_info",
Help: "Build Information",
}, []string{"version"}),
}
// prometheus.Register(
// m.Counter,
// )
_ = prometheus.Register(
m.BuildInfo,
)
return m
}
+24
View File
@@ -0,0 +1,24 @@
package middleware
import (
"context"
"net/http"
"github.com/go-chi/render"
)
// OCSFormatCtx middleware is used to determine the content type from
// the format URL parameter passed in an ocs request. Defaults to XML
func OCSFormatCtx(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Query().Get("format") {
case "", "xml":
r.Header.Set("Accept", "application/xml")
r = r.WithContext(context.WithValue(r.Context(), render.ContentTypeCtxKey, render.ContentTypeXML))
case "json":
r.Header.Set("Accept", "application/json")
r = r.WithContext(context.WithValue(r.Context(), render.ContentTypeCtxKey, render.ContentTypeJSON))
}
next.ServeHTTP(w, r)
})
}
+24
View File
@@ -0,0 +1,24 @@
package middleware
import (
"net/http"
ocstracing "github.com/owncloud/ocis/extensions/ocs/pkg/tracing"
"go.opentelemetry.io/otel/propagation"
)
var propagator = propagation.NewCompositeTextMapPropagator(
propagation.Baggage{},
propagation.TraceContext{},
)
// LogTrace Sets the initial trace in the ocs service.
func LogTrace(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, span := ocstracing.TraceProvider.Tracer("ocs").Start(r.Context(), r.URL.Path)
defer span.End()
propagator.Inject(ctx, propagation.HeaderCarrier(r.Header))
next.ServeHTTP(w, r.WithContext(ctx))
})
}
+42
View File
@@ -0,0 +1,42 @@
package middleware
import (
"github.com/owncloud/ocis/ocis-pkg/log"
"github.com/owncloud/ocis/ocis-pkg/roles"
)
// Option defines a single option function.
type Option func(o *Options)
// Options defines the available options for this package.
type Options struct {
// Logger to use for logging, must be set
Logger log.Logger
// RoleManager for looking up permissions
RoleManager *roles.Manager
}
// 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(l log.Logger) Option {
return func(o *Options) {
o.Logger = l
}
}
// RoleManager provides a function to set the RoleManager option.
func RoleManager(val *roles.Manager) Option {
return func(o *Options) {
o.RoleManager = val
}
}
@@ -0,0 +1,42 @@
package middleware
import (
"net/http"
"github.com/go-chi/render"
accounts "github.com/owncloud/ocis/extensions/accounts/pkg/service/v0"
"github.com/owncloud/ocis/extensions/ocs/pkg/service/v0/data"
"github.com/owncloud/ocis/extensions/ocs/pkg/service/v0/response"
"github.com/owncloud/ocis/ocis-pkg/roles"
)
// RequireAdmin middleware is used to require the user in context to be an admin / have account management permissions
func RequireAdmin(opts ...Option) func(next http.Handler) http.Handler {
opt := newOptions(opts...)
mustRender := func(w http.ResponseWriter, r *http.Request, renderer render.Renderer) {
if err := render.Render(w, r, renderer); err != nil {
opt.Logger.Err(err).Msgf("failed to write response for ocs request %s on %s", r.Method, r.URL)
}
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// get roles from context
roleIDs, ok := roles.ReadRoleIDsFromContext(r.Context())
if !ok {
mustRender(w, r, response.ErrRender(data.MetaUnauthorized.StatusCode, "Unauthorized"))
return
}
// check if permission is present in roles of the authenticated account
if opt.RoleManager.FindPermissionByID(r.Context(), roleIDs, accounts.AccountManagementPermissionID) != nil {
next.ServeHTTP(w, r)
return
}
mustRender(w, r, response.ErrRender(data.MetaUnauthorized.StatusCode, "Unauthorized"))
})
}
}
@@ -0,0 +1,82 @@
package middleware
import (
"net/http"
"net/url"
revactx "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
accounts "github.com/owncloud/ocis/extensions/accounts/pkg/service/v0"
"github.com/owncloud/ocis/extensions/ocs/pkg/service/v0/data"
"github.com/owncloud/ocis/extensions/ocs/pkg/service/v0/response"
"github.com/owncloud/ocis/ocis-pkg/roles"
settingsService "github.com/owncloud/ocis/settings/pkg/service/v0"
)
// RequireSelfOrAdmin middleware is used to require the requesting user to be an admin or the requested user himself
func RequireSelfOrAdmin(opts ...Option) func(next http.Handler) http.Handler {
opt := newOptions(opts...)
mustRender := func(w http.ResponseWriter, r *http.Request, renderer render.Renderer) {
if err := render.Render(w, r, renderer); err != nil {
opt.Logger.Err(err).Msgf("failed to write response for ocs request %s on %s", r.Method, r.URL)
}
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
u, ok := revactx.ContextGetUser(r.Context())
if !ok {
mustRender(w, r, response.ErrRender(data.MetaUnauthorized.StatusCode, "Unauthorized"))
return
}
if u.Id == nil || u.Id.OpaqueId == "" {
mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, "user is missing an id"))
return
}
// get roles from context
roleIDs, ok := roles.ReadRoleIDsFromContext(r.Context())
if !ok {
opt.Logger.Debug().Str("userid", u.Id.OpaqueId).Msg("No roles in context, contacting settings service")
var err error
roleIDs, err = opt.RoleManager.FindRoleIDsForUser(r.Context(), u.Id.OpaqueId)
if err != nil {
opt.Logger.Err(err).Str("userid", u.Id.OpaqueId).Msg("failed to get roles for user")
mustRender(w, r, response.ErrRender(data.MetaUnauthorized.StatusCode, "Unauthorized"))
return
}
if len(roleIDs) == 0 {
roleIDs = append(roleIDs, settingsService.BundleUUIDRoleUser, settingsService.SelfManagementPermissionID)
// if roles are empty, assume we haven't seen the user before and assign a default user role. At least until
// proper roles are provided. See https://github.com/owncloud/ocis/issues/1825 for more context.
//return user, nil
}
}
// check if account management permission is present in roles of the authenticated account
if opt.RoleManager.FindPermissionByID(r.Context(), roleIDs, accounts.AccountManagementPermissionID) != nil {
next.ServeHTTP(w, r)
return
}
// check if self management permission is present in roles of the authenticated account
if opt.RoleManager.FindPermissionByID(r.Context(), roleIDs, accounts.SelfManagementPermissionID) != nil {
userid := chi.URLParam(r, "userid")
var err error
if userid, err = url.PathUnescape(userid); err != nil {
mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, "malformed username"))
}
if userid == "" || userid == u.Id.OpaqueId || userid == u.Username {
next.ServeHTTP(w, r)
return
}
}
mustRender(w, r, response.ErrRender(data.MetaUnauthorized.StatusCode, "Unauthorized"))
})
}
}
@@ -0,0 +1,39 @@
package middleware
import (
"net/http"
revactx "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/go-chi/render"
"github.com/owncloud/ocis/extensions/ocs/pkg/service/v0/data"
"github.com/owncloud/ocis/extensions/ocs/pkg/service/v0/response"
)
// RequireUser middleware is used to require a user in context
func RequireUser(opts ...Option) func(next http.Handler) http.Handler {
opt := newOptions(opts...)
mustRender := func(w http.ResponseWriter, r *http.Request, renderer render.Renderer) {
if err := render.Render(w, r, renderer); err != nil {
opt.Logger.Err(err).Msgf("failed to write response for ocs request %s on %s", r.Method, r.URL)
}
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
u, ok := revactx.ContextGetUser(r.Context())
if !ok {
mustRender(w, r, response.ErrRender(data.MetaUnauthorized.StatusCode, "Unauthorized"))
return
}
if u.Id == nil || u.Id.OpaqueId == "" {
mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, "user is missing an id"))
return
}
next.ServeHTTP(w, r)
})
}
}
+50
View File
@@ -0,0 +1,50 @@
package debug
import (
"context"
"github.com/owncloud/ocis/extensions/ocs/pkg/config"
"github.com/owncloud/ocis/ocis-pkg/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
Context context.Context
Config *config.Config
}
// 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
}
}
// Context provides a function to set the context option.
func Context(val context.Context) Option {
return func(o *Options) {
o.Context = val
}
}
// Config provides a function to set the config option.
func Config(val *config.Config) Option {
return func(o *Options) {
o.Config = val
}
}
+63
View File
@@ -0,0 +1,63 @@
package debug
import (
"io"
"net/http"
"github.com/owncloud/ocis/extensions/ocs/pkg/config"
"github.com/owncloud/ocis/ocis-pkg/service/debug"
"github.com/owncloud/ocis/ocis-pkg/version"
)
// Server initializes the debug service and server.
func Server(opts ...Option) (*http.Server, error) {
options := newOptions(opts...)
return debug.NewService(
debug.Logger(options.Logger),
debug.Name(options.Config.Service.Name),
debug.Version(version.String),
debug.Address(options.Config.Debug.Addr),
debug.Token(options.Config.Debug.Token),
debug.Pprof(options.Config.Debug.Pprof),
debug.Zpages(options.Config.Debug.Zpages),
debug.Health(health(options.Config)),
debug.Ready(ready(options.Config)),
debug.CorsAllowedOrigins(options.Config.HTTP.CORS.AllowedOrigins),
debug.CorsAllowedMethods(options.Config.HTTP.CORS.AllowedMethods),
debug.CorsAllowedHeaders(options.Config.HTTP.CORS.AllowedHeaders),
debug.CorsAllowCredentials(options.Config.HTTP.CORS.AllowCredentials),
), nil
}
// health implements the health check.
func health(cfg *config.Config) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
// TODO: check if services are up and running
_, err := io.WriteString(w, http.StatusText(http.StatusOK))
// io.WriteString should not fail but if it does we want to know.
if err != nil {
panic(err)
}
}
}
// ready implements the ready check.
func ready(cfg *config.Config) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
// TODO: check if services are up and running
_, err := io.WriteString(w, http.StatusText(http.StatusOK))
// io.WriteString should not fail but if it does we want to know.
if err != nil {
panic(err)
}
}
}
+76
View File
@@ -0,0 +1,76 @@
package http
import (
"context"
"github.com/owncloud/ocis/extensions/ocs/pkg/config"
"github.com/owncloud/ocis/extensions/ocs/pkg/metrics"
"github.com/owncloud/ocis/ocis-pkg/log"
"github.com/urfave/cli/v2"
)
// Option defines a single option function.
type Option func(o *Options)
// Options defines the available options for this package.
type Options struct {
Namespace string
Logger log.Logger
Context context.Context
Config *config.Config
Metrics *metrics.Metrics
Flags []cli.Flag
}
// 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
}
}
// Context provides a function to set the context option.
func Context(val context.Context) Option {
return func(o *Options) {
o.Context = val
}
}
// Config provides a function to set the config option.
func Config(val *config.Config) Option {
return func(o *Options) {
o.Config = val
}
}
// Metrics provides a function to set the metrics option.
func Metrics(val *metrics.Metrics) Option {
return func(o *Options) {
o.Metrics = val
}
}
// Flags provides a function to set the flags option.
func Flags(val []cli.Flag) Option {
return func(o *Options) {
o.Flags = append(o.Flags, val...)
}
}
// Namespace provides a function to set the Namespace option.
func Namespace(val string) Option {
return func(o *Options) {
o.Namespace = val
}
}
+63
View File
@@ -0,0 +1,63 @@
package http
import (
chimiddleware "github.com/go-chi/chi/v5/middleware"
ocsmw "github.com/owncloud/ocis/extensions/ocs/pkg/middleware"
svc "github.com/owncloud/ocis/extensions/ocs/pkg/service/v0"
"github.com/owncloud/ocis/ocis-pkg/cors"
"github.com/owncloud/ocis/ocis-pkg/middleware"
"github.com/owncloud/ocis/ocis-pkg/service/http"
"github.com/owncloud/ocis/ocis-pkg/version"
"go-micro.dev/v4"
)
// Server initializes the http service and server.
func Server(opts ...Option) (http.Service, error) {
options := newOptions(opts...)
service := http.NewService(
http.Logger(options.Logger),
http.Name(options.Config.Service.Name),
http.Version(version.String),
http.Namespace(options.Config.HTTP.Namespace),
http.Address(options.Config.HTTP.Addr),
http.Context(options.Context),
http.Flags(options.Flags...),
)
handle := svc.NewService(
svc.Logger(options.Logger),
svc.Config(options.Config),
svc.Middleware(
chimiddleware.RealIP,
chimiddleware.RequestID,
middleware.NoCache,
middleware.Cors(
cors.Logger(options.Logger),
cors.AllowedOrigins(options.Config.HTTP.CORS.AllowedOrigins),
cors.AllowedMethods(options.Config.HTTP.CORS.AllowedMethods),
cors.AllowedHeaders(options.Config.HTTP.CORS.AllowedHeaders),
cors.AllowCredentials(options.Config.HTTP.CORS.AllowCredentials),
),
middleware.Secure,
middleware.Version(
options.Config.Service.Name,
version.String,
),
middleware.Logger(options.Logger),
ocsmw.LogTrace,
),
)
{
handle = svc.NewInstrument(handle, options.Metrics)
handle = svc.NewLogging(handle, options.Logger)
handle = svc.NewTracing(handle)
}
if err := micro.RegisterHandler(service.Server(), handle); err != nil {
return http.Service{}, err
}
return service, nil
}
File diff suppressed because it is too large Load Diff
+19
View File
@@ -0,0 +1,19 @@
package svc
import (
"net/http"
"github.com/owncloud/ocis/extensions/ocs/pkg/service/v0/data"
"github.com/owncloud/ocis/extensions/ocs/pkg/service/v0/response"
)
// GetConfig renders the ocs config endpoint
func (o Ocs) GetConfig(w http.ResponseWriter, r *http.Request) {
o.mustRender(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
}))
}
@@ -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"`
}
@@ -0,0 +1,6 @@
package data
// Groups holds group ids for the groups listing
type Groups struct {
Groups []string `json:"groups" xml:"groups>element"`
}
@@ -0,0 +1,43 @@
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"}
// MetaForbidden is an error response with code 104
var MetaForbidden = Meta{Status: "", StatusCode: 104, Message: "Forbidden"}
// 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"}
// MessageUserNotFound is used when a user can not be found
var MessageUserNotFound = "The requested user could not be found"
// MessageGroupNotFound is used when a group can not be found
var MessageGroupNotFound = "The requested group could not be found"
@@ -0,0 +1,33 @@
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 {
Enabled string `json:"enabled" xml:"enabled"`
UserID string `json:"id" xml:"id"`// UserID is mapped to the preferred_name attribute in accounts
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"`
}
+455
View File
@@ -0,0 +1,455 @@
package svc
import (
"context"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"regexp"
"strconv"
accountsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/accounts/v0"
accountssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/accounts/v0"
revactx "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/go-chi/chi/v5"
"github.com/owncloud/ocis/extensions/ocs/pkg/service/v0/data"
"github.com/owncloud/ocis/extensions/ocs/pkg/service/v0/response"
ocstracing "github.com/owncloud/ocis/extensions/ocs/pkg/tracing"
merrors "go-micro.dev/v4/errors"
"go.opentelemetry.io/otel/attribute"
)
// ListUserGroups lists a users groups
func (o Ocs) ListUserGroups(w http.ResponseWriter, r *http.Request) {
userid := chi.URLParam(r, "userid")
userid, err := url.PathUnescape(userid)
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
var account *accountsmsg.Account
if o.config.AccountBackend == "cs3" {
o.mustRender(w, r, response.DataRender(&data.Groups{}))
return
}
// short circuit if there is a user already in the context
if u, ok := revactx.ContextGetUser(r.Context()); ok {
// we are not sure whether the current user in the context is the admin or the authenticated user.
if u.Username == userid {
// the OCS API is a REST API and it uses the username to look for groups. If the id from the user in the context
// differs from that of the url we can assume we are an admin because we are past the selfOrAdmin middleware.
_, span := ocstracing.TraceProvider.
Tracer("ocs").
Start(r.Context(), "ListUserGroups")
defer span.End()
span.SetAttributes(attribute.StringSlice("groups", u.Groups))
if len(u.Groups) > 0 {
o.mustRender(w, r, response.DataRender(&data.Groups{Groups: u.Groups}))
return
}
}
}
if isValidUUID(userid) {
account, err = o.getAccountService().GetAccount(r.Context(), &accountssvc.GetAccountRequest{
Id: userid,
})
} else {
// despite the confusion, if we make it here we got ourselves a username
account, err = o.fetchAccountByUsername(r.Context(), userid)
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageUserNotFound))
} else {
o.mustRender(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 := make([]string, 0, len(account.MemberOf))
for i := range account.MemberOf {
if account.MemberOf[i].OnPremisesSamAccountName == "" {
o.logger.Warn().Str("groupid", account.MemberOf[i].Id).Msg("group on_premises_sam_account_name is empty, trying to lookup by id")
// we can try to look up the name
group, err := o.getGroupsService().GetGroup(r.Context(), &accountssvc.GetGroupRequest{
Id: account.MemberOf[i].Id,
})
if err != nil {
o.logger.Error().Err(err).Str("groupid", account.MemberOf[i].Id).Msg("could not get group")
continue
}
if group.OnPremisesSamAccountName == "" {
o.logger.Error().Err(err).Str("groupid", account.MemberOf[i].Id).Msg("group on_premises_sam_account_name is empty")
continue
}
groups = append(groups, group.OnPremisesSamAccountName)
} else {
groups = append(groups, account.MemberOf[i].OnPremisesSamAccountName)
}
}
o.logger.Error().Err(err).Int("count", len(groups)).Str("userid", account.Id).Msg("listing groups for user")
_, span := ocstracing.TraceProvider.
Tracer("ocs").
Start(r.Context(), "ListUserGroups")
defer span.End()
span.SetAttributes(attribute.StringSlice("groups", groups))
o.mustRender(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) {
groupid := r.PostFormValue("groupid")
userid := chi.URLParam(r, "userid")
userid, err := url.PathUnescape(userid)
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
if groupid == "" {
o.mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, "empty group assignment: unspecified group"))
return
}
account, err := o.fetchAccountByUsername(r.Context(), userid)
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageUserNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
return
}
// ocs only knows about names so we have to look up the internal id
group, err := o.fetchGroupByName(r.Context(), groupid)
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageGroupNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
return
}
_, err = o.getGroupsService().AddMember(r.Context(), &accountssvc.AddMemberRequest{
AccountId: account.Id,
GroupId: group.Id,
})
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageGroupNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("userid", account.Id).Str("groupid", group.Id).Msg("could not add user to group")
return
}
o.logger.Debug().Str("userid", account.Id).Str("groupid", group.Id).Msg("added user to group")
o.mustRender(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")
userid, err := url.PathUnescape(userid)
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
// Really? a DELETE with form encoded body?!?
// but it is not encoded as mime, so we cannot just call r.ParseForm()
// read it manually
body, err := ioutil.ReadAll(r.Body)
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, err.Error()))
return
}
if err = r.Body.Close(); err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
return
}
values, err := url.ParseQuery(string(body))
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, err.Error()))
return
}
groupid := values.Get("groupid")
if groupid == "" {
o.mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, "no group id"))
return
}
var account *accountsmsg.Account
if isValidUUID(userid) {
account, _ = o.getAccountService().GetAccount(r.Context(), &accountssvc.GetAccountRequest{
Id: userid,
})
} else {
// despite the confusion, if we make it here we got ourselves a username
account, err = o.fetchAccountByUsername(r.Context(), userid)
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, data.MessageUserNotFound))
} else {
o.mustRender(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
}
}
// ocs only knows about names so we have to look up the internal id
group, err := o.fetchGroupByName(r.Context(), groupid)
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageGroupNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
return
}
_, err = o.getGroupsService().RemoveMember(r.Context(), &accountssvc.RemoveMemberRequest{
AccountId: account.Id,
GroupId: group.Id,
})
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageGroupNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("userid", account.Id).Str("groupid", group.Id).Msg("could not remove user from group")
return
}
o.logger.Debug().Str("userid", account.Id).Str("groupid", group.Id).Msg("removed user from group")
o.mustRender(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(), &accountssvc.ListGroupsRequest{
Query: query,
})
if err != nil {
o.logger.Err(err).Msg("could not list users")
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, "could not list users"))
return
}
groups := make([]string, 0, len(res.Groups))
for i := range res.Groups {
groups = append(groups, res.Groups[i].OnPremisesSamAccountName)
}
_, span := ocstracing.TraceProvider.
Tracer("ocs").
Start(r.Context(), "ListGroups")
defer span.End()
span.SetAttributes(attribute.StringSlice("groups", groups))
o.mustRender(w, r, response.DataRender(&data.Groups{Groups: groups}))
}
// AddGroup adds a group
// oC10 implementation: https://github.com/owncloud/core/blob/762780a23c9eadda4fb5fa8db99eba66a5100b6e/apps/provisioning_api/lib/Groups.php#L126-L154
func (o Ocs) AddGroup(w http.ResponseWriter, r *http.Request) {
groupid := r.PostFormValue("groupid")
displayname := r.PostFormValue("displayname")
gid := r.PostFormValue("gidnumber")
if displayname == "" && groupid == "" {
code := data.MetaFailure.StatusCode // v1
if response.APIVersion(r.Context()) == "2" {
code = data.MetaBadRequest.StatusCode
}
o.mustRender(w, r, response.ErrRender(code, "No groupid or display name provided"))
return
}
if displayname == "" {
// oC10 OCS does not know about a group displayname
// therefore we fall back to the oC10 parameter groupid (which is the groupname in the oC10 world)
displayname = groupid
}
var gidNumber int64
var err error
if gid != "" {
gidNumber, err = strconv.ParseInt(gid, 10, 64)
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, "Cannot use the gidnumber provided"))
o.logger.Error().Err(err).Str("gid", gid).Str("groupid", groupid).Msg("Cannot use the gidnumber provided")
return
}
}
newGroup := &accountsmsg.Group{
Id: groupid,
DisplayName: displayname,
OnPremisesSamAccountName: groupid,
GidNumber: gidNumber,
}
group, err := o.getGroupsService().CreateGroup(r.Context(), &accountssvc.CreateGroupRequest{
Group: newGroup,
})
if err != nil {
merr := merrors.FromError(err)
switch merr.Code {
case http.StatusBadRequest:
o.mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, merr.Detail))
case http.StatusConflict:
if response.APIVersion(r.Context()) == "2" {
// it seems the application framework sets the ocs status code to the httpstatus code, which affects the provisioning api
// see https://github.com/owncloud/core/blob/b9ff4c93e051c94adfb301545098ae627e52ef76/lib/public/AppFramework/OCSController.php#L142-L150
o.mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, merr.Detail))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaInvalidInput.StatusCode, merr.Detail))
}
default:
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("groupid", groupid).Msg("could not add group")
// TODO check error if group already existed
return
}
o.logger.Debug().Interface("group", group).Msg("added group")
o.mustRender(w, r, response.DataRender(struct{}{}))
}
// DeleteGroup deletes a group
func (o Ocs) DeleteGroup(w http.ResponseWriter, r *http.Request) {
groupid := chi.URLParam(r, "groupid")
groupid, err := url.PathUnescape(groupid)
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
// ocs only knows about names so we have to look up the internal id
group, err := o.fetchGroupByName(r.Context(), groupid)
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageGroupNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
return
}
_, err = o.getGroupsService().DeleteGroup(r.Context(), &accountssvc.DeleteGroupRequest{
Id: group.Id,
})
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageGroupNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("groupid", group.Id).Msg("could not remove group")
return
}
o.logger.Debug().Str("groupid", group.Id).Msg("removed group")
o.mustRender(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")
groupid, err := url.PathUnescape(groupid)
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
// ocs only knows about names so we have to look up the internal id
group, err := o.fetchGroupByName(r.Context(), groupid)
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageGroupNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
return
}
res, err := o.getGroupsService().ListMembers(r.Context(), &accountssvc.ListMembersRequest{Id: group.Id})
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageGroupNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("groupid", group.Id).Msg("could not get list of members")
return
}
members := make([]string, 0, len(res.Members))
for i := range res.Members {
members = append(members, res.Members[i].OnPremisesSamAccountName)
}
o.logger.Error().Err(err).Int("count", len(members)).Str("groupid", groupid).Msg("listing group members")
o.mustRender(w, r, response.DataRender(&data.Users{Users: members}))
}
func isValidUUID(uuid string) bool {
r := regexp.MustCompile("^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$")
return r.MatchString(uuid)
}
func (o Ocs) fetchGroupByName(ctx context.Context, name string) (*accountsmsg.Group, error) {
var res *accountssvc.ListGroupsResponse
res, err := o.getGroupsService().ListGroups(ctx, &accountssvc.ListGroupsRequest{
Query: fmt.Sprintf("on_premises_sam_account_name eq '%v'", escapeValue(name)),
})
if err != nil {
return nil, err
}
if res != nil && len(res.Groups) == 1 {
return res.Groups[0], nil
}
return nil, merrors.NotFound("", data.MessageGroupNotFound)
}
@@ -0,0 +1,30 @@
package svc
import (
"net/http"
"github.com/owncloud/ocis/extensions/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/ocis-pkg/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)
}
+68
View File
@@ -0,0 +1,68 @@
package svc
import (
"net/http"
"github.com/owncloud/ocis/extensions/ocs/pkg/config"
"github.com/owncloud/ocis/ocis-pkg/log"
"github.com/owncloud/ocis/ocis-pkg/roles"
settingssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/settings/v0"
)
// 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
RoleService settingssvc.RoleService
RoleManager *roles.Manager
}
// 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
}
}
// RoleService provides a function to set the RoleService option.
func RoleService(val settingssvc.RoleService) Option {
return func(o *Options) {
o.RoleService = val
}
}
// RoleManager provides a function to set the RoleManager option.
func RoleManager(val *roles.Manager) Option {
return func(o *Options) {
o.RoleManager = val
}
}
@@ -0,0 +1,114 @@
package response
import (
"encoding/xml"
"net/http"
"reflect"
"github.com/go-chi/render"
"github.com/owncloud/ocis/extensions/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 Payload 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
}
@@ -0,0 +1,92 @@
package response
import (
"context"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
"github.com/owncloud/ocis/extensions/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 {
if meta.StatusCode == data.MetaUnauthorized.StatusCode {
return http.StatusUnauthorized
}
return http.StatusOK
}
// OcsV2StatusCodes maps the OCS codes to http status codes for the ocs API v2.
// see https://github.com/owncloud/core/blob/c08baf580927ecb8ec179028dda255fdd85b4568/lib/private/legacy/api.php#L528
// also HTTP status codes for apps are the same as OCS codes
// see https://github.com/owncloud/core/blob/b9ff4c93e051c94adfb301545098ae627e52ef76/lib/public/AppFramework/OCSController.php#L142-L150
// I think this is a bug in the ocs v2 api, but since we are going to mimic bugs in ocis ... here goes
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 data.MetaOK.StatusCode:
// TODO mustn't data.Meta be a pointer so this assignment has an effect
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? yes, align with oc10 core mapStatusCodes
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))
})
}
+187
View File
@@ -0,0 +1,187 @@
package svc
import (
"net/http"
"time"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/owncloud/ocis/ocis-pkg/service/grpc"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/render"
accountssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/accounts/v0"
"github.com/owncloud/ocis/extensions/ocs/pkg/config"
ocsm "github.com/owncloud/ocis/extensions/ocs/pkg/middleware"
"github.com/owncloud/ocis/extensions/ocs/pkg/service/v0/data"
"github.com/owncloud/ocis/extensions/ocs/pkg/service/v0/response"
"github.com/owncloud/ocis/ocis-pkg/account"
"github.com/owncloud/ocis/ocis-pkg/log"
opkgm "github.com/owncloud/ocis/ocis-pkg/middleware"
"github.com/owncloud/ocis/ocis-pkg/roles"
settingssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/settings/v0"
"github.com/owncloud/ocis/proxy/pkg/user/backend"
)
// 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...)
roleService := options.RoleService
if roleService == nil {
roleService = settingssvc.NewRoleService("com.owncloud.api.settings", grpc.DefaultClient)
}
roleManager := options.RoleManager
if roleManager == nil {
m := roles.NewManager(
roles.CacheSize(1024),
roles.CacheTTL(time.Hour*24*7),
roles.Logger(options.Logger),
roles.RoleService(roleService),
)
roleManager = &m
}
svc := Ocs{
config: options.Config,
mux: m,
RoleManager: roleManager,
logger: options.Logger,
}
if svc.config.AccountBackend == "" {
svc.config.AccountBackend = "accounts"
}
requireUser := ocsm.RequireUser(
ocsm.Logger(options.Logger),
)
requireAdmin := ocsm.RequireAdmin(
ocsm.RoleManager(roleManager),
ocsm.Logger(options.Logger),
)
requireSelfOrAdmin := ocsm.RequireSelfOrAdmin(
ocsm.RoleManager(roleManager),
ocsm.Logger(options.Logger),
)
m.Route(options.Config.HTTP.Root, func(r chi.Router) {
r.NotFound(svc.NotFound)
r.Use(middleware.StripSlashes)
r.Use(opkgm.ExtractAccountUUID(
account.Logger(options.Logger),
account.JWTSecret(options.Config.TokenManager.JWTSecret)),
)
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) {})
// TODO /apps
r.Route("/user", func(r chi.Router) {
r.With(requireSelfOrAdmin).Get("/", svc.GetSelf)
r.Get("/signing-key", svc.GetSigningKey)
})
// for /users endpoints see https://github.com/owncloud/core/blob/master/apps/provisioning_api/appinfo/routes.php#L44-L56
r.Route("/users", func(r chi.Router) {
r.With(requireAdmin).Get("/", svc.ListUsers)
r.With(requireAdmin).Post("/", svc.AddUser)
r.Route("/{userid}", func(r chi.Router) {
r.With(requireUser).Get("/", svc.GetUser)
r.With(requireSelfOrAdmin).Put("/", svc.EditUser)
r.With(requireAdmin).Delete("/", svc.DeleteUser)
r.With(requireAdmin).Put("/enable", svc.EnableUser)
r.With(requireAdmin).Put("/disable", svc.DisableUser)
})
r.Route("/{userid}/groups", func(r chi.Router) {
r.With(requireSelfOrAdmin).Get("/", svc.ListUserGroups)
r.With(requireAdmin).Post("/", svc.AddToGroup)
r.With(requireAdmin).Delete("/", svc.RemoveFromGroup)
})
r.Route("/{userid}/subadmins", func(r chi.Router) {
r.With(requireAdmin).Post("/", svc.NotImplementedStub)
r.With(requireSelfOrAdmin).Get("/", svc.NotImplementedStub)
r.With(requireAdmin).Delete("/", svc.NotImplementedStub)
})
})
// for /groups endpoints see https://github.com/owncloud/core/blob/master/apps/provisioning_api/appinfo/routes.php#L65-L69
r.Route("/groups", func(r chi.Router) {
r.With(requireAdmin).Get("/", svc.ListGroups)
r.With(requireAdmin).Post("/", svc.AddGroup)
r.With(requireSelfOrAdmin).Get("/{groupid}", svc.GetGroupMembers)
r.With(requireAdmin).Delete("/{groupid}", svc.DeleteGroup)
r.With(requireAdmin).Get("/{groupid}/subadmins", svc.NotImplementedStub)
})
})
r.Route("/config", func(r chi.Router) {
r.With(requireUser).Get("/", svc.GetConfig)
})
})
})
return svc
}
// Ocs defines implements the business logic for Service.
type Ocs struct {
config *config.Config
logger log.Logger
RoleService settingssvc.RoleService
RoleManager *roles.Manager
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) {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, "not found"))
}
func (o Ocs) getAccountService() accountssvc.AccountsService {
return accountssvc.NewAccountsService("com.owncloud.api.accounts", grpc.DefaultClient)
}
func (o Ocs) getCS3Backend() backend.UserBackend {
revaClient, err := pool.GetGatewayServiceClient(o.config.Reva.Address)
if err != nil {
o.logger.Fatal().Msgf("could not get reva client at address %s", o.config.Reva.Address)
}
return backend.NewCS3UserBackend(nil, revaClient, o.config.MachineAuthAPIKey, o.logger)
}
func (o Ocs) getGroupsService() accountssvc.GroupsService {
return accountssvc.NewGroupsService("com.owncloud.api.accounts", grpc.DefaultClient)
}
// NotImplementedStub returns a not implemented error
func (o Ocs) NotImplementedStub(w http.ResponseWriter, r *http.Request) {
o.mustRender(w, r, response.ErrRender(data.MetaUnknownError.StatusCode, "Not implemented"))
}
func (o Ocs) mustRender(w http.ResponseWriter, r *http.Request, renderer render.Renderer) {
if err := render.Render(w, r, renderer); err != nil {
o.logger.Err(err).Msgf("failed to write response for ocs request %s on %s", r.Method, r.URL)
}
}
+28
View File
@@ -0,0 +1,28 @@
package svc
import (
"net/http"
"github.com/owncloud/ocis/ocis-pkg/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.TraceContext(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)
}
+784
View File
@@ -0,0 +1,784 @@
package svc
import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
accountsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/accounts/v0"
accountssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/accounts/v0"
storemsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/store/v0"
storesvc "github.com/owncloud/ocis/protogen/gen/ocis/services/store/v0"
"github.com/asim/go-micro/plugins/client/grpc/v4"
revauser "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/cs3org/reva/v2/pkg/auth/scope"
revactx "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/v2/pkg/token/manager/jwt"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/owncloud/ocis/extensions/ocs/pkg/service/v0/data"
"github.com/owncloud/ocis/extensions/ocs/pkg/service/v0/response"
ocstracing "github.com/owncloud/ocis/extensions/ocs/pkg/tracing"
"github.com/pkg/errors"
merrors "go-micro.dev/v4/errors"
"google.golang.org/genproto/protobuf/field_mask"
"google.golang.org/grpc/metadata"
"google.golang.org/protobuf/types/known/fieldmaskpb"
)
// GetSelf returns the currently logged in user
func (o Ocs) GetSelf(w http.ResponseWriter, r *http.Request) {
var account *accountsmsg.Account
var err error
u, ok := revactx.ContextGetUser(r.Context())
if !ok || u.Id == nil || u.Id.OpaqueId == "" {
o.mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, "user is missing an id"))
return
}
account, err = o.getAccountService().GetAccount(r.Context(), &accountssvc.GetAccountRequest{
Id: u.Id.OpaqueId,
})
if err != nil {
merr := merrors.FromError(err)
// TODO(someone) this fix is in place because if the user backend (PROXY_ACCOUNT_BACKEND_TYPE) is set to, for instance,
// cs3, we cannot count with the accounts service.
if u != nil {
d := &data.User{
UserID: u.Username,
DisplayName: u.DisplayName,
LegacyDisplayName: u.DisplayName,
Email: u.Mail,
UIDNumber: u.UidNumber,
GIDNumber: u.GidNumber,
}
o.mustRender(w, r, response.DataRender(d))
return
}
o.logger.Error().Err(merr).Interface("user", u).Msg("could not get account for 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")
d := &data.User{
UserID: account.OnPremisesSamAccountName,
DisplayName: account.DisplayName,
LegacyDisplayName: account.DisplayName,
Email: account.Mail,
UIDNumber: account.UidNumber,
GIDNumber: account.GidNumber,
// TODO hide enabled flag or it might get rendered as false
}
o.mustRender(w, r, response.DataRender(d))
}
// GetUser returns the user with the given userid
func (o Ocs) GetUser(w http.ResponseWriter, r *http.Request) {
userid := chi.URLParam(r, "userid")
userid, err := url.PathUnescape(userid)
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
var account *accountsmsg.Account
switch {
case userid == "":
o.mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, "missing user in context"))
case o.config.AccountBackend == "accounts":
account, err = o.fetchAccountByUsername(r.Context(), userid)
case o.config.AccountBackend == "cs3":
account, err = o.fetchAccountFromCS3Backend(r.Context(), userid)
default:
o.logger.Fatal().Msgf("Invalid accounts backend type '%s'", o.config.AccountBackend)
}
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageUserNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(merr).Str("userid", userid).Msg("could not get account for 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"
}
d := &data.User{
UserID: account.OnPremisesSamAccountName,
DisplayName: account.DisplayName,
LegacyDisplayName: account.DisplayName,
Email: account.Mail,
UIDNumber: account.UidNumber,
GIDNumber: account.GidNumber,
Enabled: enabled, // TODO include in response only when admin?
// 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",
},
}
_, span := ocstracing.TraceProvider.
Tracer("ocs").
Start(r.Context(), "GetUser")
defer span.End()
o.mustRender(w, r, response.DataRender(d))
}
// AddUser creates a new user account
func (o Ocs) AddUser(w http.ResponseWriter, r *http.Request) {
userid := r.PostFormValue("userid")
password := r.PostFormValue("password")
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 {
o.mustRender(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 {
o.mustRender(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
}
}
if strings.TrimSpace(password) == "" {
o.mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, "empty password not allowed"))
o.logger.Error().Str("userid", userid).Msg("empty password not allowed")
return
}
// fallbacks
/* TODO decide if we want to make these fallbacks. Keep in mind:
- oCIS requires a preferred_name and email
*/
if displayname == "" {
displayname = userid
}
newAccount := &accountsmsg.Account{
Id: uuid.New().String(),
DisplayName: displayname,
PreferredName: userid,
OnPremisesSamAccountName: userid,
PasswordProfile: &accountsmsg.PasswordProfile{
Password: password,
},
Mail: email,
AccountEnabled: true,
}
if uidNumber != 0 {
newAccount.UidNumber = uidNumber
}
if gidNumber != 0 {
newAccount.GidNumber = gidNumber
}
var account *accountsmsg.Account
switch o.config.AccountBackend {
case "accounts":
account, err = o.getAccountService().CreateAccount(r.Context(), &accountssvc.CreateAccountRequest{
Account: newAccount,
})
case "cs3":
o.logger.Fatal().Msg("cs3 backend doesn't support adding users")
default:
o.logger.Fatal().Msgf("Invalid accounts backend type '%s'", o.config.AccountBackend)
}
if err != nil {
merr := merrors.FromError(err)
switch merr.Code {
case http.StatusBadRequest:
o.mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, merr.Detail))
case http.StatusConflict:
if response.APIVersion(r.Context()) == "2" {
// it seems the application framework sets the ocs status code to the httpstatus code, which affects the provisioning api
// see https://github.com/owncloud/core/blob/b9ff4c93e051c94adfb301545098ae627e52ef76/lib/public/AppFramework/OCSController.php#L142-L150
o.mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, merr.Detail))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaInvalidInput.StatusCode, merr.Detail))
}
default:
o.mustRender(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"
}
o.mustRender(w, r, response.DataRender(&data.User{
UserID: account.OnPremisesSamAccountName,
DisplayName: account.DisplayName,
LegacyDisplayName: account.DisplayName,
Email: account.Mail,
UIDNumber: account.UidNumber,
GIDNumber: account.GidNumber,
Enabled: enabled,
}))
}
// EditUser creates a new user account
func (o Ocs) EditUser(w http.ResponseWriter, r *http.Request) {
userid := chi.URLParam(r, "userid")
userid, err := url.PathUnescape(userid)
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
var account *accountsmsg.Account
switch o.config.AccountBackend {
case "accounts":
account, err = o.fetchAccountByUsername(r.Context(), userid)
case "cs3":
o.logger.Fatal().Msg("cs3 backend doesn't support editing users")
default:
o.logger.Fatal().Msgf("Invalid accounts backend type '%s'", o.config.AccountBackend)
}
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageUserNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("userid", userid).Msg("could not edit user")
return
}
req := accountssvc.UpdateAccountRequest{
Account: &accountsmsg.Account{
Id: account.Id,
},
}
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 = &accountsmsg.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
o.mustRender(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.StatusBadRequest:
o.mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, merr.Detail))
default:
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("account_id", req.Account.Id).Str("user_id", userid).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")
o.mustRender(w, r, response.DataRender(struct{}{}))
}
// DeleteUser deletes a user
func (o Ocs) DeleteUser(w http.ResponseWriter, r *http.Request) {
userid := chi.URLParam(r, "userid")
userid, err := url.PathUnescape(userid)
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
var account *accountsmsg.Account
switch o.config.AccountBackend {
case "accounts":
account, err = o.fetchAccountByUsername(r.Context(), userid)
case "cs3":
o.logger.Fatal().Msg("cs3 backend doesn't support deleting users")
default:
o.logger.Fatal().Msgf("Invalid accounts backend type '%s'", o.config.AccountBackend)
}
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageUserNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("userid", userid).Msg("could not delete user")
return
}
if o.config.Reva.Address != "" && o.config.StorageUsersDriver != "owncloud" {
t, err := o.mintTokenForUser(r.Context(), account)
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, errors.Wrap(err, "error minting token").Error()))
return
}
ctx := metadata.AppendToOutgoingContext(r.Context(), revactx.TokenHeader, t)
gwc, err := pool.GetGatewayServiceClient(o.config.Reva.Address)
if err != nil {
o.logger.Error().Err(err).Msg("error securing a connection to Reva gateway")
}
lsRes, err := gwc.ListStorageSpaces(ctx, &provider.ListStorageSpacesRequest{
Filters: []*provider.ListStorageSpacesRequest_Filter{
{
Type: provider.ListStorageSpacesRequest_Filter_TYPE_OWNER,
Term: &provider.ListStorageSpacesRequest_Filter_Owner{
Owner: &revauser.UserId{
Idp: o.config.IdentityManagement.Address,
OpaqueId: account.Id,
},
},
},
},
})
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, errors.Wrap(err, "could not list owned personal spaces").Error()))
return
}
if lsRes.Status.Code != rpcv1beta1.Code_CODE_OK {
o.logger.Error().
Interface("status", lsRes.Status).
Msg("DeleteUser: could not list personal spaces")
return
}
for _, space := range lsRes.StorageSpaces {
dsRes, err := gwc.DeleteStorageSpace(ctx, &provider.DeleteStorageSpaceRequest{
Id: space.Id,
})
if err != nil {
o.logger.Error().Err(err).Msg("DeleteUser: could not make delete space request")
continue
}
if dsRes.Status.Code != rpcv1beta1.Code_CODE_OK && dsRes.Status.Code != rpcv1beta1.Code_CODE_NOT_FOUND {
o.logger.Error().
Interface("status", dsRes.Status).
Msg("DeleteUser: could not delete space")
continue
}
}
lsRes, err = gwc.ListStorageSpaces(ctx, &provider.ListStorageSpacesRequest{
Filters: []*provider.ListStorageSpacesRequest_Filter{
{
Type: provider.ListStorageSpacesRequest_Filter_TYPE_OWNER,
Term: &provider.ListStorageSpacesRequest_Filter_Owner{
Owner: &revauser.UserId{
Idp: o.config.IdentityManagement.Address,
OpaqueId: account.Id,
},
},
},
},
})
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, errors.Wrap(err, "could not list owned personal spaces").Error()))
return
}
if lsRes.Status.Code != rpcv1beta1.Code_CODE_OK {
o.logger.Error().
Interface("status", lsRes.Status).
Msg("DeleteUser: could not list personal spaces")
return
}
for _, space := range lsRes.StorageSpaces {
dsRes, err := gwc.DeleteStorageSpace(ctx, &provider.DeleteStorageSpaceRequest{
Opaque: &typesv1beta1.Opaque{
Map: map[string]*typesv1beta1.OpaqueEntry{
"purge": {},
},
},
Id: space.Id,
})
if err != nil {
o.logger.Error().Err(err).Msg("DeleteUser: could not make delete space request")
continue
}
if dsRes.Status.Code != rpcv1beta1.Code_CODE_OK && dsRes.Status.Code != rpcv1beta1.Code_CODE_NOT_FOUND {
o.logger.Error().
Interface("status", dsRes.Status).
Msg("DeleteUser: could not delete space")
continue
}
}
}
req := accountssvc.DeleteAccountRequest{
Id: account.Id,
}
_, err = o.getAccountService().DeleteAccount(r.Context(), &req)
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageUserNotFound))
} else {
o.mustRender(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")
o.mustRender(w, r, response.DataRender(struct{}{}))
}
// TODO(refs) this to ocis-pkg ... we are minting tokens all over the place ... or use a service? ... like reva?
func (o Ocs) mintTokenForUser(ctx context.Context, account *accountsmsg.Account) (string, error) {
tm, _ := jwt.New(map[string]interface{}{
"secret": o.config.TokenManager.JWTSecret,
"expires": int64(24 * 60 * 60),
})
u := &revauser.User{
Id: &revauser.UserId{
OpaqueId: account.Id,
Idp: o.config.IdentityManagement.Address,
},
Groups: []string{},
UidNumber: account.UidNumber,
GidNumber: account.GidNumber,
}
s, err := scope.AddOwnerScope(nil)
if err != nil {
return "", err
}
return tm.MintToken(ctx, u, s)
}
// EnableUser enables a user
func (o Ocs) EnableUser(w http.ResponseWriter, r *http.Request) {
userid := chi.URLParam(r, "userid")
userid, err := url.PathUnescape(userid)
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
var account *accountsmsg.Account
switch o.config.AccountBackend {
case "accounts":
account, err = o.fetchAccountByUsername(r.Context(), userid)
case "cs3":
o.logger.Fatal().Msg("cs3 backend doesn't support enabling users")
default:
o.logger.Fatal().Msgf("Invalid accounts backend type '%s'", o.config.AccountBackend)
}
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageUserNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("userid", userid).Msg("could not enable user")
return
}
account.AccountEnabled = true
req := accountssvc.UpdateAccountRequest{
Account: account,
UpdateMask: &field_mask.FieldMask{
Paths: []string{"AccountEnabled"},
},
}
_, err = o.getAccountService().UpdateAccount(r.Context(), &req)
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, "The requested account could not be found"))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("account_id", account.Id).Msg("could not enable account")
return
}
o.logger.Debug().Str("account_id", account.Id).Msg("enabled user")
o.mustRender(w, r, response.DataRender(struct{}{}))
}
// DisableUser disables a user
func (o Ocs) DisableUser(w http.ResponseWriter, r *http.Request) {
userid := chi.URLParam(r, "userid")
userid, err := url.PathUnescape(userid)
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
var account *accountsmsg.Account
switch o.config.AccountBackend {
case "accounts":
account, err = o.fetchAccountByUsername(r.Context(), userid)
case "cs3":
o.logger.Fatal().Msg("cs3 backend doesn't support disabling users")
default:
o.logger.Fatal().Msgf("Invalid accounts backend type '%s'", o.config.AccountBackend)
}
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageUserNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("userid", userid).Msg("could not disable user")
return
}
account.AccountEnabled = false
req := accountssvc.UpdateAccountRequest{
Account: account,
UpdateMask: &field_mask.FieldMask{
Paths: []string{"AccountEnabled"},
},
}
_, err = o.getAccountService().UpdateAccount(r.Context(), &req)
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, "The requested account could not be found"))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("account_id", account.Id).Msg("could not disable account")
return
}
o.logger.Debug().Str("account_id", account.Id).Msg("disabled user")
o.mustRender(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 := revactx.ContextGetUser(r.Context())
if !ok {
//o.logger.Error().Msg("missing user in context")
o.mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, "missing user in context"))
return
}
// use the user's UUID
userID := u.Id.OpaqueId
c := storesvc.NewStoreService("com.owncloud.api.store", grpc.NewClient())
res, err := c.Read(r.Context(), &storesvc.ReadRequest{
Options: &storemsg.ReadOptions{
Database: "proxy",
Table: "signing-keys",
},
Key: userID,
})
if err == nil && len(res.Records) > 0 {
o.mustRender(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 {
o.mustRender(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.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, "could not generate signing key"))
return
}
signingKey := hex.EncodeToString(key)
_, err = c.Write(r.Context(), &storesvc.WriteRequest{
Options: &storemsg.WriteOptions{
Database: "proxy",
Table: "signing-keys",
},
Record: &storemsg.Record{
Key: userID,
Value: []byte(signingKey),
// TODO Expiry?
},
})
if err != nil {
//o.logger.Error().Err(err).Msg("error writing key")
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, "could not persist signing key"))
return
}
o.mustRender(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("on_premises_sam_account_name eq '%s'", escapeValue(search))
}
var res *accountssvc.ListAccountsResponse
var err error
switch o.config.AccountBackend {
case "accounts":
res, err = o.getAccountService().ListAccounts(r.Context(), &accountssvc.ListAccountsRequest{
Query: query,
})
case "cs3":
// TODO
o.logger.Fatal().Msg("cs3 backend doesn't support listing users")
default:
o.logger.Fatal().Msgf("Invalid accounts backend type '%s'", o.config.AccountBackend)
}
if err != nil {
o.logger.Err(err).Msg("could not list users")
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, "could not list users"))
return
}
users := make([]string, 0, len(res.Accounts))
for i := range res.Accounts {
users = append(users, res.Accounts[i].OnPremisesSamAccountName)
}
o.mustRender(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, "'", "''")
}
func (o Ocs) fetchAccountByUsername(ctx context.Context, name string) (*accountsmsg.Account, error) {
var res *accountssvc.ListAccountsResponse
res, err := o.getAccountService().ListAccounts(ctx, &accountssvc.ListAccountsRequest{
Query: fmt.Sprintf("on_premises_sam_account_name eq '%v'", escapeValue(name)),
})
if err != nil {
return nil, err
}
if res != nil && len(res.Accounts) == 1 {
return res.Accounts[0], nil
}
return nil, merrors.NotFound("", data.MessageUserNotFound)
}
func (o Ocs) fetchAccountFromCS3Backend(ctx context.Context, name string) (*accountsmsg.Account, error) {
backend := o.getCS3Backend()
u, _, err := backend.GetUserByClaims(ctx, "username", name, false)
if err != nil {
return nil, err
}
return &accountsmsg.Account{
OnPremisesSamAccountName: u.Username,
DisplayName: u.DisplayName,
Mail: u.Mail,
UidNumber: u.UidNumber,
GidNumber: u.GidNumber,
}, nil
}
+23
View File
@@ -0,0 +1,23 @@
package tracing
import (
"github.com/owncloud/ocis/extensions/ocs/pkg/config"
pkgtrace "github.com/owncloud/ocis/ocis-pkg/tracing"
"go.opentelemetry.io/otel/trace"
)
var (
// TraceProvider is the global trace provider for the ocs service.
TraceProvider = trace.NewNoopTracerProvider()
)
func Configure(cfg *config.Config) error {
var err error
if cfg.Tracing.Enabled {
if TraceProvider, err = pkgtrace.GetTraceProvider(cfg.Tracing.Endpoint, cfg.Tracing.Collector, cfg.Service.Name, cfg.Tracing.Type); err != nil {
return err
}
}
return nil
}
+2
View File
@@ -0,0 +1,2 @@
# backend
-r '^(cmd|pkg)/.*\.go$' -R '^node_modules/' -s -- sh -c 'make bin/ocis-ocs-debug && bin/ocis-ocs-debug --log-level debug server --debug-pprof --debug-zpages'