mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-05 19:59:37 -06:00
feat: use short tokens as access tokens
The "real" access token will be stored using the short token as key. This short token will be sent to the clients to be used as access token for the WOPI server. This is configurable, and requires a store in order to keep the tokens.
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"net"
|
||||
|
||||
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/cs3org/reva/v2/pkg/store"
|
||||
"github.com/oklog/run"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/config/configlog"
|
||||
registry "github.com/owncloud/ocis/v2/ocis-pkg/registry"
|
||||
@@ -19,6 +20,7 @@ import (
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/server/grpc"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/server/http"
|
||||
"github.com/urfave/cli/v2"
|
||||
microstore "go-micro.dev/v4/store"
|
||||
)
|
||||
|
||||
// Server is the entrypoint for the server command.
|
||||
@@ -70,12 +72,22 @@ func Server(cfg *config.Config) *cli.Command {
|
||||
return err
|
||||
}
|
||||
|
||||
st := store.Create(
|
||||
store.Store(cfg.Store.Store),
|
||||
store.TTL(cfg.Store.TTL),
|
||||
microstore.Nodes(cfg.Store.Nodes...),
|
||||
microstore.Database(cfg.Store.Database),
|
||||
microstore.Table(cfg.Store.Table),
|
||||
store.Authentication(cfg.Store.AuthUsername, cfg.Store.AuthPassword),
|
||||
)
|
||||
|
||||
// start GRPC server
|
||||
grpcServer, teardown, err := grpc.Server(
|
||||
grpc.AppURLs(appUrls),
|
||||
grpc.Config(cfg),
|
||||
grpc.Logger(logger),
|
||||
grpc.TraceProvider(traceProvider),
|
||||
grpc.Store(st),
|
||||
)
|
||||
defer teardown()
|
||||
if err != nil {
|
||||
@@ -113,11 +125,12 @@ func Server(cfg *config.Config) *cli.Command {
|
||||
|
||||
// start HTTP server
|
||||
httpServer, err := http.Server(
|
||||
http.Adapter(connector.NewHttpAdapter(gatewaySelector, cfg)),
|
||||
http.Adapter(connector.NewHttpAdapter(gatewaySelector, cfg, st)),
|
||||
http.Logger(logger),
|
||||
http.Config(cfg),
|
||||
http.Context(ctx),
|
||||
http.TracerProvider(traceProvider),
|
||||
http.Store(st),
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Str("transport", "http").Msg("Failed to initialize server")
|
||||
|
||||
@@ -13,6 +13,7 @@ type Config struct {
|
||||
|
||||
Service Service `yaml:"-"`
|
||||
App App `yaml:"app"`
|
||||
Store Store `yaml:"store"`
|
||||
|
||||
TokenManager *TokenManager `yaml:"token_manager"`
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package defaults
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/shared"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/structs"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
|
||||
@@ -32,6 +34,13 @@ func DefaultConfig() *config.Config {
|
||||
Duration: "12h",
|
||||
},
|
||||
},
|
||||
Store: config.Store{
|
||||
Store: "nats-js-kv",
|
||||
Nodes: []string{"127.0.0.1:9233"},
|
||||
Database: "collaboration",
|
||||
Table: "",
|
||||
TTL: 30 * time.Minute,
|
||||
},
|
||||
GRPC: config.GRPC{
|
||||
Addr: "127.0.0.1:9301",
|
||||
Protocol: "tcp",
|
||||
|
||||
14
services/collaboration/pkg/config/store.go
Normal file
14
services/collaboration/pkg/config/store.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package config
|
||||
|
||||
import "time"
|
||||
|
||||
// Store configures the store to use
|
||||
type Store struct {
|
||||
Store string `yaml:"store" env:"OCIS_PERSISTENT_STORE;COLLABORATION_STORE" desc:"The type of the store. Supported values are: 'memory', 'nats-js-kv', 'redis-sentinel', 'noop'. See the text description for details." introductionVersion:"pre5.0"`
|
||||
Nodes []string `yaml:"nodes" env:"OCIS_PERSISTENT_STORE_NODES;COLLABORATION_STORE_NODES" desc:"A list of nodes to access the configured store. This has no effect when 'memory' store is configured. Note that the behaviour how nodes are used is dependent on the library of the configured store. See the Environment Variable Types description for more details." introductionVersion:"pre5.0"`
|
||||
Database string `yaml:"database" env:"COLLABORATION_STORE_DATABASE" desc:"The database name the configured store should use." introductionVersion:"pre5.0"`
|
||||
Table string `yaml:"table" env:"COLLABORATION_STORE_TABLE" desc:"The database table the store should use." introductionVersion:"pre5.0"`
|
||||
TTL time.Duration `yaml:"ttl" env:"OCIS_PERSISTENT_STORE_TTL;COLLABORATION_STORE_TTL" desc:"Time to live for events in the store. Defaults to '30m' (30 minutes). See the Environment Variable Types description for more details." introductionVersion:"pre5.0"`
|
||||
AuthUsername string `yaml:"username" env:"OCIS_PERSISTENT_STORE_AUTH_USERNAME;COLLABORATION_STORE_AUTH_USERNAME" desc:"The username to authenticate with the store. Only applies when store type 'nats-js-kv' is configured." introductionVersion:"5.0"`
|
||||
AuthPassword string `yaml:"password" env:"OCIS_PERSISTENT_STORE_AUTH_PASSWORD;COLLABORATION_STORE_AUTH_PASSWORD" desc:"The password to authenticate with the store. Only applies when store type 'nats-js-kv' is configured." introductionVersion:"5.0"`
|
||||
}
|
||||
@@ -7,4 +7,5 @@ type Wopi struct {
|
||||
DisableChat bool `yaml:"disable_chat" env:"COLLABORATION_WOPI_DISABLE_CHAT;OCIS_WOPI_DISABLE_CHAT" desc:"Disable chat in the office web frontend. This feature applies to OnlyOffice and Microsoft." introductionVersion:"7.0.0"`
|
||||
ProxyURL string `yaml:"proxy_url" env:"COLLABORATION_WOPI_PROXY_URL" desc:"The URL to the ownCloud Office365 WOPI proxy. Optional. To use this feature, you need an office365 proxy subscription. If you become part of the Microsoft CSP program (https://learn.microsoft.com/en-us/partner-center/enroll/csp-overview), you can use WebOffice without a proxy." introductionVersion:"7.0.0"`
|
||||
ProxySecret string `yaml:"proxy_secret" env:"COLLABORATION_WOPI_PROXY_SECRET" desc:"Optional, the secret to authenticate against the ownCloud Office365 WOPI proxy. This secret can be obtained from ownCloud via the office365 proxy subscription." introductionVersion:"7.0.0"`
|
||||
ShortTokens bool `yaml:"short_tokens" env:"COLLABORATION_WOPI_SHORTTOKENS" desc:"Use short access tokens for WOPI access. This is useful for Office Online, which has URL length restrictions." introductionVersion:"7.0.0"`
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/middleware"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/wopisrc"
|
||||
"github.com/rs/zerolog"
|
||||
microstore "go-micro.dev/v4/store"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -95,15 +96,17 @@ type FileConnectorService interface {
|
||||
// Currently, it handles file locks and getting the file info.
|
||||
// Note that operations might return any kind of error, not just ConnectorError
|
||||
type FileConnector struct {
|
||||
gws pool.Selectable[gatewayv1beta1.GatewayAPIClient]
|
||||
cfg *config.Config
|
||||
gws pool.Selectable[gatewayv1beta1.GatewayAPIClient]
|
||||
cfg *config.Config
|
||||
store microstore.Store
|
||||
}
|
||||
|
||||
// NewFileConnector creates a new file connector
|
||||
func NewFileConnector(gws pool.Selectable[gatewayv1beta1.GatewayAPIClient], cfg *config.Config) *FileConnector {
|
||||
func NewFileConnector(gws pool.Selectable[gatewayv1beta1.GatewayAPIClient], cfg *config.Config, st microstore.Store) *FileConnector {
|
||||
return &FileConnector{
|
||||
gws: gws,
|
||||
cfg: cfg,
|
||||
gws: gws,
|
||||
cfg: cfg,
|
||||
store: st,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1265,7 +1268,7 @@ func (f *FileConnector) createDownloadURL(wopiContext middleware.WopiContext, co
|
||||
templateContext.FileReference = wopiContext.TemplateReference
|
||||
templateContext.TemplateReference = nil
|
||||
|
||||
token, _, err := middleware.GenerateWopiToken(templateContext, f.cfg)
|
||||
token, _, err := middleware.GenerateWopiToken(templateContext, f.cfg, f.store)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -1389,7 +1392,7 @@ func (f *FileConnector) generatePrefix() string {
|
||||
// will be ignored
|
||||
func (f *FileConnector) generateWOPISrc(wopiContext middleware.WopiContext, logger zerolog.Logger) (*url.URL, error) {
|
||||
// get the WOPI token for the new file
|
||||
accessToken, _, err := middleware.GenerateWopiToken(wopiContext, f.cfg)
|
||||
accessToken, _, err := middleware.GenerateWopiToken(wopiContext, f.cfg, f.store)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("generateWOPISrc: failed to generate access token for the new file")
|
||||
return nil, err
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/connector/utf7"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/locks"
|
||||
"github.com/rs/zerolog"
|
||||
microstore "go-micro.dev/v4/store"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -44,10 +45,10 @@ type HttpAdapter struct {
|
||||
|
||||
// NewHttpAdapter will create a new HTTP adapter. A new connector using the
|
||||
// provided gateway API client and configuration will be used in the adapter
|
||||
func NewHttpAdapter(gws pool.Selectable[gatewayv1beta1.GatewayAPIClient], cfg *config.Config) *HttpAdapter {
|
||||
func NewHttpAdapter(gws pool.Selectable[gatewayv1beta1.GatewayAPIClient], cfg *config.Config, st microstore.Store) *HttpAdapter {
|
||||
httpAdapter := &HttpAdapter{
|
||||
con: NewConnector(
|
||||
NewFileConnector(gws, cfg),
|
||||
NewFileConnector(gws, cfg, st),
|
||||
NewContentConnector(gws, cfg),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -2,10 +2,13 @@ package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
appproviderv1beta1 "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
|
||||
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
@@ -15,6 +18,7 @@ import (
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/helpers"
|
||||
"github.com/rs/zerolog"
|
||||
microstore "go-micro.dev/v4/store"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
@@ -44,7 +48,7 @@ type WopiContext struct {
|
||||
// * The created WopiContext for the request
|
||||
// * A contextual zerologger containing information about the request
|
||||
// and the WopiContext
|
||||
func WopiContextAuthMiddleware(cfg *config.Config, next http.Handler) http.Handler {
|
||||
func WopiContextAuthMiddleware(cfg *config.Config, st microstore.Store, next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
@@ -67,6 +71,23 @@ func WopiContextAuthMiddleware(cfg *config.Config, next http.Handler) http.Handl
|
||||
return
|
||||
}
|
||||
|
||||
if cfg.Wopi.ShortTokens {
|
||||
records, err := st.Read(accessToken)
|
||||
if err != nil {
|
||||
wopiLogger.Error().Err(err).Msg("cannot retrieve access token from store")
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if len(records) != 1 {
|
||||
wopiLogger.Error().Int("records", len(records)).Msg("no record found for the token")
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
accessToken = string(records[0].Value)
|
||||
}
|
||||
|
||||
claims := &Claims{}
|
||||
_, err := jwt.ParseWithClaims(accessToken, claims, func(token *jwt.Token) (interface{}, error) {
|
||||
|
||||
@@ -163,7 +184,11 @@ func WopiContextToCtx(ctx context.Context, wopiContext WopiContext) context.Cont
|
||||
// The access token inside the wopiContext is expected to be decrypted.
|
||||
// In order to generate the access token for WOPI, the reva token inside the
|
||||
// wopiContext will be encrypted
|
||||
func GenerateWopiToken(wopiContext WopiContext, cfg *config.Config) (string, int64, error) {
|
||||
func GenerateWopiToken(wopiContext WopiContext, cfg *config.Config, st microstore.Store) (string, int64, error) {
|
||||
if cfg.Wopi.ShortTokens && st == nil {
|
||||
return "", 0, errors.New("Cannot generate a short token without microstore")
|
||||
}
|
||||
|
||||
cryptedReqAccessToken, err := EncryptAES([]byte(cfg.Wopi.Secret), wopiContext.AccessToken)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
@@ -187,6 +212,20 @@ func GenerateWopiToken(wopiContext WopiContext, cfg *config.Config) (string, int
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
accessToken, err := token.SignedString([]byte(cfg.Wopi.Secret))
|
||||
|
||||
if cfg.Wopi.ShortTokens {
|
||||
c := sha256.New()
|
||||
c.Write([]byte(accessToken))
|
||||
shortAccessToken := hex.EncodeToString(c.Sum(nil))
|
||||
|
||||
errWrite := st.Write(µstore.Record{
|
||||
Key: shortAccessToken,
|
||||
Value: []byte(accessToken),
|
||||
Expiry: claims.ExpiresAt.Sub(time.Now()),
|
||||
})
|
||||
|
||||
return shortAccessToken, claims.ExpiresAt.UnixMilli(), errWrite
|
||||
}
|
||||
|
||||
return accessToken, claims.ExpiresAt.UnixMilli(), err
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
|
||||
microstore "go-micro.dev/v4/store"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
@@ -19,6 +20,7 @@ type Options struct {
|
||||
Context context.Context
|
||||
Config *config.Config
|
||||
TraceProvider trace.TracerProvider
|
||||
Store microstore.Store
|
||||
}
|
||||
|
||||
// newOptions initializes the available default options.
|
||||
@@ -73,3 +75,10 @@ func TraceProvider(val trace.TracerProvider) Option {
|
||||
o.TraceProvider = val
|
||||
}
|
||||
}
|
||||
|
||||
// Store provides a funtion to set the Store option
|
||||
func Store(val microstore.Store) Option {
|
||||
return func(o *Options) {
|
||||
o.Store = val
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ func Server(opts ...Option) (*grpc.Server, func(), error) {
|
||||
svc.Config(options.Config),
|
||||
svc.Logger(options.Logger),
|
||||
svc.AppURLs(options.AppURLs),
|
||||
svc.Store(options.Store),
|
||||
)
|
||||
if err != nil {
|
||||
options.Logger.Error().
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/connector"
|
||||
microstore "go-micro.dev/v4/store"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
@@ -19,6 +20,7 @@ type Options struct {
|
||||
Context context.Context
|
||||
Config *config.Config
|
||||
TracerProvider trace.TracerProvider
|
||||
Store microstore.Store
|
||||
}
|
||||
|
||||
// newOptions initializes the available default options.
|
||||
@@ -66,3 +68,10 @@ func TracerProvider(val trace.TracerProvider) Option {
|
||||
o.TracerProvider = val
|
||||
}
|
||||
}
|
||||
|
||||
// Store provides a funtion to set the Store option
|
||||
func Store(val microstore.Store) Option {
|
||||
return func(o *Options) {
|
||||
o.Store = val
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ func prepareRoutes(r *chi.Mux, options Options) {
|
||||
r.Use(
|
||||
func(h stdhttp.Handler) stdhttp.Handler {
|
||||
// authentication and wopi context
|
||||
return colabmiddleware.WopiContextAuthMiddleware(options.Config, h)
|
||||
return colabmiddleware.WopiContextAuthMiddleware(options.Config, options.Store, h)
|
||||
},
|
||||
colabmiddleware.CollaborationTracingMiddleware,
|
||||
)
|
||||
@@ -186,7 +186,7 @@ func prepareRoutes(r *chi.Mux, options Options) {
|
||||
r.Use(
|
||||
func(h stdhttp.Handler) stdhttp.Handler {
|
||||
// authentication and wopi context
|
||||
return colabmiddleware.WopiContextAuthMiddleware(options.Config, h)
|
||||
return colabmiddleware.WopiContextAuthMiddleware(options.Config, options.Store, h)
|
||||
},
|
||||
colabmiddleware.CollaborationTracingMiddleware,
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
|
||||
microstore "go-micro.dev/v4/store"
|
||||
)
|
||||
|
||||
// Option defines a single option function.
|
||||
@@ -15,6 +16,7 @@ type Options struct {
|
||||
Config *config.Config
|
||||
AppURLs map[string]map[string]string
|
||||
Gwc gatewayv1beta1.GatewayAPIClient
|
||||
Store microstore.Store
|
||||
}
|
||||
|
||||
// newOptions initializes the available default options.
|
||||
@@ -55,3 +57,10 @@ func GatewayAPIClient(val gatewayv1beta1.GatewayAPIClient) Option {
|
||||
o.Gwc = val
|
||||
}
|
||||
}
|
||||
|
||||
// Store proivdes a function to set the store
|
||||
func Store(val microstore.Store) Option {
|
||||
return func(o *Options) {
|
||||
o.Store = val
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/helpers"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/middleware"
|
||||
microstore "go-micro.dev/v4/store"
|
||||
)
|
||||
|
||||
// NewHandler creates a new grpc service implementing the OpenInApp interface
|
||||
@@ -47,6 +48,7 @@ func NewHandler(opts ...Option) (*Service, func(), error) {
|
||||
logger: options.Logger,
|
||||
config: options.Config,
|
||||
gwc: gwc,
|
||||
store: options.Store,
|
||||
}, teardown, nil
|
||||
}
|
||||
|
||||
@@ -57,6 +59,7 @@ type Service struct {
|
||||
logger log.Logger
|
||||
config *config.Config
|
||||
gwc gatewayv1beta1.GatewayAPIClient
|
||||
store microstore.Store
|
||||
}
|
||||
|
||||
// OpenInApp will implement the OpenInApp interface of the app provider
|
||||
@@ -138,7 +141,7 @@ func (s *Service) OpenInApp(
|
||||
}
|
||||
}
|
||||
|
||||
accessToken, accessExpiration, err := middleware.GenerateWopiToken(wopiContext, s.config)
|
||||
accessToken, accessExpiration, err := middleware.GenerateWopiToken(wopiContext, s.config, s.store)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("OpenInApp: error generating the token")
|
||||
return &appproviderv1beta1.OpenInAppResponse{
|
||||
|
||||
Reference in New Issue
Block a user