Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
SHELL := bash
NAME := frontend
ifneq (, $(shell command -v go 2> /dev/null)) # suppress `command not found warnings` for non go targets in CI
include ../../.bingo/Variables.mk
endif
include ../../.make/default.mk
include ../../.make/go.mk
include ../../.make/release.mk
include ../../.make/docs.mk
+142
View File
@@ -0,0 +1,142 @@
# Frontend
The frontend service translates various КуСфера related HTTP APIs to CS3 requests.
## Endpoints Overview
Currently, the frontend service handles requests for three functionalities, which are `appprovider`, `archiver`, `datagateway` and `ocs`.
### appprovider
The appprovider endpoint, by default `/app`, forwards HTTP requests to the CS3 [App Registry API](https://cs3org.github.io/cs3apis/#cs3.app.registry.v1beta1.RegistryAPI)
### archiver
The archiver endpoint, by default `/archiver`, implements zip and tar download for collections of files. It will internally use the CS3 API to initiate downloads and then stream the individual files as part of a compressed file.
### datagateway
The datagateway endpoint, by default `/data`, forwards file up- and download requests to the correct CS3 data provider. КуСфера starts a dataprovider as part of the storage-* services. The routing happens based on the JWT that was created by a storage provider in response to an `InitiateFileDownload` or `InitiateFileUpload` request.
### ocs
The ocs endpoint, by default `/ocs`, implements the Open Collaboration Services API by translating it into CS3 API requests. It can handle users, groups, capabilities and also implements the files sharing functionality on top of CS3. The `/ocs/v[12].php/cloud/user/signing-key` is currently handled by the dedicated [ocs](https://github.com/qsfera/server/tree/main/services/ocs) service.
#### Event Handler
The `frontend` service contains an eventhandler for handling `ocs` related events. As of now, it only listens to the `ShareCreated` event.
### Sharing
Aggregating share information is one of the most time consuming operations in КуСфера. The service fetches a list of either received or created shares and has to stat every resource individually. While stats are fast, the default behavior scales linearly with the number of shares.
To save network trips the sharing implementation can cache the stat requests with an in memory cache or in Redis. It will shorten the response time by the network round-trip overhead at the cost of the API only eventually being updated.
Setting `FRONTEND_OCS_RESOURCE_INFO_CACHE_TTL=60` (deprecated) would cache the stat info for 60 seconds. Increasing this value makes sense for large deployments with thousands of active users that keep the cache up to date. Low frequency usage scenarios should not expect a noticeable improvement.
## Scalability
While the frontend service does not persist any data, it does cache information about files and filesystem (`Stat()`) responses and user information. Therefore, multiple instances of this service can be spawned in a bigger deployment like when using container orchestration with Kubernetes, when configuring `FRONTEND_OCS_RESOURCE_INFO_CACHE_STORE` (deprecated) and the related config options.
## Define Read-Only Attributes
A lot of user management is made via the standardized libregraph API. Depending on how the system is configured, there might be some user attributes that an КуСфера instance admin can't change because of properties coming from an external LDAP server, or similar. This can be the case when the КуСфера admin is not the LDAP admin. To ease life for admins, there are hints as capabilites telling the frontend which attributes are read-only to enable a different optical representation like being grayed out. To configure these hints, use the environment variable `FRONTEND_READONLY_USER_ATTRIBUTES`, which takes a comma separated list of attributes, see the envvar for supported values.
You can find more details regarding available attributes at the [libre-graph-api openapi-spec](https://github.com/qsfera-eu/libre-graph-api/blob/main/api/openapi-spec/v1.0.yaml) and on [docs.qsfera.eu](https://docs.qsfera.eu/swagger/libre-graph-api/).
## Caching
The `frontend` service can use a configured store via `FRONTEND_OCS_STAT_CACHE_STORE` (deprecated). Possible stores are:
- `memory`: Basic in-memory store and the default.
- `redis-sentinel`: Stores data in a configured Redis Sentinel cluster.
- `nats-js-kv`: Stores data using key-value-store feature of [nats jetstream](https://docs.nats.io/nats-concepts/jetstream/key-value-store)
- `noop`: Stores nothing. Useful for testing. Not recommended in production environments.
Other store types may work but are not supported currently.
Note: The service can only be scaled if not using `memory` store and the stores are configured identically over all instances!
Note that if you have used one of the deprecated stores, you should reconfigure to one of the supported ones as the deprecated stores will be removed in a later version.
Store specific notes:
- When using `redis-sentinel`, the Redis master to use is configured via e.g. `OC_CACHE_STORE_NODES` in the form of `<sentinel-host>:<sentinel-port>/<redis-master>` like `10.10.0.200:26379/mymaster`.
- When using `nats-js-kv` it is recommended to set `OC_CACHE_STORE_NODES` to the same value as `OC_EVENTS_ENDPOINT`. That way the cache uses the same nats instance as the event bus.
- When using the `nats-js-kv` store, it is possible to set `OC_CACHE_DISABLE_PERSISTENCE` to instruct nats to not persist cache data on disc.
### Auto-Accept Shares
When setting the `FRONTEND_AUTO_ACCEPT_SHARES` to `true`, all incoming shares will be accepted automatically. Users can overwrite this setting individually in their profile.
## Passwords
### The Password Policy
Note that the password policy currently impacts only **public link password validation**.
In КуСфера, the password policy is always enabled because the max-length restriction is always applying and should be taken into account by the clients.
With the password policy, mandatory criteria for the password can be defined via the environment variables listed below.
Generally, a password can contain any UTF-8 characters, however some characters are regarded as special since they are not used in ordinary texts. Which characters should be treated as special is defined by "The OWASP® Foundation" [password-special-characters](https://owasp.org/www-community/password-special-characters) (between double quotes): ```" !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~"```
The validation against the banned passwords list can be configured via a text file with words separated by new lines. If a user tries to set a password listed in the banned passwords list, the password can not be used (is invalid) even if the other mandatory criteria are passed. The admin can define the path of the banned passwords list file. If the file doesn't exist in a location, КуСфера tries to load a file from the `OC_CONFIG_DIR/OC_PASSWORD_POLICY_BANNED_PASSWORDS_LIST`. An option will be enabled when the file has been loaded successfully.
Following environment variables can be set to define the password policy behaviour:
- `OC_PASSWORD_POLICY_DISABLED`
Disable the password policy
- `OC_PASSWORD_POLICY_MIN_CHARACTERS`
Define the minimum password length.
- `OC_PASSWORD_POLICY_MIN_LOWERCASE_CHARACTERS`
Define the minimum number of uppercase letters.
- `OC_PASSWORD_POLICY_MIN_UPPERCASE_CHARACTERS`
Define the minimum number of lowercase letters.
- `OC_PASSWORD_POLICY_MIN_DIGITS`
Define the minimum number of digits.
- `OC_PASSWORD_POLICY_MIN_SPECIAL_CHARACTERS`
Define the minimum number of special characters.
- `OC_PASSWORD_POLICY_BANNED_PASSWORDS_LIST`
Path to the 'banned passwords list' file.
These variables are global КуСфера variables because they are used not only in the frontend service, but also in the sharing service.
Note that a password can have a maximum length of **72 bytes**. Depending on the alphabet used, a character is encoded by 1 to 4 bytes, defining the maximum length of a password indirectly. While US-ASCII will only need one byte, Latin alphabets and also Greek or Cyrillic ones need two bytes. Three bytes are needed for characters in Chinese, Japanese and Korean etc.
### The Password Policy Capability
The capabilities endpoint (e.g. https://cloud.qsfera.test/ocs/v1.php/cloud/capabilities?format=json) gives you following capabilities which are relevant for the password policy:
```json
{
"ocs": {
"data": {
"capabilities": {
"password_policy": {
"min_characters": 10,
"max_characters": 72,
"min_lowercase_characters": 1,
"min_uppercase_characters": 2,
"min_digits": 1,
"min_special_characters": 1
}
}
}
}
}
```
### Password Enforcement for all Public Links
For public accessible shares, independent if read only or writable, a password is enforced. To change this requirement, set the following environment variable to `false`:
`OC_SHARING_PUBLIC_SHARE_MUST_HAVE_PASSWORD`
### Password Enforcement for Writeable Public Links
For public accessible writable shares, a password can be enforced. To change the current setting, set the following environment variable to `true`:
`OC_SHARING_PUBLIC_WRITEABLE_SHARE_MUST_HAVE_PASSWORD`
Note that changing this environment variable only makes sense if\
`OC_SHARING_PUBLIC_SHARE_MUST_HAVE_PASSWORD`\
is set to `false`.
@@ -0,0 +1,232 @@
package command
import (
"context"
"errors"
"sync"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
group "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/events"
"github.com/opencloud-eu/reva/v2/pkg/events/stream"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/rs/zerolog"
"go-micro.dev/v4/metadata"
"google.golang.org/protobuf/types/known/fieldmaskpb"
"github.com/qsfera/server/pkg/generators"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/pkg/middleware"
"github.com/qsfera/server/pkg/registry"
"github.com/qsfera/server/pkg/service/grpc"
"github.com/qsfera/server/pkg/tracing"
settingssvc "github.com/qsfera/server/protogen/gen/qsfera/services/settings/v0"
"github.com/qsfera/server/services/frontend/pkg/config"
"github.com/qsfera/server/services/settings/pkg/store/defaults"
)
var _registeredEvents = []events.Unmarshaller{
events.ShareCreated{},
}
// ListenForEvents listens for events and acts accordingly
func ListenForEvents(ctx context.Context, cfg *config.Config, l log.Logger) error {
connName := generators.GenerateConnectionName(cfg.Service.Name, generators.NTypeBus)
bus, err := stream.NatsFromConfig(connName, false, stream.NatsConfig(cfg.Events))
if err != nil {
l.Error().Err(err).Msg("cannot connect to nats")
return err
}
evChannel, err := events.Consume(bus, "frontend", _registeredEvents...)
if err != nil {
l.Error().Err(err).Msg("cannot consume from nats")
return err
}
tm, err := pool.StringToTLSMode(cfg.GRPCClientTLS.Mode)
if err != nil {
return err
}
traceProvider, err := tracing.GetTraceProvider(ctx, cfg.Commons.TracesExporter, cfg.Service.Name)
if err != nil {
l.Error().Err(err).Msg("cannot initialize tracing")
return err
}
gatewaySelector, err := pool.GatewaySelector(
cfg.Reva.Address,
pool.WithTLSCACert(cfg.GRPCClientTLS.CACert),
pool.WithTLSMode(tm),
pool.WithRegistry(registry.GetRegistry()),
pool.WithTracerProvider(traceProvider),
)
if err != nil {
l.Error().Err(err).Msg("cannot get gateway selector")
return err
}
grpcClient, err := grpc.NewClient(
append(
grpc.GetClientOptions(cfg.GRPCClientTLS),
grpc.WithTraceProvider(traceProvider),
)...,
)
if err != nil {
l.Error().Err(err).Msg("cannot create grpc client")
return err
}
valueService := settingssvc.NewValueService("qsfera.api.settings", grpcClient)
wg := sync.WaitGroup{}
for i := 0; i < cfg.MaxConcurrency; i++ {
wg.Add(1)
go func(ch <-chan events.Event) {
defer wg.Done()
for {
select {
case e := <-ch:
switch ev := e.Event.(type) {
default:
l.Error().Interface("event", e).Msg("unhandled event")
case events.ShareCreated:
AutoAcceptShares(ev, cfg.AutoAcceptShares, l, gatewaySelector, valueService, cfg.ServiceAccount, cfg.MaxConcurrency)
}
case <-ctx.Done():
l.Info().Msg("context cancelled")
return
}
}
}(evChannel)
}
// Wait for all goroutines to finish
wg.Wait()
return nil
}
// AutoAcceptShares automatically accepts shares if configured by the admin or user
func AutoAcceptShares(ev events.ShareCreated, autoAcceptDefault bool, l log.Logger, gatewaySelector pool.Selectable[gateway.GatewayAPIClient], vs settingssvc.ValueService, cfg config.ServiceAccount, maxConcurrency int) {
gwc, err := gatewaySelector.Next()
if err != nil {
l.Error().Err(err).Msg("cannot get gateway client")
return
}
ctx, err := utils.GetServiceUserContextWithContext(context.Background(), gwc, cfg.ServiceAccountID, cfg.ServiceAccountSecret)
if err != nil {
l.Error().Err(err).Msg("cannot impersonate user")
return
}
userIDs, err := getUserIDs(ctx, gatewaySelector, ev.GranteeUserID, ev.GranteeGroupID)
if err != nil {
l.Error().Err(err).Msg("cannot get grantees")
return
}
work := make(chan *user.UserId, len(userIDs))
// Distribute work
go func() {
defer close(work)
for _, userID := range userIDs {
select {
case work <- userID:
case <-ctx.Done():
return
}
}
}()
wg := sync.WaitGroup{}
for i := 0; i < maxConcurrency; i++ {
wg.Go(func() {
for userID := range work {
if !autoAcceptShares(ctx, userID, autoAcceptDefault, vs) {
continue
}
gwc, err := gatewaySelector.Next()
if err != nil {
l.Error().Err(err).Msg("cannot get gateway client")
continue
}
resp, err := gwc.UpdateReceivedShare(ctx, updateShareRequest(ev.ShareID, userID))
if err != nil {
l.Error().Err(err).Msg("error sending grpc request")
continue
}
if code := resp.GetStatus().GetCode(); code != rpc.Code_CODE_OK {
// log unexpected status codes if a share cannot be accepted...
func() *zerolog.Event {
switch code {
// ... not found is not an error in the context of auto-accepting shares
case rpc.Code_CODE_NOT_FOUND:
return l.Debug()
default:
return l.Error()
}
}().Interface("status", resp.GetStatus()).Str("userid", userID.GetOpaqueId()).Msg("unexpected status code while accepting share")
}
}
})
}
// Wait for all goroutines to finish
wg.Wait()
}
func getUserIDs(ctx context.Context, gatewaySelector pool.Selectable[gateway.GatewayAPIClient], uid *user.UserId, gid *group.GroupId) ([]*user.UserId, error) {
if uid != nil {
return []*user.UserId{uid}, nil
}
gwc, err := gatewaySelector.Next()
if err != nil {
return nil, err
}
res, err := gwc.GetGroup(ctx, &group.GetGroupRequest{GroupId: gid})
if err != nil {
return nil, err
}
if res.GetStatus().GetCode() != rpc.Code_CODE_OK {
return nil, errors.New("could not get group")
}
return res.GetGroup().GetMembers(), nil
}
func autoAcceptShares(ctx context.Context, uid *user.UserId, defaultValue bool, vs settingssvc.ValueService) bool {
granteeCtx := metadata.Set(ctx, middleware.AccountID, uid.GetOpaqueId())
if resp, err := vs.GetValueByUniqueIdentifiers(granteeCtx,
&settingssvc.GetValueByUniqueIdentifiersRequest{
AccountUuid: uid.GetOpaqueId(),
SettingId: defaults.SettingUUIDProfileAutoAcceptShares,
},
); err == nil {
return resp.GetValue().GetValue().GetBoolValue()
}
return defaultValue
}
func updateShareRequest(shareID *collaboration.ShareId, uid *user.UserId) *collaboration.UpdateReceivedShareRequest {
return &collaboration.UpdateReceivedShareRequest{
Opaque: utils.AppendJSONToOpaque(nil, "userid", uid),
Share: &collaboration.ReceivedShare{
Share: &collaboration.Share{
Id: shareID,
},
State: collaboration.ShareState_SHARE_STATE_ACCEPTED,
},
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"state"}},
}
}
@@ -0,0 +1,59 @@
package command
import (
"errors"
"fmt"
"net/http"
"github.com/qsfera/server/pkg/config/configlog"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/services/frontend/pkg/config"
"github.com/qsfera/server/services/frontend/pkg/config/parser"
"github.com/spf13/cobra"
)
// Health is the entrypoint for the health command.
func Health(cfg *config.Config) *cobra.Command {
return &cobra.Command{
Use: "health",
Short: "check health status",
PreRunE: func(cmd *cobra.Command, args []string) error {
return configlog.ReturnError(parser.ParseConfig(cfg))
},
RunE: func(cmd *cobra.Command, args []string) error {
logger := log.Configure(cfg.Service.Name, cfg.Commons, cfg.LogLevel)
resp, err := http.Get(
fmt.Sprintf(
"http://%s/healthz",
cfg.Debug.Addr,
),
)
if resp.StatusCode != http.StatusOK {
err = errors.New("foo")
}
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
},
}
}
@@ -0,0 +1,36 @@
package command
import (
"os"
"github.com/qsfera/server/pkg/clihelper"
"github.com/qsfera/server/services/frontend/pkg/config"
"github.com/spf13/cobra"
)
// GetCommands provides all commands for this service
func GetCommands(cfg *config.Config) []*cobra.Command {
return []*cobra.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 qsfera-frontend command.
func Execute(cfg *config.Config) error {
app := clihelper.DefaultApp(&cobra.Command{
Use: "frontend",
Short: "Provide various HTTP apis for КуСфера",
})
app.AddCommand(GetCommands(cfg)...)
app.SetArgs(os.Args[1:])
return app.ExecuteContext(cfg.Context)
}
@@ -0,0 +1,109 @@
package command
import (
"context"
"fmt"
"os/signal"
"github.com/qsfera/server/pkg/config/configlog"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/pkg/registry"
"github.com/qsfera/server/pkg/runner"
"github.com/qsfera/server/pkg/tracing"
"github.com/qsfera/server/pkg/version"
"github.com/qsfera/server/services/frontend/pkg/config"
"github.com/qsfera/server/services/frontend/pkg/config/parser"
"github.com/qsfera/server/services/frontend/pkg/revaconfig"
"github.com/qsfera/server/services/frontend/pkg/server/debug"
"github.com/opencloud-eu/reva/v2/cmd/revad/runtime"
"github.com/spf13/cobra"
)
// Server is the entry point for the server command.
func Server(cfg *config.Config) *cobra.Command {
return &cobra.Command{
Use: "server",
Short: fmt.Sprintf("start the %s service without runtime (unsupervised mode)", cfg.Service.Name),
PreRunE: func(cmd *cobra.Command, args []string) error {
return configlog.ReturnFatal(parser.ParseConfig(cfg))
},
RunE: func(cmd *cobra.Command, args []string) error {
logger := log.Configure(cfg.Service.Name, cfg.Commons, cfg.LogLevel)
traceProvider, err := tracing.GetTraceProvider(cmd.Context(), cfg.Commons.TracesExporter, cfg.Service.Name)
if err != nil {
return err
}
var cancel context.CancelFunc
if cfg.Context == nil {
cfg.Context, cancel = signal.NotifyContext(context.Background(), runner.StopSignals...)
defer cancel()
}
ctx := cfg.Context
gr := runner.NewGroup()
{
rCfg, err := revaconfig.FrontendConfigFromStruct(cfg, logger)
if err != nil {
return err
}
// run the appropriate reva servers based on the config
if rServer := runtime.NewDrivenHTTPServerWithOptions(rCfg,
runtime.WithLogger(&logger.Logger),
runtime.WithRegistry(registry.GetRegistry()),
runtime.WithTraceProvider(traceProvider),
); rServer != nil {
gr.Add(runner.NewRevaServiceRunner(cfg.Service.Name+".rhttp", rServer))
}
if rServer := runtime.NewDrivenGRPCServerWithOptions(rCfg,
runtime.WithLogger(&logger.Logger),
runtime.WithRegistry(registry.GetRegistry()),
runtime.WithTraceProvider(traceProvider),
); rServer != nil {
gr.Add(runner.NewRevaServiceRunner(cfg.Service.Name+".rgrpc", rServer))
}
}
{
debugServer, err := debug.Server(
debug.Logger(logger),
debug.Context(ctx),
debug.Config(cfg),
)
if err != nil {
logger.Info().Err(err).Str("server", "debug").Msg("Failed to initialize server")
return err
}
gr.Add(runner.NewGolangHttpServerRunner(cfg.Service.Name+".debug", debugServer))
}
httpSvc := registry.BuildHTTPService(cfg.HTTP.Namespace+"."+cfg.Service.Name, cfg.HTTP.Addr, version.GetString())
if err := registry.RegisterService(ctx, logger, httpSvc, cfg.Debug.Addr); err != nil {
logger.Fatal().Err(err).Msg("failed to register the http service")
}
// add event handler
gr.Add(runner.New(cfg.Service.Name+".event",
func() error {
return ListenForEvents(ctx, cfg, logger)
}, func() {
logger.Info().Msg("stopping event handler")
},
))
grResults := gr.Run(ctx)
// return the first non-nil error found in the results
for _, grResult := range grResults {
if grResult.RunnerError != nil {
return grResult.RunnerError
}
}
return nil
},
}
}
@@ -0,0 +1,49 @@
package command
import (
"fmt"
"os"
"github.com/qsfera/server/pkg/registry"
"github.com/qsfera/server/pkg/version"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/qsfera/server/services/frontend/pkg/config"
"github.com/spf13/cobra"
)
// Version prints the service versions of all running instances.
func Version(cfg *config.Config) *cobra.Command {
return &cobra.Command{
Use: "version",
Short: "print the version of this binary and the running service instances",
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Println("Version: " + version.GetString())
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 := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"Version", "Address", "Id"})
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})
}
}
table.Render()
return nil
},
}
}
@@ -0,0 +1,225 @@
package config
import (
"context"
"time"
"github.com/qsfera/server/pkg/shared"
)
type Config struct {
Commons *shared.Commons `yaml:"-"` // don't use this directly as configuration for a service
Service Service `yaml:"-"`
LogLevel string `yaml:"loglevel" env:"OC_LOG_LEVEL;FRONTEND_LOG_LEVEL" desc:"The log level. Valid values are: 'panic', 'fatal', 'error', 'warn', 'info', 'debug', 'trace'." introductionVersion:"1.0.0"`
Debug Debug `yaml:"debug"`
HTTP HTTPConfig `yaml:"http"`
// JWTSecret used to verify reva access token
TransferSecret string `yaml:"transfer_secret" env:"OC_TRANSFER_SECRET" desc:"Transfer secret for signing file up- and download requests." introductionVersion:"1.0.0"`
TokenManager *TokenManager `yaml:"token_manager"`
Reva *shared.Reva `yaml:"reva"`
MachineAuthAPIKey string `yaml:"machine_auth_api_key" env:"OC_MACHINE_AUTH_API_KEY;FRONTEND_MACHINE_AUTH_API_KEY" desc:"The machine auth API key used to validate internal requests necessary to access resources from other services." introductionVersion:"1.0.0"`
SkipUserGroupsInToken bool `yaml:"skip_user_groups_in_token" env:"FRONTEND_SKIP_USER_GROUPS_IN_TOKEN" desc:"Disables the loading of user's group memberships from the reva access token." introductionVersion:"1.0.0"`
MaxQuota uint64 `yaml:"max_quota" env:"OC_SPACES_MAX_QUOTA;FRONTEND_MAX_QUOTA" desc:"Set the global max quota value in bytes. A value of 0 equals unlimited. The value is provided via capabilities." introductionVersion:"1.0.0"`
UploadMaxChunkSize int `yaml:"upload_max_chunk_size" env:"FRONTEND_UPLOAD_MAX_CHUNK_SIZE" desc:"Sets the max chunk sizes in bytes for uploads via the clients." introductionVersion:"1.0.0"`
UploadHTTPMethodOverride string `yaml:"upload_http_method_override" env:"FRONTEND_UPLOAD_HTTP_METHOD_OVERRIDE" desc:"Advise TUS to replace PATCH requests by POST requests." introductionVersion:"1.0.0"`
DefaultUploadProtocol string `yaml:"default_upload_protocol" env:"FRONTEND_DEFAULT_UPLOAD_PROTOCOL" desc:"The default upload protocol to use in clients. Currently only 'tus' is available. See the developer API documentation for more details about TUS." introductionVersion:"1.0.0"`
EnableFederatedSharingIncoming bool `yaml:"enable_federated_sharing_incoming" env:"OC_ENABLE_OCM;FRONTEND_ENABLE_FEDERATED_SHARING_INCOMING" desc:"Changing this value is NOT supported. Enables support for incoming federated sharing for clients. The backend behaviour is not changed." introductionVersion:"1.0.0"`
EnableFederatedSharingOutgoing bool `yaml:"enable_federated_sharing_outgoing" env:"OC_ENABLE_OCM;FRONTEND_ENABLE_FEDERATED_SHARING_OUTGOING" desc:"Changing this value is NOT supported. Enables support for outgoing federated sharing for clients. The backend behaviour is not changed." introductionVersion:"1.0.0"`
SearchMinLength int `yaml:"search_min_length" env:"FRONTEND_SEARCH_MIN_LENGTH" desc:"Minimum number of characters to enter before a client should start a search for Share receivers. This setting can be used to customize the user experience if e.g too many results are displayed." introductionVersion:"1.0.0"`
DisableSSE bool `yaml:"disable_sse" env:"OC_DISABLE_SSE;FRONTEND_DISABLE_SSE" desc:"When set to true, clients are informed that the Server-Sent Events endpoint is not accessible." introductionVersion:"1.0.0"`
DisableRadicale bool `yaml:"disable_radicale" env:"FRONTEND_DISABLE_RADICALE" desc:"When set to true, clients are informed that the Radicale (CalDAV/CardDAV) is not accessible." introductionVersion:"4.0.0"`
DefaultLinkPermissions int `yaml:"default_link_permissions" env:"FRONTEND_DEFAULT_LINK_PERMISSIONS" desc:"Defines the default permissions a link is being created with. Possible values are 0 (= internal link, for instance members only) and 1 (= public link with viewer permissions). Defaults to 1." introductionVersion:"1.0.0"`
PublicURL string `yaml:"public_url" env:"OC_URL;FRONTEND_PUBLIC_URL" desc:"The public facing URL of the КуСфера frontend." introductionVersion:"1.0.0"`
MaxConcurrency int `yaml:"max_concurrency" env:"OC_MAX_CONCURRENCY;FRONTEND_MAX_CONCURRENCY" desc:"Maximum number of concurrent go-routines. Higher values can potentially get work done faster but will also cause more load on the system. Values of 0 or below will be ignored and the default value will be used." introductionVersion:"1.0.0"`
AppHandler AppHandler `yaml:"app_handler"`
Archiver Archiver `yaml:"archiver"`
DataGateway DataGateway `yaml:"data_gateway"`
OCS OCS `yaml:"ocs"`
OCDav OCDav `yaml:"ocdav"`
Checksums Checksums `yaml:"checksums"`
ReadOnlyUserAttributes []string `yaml:"read_only_user_attributes" env:"FRONTEND_READONLY_USER_ATTRIBUTES" desc:"A list of user attributes to indicate as read-only. Supported values: 'user.onPremisesSamAccountName' (username), 'user.displayName', 'user.mail', 'user.passwordProfile' (password), 'user.appRoleAssignments' (role), 'user.memberOf' (groups), 'user.accountEnabled' (login allowed), 'drive.quota' (quota). See the Environment Variable Types description for more details." introductionVersion:"1.0.0"`
LDAPServerWriteEnabled bool `yaml:"ldap_server_write_enabled" env:"OC_LDAP_SERVER_WRITE_ENABLED;FRONTEND_LDAP_SERVER_WRITE_ENABLED" desc:"Allow creating, modifying and deleting LDAP users via the GRAPH API. This can only be set to 'true' when keeping default settings for the LDAP user and group attribute types (the 'OC_LDAP_USER_SCHEMA_* and 'OC_LDAP_GROUP_SCHEMA_* variables)." introductionVersion:"1.0.0"`
EditLoginAllowedDisabled bool `yaml:"edit_login_allowed_disabled" env:"FRONTEND_EDIT_LOGIN_ALLOWED_DISABLED" desc:"Used to set if login is allowed/forbidden for for User." introductionVersion:"3.4.0"`
FullTextSearch bool `yaml:"full_text_search" env:"FRONTEND_FULL_TEXT_SEARCH_ENABLED" desc:"Set to true to signal the web client that full-text search is enabled." introductionVersion:"1.0.0"`
CheckForUpdates bool `yaml:"check_for_updates" env:"FRONTEND_CHECK_FOR_UPDATES" desc:"Enable automatic checking for updates. Defaults to true." introductionVersion:"3.6.0"`
Middleware Middleware `yaml:"middleware"`
Events Events `yaml:"events"`
GRPCClientTLS *shared.GRPCClientTLS `yaml:"grpc_client_tls"`
AutoAcceptShares bool `yaml:"auto_accept_shares" env:"FRONTEND_AUTO_ACCEPT_SHARES" desc:"Defines if shares should be auto accepted by default. Users can change this setting individually in their profile." introductionVersion:"1.0.0"`
ServiceAccount ServiceAccount `yaml:"service_account"`
PasswordPolicy PasswordPolicy `yaml:"password_policy"`
ConfigurableNotifications bool `yaml:"configurable_notifications" env:"FRONTEND_CONFIGURABLE_NOTIFICATIONS" desc:"Allow configuring notifications via web client." introductionVersion:"1.0.0"`
Groupware Groupware `yaml:"groupware"`
Context context.Context `yaml:"-"`
}
type Service struct {
Name string `yaml:"-"`
}
type Debug struct {
Addr string `yaml:"addr" env:"FRONTEND_DEBUG_ADDR" desc:"Bind address of the debug server, where metrics, health, config and debug endpoints will be exposed." introductionVersion:"1.0.0"`
Token string `yaml:"token" env:"FRONTEND_DEBUG_TOKEN" desc:"Token to secure the metrics endpoint." introductionVersion:"1.0.0"`
Pprof bool `yaml:"pprof" env:"FRONTEND_DEBUG_PPROF" desc:"Enables pprof, which can be used for profiling." introductionVersion:"1.0.0"`
Zpages bool `yaml:"zpages" env:"FRONTEND_DEBUG_ZPAGES" desc:"Enables zpages, which can be used for collecting and viewing in-memory traces." introductionVersion:"1.0.0"`
}
type HTTPConfig struct {
Addr string `yaml:"addr" env:"FRONTEND_HTTP_ADDR" desc:"The bind address of the HTTP service." introductionVersion:"1.0.0"`
Namespace string `yaml:"-"`
Protocol string `yaml:"protocol" env:"FRONTEND_HTTP_PROTOCOL" desc:"The transport protocol of the HTTP service." introductionVersion:"1.0.0"`
Prefix string `yaml:"prefix" env:"FRONTEND_HTTP_PREFIX" desc:"The Path prefix where the frontend can be accessed (defaults to /)." introductionVersion:"1.0.0"`
CORS CORS `yaml:"cors"`
}
// CORS defines the available cors configuration.
type CORS struct {
AllowedOrigins []string `yaml:"allow_origins" env:"OC_CORS_ALLOW_ORIGINS;FRONTEND_CORS_ALLOW_ORIGINS" desc:"A list of allowed CORS origins. See following chapter for more details: *Access-Control-Allow-Origin* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin. See the Environment Variable Types description for more details." introductionVersion:"1.0.0"`
AllowedMethods []string `yaml:"allow_methods" env:"OC_CORS_ALLOW_METHODS;FRONTEND_CORS_ALLOW_METHODS" desc:"A list of allowed CORS methods. See following chapter for more details: *Access-Control-Request-Method* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Method. See the Environment Variable Types description for more details." introductionVersion:"1.0.0"`
AllowedHeaders []string `yaml:"allow_headers" env:"OC_CORS_ALLOW_HEADERS;FRONTEND_CORS_ALLOW_HEADERS" desc:"A list of allowed CORS headers. See following chapter for more details: *Access-Control-Request-Headers* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Headers. See the Environment Variable Types description for more details." introductionVersion:"1.0.0"`
AllowCredentials bool `yaml:"allow_credentials" env:"OC_CORS_ALLOW_CREDENTIALS;FRONTEND_CORS_ALLOW_CREDENTIALS" desc:"Allow credentials for CORS.See following chapter for more details: *Access-Control-Allow-Credentials* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials." introductionVersion:"1.0.0"`
}
// Middleware configures reva middlewares.
type Middleware struct {
Auth Auth `yaml:"auth"`
}
// Auth configures reva http auth middleware.
type Auth struct {
CredentialsByUserAgent map[string]string `yaml:"credentials_by_user_agent"`
}
type AppHandler struct {
Prefix string `yaml:"-"`
Insecure bool `yaml:"insecure" env:"OC_INSECURE;FRONTEND_APP_HANDLER_INSECURE" desc:"Allow insecure connections to the frontend." introductionVersion:"1.0.0"`
SecureViewAppAddr string `yaml:"secure_view_app_addr" env:"FRONTEND_APP_HANDLER_SECURE_VIEW_APP_ADDR" desc:"Service name or address of the app provider to use for secure view. Should match the service name or address of the registered CS3 app provider." introductionVersion:"1.0.0"`
}
type Archiver struct {
MaxNumFiles int64 `yaml:"max_num_files" env:"FRONTEND_ARCHIVER_MAX_NUM_FILES" desc:"Max number of files that can be packed into an archive." introductionVersion:"1.0.0"`
MaxSize int64 `yaml:"max_size" env:"FRONTEND_ARCHIVER_MAX_SIZE" desc:"Max size in bytes of the zip archive the archiver can create." introductionVersion:"1.0.0"`
Prefix string `yaml:"-"`
Insecure bool `yaml:"insecure" env:"OC_INSECURE;FRONTEND_ARCHIVER_INSECURE" desc:"Allow insecure connections to the archiver." introductionVersion:"1.0.0"`
}
type DataGateway struct {
Prefix string `yaml:"prefix" env:"FRONTEND_DATA_GATEWAY_PREFIX" desc:"Path prefix for the data gateway." introductionVersion:"1.0.0"`
}
type OCS struct {
Prefix string `yaml:"prefix" env:"FRONTEND_OCS_PREFIX" desc:"URL path prefix for the OCS service. Note that the string must not start with '/'." introductionVersion:"1.0.0" deprecationVersion:"1.0.0" removalVersion:"%%NEXT_PRODUCTION_VERSION%%" deprecationInfo:"The OCS API is deprecated" deprecationReplacement:""`
SharePrefix string `yaml:"share_prefix" env:"FRONTEND_OCS_SHARE_PREFIX" desc:"Path prefix for shares as part of a CS3 resource. Note that the path must start with '/'." introductionVersion:"1.0.0" deprecationVersion:"1.0.0" removalVersion:"%%NEXT_PRODUCTION_VERSION%%" deprecationInfo:"The OCS API is deprecated" deprecationReplacement:""`
HomeNamespace string `yaml:"home_namespace" env:"FRONTEND_OCS_PERSONAL_NAMESPACE" desc:"Home namespace identifier." introductionVersion:"1.0.0" deprecationVersion:"1.0.0" removalVersion:"%%NEXT_PRODUCTION_VERSION%%" deprecationInfo:"The OCS API is deprecated" deprecationReplacement:""`
AdditionalInfoAttribute string `yaml:"additional_info_attribute" env:"FRONTEND_OCS_ADDITIONAL_INFO_ATTRIBUTE" desc:"Additional information attribute for the user like {{.Mail}}." introductionVersion:"1.0.0" deprecationVersion:"1.0.0" removalVersion:"%%NEXT_PRODUCTION_VERSION%%" deprecationInfo:"The OCS API is deprecated" deprecationReplacement:""`
StatCacheType string `yaml:"stat_cache_type" env:"OC_CACHE_STORE;FRONTEND_OCS_STAT_CACHE_STORE" desc:"The type of the cache store. Supported values are: 'memory', 'redis-sentinel', 'nats-js-kv', 'noop'. See the text description for details." introductionVersion:"1.0.0" deprecationVersion:"1.0.0" removalVersion:"%%NEXT_PRODUCTION_VERSION%%" deprecationInfo:"FRONTEND_OCS_STAT_CACHE_STORE, the OCS API is deprecated" deprecationReplacement:""`
StatCacheNodes []string `yaml:"stat_cache_nodes" env:"OC_CACHE_STORE_NODES;FRONTEND_OCS_STAT_CACHE_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:"1.0.0" deprecationVersion:"1.0.0" removalVersion:"%%NEXT_PRODUCTION_VERSION%%" deprecationInfo:"FRONTEND_OCS_STAT_CACHE_STORE_NODES, the OCS API is deprecated" deprecationReplacement:""`
StatCacheDatabase string `yaml:"stat_cache_database" env:"OC_CACHE_DATABASE" desc:"The database name the configured store should use." introductionVersion:"1.0.0"`
StatCacheTable string `yaml:"stat_cache_table" env:"FRONTEND_OCS_STAT_CACHE_TABLE" desc:"The database table the store should use." introductionVersion:"1.0.0" deprecationVersion:"1.0.0" removalVersion:"%%NEXT_PRODUCTION_VERSION%%" deprecationInfo:"The OCS API is deprecated" deprecationReplacement:""`
StatCacheTTL time.Duration `yaml:"stat_cache_ttl" env:"OC_CACHE_TTL;FRONTEND_OCS_STAT_CACHE_TTL" desc:"Default time to live for user info in the cache. Only applied when access tokens has no expiration. See the Environment Variable Types description for more details." introductionVersion:"1.0.0" deprecationVersion:"1.0.0" removalVersion:"%%NEXT_PRODUCTION_VERSION%%" deprecationInfo:"FRONTEND_OCS_STAT_CACHE_TTL, the OCS API is deprecated" deprecationReplacement:""`
StatCacheDisablePersistence bool `yaml:"stat_cache_disable_persistence" env:"OC_CACHE_DISABLE_PERSISTENCE;FRONTEND_OCS_STAT_CACHE_DISABLE_PERSISTENCE" desc:"Disable persistence of the cache. Only applies when using the 'nats-js-kv' store type. Defaults to false." introductionVersion:"1.0.0" deprecationVersion:"1.0.0" removalVersion:"%%NEXT_PRODUCTION_VERSION%%" deprecationInfo:"FRONTEND_OCS_STAT_CACHE_DISABLE_PERSISTENCE, the OCS API is deprecated" deprecationReplacement:""`
StatCacheAuthUsername string `yaml:"stat_cache_auth_username" env:"OC_CACHE_AUTH_USERNAME;FRONTEND_OCS_STAT_CACHE_AUTH_USERNAME" desc:"The username to use for authentication. Only applies when using the 'nats-js-kv' store type." introductionVersion:"1.0.0" deprecationVersion:"1.0.0" removalVersion:"%%NEXT_PRODUCTION_VERSION%%" deprecationInfo:"FRONTEND_OCS_STAT_CACHE_AUTH_USERNAME, the OCS API is deprecated" deprecationReplacement:""`
StatCacheAuthPassword string `yaml:"stat_cache_auth_password" env:"OC_CACHE_AUTH_PASSWORD;FRONTEND_OCS_STAT_CACHE_AUTH_PASSWORD" desc:"The password to use for authentication. Only applies when using the 'nats-js-kv' store type." introductionVersion:"1.0.0" deprecationVersion:"1.0.0" removalVersion:"%%NEXT_PRODUCTION_VERSION%%" deprecationInfo:"FRONTEND_OCS_STAT_CACHE_AUTH_PASSWORD, the OCS API is deprecated" deprecationReplacement:""`
CacheWarmupDriver string `yaml:"cache_warmup_driver,omitempty"` // not supported by the КуСфера product, therefore not part of docs
CacheWarmupDrivers CacheWarmupDrivers `yaml:"cache_warmup_drivers,omitempty"` // not supported by the КуСфера product, therefore not part of docs
EnableDenials bool `yaml:"enable_denials" env:"FRONTEND_OCS_ENABLE_DENIALS" desc:"EXPERIMENTAL: enable the feature to deny access on folders." introductionVersion:"1.0.0" deprecationVersion:"1.0.0" removalVersion:"%%NEXT_PRODUCTION_VERSION%%" deprecationInfo:"The OCS API is deprecated" deprecationReplacement:""`
ListOCMShares bool `yaml:"list_ocm_shares" env:"OC_ENABLE_OCM;FRONTEND_OCS_LIST_OCM_SHARES" desc:"Include OCM shares when listing shares. See the OCM service documentation for more details." introductionVersion:"1.0.0" deprecationVersion:"1.0.0" removalVersion:"%%NEXT_PRODUCTION_VERSION%%" deprecationInfo:"FRONTEND_OCS_LIST_OCM_SHARES, the OCS API is deprecated" deprecationReplacement:""`
IncludeOCMSharees bool `yaml:"include_ocm_sharees" env:"OC_ENABLE_OCM;FRONTEND_OCS_INCLUDE_OCM_SHAREES" desc:"Include OCM sharees when listing sharees." introductionVersion:"1.0.0" deprecationVersion:"1.0.0" removalVersion:"%%NEXT_PRODUCTION_VERSION%%" deprecationInfo:"FRONTEND_OCS_INCLUDE_OCM_SHAREES, the OCS API is deprecated" deprecationReplacement:""`
PublicShareMustHavePassword bool `yaml:"public_sharing_share_must_have_password" env:"OC_SHARING_PUBLIC_SHARE_MUST_HAVE_PASSWORD;FRONTEND_OCS_PUBLIC_SHARE_MUST_HAVE_PASSWORD" desc:"Set this to true if you want to enforce passwords on all public shares." introductionVersion:"1.0.0" deprecationVersion:"1.0.0" removalVersion:"%%NEXT_PRODUCTION_VERSION%%" deprecationInfo:"FRONTEND_OCS_PUBLIC_SHARE_MUST_HAVE_PASSWORD, the OCS API is deprecated" deprecationReplacement:""`
WriteablePublicShareMustHavePassword bool `yaml:"public_sharing_writeableshare_must_have_password" env:"OC_SHARING_PUBLIC_WRITEABLE_SHARE_MUST_HAVE_PASSWORD;FRONTEND_OCS_PUBLIC_WRITEABLE_SHARE_MUST_HAVE_PASSWORD" desc:"Set this to true if you want to enforce passwords for writable shares. Only effective if the setting for 'passwords on all public shares' is set to false." introductionVersion:"1.0.0" deprecationVersion:"1.0.0" removalVersion:"%%NEXT_PRODUCTION_VERSION%%" deprecationInfo:"FRONTEND_OCS_PUBLIC_WRITABLE_SHARE_MUST_HAVE_PASSWORD, the OCS API is deprecated" deprecationReplacement:""`
ShowUserEmailInResults bool `yaml:"show_email_in_results" env:"OC_SHOW_USER_EMAIL_IN_RESULTS" desc:"Include user email addresses in responses. If absent or set to false emails will be omitted from results. Please note that admin users can always see all email addresses." introductionVersion:"1.0.0"`
}
type OCDav struct {
Prefix string `yaml:"prefix" env:"OCDAV_HTTP_PREFIX;FRONTENT_OCDAV_HTTP_PREFIX" desc:"A URL path prefix for the handler." introductionVersion:"1.0.0"`
SkipUserGroupsInToken bool `yaml:"skip_user_groups_in_token" env:"OCDAV_SKIP_USER_GROUPS_IN_TOKEN;FRONTENT_OCDAV_SKIP_USER_GROUPS_IN_TOKEN" desc:"Disables the loading of user's group memberships from the reva access token." introductionVersion:"1.0.0"`
WebdavNamespace string `yaml:"webdav_namespace" env:"OCDAV_WEBDAV_NAMESPACE;FRONTENT_OCDAV_WEBDAV_NAMESPACE" desc:"Jail requests to /dav/webdav into this CS3 namespace. Supports template layouting with CS3 User properties." introductionVersion:"1.0.0"`
FilesNamespace string `yaml:"files_namespace" env:"OCDAV_FILES_NAMESPACE;FRONTENT_OCDAV_FILES_NAMESPACE" desc:"Jail requests to /dav/files/{username} into this CS3 namespace. Supports template layouting with CS3 User properties." introductionVersion:"1.0.0"`
SharesNamespace string `yaml:"shares_namespace" env:"OCDAV_SHARES_NAMESPACE;FRONTENT_OCDAV_SHARES_NAMESPACE" desc:"The human readable path for the share jail. Relative to a users personal space root. Upcased intentionally." introductionVersion:"1.0.0"`
OCMNamespace string `yaml:"ocm_namespace" env:"OCDAV_OCM_NAMESPACE;FRONTENT_OCDAV_OCM_NAMESPACE" desc:"The human readable path prefix for the ocm shares." introductionVersion:"1.0.0"`
// PublicURL used to redirect /s/{token} URLs to
PublicURL string `yaml:"public_url" env:"OC_URL;OCDAV_PUBLIC_URL;FRONTENT_OCDAV_PUBLIC_URL" desc:"URL where КуСфера is reachable for users." introductionVersion:"1.0.0"`
// Insecure certificates allowed when making requests to the gateway
Insecure bool `yaml:"insecure" env:"OC_INSECURE;OCDAV_INSECURE;FRONTENT_OCDAV_INSECURE" desc:"Allow insecure connections to the GATEWAY service." introductionVersion:"1.0.0"`
EnableHTTPTPC bool `yaml:"enable_http_tpc" env:"OCDAV_ENABLE_HTTP_TPC;FRONTENT_OCDAV_ENABLE_HTTP_TPC" desc:"Enable HTTP / WebDAV Third-Party-Copy support." introductionVersion:"6.0.0"`
// Timeout in seconds when making requests to the gateway
Timeout int64 `yaml:"gateway_request_timeout" env:"OCDAV_GATEWAY_REQUEST_TIME;FRONTENT_OUTOCDAV_GATEWAY_REQUEST_TIMEOUT" desc:"Request timeout in seconds for requests from the oCDAV service to the GATEWAY service." introductionVersion:"1.0.0"`
MachineAuthAPIKey string `yaml:"machine_auth_api_key" env:"OC_MACHINE_AUTH_API_KEY;OCDAV_MACHINE_AUTH_API_KEY;FRONTENT_OCDAV_MACHINE_AUTH_API_KEY" desc:"Machine auth API key used to validate internal requests necessary for the access to resources from other services." introductionVersion:"1.0.0"`
AllowPropfindDepthInfinity bool `yaml:"allow_propfind_depth_infinity" env:"OCDAV_ALLOW_PROPFIND_DEPTH_INFINITY;FRONTENT_OCDAV_ALLOW_PROPFIND_DEPTH_INFINITY" desc:"Allow the use of depth infinity in PROPFINDS. When enabled, a propfind will traverse through all subfolders. If many subfolders are expected, depth infinity can cause heavy server load and/or delayed response times." introductionVersion:"1.0.0"`
NameValidation NameValidation `yaml:"name_validation"`
}
type NameValidation struct {
InvalidChars []string `yaml:"invalid_chars" env:"OCDAV_NAME_VALIDATION_INVALID_CHARS;FRONTENT_OCDAV_NAME_VALIDATION_INVALID_CHARS" desc:"List of characters that are not allowed in file or folder names." introductionVersion:"6.0.0"`
MaxLength int `yaml:"max_length" env:"OCDAV_NAME_VALIDATION_MAX_LENGTH;FRONTENT_OCDAV_NAME_VALIDATION_MAX_LENGTH" desc:"Max length of file or folder names." introductionVersion:"6.0.0"`
}
type CacheWarmupDrivers struct {
CBOX CBOXDriver `yaml:"cbox,omitempty"`
}
type CBOXDriver struct {
DBUsername string `yaml:"db_username,omitempty"`
DBPassword string `yaml:"db_password,omitempty"`
DBHost string `yaml:"db_host,omitempty"`
DBPort int `yaml:"db_port,omitempty"`
DBName string `yaml:"db_name,omitempty"`
Namespace string `yaml:"namespace,omitempty"`
}
type Checksums struct {
SupportedTypes []string `yaml:"supported_types" env:"FRONTEND_CHECKSUMS_SUPPORTED_TYPES" desc:"A list of checksum types that indicate to clients which hashes the server can use to verify upload integrity. Supported types are 'sha1', 'md5' and 'adler32'. See the Environment Variable Types description for more details." introductionVersion:"1.0.0"`
PreferredUploadType string `yaml:"preferred_upload_type" env:"FRONTEND_CHECKSUMS_PREFERRED_UPLOAD_TYPE" desc:"The supported checksum type for uploads that indicates to clients supporting multiple hash algorithms which one is preferred by the server. Must be one out of the defined list of SUPPORTED_TYPES." introductionVersion:"1.0.0"`
}
// Events combines the configuration options for the event bus.
type Events struct {
Endpoint string `yaml:"endpoint" env:"OC_EVENTS_ENDPOINT;FRONTEND_EVENTS_ENDPOINT" desc:"The address of the event system. The event system is the message queuing service. It is used as message broker for the microservice architecture." introductionVersion:"1.0.0"`
Cluster string `yaml:"cluster" env:"OC_EVENTS_CLUSTER;FRONTEND_EVENTS_CLUSTER" desc:"The clusterID of the event system. The event system is the message queuing service. It is used as message broker for the microservice architecture. Mandatory when using NATS as event system." introductionVersion:"1.0.0"`
TLSInsecure bool `yaml:"tls_insecure" env:"OC_INSECURE;OC_EVENTS_TLS_INSECURE;FRONTEND_EVENTS_TLS_INSECURE" desc:"Whether to verify the server TLS certificates." introductionVersion:"1.0.0"`
TLSRootCACertificate string `yaml:"tls_root_ca_certificate" env:"OC_EVENTS_TLS_ROOT_CA_CERTIFICATE;FRONTEND_EVENTS_TLS_ROOT_CA_CERTIFICATE;OCS_EVENTS_TLS_ROOT_CA_CERTIFICATE" desc:"The root CA certificate used to validate the server's TLS certificate. If provided NOTIFICATIONS_EVENTS_TLS_INSECURE will be seen as false." introductionVersion:"1.0.0"`
EnableTLS bool `yaml:"enable_tls" env:"OC_EVENTS_ENABLE_TLS;FRONTEND_EVENTS_ENABLE_TLS" desc:"Enable TLS for the connection to the events broker. The events broker is the КуСфера service which receives and delivers events between the services." introductionVersion:"1.0.0"`
AuthUsername string `yaml:"username" env:"OC_EVENTS_AUTH_USERNAME;FRONTEND_EVENTS_AUTH_USERNAME" desc:"The username to authenticate with the events broker. The events broker is the КуСфера service which receives and delivers events between the services." introductionVersion:"1.0.0"`
AuthPassword string `yaml:"password" env:"OC_EVENTS_AUTH_PASSWORD;FRONTEND_EVENTS_AUTH_PASSWORD" desc:"The password to authenticate with the events broker. The events broker is the КуСфера service which receives and delivers events between the services." introductionVersion:"1.0.0"`
}
// ServiceAccount is the configuration for the used service account
type ServiceAccount struct {
ServiceAccountID string `yaml:"service_account_id" env:"OC_SERVICE_ACCOUNT_ID;FRONTEND_SERVICE_ACCOUNT_ID" desc:"The ID of the service account the service should use. See the 'auth-service' service description for more details." introductionVersion:"1.0.0"`
ServiceAccountSecret string `yaml:"service_account_secret" env:"OC_SERVICE_ACCOUNT_SECRET;FRONTEND_SERVICE_ACCOUNT_SECRET" desc:"The service account secret." introductionVersion:"1.0.0"`
}
// PasswordPolicy configures reva password policy
type PasswordPolicy struct {
Disabled bool `yaml:"disabled,omitempty" env:"OC_PASSWORD_POLICY_DISABLED;FRONTEND_PASSWORD_POLICY_DISABLED" desc:"Disable the password policy. Defaults to false if not set." introductionVersion:"1.0.0"`
MinCharacters int `yaml:"min_characters,omitempty" env:"OC_PASSWORD_POLICY_MIN_CHARACTERS;FRONTEND_PASSWORD_POLICY_MIN_CHARACTERS" desc:"Define the minimum password length. Defaults to 8 if not set." introductionVersion:"1.0.0"`
MinLowerCaseCharacters int `yaml:"min_lowercase_characters" env:"OC_PASSWORD_POLICY_MIN_LOWERCASE_CHARACTERS;FRONTEND_PASSWORD_POLICY_MIN_LOWERCASE_CHARACTERS" desc:"Define the minimum number of uppercase letters. Defaults to 1 if not set." introductionVersion:"1.0.0"`
MinUpperCaseCharacters int `yaml:"min_uppercase_characters" env:"OC_PASSWORD_POLICY_MIN_UPPERCASE_CHARACTERS;FRONTEND_PASSWORD_POLICY_MIN_UPPERCASE_CHARACTERS" desc:"Define the minimum number of lowercase letters. Defaults to 1 if not set." introductionVersion:"1.0.0"`
MinDigits int `yaml:"min_digits" env:"OC_PASSWORD_POLICY_MIN_DIGITS;FRONTEND_PASSWORD_POLICY_MIN_DIGITS" desc:"Define the minimum number of digits. Defaults to 1 if not set." introductionVersion:"1.0.0"`
MinSpecialCharacters int `yaml:"min_special_characters" env:"OC_PASSWORD_POLICY_MIN_SPECIAL_CHARACTERS;FRONTEND_PASSWORD_POLICY_MIN_SPECIAL_CHARACTERS" desc:"Define the minimum number of characters from the special characters list to be present. Defaults to 1 if not set." introductionVersion:"1.0.0"`
BannedPasswordsList string `yaml:"banned_passwords_list" env:"OC_PASSWORD_POLICY_BANNED_PASSWORDS_LIST;FRONTEND_PASSWORD_POLICY_BANNED_PASSWORDS_LIST" desc:"Path to the 'banned passwords list' file. This only impacts public link password validation. See the documentation for more details." introductionVersion:"1.0.0"`
}
type Groupware struct {
Enabled bool `yaml:"enabled" env:"FRONTEND_GROUPWARE_ENABLED" desc:"Enable groupware features. Defaults to false." introductionVersion:"3.7.0"`
}
@@ -0,0 +1,203 @@
package defaults
import (
"time"
"github.com/qsfera/server/pkg/shared"
"github.com/qsfera/server/pkg/structs"
"github.com/qsfera/server/services/frontend/pkg/config"
)
// FullDefaultConfig returns a fully initialized default configuration
func FullDefaultConfig() *config.Config {
cfg := DefaultConfig()
EnsureDefaults(cfg)
Sanitize(cfg)
return cfg
}
// DefaultConfig returns a basic default configuration
func DefaultConfig() *config.Config {
return &config.Config{
Debug: config.Debug{
Addr: "127.0.0.1:9141",
Token: "",
Pprof: false,
Zpages: false,
},
HTTP: config.HTTPConfig{
Addr: "127.0.0.1:9140",
Namespace: "qsfera.web",
Protocol: "tcp",
Prefix: "",
CORS: config.CORS{
AllowedOrigins: []string{"https://localhost:9200"},
AllowedMethods: []string{
"OPTIONS",
"HEAD",
"GET",
"PUT",
"POST",
"PATCH",
"DELETE",
"MKCOL",
"PROPFIND",
"PROPPATCH",
"MOVE",
"COPY",
"REPORT",
"SEARCH",
},
AllowedHeaders: []string{
"Origin",
"Accept",
"Content-Type",
"Depth",
"Authorization",
"Ocs-Apirequest",
"If-None-Match",
"If-Match",
"Destination",
"Overwrite",
"X-Request-Id",
"X-Requested-With",
"Tus-Resumable",
"Tus-Checksum-Algorithm",
"Upload-Concat",
"Upload-Length",
"Upload-Metadata",
"Upload-Defer-Length",
"Upload-Expires",
"Upload-Checksum",
"Upload-Offset",
"X-HTTP-Method-Override",
"Cache-Control",
},
AllowCredentials: false,
},
},
Service: config.Service{
Name: "frontend",
},
Reva: shared.DefaultRevaConfig(),
PublicURL: "https://localhost:9200",
UploadMaxChunkSize: 1e+7,
UploadHTTPMethodOverride: "",
DefaultUploadProtocol: "tus",
DefaultLinkPermissions: 1,
SearchMinLength: 3,
CheckForUpdates: true,
Checksums: config.Checksums{
SupportedTypes: []string{"sha1", "md5", "adler32"},
PreferredUploadType: "sha1",
},
AppHandler: config.AppHandler{
Prefix: "app",
SecureViewAppAddr: "qsfera.api.collaboration",
},
Archiver: config.Archiver{
Insecure: false,
Prefix: "archiver",
MaxNumFiles: 10000,
MaxSize: 1073741824,
},
DataGateway: config.DataGateway{
Prefix: "data",
},
OCS: config.OCS{
Prefix: "ocs",
SharePrefix: "/Shares",
HomeNamespace: "/users/{{.Id.OpaqueId}}",
AdditionalInfoAttribute: "{{.Mail}}",
StatCacheType: "memory",
StatCacheNodes: []string{"127.0.0.1:9233"},
StatCacheDatabase: "cache-stat",
StatCacheTTL: 300 * time.Second,
ListOCMShares: true,
PublicShareMustHavePassword: true,
IncludeOCMSharees: false,
},
OCDav: config.OCDav{
Prefix: "",
SkipUserGroupsInToken: false,
WebdavNamespace: "/users/{{.Id.OpaqueId}}",
FilesNamespace: "/users/{{.Id.OpaqueId}}",
SharesNamespace: "/Shares",
OCMNamespace: "/public",
PublicURL: "https://localhost:9200",
Insecure: false,
EnableHTTPTPC: false,
Timeout: 84300,
AllowPropfindDepthInfinity: false,
NameValidation: config.NameValidation{
InvalidChars: []string{"\f", "\r", "\n", "\\"},
MaxLength: 255,
},
},
Middleware: config.Middleware{
Auth: config.Auth{
CredentialsByUserAgent: map[string]string{},
},
},
LDAPServerWriteEnabled: true,
AutoAcceptShares: true,
Events: config.Events{
Endpoint: "127.0.0.1:9233",
Cluster: "qsfera-cluster",
EnableTLS: false,
},
MaxConcurrency: 1,
PasswordPolicy: config.PasswordPolicy{
MinCharacters: 8,
MinLowerCaseCharacters: 1,
MinUpperCaseCharacters: 1,
MinDigits: 1,
MinSpecialCharacters: 1,
},
Groupware: config.Groupware{
Enabled: false,
},
}
}
// EnsureDefaults adds default values to the configuration if they are not set yet
func EnsureDefaults(cfg *config.Config) {
if cfg.LogLevel == "" {
cfg.LogLevel = "error"
}
if cfg.Reva == nil && cfg.Commons != nil {
cfg.Reva = structs.CopyOrZeroValue(cfg.Commons.Reva)
}
if cfg.TokenManager == nil && cfg.Commons != nil && cfg.Commons.TokenManager != nil {
cfg.TokenManager = &config.TokenManager{
JWTSecret: cfg.Commons.TokenManager.JWTSecret,
}
} else if cfg.TokenManager == nil {
cfg.TokenManager = &config.TokenManager{}
}
if cfg.TransferSecret == "" && cfg.Commons != nil && cfg.Commons.TransferSecret != "" {
cfg.TransferSecret = cfg.Commons.TransferSecret
}
if cfg.MachineAuthAPIKey == "" && cfg.Commons != nil && cfg.Commons.MachineAuthAPIKey != "" {
cfg.MachineAuthAPIKey = cfg.Commons.MachineAuthAPIKey
}
if (cfg.Commons != nil && cfg.Commons.QsferaURL != "") &&
(cfg.HTTP.CORS.AllowedOrigins == nil ||
len(cfg.HTTP.CORS.AllowedOrigins) == 1 &&
cfg.HTTP.CORS.AllowedOrigins[0] == "https://localhost:9200") {
cfg.HTTP.CORS.AllowedOrigins = []string{cfg.Commons.QsferaURL}
}
}
// Sanitize sanitized the configuration
func Sanitize(cfg *config.Config) {
if cfg.MaxConcurrency <= 0 {
cfg.MaxConcurrency = 5
}
}
@@ -0,0 +1,67 @@
package parser
import (
"errors"
occfg "github.com/qsfera/server/pkg/config"
"github.com/qsfera/server/pkg/shared"
"github.com/qsfera/server/pkg/structs"
"github.com/qsfera/server/services/frontend/pkg/config"
"github.com/qsfera/server/services/frontend/pkg/config/defaults"
"github.com/qsfera/server/pkg/config/envdecode"
)
// ParseConfig loads configuration from known paths.
func ParseConfig(cfg *config.Config) error {
err := occfg.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 Validate(cfg)
}
func Validate(cfg *config.Config) error {
if cfg.TokenManager.JWTSecret == "" {
return shared.MissingJWTTokenError(cfg.Service.Name)
}
if cfg.TransferSecret == "" {
return shared.MissingRevaTransferSecretError(cfg.Service.Name)
}
if cfg.MachineAuthAPIKey == "" {
return shared.MissingMachineAuthApiKeyError(cfg.Service.Name)
}
if cfg.GRPCClientTLS == nil && cfg.Commons != nil {
cfg.GRPCClientTLS = structs.CopyOrZeroValue(cfg.Commons.GRPCClientTLS)
}
// Set password enforcement on all public links when config is set
if cfg.OCS.PublicShareMustHavePassword {
cfg.OCS.WriteablePublicShareMustHavePassword = true
}
if cfg.ServiceAccount.ServiceAccountID == "" {
return shared.MissingServiceAccountID(cfg.Service.Name)
}
if cfg.ServiceAccount.ServiceAccountSecret == "" {
return shared.MissingServiceAccountSecret(cfg.Service.Name)
}
return nil
}
@@ -0,0 +1,6 @@
package config
// TokenManager is the config for using the reva token manager
type TokenManager struct {
JWTSecret string `yaml:"jwt_secret" env:"OC_JWT_SECRET;FRONTEND_JWT_SECRET" desc:"The secret to mint and validate jwt tokens." introductionVersion:"1.0.0"`
}
@@ -0,0 +1,448 @@
package revaconfig
import (
"bufio"
"fmt"
"net/url"
"os"
"path"
"path/filepath"
"slices"
"strconv"
"github.com/qsfera/server/pkg/capabilities"
"github.com/qsfera/server/pkg/config/defaults"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/pkg/version"
"github.com/qsfera/server/services/frontend/pkg/config"
)
const productName = "КуСфера"
// FrontendConfigFromStruct will adapt a КуСфера config struct into a reva mapstructure to start a reva service.
func FrontendConfigFromStruct(cfg *config.Config, logger log.Logger) (map[string]any, error) {
webURL, err := url.Parse(cfg.PublicURL)
if err != nil {
return nil, err
}
webURL.Path = path.Join(webURL.Path, "external")
webOpenInAppURL := webURL.String()
passwordPolicyCfg, err := passwordPolicyConfig(cfg)
if err != nil {
logger.Err(err).Send()
return nil, err
}
archivers := []map[string]any{
{
"enabled": true,
"version": "2.0.0",
"formats": []string{"tar", "zip"},
"archiver_url": path.Join("/", cfg.Archiver.Prefix),
"max_num_files": strconv.FormatInt(cfg.Archiver.MaxNumFiles, 10),
"max_size": strconv.FormatInt(cfg.Archiver.MaxSize, 10),
},
}
appProviders := []map[string]any{
{
"enabled": true,
"version": "1.1.0",
"apps_url": "/app/list",
"open_url": "/app/open",
"open_web_url": "/app/open-with-web",
"new_url": "/app/new",
},
}
filesCfg := map[string]any{
"private_links": true,
"bigfilechunking": false,
"blacklisted_files": []string{},
"undelete": true,
"versioning": true,
"tags": true,
"archivers": archivers,
"app_providers": appProviders,
"favorites": false,
"full_text_search": cfg.FullTextSearch,
}
if cfg.DefaultUploadProtocol == "tus" {
filesCfg["tus_support"] = map[string]any{
"version": "1.0.0",
"resumable": "1.0.0",
"extension": "creation,creation-with-upload",
"http_method_override": cfg.UploadHTTPMethodOverride,
"max_chunk_size": cfg.UploadMaxChunkSize,
}
}
readOnlyUserAttributes := []string{}
if cfg.ReadOnlyUserAttributes != nil {
readOnlyUserAttributes = cfg.ReadOnlyUserAttributes
}
changePasswordDisabled := !cfg.LDAPServerWriteEnabled
if slices.Contains(readOnlyUserAttributes, "user.passwordProfile") {
changePasswordDisabled = true
}
return map[string]any{
"shared": map[string]any{
"jwt_secret": cfg.TokenManager.JWTSecret,
"gatewaysvc": cfg.Reva.Address, // Todo or address?
"skip_user_groups_in_token": cfg.SkipUserGroupsInToken,
"grpc_client_options": cfg.Reva.GetGRPCClientConfig(),
"multi_tenant_enabled": cfg.Commons.MultiTenantEnabled,
},
"http": map[string]any{
"network": cfg.HTTP.Protocol,
"address": cfg.HTTP.Addr,
"middlewares": map[string]any{
"cors": map[string]any{
"allowed_origins": cfg.HTTP.CORS.AllowedOrigins,
"allowed_methods": cfg.HTTP.CORS.AllowedMethods,
"allowed_headers": cfg.HTTP.CORS.AllowedHeaders,
"allow_credentials": cfg.HTTP.CORS.AllowCredentials,
// currently unused
//"options_passthrough": ,
//"debug": ,
//"max_age": ,
//"priority": ,
//"exposed_headers": ,
},
"auth": map[string]any{
"credentials_by_user_agent": cfg.Middleware.Auth.CredentialsByUserAgent,
},
"prometheus": map[string]any{
"namespace": "qsfera",
"subsystem": "frontend",
},
"requestid": map[string]any{},
},
// TODO build services dynamically
"services": map[string]any{
// this reva service called "appprovider" comes from
// `internal/http/services/appprovider` and is a translation
// layer from the grpc app registry to http, used by e.g. КуСфера Web
// It should not be confused with `internal/grpc/services/appprovider`
// which is currently only the driver for the CS3org WOPI server
"appprovider": map[string]any{
"prefix": cfg.AppHandler.Prefix,
"transfer_shared_secret": cfg.TransferSecret,
"timeout": 86400,
"insecure": cfg.AppHandler.Insecure,
"webbaseuri": webOpenInAppURL,
"web": map[string]any{
"urlparamsmapping": map[string]string{
// param -> value mapper
// these mappers are static and are only subject to change when changed in oC Web
"fileId": "fileid",
"app": "appname",
},
"staticurlparams": map[string]string{
"contextRouteName": "files-spaces-personal", // FIXME: remove when https://github.com/owncloud/web/pull/7437 arrived in КуСфера
},
},
"secure_view_app_addr": cfg.AppHandler.SecureViewAppAddr,
},
"archiver": map[string]any{
"prefix": cfg.Archiver.Prefix,
"timeout": 86400,
"insecure": cfg.Archiver.Insecure,
"max_num_files": cfg.Archiver.MaxNumFiles,
"max_size": cfg.Archiver.MaxSize,
},
"datagateway": map[string]any{
"prefix": cfg.DataGateway.Prefix,
"transfer_shared_secret": cfg.TransferSecret,
"timeout": 86400,
"insecure": true,
},
"ocs": map[string]any{
"storage_registry_svc": cfg.Reva.Address,
"share_prefix": cfg.OCS.SharePrefix,
"home_namespace": cfg.OCS.HomeNamespace,
"stat_cache_config": map[string]any{
"cache_store": cfg.OCS.StatCacheType,
"cache_nodes": cfg.OCS.StatCacheNodes,
"cache_database": cfg.OCS.StatCacheDatabase,
"cache_table": cfg.OCS.StatCacheTable,
"cache_ttl": cfg.OCS.StatCacheTTL,
"cache_disable_persistence": cfg.OCS.StatCacheDisablePersistence,
"cache_auth_username": cfg.OCS.StatCacheAuthUsername,
"cache_auth_password": cfg.OCS.StatCacheAuthPassword,
},
"prefix": cfg.OCS.Prefix,
"additional_info_attribute": cfg.OCS.AdditionalInfoAttribute,
"machine_auth_apikey": cfg.MachineAuthAPIKey,
"enable_denials": cfg.OCS.EnableDenials,
"list_ocm_shares": cfg.OCS.ListOCMShares,
"cache_warmup_driver": cfg.OCS.CacheWarmupDriver,
"cache_warmup_drivers": map[string]any{
"cbox": map[string]any{
"db_username": cfg.OCS.CacheWarmupDrivers.CBOX.DBUsername,
"db_password": cfg.OCS.CacheWarmupDrivers.CBOX.DBPassword,
"db_host": cfg.OCS.CacheWarmupDrivers.CBOX.DBHost,
"db_port": cfg.OCS.CacheWarmupDrivers.CBOX.DBPort,
"db_name": cfg.OCS.CacheWarmupDrivers.CBOX.DBName,
"namespace": cfg.OCS.CacheWarmupDrivers.CBOX.Namespace,
"gatewaysvc": cfg.Reva.Address,
},
},
"config": map[string]any{
"version": "1.7",
"website": productName,
"host": cfg.PublicURL,
"contact": "",
"ssl": "false",
},
"default_upload_protocol": cfg.DefaultUploadProtocol,
"capabilities": map[string]any{
"capabilities": map[string]any{
"core": map[string]any{
"poll_interval": 60,
"webdav_root": "remote.php/webdav",
"status": map[string]any{
"installed": true,
"maintenance": false,
"needsDbUpgrade": false,
"version": version.Legacy,
"versionstring": version.LegacyString,
"edition": version.Edition,
"productname": productName,
"product": productName,
"productversion": version.GetString(),
"hostname": "",
},
"check_for_updates": cfg.CheckForUpdates,
"support_url_signing": true,
"support_sse": !cfg.DisableSSE,
"support_radicale": !cfg.DisableRadicale,
},
"graph": map[string]any{
"personal_data_export": true,
"users": map[string]any{
"read_only_attributes": readOnlyUserAttributes,
"create_disabled": !cfg.LDAPServerWriteEnabled,
"delete_disabled": !cfg.LDAPServerWriteEnabled,
"change_password_self_disabled": changePasswordDisabled,
"edit_login_allowed_disabled": cfg.EditLoginAllowedDisabled,
},
},
"checksums": map[string]any{
"supported_types": cfg.Checksums.SupportedTypes,
"preferred_upload_type": cfg.Checksums.PreferredUploadType,
},
"files": filesCfg,
"dav": map[string]any{
"reports": []string{"search-files"},
},
"files_sharing": map[string]any{
"api_enabled": true,
"group_sharing": true,
"sharing_roles": true,
"deny_access": cfg.OCS.EnableDenials,
"auto_accept_share": true,
"share_with_group_members_only": true,
"share_with_membership_groups_only": true,
"default_permissions": 22,
"search_min_length": cfg.SearchMinLength,
"public": map[string]any{
"alias": true,
"enabled": true,
"send_mail": true,
"defaultPublicLinkShareName": "Public link",
"social_share": true,
"upload": true,
"multiple": true,
"supports_upload_only": true,
"default_permissions": cfg.DefaultLinkPermissions,
"password": map[string]any{
"enforced": false,
"enforced_for": map[string]any{
"read_only": cfg.OCS.PublicShareMustHavePassword,
"read_write": cfg.OCS.WriteablePublicShareMustHavePassword,
"read_write_delete": cfg.OCS.WriteablePublicShareMustHavePassword,
"upload_only": cfg.OCS.WriteablePublicShareMustHavePassword,
},
},
"expire_date": map[string]any{
"enabled": false,
},
"can_edit": true,
},
"user": map[string]any{
"send_mail": true,
"profile_picture": false,
"settings": []map[string]any{
{
"enabled": true,
"version": "1.0.0",
},
},
"expire_date": map[string]any{
"enabled": true,
},
},
"user_enumeration": map[string]any{
"enabled": true,
"group_members_only": true,
},
"federation": map[string]any{
"outgoing": cfg.EnableFederatedSharingOutgoing,
"incoming": cfg.EnableFederatedSharingIncoming,
},
},
"spaces": map[string]any{
"version": "1.0.0",
"enabled": true,
"projects": true,
"share_jail": true,
"max_quota": cfg.MaxQuota,
},
"theme": capabilities.Default().Theme,
"search": map[string]any{
"property": map[string]any{
"name": map[string]any{
"enabled": true,
},
"mtime": map[string]any{
"keywords": []string{"today", "last 7 days", "last 30 days", "this year", "last year"},
"enabled": true,
},
"size": map[string]any{
"enabled": false,
},
"mediatype": map[string]any{
"keywords": []string{"document", "spreadsheet", "presentation", "pdf", "image", "video", "audio", "folder", "archive"},
"enabled": true,
},
"type": map[string]any{
"enabled": true,
},
"tag": map[string]any{
"enabled": true,
},
"tags": map[string]any{
"enabled": true,
},
"content": map[string]any{
"enabled": true,
},
"scope": map[string]any{
"enabled": true,
},
},
},
"password_policy": passwordPolicyCfg,
"notifications": map[string]any{
"endpoints": []string{"list", "get", "delete"},
"configurable": cfg.ConfigurableNotifications,
},
"groupware": map[string]any{
"enabled": cfg.Groupware.Enabled,
},
},
"version": map[string]any{
"product": productName,
"edition": version.Edition,
"major": version.ParsedLegacy().Major(),
"minor": version.ParsedLegacy().Minor(),
"micro": version.ParsedLegacy().Patch(),
"string": version.LegacyString,
"productversion": version.GetString(),
},
},
"include_ocm_sharees": cfg.OCS.IncludeOCMSharees,
"show_email_in_results": cfg.OCS.ShowUserEmailInResults,
},
"ocdav": map[string]any{
"prefix": cfg.OCDav.Prefix,
"files_namespace": cfg.OCDav.FilesNamespace,
"webdav_namespace": cfg.OCDav.WebdavNamespace,
"shares_namespace": cfg.OCDav.SharesNamespace,
"ocm_namespace": cfg.OCDav.OCMNamespace,
"gatewaysvc": cfg.Reva.Address,
"timeout": cfg.OCDav.Timeout,
"insecure": cfg.OCDav.Insecure,
"enable_http_tpc": cfg.OCDav.EnableHTTPTPC,
"public_url": cfg.OCDav.PublicURL,
// still not supported
//"favorite_storage_driver": unused,
//"favorite_storage_drivers": unused,
"version": version.Legacy,
"version_string": version.LegacyString,
"edition": version.Edition,
"product": productName,
"product_name": productName,
"product_version": version.GetString(),
"allow_depth_infinity": cfg.OCDav.AllowPropfindDepthInfinity,
"validation": map[string]any{
"invalid_chars": cfg.OCDav.NameValidation.InvalidChars,
"max_length": cfg.OCDav.NameValidation.MaxLength,
},
"url_signing_shared_secret": cfg.Commons.URLSigningSecret,
"machine_auth_apikey": cfg.MachineAuthAPIKey,
},
},
},
}, nil
}
func readMultilineFile(path string) (map[string]struct{}, error) {
if !fileExists(path) {
path = filepath.Join(defaults.BaseConfigPath(), path)
}
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
data := make(map[string]struct{})
for scanner.Scan() {
line := scanner.Text()
if line != "" {
data[line] = struct{}{}
}
}
return data, err
}
func fileExists(path string) bool {
info, err := os.Stat(path)
if err != nil {
return false
}
return !info.IsDir()
}
func passwordPolicyConfig(cfg *config.Config) (map[string]any, error) {
_maxCharacters := 72
if cfg.PasswordPolicy.Disabled {
return map[string]any{
"max_characters": _maxCharacters,
"banned_passwords_list": nil,
}, nil
}
var bannedPasswordsList map[string]struct{}
var err error
if cfg.PasswordPolicy.BannedPasswordsList != "" {
bannedPasswordsList, err = readMultilineFile(cfg.PasswordPolicy.BannedPasswordsList)
if err != nil {
return nil, fmt.Errorf("failed to load the banned passwords from a file %s: %w", cfg.PasswordPolicy.BannedPasswordsList, err)
}
}
return map[string]any{
"max_characters": _maxCharacters,
"min_digits": cfg.PasswordPolicy.MinDigits,
"min_characters": cfg.PasswordPolicy.MinCharacters,
"min_lowercase_characters": cfg.PasswordPolicy.MinLowerCaseCharacters,
"min_uppercase_characters": cfg.PasswordPolicy.MinUpperCaseCharacters,
"min_special_characters": cfg.PasswordPolicy.MinSpecialCharacters,
"banned_passwords_list": bannedPasswordsList,
}, nil
}
@@ -0,0 +1,50 @@
package debug
import (
"context"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/services/frontend/pkg/config"
)
// 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
}
}
@@ -0,0 +1,38 @@
package debug
import (
"net/http"
"github.com/qsfera/server/pkg/checks"
"github.com/qsfera/server/pkg/handlers"
"github.com/qsfera/server/pkg/service/debug"
"github.com/qsfera/server/pkg/version"
)
// Server initializes the debug service and server.
func Server(opts ...Option) (*http.Server, error) {
options := newOptions(opts...)
healthHandlerConfiguration := handlers.NewCheckHandlerConfiguration().
WithLogger(options.Logger).
WithCheck("web reachability", checks.NewHTTPCheck(options.Config.HTTP.Addr))
readyHandlerConfiguration := healthHandlerConfiguration.
WithCheck("nats reachability", checks.NewNatsCheck(options.Config.Events.Endpoint))
return debug.NewService(
debug.Logger(options.Logger),
debug.Name(options.Config.Service.Name),
debug.Version(version.GetString()),
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(handlers.NewCheckHandler(healthHandlerConfiguration)),
debug.Ready(handlers.NewCheckHandler(readyHandlerConfiguration)),
//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
}