Merge pull request #6427 from kobergj/ServiceAccounts

[full-ci] Service Accounts
This commit is contained in:
kobergj
2023-08-31 13:16:21 +02:00
committed by GitHub
69 changed files with 1213 additions and 325 deletions
+1
View File
@@ -61,6 +61,7 @@ config = {
"services/auth-basic",
"services/auth-bearer",
"services/auth-machine",
"services/auth-service",
"services/eventhistory",
"services/frontend",
"services/gateway",
+1
View File
@@ -26,6 +26,7 @@ OCIS_MODULES = \
services/auth-basic \
services/auth-bearer \
services/auth-machine \
services/auth-service \
services/eventhistory \
services/frontend \
services/gateway \
+1
View File
@@ -3,3 +3,4 @@ Enhancement: Bump Reva
bumps reva version
https://github.com/owncloud/ocis/pull/7138
https://github.com/owncloud/ocis/pull/6427
+5
View File
@@ -0,0 +1,5 @@
Enhancement: Introduce service accounts
Introduces service accounts to avoid impersonating users in async processes
https://github.com/owncloud/ocis/pull/6427
+1 -1
View File
@@ -13,7 +13,7 @@ require (
github.com/coreos/go-oidc v2.2.1+incompatible
github.com/coreos/go-oidc/v3 v3.6.0
github.com/cs3org/go-cs3apis v0.0.0-20230516150832-730ac860c71d
github.com/cs3org/reva/v2 v2.16.1-0.20230828111521-594d4e103741
github.com/cs3org/reva/v2 v2.16.1-0.20230829124655-8ba013d7a129
github.com/disintegration/imaging v1.6.2
github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e
github.com/egirna/icap-client v0.1.1
+2 -2
View File
@@ -858,8 +858,8 @@ github.com/crewjam/httperr v0.2.0 h1:b2BfXR8U3AlIHwNeFFvZ+BV1LFvKLlzMjzaTnZMybNo
github.com/crewjam/httperr v0.2.0/go.mod h1:Jlz+Sg/XqBQhyMjdDiC+GNNRzZTD7x39Gu3pglZ5oH4=
github.com/crewjam/saml v0.4.13 h1:TYHggH/hwP7eArqiXSJUvtOPNzQDyQ7vwmwEqlFWhMc=
github.com/crewjam/saml v0.4.13/go.mod h1:igEejV+fihTIlHXYP8zOec3V5A8y3lws5bQBFsTm4gA=
github.com/cs3org/reva/v2 v2.16.1-0.20230828111521-594d4e103741 h1:y3Tw/ZVGPSDRiCslFUESomgSUOa3SAguOJKpiSk9pls=
github.com/cs3org/reva/v2 v2.16.1-0.20230828111521-594d4e103741/go.mod h1:RvhuweTFqzezjUFU0SIdTXakrEx9vJlMvQ7znPXSP1g=
github.com/cs3org/reva/v2 v2.16.1-0.20230829124655-8ba013d7a129 h1:259bY0/RA/xOxN+7SnRryP5MXbj/GmXgNRqv4LYb8Co=
github.com/cs3org/reva/v2 v2.16.1-0.20230829124655-8ba013d7a129/go.mod h1:RvhuweTFqzezjUFU0SIdTXakrEx9vJlMvQ7znPXSP1g=
github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+2
View File
@@ -9,6 +9,7 @@ import (
authbasic "github.com/owncloud/ocis/v2/services/auth-basic/pkg/config"
authbearer "github.com/owncloud/ocis/v2/services/auth-bearer/pkg/config"
authmachine "github.com/owncloud/ocis/v2/services/auth-machine/pkg/config"
authservice "github.com/owncloud/ocis/v2/services/auth-service/pkg/config"
eventhistory "github.com/owncloud/ocis/v2/services/eventhistory/pkg/config"
frontend "github.com/owncloud/ocis/v2/services/frontend/pkg/config"
gateway "github.com/owncloud/ocis/v2/services/gateway/pkg/config"
@@ -83,6 +84,7 @@ type Config struct {
AuthBasic *authbasic.Config `yaml:"auth_basic"`
AuthBearer *authbearer.Config `yaml:"auth_bearer"`
AuthMachine *authmachine.Config `yaml:"auth_machine"`
AuthService *authservice.Config `yaml:"auth_service"`
EventHistory *eventhistory.Config `yaml:"eventhistory"`
Frontend *frontend.Config `yaml:"frontend"`
Gateway *gateway.Config `yaml:"gateway"`
+2
View File
@@ -8,6 +8,7 @@ import (
authbasic "github.com/owncloud/ocis/v2/services/auth-basic/pkg/config/defaults"
authbearer "github.com/owncloud/ocis/v2/services/auth-bearer/pkg/config/defaults"
authmachine "github.com/owncloud/ocis/v2/services/auth-machine/pkg/config/defaults"
authservice "github.com/owncloud/ocis/v2/services/auth-service/pkg/config/defaults"
eventhistory "github.com/owncloud/ocis/v2/services/eventhistory/pkg/config/defaults"
frontend "github.com/owncloud/ocis/v2/services/frontend/pkg/config/defaults"
gateway "github.com/owncloud/ocis/v2/services/gateway/pkg/config/defaults"
@@ -55,6 +56,7 @@ func DefaultConfig() *Config {
AuthBasic: authbasic.DefaultConfig(),
AuthBearer: authbearer.DefaultConfig(),
AuthMachine: authmachine.DefaultConfig(),
AuthService: authservice.DefaultConfig(),
EventHistory: eventhistory.DefaultConfig(),
Frontend: frontend.DefaultConfig(),
Gateway: gateway.DefaultConfig(),
+30
View File
@@ -0,0 +1,30 @@
package command
import (
"github.com/owncloud/ocis/v2/ocis-pkg/config"
"github.com/owncloud/ocis/v2/ocis-pkg/config/configlog"
"github.com/owncloud/ocis/v2/ocis-pkg/config/parser"
"github.com/owncloud/ocis/v2/ocis/pkg/command/helper"
"github.com/owncloud/ocis/v2/ocis/pkg/register"
"github.com/owncloud/ocis/v2/services/auth-service/pkg/command"
"github.com/urfave/cli/v2"
)
// AuthServiceCommand is the entrypoint for the AuthService command.
func AuthServiceCommand(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: cfg.AuthService.Service.Name,
Usage: helper.SubcommandDescription(cfg.AuthService.Service.Name),
Category: "services",
Before: func(c *cli.Context) error {
configlog.Error(parser.ParseConfig(cfg, true))
cfg.AuthService.Commons = cfg.Commons
return nil
},
Subcommands: command.GetCommands(cfg.AuthService),
}
}
func init() {
register.AddCommand(AuthServiceCommand)
}
+6
View File
@@ -15,6 +15,7 @@ import (
authbasic "github.com/owncloud/ocis/v2/services/auth-basic/pkg/command"
authbearer "github.com/owncloud/ocis/v2/services/auth-bearer/pkg/command"
authmachine "github.com/owncloud/ocis/v2/services/auth-machine/pkg/command"
authservice "github.com/owncloud/ocis/v2/services/auth-service/pkg/command"
eventhistory "github.com/owncloud/ocis/v2/services/eventhistory/pkg/command"
frontend "github.com/owncloud/ocis/v2/services/frontend/pkg/command"
gateway "github.com/owncloud/ocis/v2/services/gateway/pkg/command"
@@ -83,6 +84,11 @@ var svccmds = []register.Command{
cfg.AuthMachine.Commons = cfg.Commons
})
},
func(cfg *config.Config) *cli.Command {
return ServiceCommand(cfg, cfg.AuthService.Service.Name, authservice.GetCommands(cfg.AuthService), func(c *config.Config) {
cfg.AuthService.Commons = cfg.Commons
})
},
func(cfg *config.Config) *cli.Command {
return ServiceCommand(cfg, cfg.EventHistory.Service.Name, eventhistory.GetCommands(cfg.EventHistory), func(c *config.Config) {
cfg.EventHistory.Commons = cfg.Commons
+53 -9
View File
@@ -55,10 +55,11 @@ type GraphApplication struct {
}
type GraphService struct {
Application GraphApplication
Events Events
Spaces InsecureService
Identity LdapBasedService
Application GraphApplication
Events Events
Spaces InsecureService
Identity LdapBasedService
ServiceAccount ServiceAccount `yaml:"service_account"`
}
type ServiceUserPasswordsSettings struct {
@@ -101,7 +102,8 @@ type ThumbnailService struct {
}
type Search struct {
Events Events
Events Events
ServiceAccount ServiceAccount `yaml:"service_account"`
}
type Audit struct {
@@ -113,8 +115,9 @@ type Sharing struct {
}
type StorageUsers struct {
Events Events
MountID string `yaml:"mount_id"`
Events Events
MountID string `yaml:"mount_id"`
ServiceAccount ServiceAccount `yaml:"service_account"`
}
type Gateway struct {
@@ -126,7 +129,16 @@ type StorageRegistry struct {
}
type Notifications struct {
Notifications struct{ Events Events } // The notifications config has a field called notifications
Notifications struct{ Events Events } // The notifications config has a field called notifications
ServiceAccount ServiceAccount `yaml:"service_account"`
}
type Userlog struct {
ServiceAccount ServiceAccount `yaml:"service_account"`
}
type AuthService struct {
ServiceAccount ServiceAccount `yaml:"service_account"`
}
type Nats struct {
@@ -136,6 +148,12 @@ type Nats struct {
}
}
// ServiceAccount is the configuration for the used service account
type ServiceAccount struct {
ServiceAccountID string `yaml:"service_account_id"`
ServiceAccountSecret string `yaml:"service_account_secret"`
}
// TODO: use the oCIS config struct instead of this custom struct
// We can't use it right now, because it would need "omitempty" on
// all elements, in order to produce a slim config file with `ocis init`.
@@ -173,6 +191,8 @@ type OcisConfig struct {
Notifications Notifications
Nats Nats
Gateway Gateway
Userlog Userlog
AuthService AuthService `yaml:"auth_service"`
}
func checkConfigPath(configPath string) error {
@@ -225,6 +245,7 @@ func CreateConfig(insecure, forceOverwrite bool, configPath, adminPassword strin
adminUserID := uuid.Must(uuid.NewV4()).String()
graphApplicationID := uuid.Must(uuid.NewV4()).String()
storageUsersMountID := uuid.Must(uuid.NewV4()).String()
serviceAccountID := uuid.Must(uuid.NewV4()).String()
idmServicePassword, err := generators.GenerateRandomPassword(passwordLength)
if err != nil {
@@ -266,6 +287,15 @@ func CreateConfig(insecure, forceOverwrite bool, configPath, adminPassword strin
if err != nil {
return fmt.Errorf("could not generate random password for thumbnailsTransferSecret: %s", err)
}
serviceAccountSecret, err := generators.GenerateRandomPassword(passwordLength)
if err != nil {
return fmt.Errorf("could not generate random password for thumbnailsTransferSecret: %s", err)
}
serviceAccount := ServiceAccount{
ServiceAccountID: serviceAccountID,
ServiceAccountSecret: serviceAccountSecret,
}
cfg := OcisConfig{
TokenManager: TokenManager{
@@ -319,6 +349,7 @@ func CreateConfig(insecure, forceOverwrite bool, configPath, adminPassword strin
BindPassword: idmServicePassword,
},
},
ServiceAccount: serviceAccount,
},
Thumbnails: ThumbnailService{
Thumbnail: ThumbnailSettings{
@@ -331,7 +362,20 @@ func CreateConfig(insecure, forceOverwrite bool, configPath, adminPassword strin
},
},
StorageUsers: StorageUsers{
MountID: storageUsersMountID,
MountID: storageUsersMountID,
ServiceAccount: serviceAccount,
},
Userlog: Userlog{
ServiceAccount: serviceAccount,
},
AuthService: AuthService{
ServiceAccount: serviceAccount,
},
Search: Search{
ServiceAccount: serviceAccount,
},
Notifications: Notifications{
ServiceAccount: serviceAccount,
},
}
+6
View File
@@ -26,6 +26,7 @@ import (
audit "github.com/owncloud/ocis/v2/services/audit/pkg/command"
authbasic "github.com/owncloud/ocis/v2/services/auth-basic/pkg/command"
authmachine "github.com/owncloud/ocis/v2/services/auth-machine/pkg/command"
authservice "github.com/owncloud/ocis/v2/services/auth-service/pkg/command"
eventhistory "github.com/owncloud/ocis/v2/services/eventhistory/pkg/command"
frontend "github.com/owncloud/ocis/v2/services/frontend/pkg/command"
gateway "github.com/owncloud/ocis/v2/services/gateway/pkg/command"
@@ -135,6 +136,11 @@ func NewService(options ...Option) (*Service, error) {
cfg.AuthMachine.Commons = cfg.Commons
return authmachine.Execute(cfg.AuthMachine)
})
reg(opts.Config.AuthService.Service.Name, func(ctx context.Context, cfg *ociscfg.Config) error {
cfg.AuthService.Context = ctx
cfg.AuthService.Commons = cfg.Commons
return authservice.Execute(cfg.AuthService)
})
reg(opts.Config.EventHistory.Service.Name, func(ctx context.Context, cfg *ociscfg.Config) error {
cfg.EventHistory.Context = ctx
cfg.EventHistory.Commons = cfg.Commons
+8
View File
@@ -6,6 +6,14 @@ The `auth-basic` service is responsible for validating authentication of incomin
To enable `auth-basic`, you first must set `PROXY_ENABLE_BASIC_AUTH` to `true`.
## The `auth` Service Family
ocis uses serveral authentication services for different use cases. All services that start with `auth-` are part of the authentication service family. Each member authenticates requests with different scopes. As of now, these services exist:
- `auth-basic` handles basic authentication
- `auth-bearer` handles oidc authentication
- `auth-machine` handles interservice authentication when a user is impersonated
- `auth-service` handles interservice authentication when using service accounts
## Auth Managers
Since the `auth-basic` service does not do any validation itself, it needs to be configured with an authentication manager. One can use the `AUTH_BASIC_AUTH_MANAGER` environment variable to configure this. Currently only one auth manager is supported: `"ldap"`
+9 -1
View File
@@ -2,7 +2,15 @@
The oCIS Auth Bearer service communicates with the configured OpenID Connect identity provider to authenticate requests. OpenID Connect is the default authentication mechanism for all clients: web, desktop and mobile. Basic auth is only used for testing and has to be explicity enabled.
## Built in OpenID Connect identity provider
## The `auth` Service Family
ocis uses serveral authentication services for different use cases. All services that start with `auth-` are part of the authentication service family. Each member authenticates requests with different scopes. As of now, these services exist:
- `auth-basic` handles basic authentication
- `auth-bearer` handles oidc authentication
- `auth-machine` handles interservice authentication when a user is impersonated
- `auth-service` handles interservice authentication when using service accounts
## Built in OpenID Connect Identity Provider
A default oCIS deployment will start a [built in OpenID Connect identity provider](https://github.com/owncloud/ocis/tree/master/services/idp) but can be configured to use an external one as well.
+17
View File
@@ -0,0 +1,17 @@
# Auth-Machine
The oCIS Auth Machine is used for interservice communication when using user impersonation.
ocis uses serveral authentication services for different use cases. All services that start with `auth-` are part of the authentication service family. Each member authenticates requests with different scopes. As of now, these services exist:
- `auth-basic` handles basic authentication
- `auth-bearer` handles oidc authentication
- `auth-machine` handles interservice authentication when a user is impersonated
- `auth-service` handles interservice authentication when using service accounts
## User Impersonation
When one ocis service is trying to talk to other ocis services, it needs to authenticate itself. To do so, it will impersonate a user using the `auth-machine` service. It will then act on behalf of this user. Any action will show up as action of this specific user, which gets visible when e.g. logged in the audit log.
## Deprecation
With the upcoming `auth-service` service, the `auth-machine` service will be used less frequently and is probably a candidate for deprecation.
+37
View File
@@ -0,0 +1,37 @@
SHELL := bash
NAME := auth-service
include ../../.make/recursion.mk
############ tooling ############
ifneq (, $(shell command -v 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:
+19
View File
@@ -0,0 +1,19 @@
# Auth-Service
The ocis Auth Service is used to authenticate service accounts. Compared to normal accounts, service accounts are ocis internal only and not available as ordinary users like via LDAP.
## The `auth` Service Family
ocis uses serveral authentication services for different use cases. All services that start with `auth-` are part of the authentication service family. Each member authenticates requests with different scopes. As of now, these services exist:
- `auth-basic` handles basic authentication
- `auth-bearer` handles oidc authentication
- `auth-machine` handles interservice authentication when a user is impersonated
- `auth-service` handles interservice authentication when using service accounts
## Service Accounts
Service accounts are user accounts that are only used for inter service communication. The users have no personal space, do not show up in user lists and cannot login via the UI. Service accounts can be configured in the settings service. Only the `admin` service user is available for now. Additionally to the actions it can do via its role, all service users can stat all files on all spaces.
## Configuring Service Accounts
By using the envvars `OCIS_SERVICE_ACCOUNT_ID` and `OCIS_SERVICE_ACCOUNT_SECRET`, one can configure the ID and the secret of the service user. The secret can be rotated regulary to increase security. For activating a new secret, all services where the envvars are used need to be restarted. The secret is always and only stored in memory and never written into any persistant store. Though you can use any string for the service account, it is recommmended to use a UUIDv4 string.
@@ -0,0 +1,14 @@
package main
import (
"os"
"github.com/owncloud/ocis/v2/services/auth-service/pkg/command"
"github.com/owncloud/ocis/v2/services/auth-service/pkg/config/defaults"
)
func main() {
if err := command.Execute(defaults.DefaultConfig()); err != nil {
os.Exit(1)
}
}
@@ -0,0 +1,54 @@
package command
import (
"fmt"
"net/http"
"github.com/owncloud/ocis/v2/ocis-pkg/config/configlog"
"github.com/owncloud/ocis/v2/services/auth-service/pkg/config"
"github.com/owncloud/ocis/v2/services/auth-service/pkg/config/parser"
"github.com/owncloud/ocis/v2/services/auth-service/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 configlog.ReturnError(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
},
}
}
+34
View File
@@ -0,0 +1,34 @@
package command
import (
"os"
"github.com/owncloud/ocis/v2/ocis-pkg/clihelper"
"github.com/owncloud/ocis/v2/services/auth-service/pkg/config"
"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-auth-service command.
func Execute(cfg *config.Config) error {
app := clihelper.DefaultApp(&cli.App{
Name: "auth-service",
Usage: "Provide service authentication for oCIS",
Commands: GetCommands(cfg),
})
return app.Run(os.Args)
}
@@ -0,0 +1,99 @@
package command
import (
"context"
"fmt"
"os"
"path"
"github.com/cs3org/reva/v2/cmd/revad/runtime"
"github.com/gofrs/uuid"
"github.com/oklog/run"
"github.com/owncloud/ocis/v2/ocis-pkg/config/configlog"
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
"github.com/owncloud/ocis/v2/ocis-pkg/sync"
"github.com/owncloud/ocis/v2/ocis-pkg/version"
"github.com/owncloud/ocis/v2/services/auth-service/pkg/config"
"github.com/owncloud/ocis/v2/services/auth-service/pkg/config/parser"
"github.com/owncloud/ocis/v2/services/auth-service/pkg/logging"
"github.com/owncloud/ocis/v2/services/auth-service/pkg/revaconfig"
"github.com/owncloud/ocis/v2/services/auth-service/pkg/server/debug"
"github.com/owncloud/ocis/v2/services/auth-service/pkg/tracing"
"github.com/urfave/cli/v2"
)
// Server is the entry point for the server command.
func Server(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "server",
Usage: fmt.Sprintf("start the %s service without runtime (unsupervised mode)", cfg.Service.Name),
Category: "server",
Before: func(c *cli.Context) error {
return configlog.ReturnFatal(parser.ParseConfig(cfg))
},
Action: func(c *cli.Context) error {
logger := logging.Configure(cfg.Service.Name, cfg.Log)
err := tracing.Configure(cfg, logger)
if err != nil {
return err
}
gr := run.Group{}
ctx, cancel := defineContext(cfg)
defer cancel()
pidFile := path.Join(os.TempDir(), "revad-"+cfg.Service.Name+"-"+uuid.Must(uuid.NewV4()).String()+".pid")
rcfg := revaconfig.AuthMachineConfigFromStruct(cfg)
gr.Add(func() error {
runtime.RunWithOptions(rcfg, pidFile, runtime.WithLogger(&logger.Logger))
return nil
}, func(err error) {
logger.Error().
Err(err).
Str("server", cfg.Service.Name).
Msg("Shutting down server")
cancel()
})
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(debugServer.ListenAndServe, func(_ error) {
cancel()
})
if !cfg.Supervised {
sync.Trap(&gr, cancel)
}
grpcSvc := registry.BuildGRPCService(cfg.GRPC.Namespace+"."+cfg.Service.Name, uuid.Must(uuid.NewV4()).String(), cfg.GRPC.Addr, version.GetString())
if err := registry.RegisterService(ctx, grpcSvc, logger); err != nil {
logger.Fatal().Err(err).Msg("failed to register the grpc service")
}
return gr.Run()
},
}
}
// defineContext sets the context for the service. If there is a context configured it will create a new child from it,
// if not, it will create a root context that can be cancelled.
func defineContext(cfg *config.Config) (context.Context, context.CancelFunc) {
return func() (context.Context, context.CancelFunc) {
if cfg.Context == nil {
return context.WithCancel(context.Background())
}
return context.WithCancel(cfg.Context)
}()
}
@@ -0,0 +1,50 @@
package command
import (
"fmt"
"os"
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
"github.com/owncloud/ocis/v2/ocis-pkg/version"
tw "github.com/olekukonko/tablewriter"
"github.com/owncloud/ocis/v2/services/auth-service/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 service instances",
Category: "info",
Action: func(c *cli.Context) error {
fmt.Println("Version: " + version.GetString())
fmt.Printf("Compiled: %s\n", version.Compiled())
fmt.Println("")
reg := registry.GetRegistry()
services, err := reg.GetService(cfg.GRPC.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
},
}
}
@@ -0,0 +1,63 @@
package config
import (
"context"
"github.com/owncloud/ocis/v2/ocis-pkg/shared"
)
type Config struct {
Commons *shared.Commons `yaml:"-"` // don't use this directly as configuration for a service
Service Service `yaml:"-"`
Tracing *Tracing `yaml:"tracing"`
Log *Log `yaml:"log"`
Debug Debug `yaml:"debug"`
GRPC GRPCConfig `yaml:"grpc"`
TokenManager *TokenManager `yaml:"token_manager"`
Reva *shared.Reva `yaml:"reva"`
// TODO: when using multiple service accounts we need to find a way to configure them
ServiceAccount ServiceAccount `yaml:"service_account"`
Supervised bool `yaml:"-"`
Context context.Context `yaml:"-"`
}
type Tracing struct {
Enabled bool `yaml:"enabled" env:"OCIS_TRACING_ENABLED;AUTH_SERVICE_TRACING_ENABLED" desc:"Activates tracing."`
Type string `yaml:"type" env:"OCIS_TRACING_TYPE;AUTH_SERVICE_TRACING_TYPE" desc:"The type of tracing. Defaults to '', which is the same as 'jaeger'. Allowed tracing types are 'jaeger' and '' as of now."`
Endpoint string `yaml:"endpoint" env:"OCIS_TRACING_ENDPOINT;AUTH_SERVICE_TRACING_ENDPOINT" desc:"The endpoint of the tracing agent."`
Collector string `yaml:"collector" env:"OCIS_TRACING_COLLECTOR;AUTH_SERVICE_TRACING_COLLECTOR" desc:"The HTTP endpoint for sending spans directly to a collector, i.e. http://jaeger-collector:14268/api/traces. Only used if the tracing endpoint is unset."`
}
type Log struct {
Level string `yaml:"level" env:"OCIS_LOG_LEVEL;AUTH_SERVICE_LOG_LEVEL" desc:"The log level. Valid values are: 'panic', 'fatal', 'error', 'warn', 'info', 'debug', 'trace'."`
Pretty bool `yaml:"pretty" env:"OCIS_LOG_PRETTY;AUTH_SERVICE_LOG_PRETTY" desc:"Activates pretty log output."`
Color bool `yaml:"color" env:"OCIS_LOG_COLOR;AUTH_SERVICE_LOG_COLOR" desc:"Activates colorized log output."`
File string `yaml:"file" env:"OCIS_LOG_FILE;AUTH_SERVICE_LOG_FILE" desc:"The path to the log file. Activates logging to this file if set."`
}
type Service struct {
Name string `yaml:"-"`
}
type Debug struct {
Addr string `yaml:"addr" env:"AUTH_SERVICE_DEBUG_ADDR" desc:"Bind address of the debug server, where metrics, health, config and debug endpoints will be exposed."`
Token string `yaml:"token" env:"AUTH_SERVICE_DEBUG_TOKEN" desc:"Token to secure the metrics endpoint."`
Pprof bool `yaml:"pprof" env:"AUTH_SERVICE_DEBUG_PPROF" desc:"Enables pprof, which can be used for profiling."`
Zpages bool `yaml:"zpages" env:"AUTH_SERVICE_DEBUG_ZPAGES" desc:"Enables zpages, which can be used for collecting and viewing in-memory traces."`
}
type GRPCConfig struct {
Addr string `yaml:"addr" env:"AUTH_SERVICE_GRPC_ADDR" desc:"The bind address of the GRPC service."`
TLS *shared.GRPCServiceTLS `yaml:"tls"`
Namespace string `yaml:"-"`
Protocol string `yaml:"protocol" env:"AUTH_SERVICE_GRPC_PROTOCOL" desc:"The transport protocol of the GRPC service."`
}
// ServiceAccount is the configuration for the used service account
type ServiceAccount struct {
ServiceAccountID string `yaml:"service_account_id" env:"OCIS_SERVICE_ACCOUNT_ID;AUTH_SERVICE_SERVICE_ACCOUNT_ID" desc:"The ID of the service account the service should use. See the 'auth-service' service description for more details."`
ServiceAccountSecret string `yaml:"service_account_secret" env:"OCIS_SERVICE_ACCOUNT_SECRET;AUTH_SERVICE_SERVICE_ACCOUNT_SECRET" desc:"The service account secret."`
}
@@ -0,0 +1,83 @@
package defaults
import (
"github.com/owncloud/ocis/v2/ocis-pkg/shared"
"github.com/owncloud/ocis/v2/ocis-pkg/structs"
"github.com/owncloud/ocis/v2/services/auth-service/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:9169",
Token: "",
Pprof: false,
Zpages: false,
},
GRPC: config.GRPCConfig{
Addr: "127.0.0.1:9199",
Namespace: "com.owncloud.api",
Protocol: "tcp",
},
Service: config.Service{
Name: "auth-service",
},
Reva: shared.DefaultRevaConfig(),
}
}
// EnsureDefaults adds default values to the configuration if they are not set yet
func EnsureDefaults(cfg *config.Config) {
// provide with defaults for shared logging, since we need a valid destination address for "envdecode".
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 "envdecode".
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{}
}
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.GRPC.TLS == nil && cfg.Commons != nil {
cfg.GRPC.TLS = structs.CopyOrZeroValue(cfg.Commons.GRPCServiceTLS)
}
}
// Sanitize sanitized the configuration
func Sanitize(cfg *config.Config) {
// nothing to sanitize here atm
}
@@ -0,0 +1,42 @@
package parser
import (
"errors"
ociscfg "github.com/owncloud/ocis/v2/ocis-pkg/config"
"github.com/owncloud/ocis/v2/ocis-pkg/shared"
"github.com/owncloud/ocis/v2/services/auth-service/pkg/config"
"github.com/owncloud/ocis/v2/services/auth-service/pkg/config/defaults"
"github.com/owncloud/ocis/v2/ocis-pkg/config/envdecode"
)
// ParseConfig loads 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 Validate(cfg)
}
func Validate(cfg *config.Config) error {
if cfg.TokenManager.JWTSecret == "" {
return shared.MissingJWTTokenError(cfg.Service.Name)
}
return nil
}
+6
View File
@@ -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:"OCIS_JWT_SECRET;AUTH_MACHINE_JWT_SECRET" desc:"The secret to mint and validate jwt tokens."`
}
@@ -0,0 +1,17 @@
package logging
import (
"github.com/owncloud/ocis/v2/ocis-pkg/log"
"github.com/owncloud/ocis/v2/services/auth-service/pkg/config"
)
// 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),
)
}
@@ -0,0 +1,53 @@
package revaconfig
import (
"github.com/owncloud/ocis/v2/services/auth-service/pkg/config"
)
// AuthMachineConfigFromStruct will adapt an oCIS config struct into a reva mapstructure to start a reva service.
func AuthMachineConfigFromStruct(cfg *config.Config) map[string]interface{} {
return map[string]interface{}{
"core": map[string]interface{}{
"tracing_enabled": cfg.Tracing.Enabled,
"tracing_exporter": cfg.Tracing.Type,
"tracing_endpoint": cfg.Tracing.Endpoint,
"tracing_collector": cfg.Tracing.Collector,
"tracing_service_name": cfg.Service.Name,
},
"shared": map[string]interface{}{
"jwt_secret": cfg.TokenManager.JWTSecret,
"gatewaysvc": cfg.Reva.Address,
"grpc_client_options": cfg.Reva.GetGRPCClientConfig(),
},
"grpc": map[string]interface{}{
"network": cfg.GRPC.Protocol,
"address": cfg.GRPC.Addr,
"tls_settings": map[string]interface{}{
"enabled": cfg.GRPC.TLS.Enabled,
"certificate": cfg.GRPC.TLS.Cert,
"key": cfg.GRPC.TLS.Key,
},
"services": map[string]interface{}{
"authprovider": map[string]interface{}{
"auth_manager": "serviceaccounts",
"auth_managers": map[string]interface{}{
"serviceaccounts": map[string]interface{}{
"service_accounts": []map[string]interface{}{
{
"id": cfg.ServiceAccount.ServiceAccountID,
"secret": cfg.ServiceAccount.ServiceAccountSecret,
},
},
},
},
},
},
"interceptors": map[string]interface{}{
"prometheus": map[string]interface{}{
"namespace": "ocis",
"subsystem": "auth_service",
},
},
},
}
}
@@ -0,0 +1,50 @@
package debug
import (
"context"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
"github.com/owncloud/ocis/v2/services/auth-service/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,63 @@
package debug
import (
"io"
"net/http"
"github.com/owncloud/ocis/v2/ocis-pkg/service/debug"
"github.com/owncloud/ocis/v2/ocis-pkg/version"
"github.com/owncloud/ocis/v2/services/auth-service/pkg/config"
)
// 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.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(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)
}
}
}
@@ -0,0 +1,25 @@
package tracing
import (
"github.com/owncloud/ocis/v2/ocis-pkg/log"
"github.com/owncloud/ocis/v2/services/auth-service/pkg/config"
"go.opentelemetry.io/otel/trace"
pkgtrace "github.com/owncloud/ocis/v2/ocis-pkg/tracing"
)
var (
// TraceProvider is the global trace provider for the service.
TraceProvider = trace.NewNoopTracerProvider()
)
func Configure(cfg *config.Config, logger log.Logger) 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
}
+1
View File
@@ -38,6 +38,7 @@ type Config struct {
AuthBasicEndpoint string `yaml:"-"`
AuthBearerEndpoint string `yaml:"-"`
AuthMachineEndpoint string `yaml:"-"`
AuthServiceEndpoint string `yaml:"-"`
StoragePublicLinkEndpoint string `yaml:"-"`
StorageUsersEndpoint string `yaml:"-"`
StorageSharesEndpoint string `yaml:"-"`
@@ -55,6 +55,7 @@ func DefaultConfig() *config.Config {
AppRegistryEndpoint: "com.owncloud.api.app-registry",
AuthBasicEndpoint: "com.owncloud.api.auth-basic",
AuthMachineEndpoint: "com.owncloud.api.auth-machine",
AuthServiceEndpoint: "com.owncloud.api.auth-service",
GroupsEndpoint: "com.owncloud.api.groups",
PermissionsEndpoint: "com.owncloud.api.settings",
SharingEndpoint: "com.owncloud.api.sharing",
+4 -3
View File
@@ -80,9 +80,10 @@ func GatewayConfigFromStruct(cfg *config.Config, logger log.Logger) map[string]i
"drivers": map[string]interface{}{
"static": map[string]interface{}{
"rules": map[string]interface{}{
"basic": cfg.AuthBasicEndpoint,
"machine": cfg.AuthMachineEndpoint,
"publicshares": cfg.StoragePublicLinkEndpoint,
"basic": cfg.AuthBasicEndpoint,
"machine": cfg.AuthMachineEndpoint,
"publicshares": cfg.StoragePublicLinkEndpoint,
"serviceaccounts": cfg.AuthServiceEndpoint,
},
},
},
+8 -2
View File
@@ -30,8 +30,8 @@ type Config struct {
Identity Identity `yaml:"identity"`
Events Events `yaml:"events"`
MachineAuthAPIKey string `yaml:"machine_auth_api_key" env:"OCIS_MACHINE_AUTH_API_KEY;USERLOG_MACHINE_AUTH_API_KEY" desc:"Machine auth API key used to validate internal requests necessary to access resources from other services."`
Keycloak Keycloak `yaml:"keycloak"`
Keycloak Keycloak `yaml:"keycloak"`
ServiceAccount ServiceAccount `yaml:"service_account"`
Context context.Context `yaml:"-"`
}
@@ -137,3 +137,9 @@ type Keycloak struct {
UserRealm string `yaml:"user_realm" env:"OCIS_KEYCLOAK_USER_REALM;GRAPH_KEYCLOAK_USER_REALM" desc:"The realm users are defined."`
InsecureSkipVerify bool `yaml:"insecure_skip_verify" env:"OCIS_KEYCLOAK_INSECURE_SKIP_VERIFY;GRAPH_KEYCLOAK_INSECURE_SKIP_VERIFY" desc:"Disable TLS certificate validation for Keycloak connections. Do not set this in production environments."`
}
// ServiceAccount is the configuration for the used service account
type ServiceAccount struct {
ServiceAccountID string `yaml:"service_account_id" env:"OCIS_SERVICE_ACCOUNT_ID;GRAPH_SERVICE_ACCOUNT_ID" desc:"The ID of the service account the service should use. See the 'auth-service' service description for more details."`
ServiceAccountSecret string `yaml:"service_account_secret" env:"OCIS_SERVICE_ACCOUNT_SECRET;GRAPH_SERVICE_ACCOUNT_SECRET" desc:"The service account secret."`
}
@@ -159,10 +159,6 @@ func EnsureDefaults(cfg *config.Config) {
cfg.HTTP.TLS = cfg.Commons.HTTPServiceTLS
}
if cfg.MachineAuthAPIKey == "" && cfg.Commons != nil && cfg.Commons.MachineAuthAPIKey != "" {
cfg.MachineAuthAPIKey = cfg.Commons.MachineAuthAPIKey
}
if cfg.Identity.LDAP.GroupCreateBaseDN == "" {
cfg.Identity.LDAP.GroupCreateBaseDN = cfg.Identity.LDAP.GroupBaseDN
}
@@ -99,7 +99,7 @@ func (g Graph) GatherPersonalData(usr *user.User, ref *provider.Reference, token
}
// the context might already be cancelled. We need to impersonate the acting user again
ctx, err := utils.ImpersonateUser(usr, gatewayClient, g.config.MachineAuthAPIKey)
ctx, err := utils.GetServiceUserContext(g.config.ServiceAccount.ServiceAccountID, gatewayClient, g.config.ServiceAccount.ServiceAccountSecret)
if err != nil {
g.logger.Error().Err(err).Str("userID", usr.GetId().GetOpaqueId()).Msg("cannot impersonate user")
}
@@ -162,7 +162,7 @@ func (g Graph) upload(u *user.User, data []byte, ref *provider.Reference, th str
return err
}
ctx, err := utils.ImpersonateUser(u, gatewayClient, g.config.MachineAuthAPIKey)
ctx, err := utils.GetServiceUserContext(g.config.ServiceAccount.ServiceAccountID, gatewayClient, g.config.ServiceAccount.ServiceAccountSecret)
if err != nil {
return err
}
+1 -1
View File
@@ -116,7 +116,7 @@ func Server(cfg *config.Config) *cli.Command {
logger.Fatal().Err(err).Str("addr", cfg.Notifications.RevaGateway).Msg("could not get reva gateway selector")
}
valueService := settingssvc.NewValueService("com.owncloud.api.settings", grpcClient)
svc := service.NewEventsNotifier(evts, channel, logger, gatewaySelector, valueService, cfg.Notifications.MachineAuthAPIKey, cfg.Notifications.EmailTemplatePath, cfg.WebUIURL)
svc := service.NewEventsNotifier(evts, channel, logger, gatewaySelector, valueService, cfg.ServiceAccount.ServiceAccountID, cfg.ServiceAccount.ServiceAccountSecret, cfg.Notifications.EmailTemplatePath, cfg.WebUIURL)
gr.Add(svc.Run, func(error) {
cancel()
+9 -3
View File
@@ -18,8 +18,9 @@ type Config struct {
WebUIURL string `yaml:"ocis_url" env:"OCIS_URL;NOTIFICATIONS_WEB_UI_URL" desc:"The public facing URL of the oCIS Web UI, used e.g. when sending notification eMails"`
Notifications Notifications `yaml:"notifications"`
GRPCClientTLS shared.GRPCClientTLS `yaml:"grpc_client_tls"`
Notifications Notifications `yaml:"notifications"`
GRPCClientTLS shared.GRPCClientTLS `yaml:"grpc_client_tls"`
ServiceAccount ServiceAccount `yaml:"service_account"`
Context context.Context `yaml:"-"`
}
@@ -28,7 +29,6 @@ type Config struct {
type Notifications struct {
SMTP SMTP `yaml:"SMTP"`
Events Events `yaml:"events"`
MachineAuthAPIKey string `yaml:"machine_auth_api_key" env:"OCIS_MACHINE_AUTH_API_KEY;NOTIFICATIONS_MACHINE_AUTH_API_KEY" desc:"Machine auth API key used to validate internal requests necessary to access resources from other services."`
EmailTemplatePath string `yaml:"email_template_path" env:"OCIS_EMAIL_TEMPLATE_PATH;NOTIFICATIONS_EMAIL_TEMPLATE_PATH" desc:"Path to Email notification templates overriding embedded ones."`
TranslationPath string `yaml:"translation_path" env:"OCIS_TRANSLATION_PATH,NOTIFICATIONS_TRANSLATION_PATH" desc:"(optional) Set this to a path with custom translations to overwrite the builtin translations. Note that file and folder naming rules apply, see the documentation for more details."`
RevaGateway string `yaml:"reva_gateway" env:"OCIS_REVA_GATEWAY" desc:"CS3 gateway used to look up user metadata"`
@@ -55,3 +55,9 @@ type Events struct {
TLSRootCACertificate string `yaml:"tls_root_ca_certificate" env:"OCIS_EVENTS_TLS_ROOT_CA_CERTIFICATE;NOTIFICATIONS_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."`
EnableTLS bool `yaml:"enable_tls" env:"OCIS_EVENTS_ENABLE_TLS;NOTIFICATIONS_EVENTS_ENABLE_TLS" desc:"Enable TLS for the connection to the events broker. The events broker is the ocis service which receives and delivers events between the services.."`
}
// ServiceAccount is the configuration for the used service account
type ServiceAccount struct {
ServiceAccountID string `yaml:"service_account_id" env:"OCIS_SERVICE_ACCOUNT_ID;NOTIFICATIONS_SERVICE_ACCOUNT_ID" desc:"The ID of the service account the service should use. See the 'auth-service' service description for more details."`
ServiceAccountSecret string `yaml:"service_account_secret" env:"OCIS_SERVICE_ACCOUNT_SECRET;NOTIFICATIONS_SERVICE_ACCOUNT_SECRET" desc:"The service account secret."`
}
@@ -73,9 +73,6 @@ func EnsureDefaults(cfg *config.Config) {
cfg.Tracing = &config.Tracing{}
}
if cfg.Notifications.MachineAuthAPIKey == "" && cfg.Commons != nil && cfg.Commons.MachineAuthAPIKey != "" {
cfg.Notifications.MachineAuthAPIKey = cfg.Commons.MachineAuthAPIKey
}
if cfg.Notifications.GRPCClientTLS == nil && cfg.Commons != nil {
cfg.Notifications.GRPCClientTLS = structs.CopyOrZeroValue(cfg.Commons.GRPCClientTLS)
}
@@ -4,7 +4,6 @@ import (
"errors"
ociscfg "github.com/owncloud/ocis/v2/ocis-pkg/config"
"github.com/owncloud/ocis/v2/ocis-pkg/shared"
"github.com/owncloud/ocis/v2/services/notifications/pkg/config"
"github.com/owncloud/ocis/v2/services/notifications/pkg/config/defaults"
@@ -34,9 +33,5 @@ func ParseConfig(cfg *config.Config) error {
}
func Validate(cfg *config.Config) error {
if cfg.Notifications.MachineAuthAPIKey == "" {
return shared.MissingMachineAuthApiKeyError(cfg.Service.Name)
}
return nil
}
+22 -20
View File
@@ -42,32 +42,34 @@ func NewEventsNotifier(
logger log.Logger,
gatewaySelector pool.Selectable[gateway.GatewayAPIClient],
valueService settingssvc.ValueService,
machineAuthAPIKey, emailTemplatePath, ocisURL string) Service {
serviceAccountID, serviceAccountSecret, emailTemplatePath, ocisURL string) Service {
return eventsNotifier{
logger: logger,
channel: channel,
events: events,
signals: make(chan os.Signal, 1),
gatewaySelector: gatewaySelector,
valueService: valueService,
machineAuthAPIKey: machineAuthAPIKey,
emailTemplatePath: emailTemplatePath,
ocisURL: ocisURL,
logger: logger,
channel: channel,
events: events,
signals: make(chan os.Signal, 1),
gatewaySelector: gatewaySelector,
valueService: valueService,
serviceAccountID: serviceAccountID,
serviceAccountSecret: serviceAccountSecret,
emailTemplatePath: emailTemplatePath,
ocisURL: ocisURL,
}
}
type eventsNotifier struct {
logger log.Logger
channel channels.Channel
events <-chan events.Event
signals chan os.Signal
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
valueService settingssvc.ValueService
machineAuthAPIKey string
emailTemplatePath string
translationPath string
ocisURL string
logger log.Logger
channel channels.Channel
events <-chan events.Event
signals chan os.Signal
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
valueService settingssvc.ValueService
emailTemplatePath string
translationPath string
ocisURL string
serviceAccountID string
serviceAccountSecret string
}
func (s eventsNotifier) Run() error {
@@ -77,7 +77,7 @@ var _ = Describe("Notifications", func() {
cfg := defaults.FullDefaultConfig()
cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
ch := make(chan events.Event)
evts := service.NewEventsNotifier(ch, tc, log.NewLogger(), gatewaySelector, vs, "", "", "")
evts := service.NewEventsNotifier(ch, tc, log.NewLogger(), gatewaySelector, vs, "", "", "", "")
go evts.Run()
ch <- ev
@@ -275,7 +275,7 @@ var _ = Describe("Notifications X-Site Scripting", func() {
cfg := defaults.FullDefaultConfig()
cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
ch := make(chan events.Event)
evts := service.NewEventsNotifier(ch, tc, log.NewLogger(), gatewaySelector, vs, "", "", "")
evts := service.NewEventsNotifier(ch, tc, log.NewLogger(), gatewaySelector, vs, "", "", "", "")
go evts.Run()
ch <- ev
+23 -11
View File
@@ -19,13 +19,13 @@ func (s eventsNotifier) handleShareCreated(e events.ShareCreated) {
return
}
ownerCtx, owner, err := utils.Impersonate(e.Sharer, gatewayClient, s.machineAuthAPIKey)
ctx, err := utils.GetServiceUserContext(s.serviceAccountID, gatewayClient, s.serviceAccountSecret)
if err != nil {
logger.Error().Err(err).Msg("Could not impersonate sharer")
logger.Error().Err(err).Msg("Could not impersonate service user")
return
}
resourceInfo, err := s.getResourceInfo(ownerCtx, e.ItemID, &fieldmaskpb.FieldMask{Paths: []string{"name"}})
resourceInfo, err := s.getResourceInfo(ctx, e.ItemID, &fieldmaskpb.FieldMask{Paths: []string{"name"}})
if err != nil {
logger.Error().
Err(err).
@@ -41,13 +41,19 @@ func (s eventsNotifier) handleShareCreated(e events.ShareCreated) {
return
}
granteeList := s.ensureGranteeList(ownerCtx, owner.GetId(), e.GranteeUserID, e.GranteeGroupID)
owner, err := utils.GetUser(e.Sharer, gatewayClient)
if err != nil {
logger.Error().Err(err).Msg("Could not get user")
return
}
granteeList := s.ensureGranteeList(ctx, owner.GetId(), e.GranteeUserID, e.GranteeGroupID)
if granteeList == nil {
return
}
sharerDisplayName := owner.GetDisplayName()
recipientList, err := s.render(ownerCtx, email.ShareCreated,
recipientList, err := s.render(ctx, email.ShareCreated,
"ShareGrantee",
map[string]string{
"ShareSharer": sharerDisplayName,
@@ -58,7 +64,7 @@ func (s eventsNotifier) handleShareCreated(e events.ShareCreated) {
s.logger.Error().Err(err).Str("event", "ShareCreated").Msg("could not get render the email")
return
}
s.send(ownerCtx, recipientList)
s.send(ctx, recipientList)
}
func (s eventsNotifier) handleShareExpired(e events.ShareExpired) {
@@ -73,13 +79,13 @@ func (s eventsNotifier) handleShareExpired(e events.ShareExpired) {
return
}
ownerCtx, owner, err := utils.Impersonate(e.ShareOwner, gatewayClient, s.machineAuthAPIKey)
ctx, err := utils.GetServiceUserContext(s.serviceAccountID, gatewayClient, s.serviceAccountSecret)
if err != nil {
logger.Error().Err(err).Msg("Could not impersonate sharer")
return
}
resourceInfo, err := s.getResourceInfo(ownerCtx, e.ItemID, &fieldmaskpb.FieldMask{Paths: []string{"name"}})
resourceInfo, err := s.getResourceInfo(ctx, e.ItemID, &fieldmaskpb.FieldMask{Paths: []string{"name"}})
if err != nil {
logger.Error().
Err(err).
@@ -87,12 +93,18 @@ func (s eventsNotifier) handleShareExpired(e events.ShareExpired) {
return
}
granteeList := s.ensureGranteeList(ownerCtx, owner.GetId(), e.GranteeUserID, e.GranteeGroupID)
owner, err := utils.GetUser(e.ShareOwner, gatewayClient)
if err != nil {
logger.Error().Err(err).Msg("Could not get user")
return
}
granteeList := s.ensureGranteeList(ctx, owner.GetId(), e.GranteeUserID, e.GranteeGroupID)
if granteeList == nil {
return
}
recipientList, err := s.render(ownerCtx, email.ShareExpired,
recipientList, err := s.render(ctx, email.ShareExpired,
"ShareGrantee",
map[string]string{
"ShareFolder": resourceInfo.GetName(),
@@ -102,5 +114,5 @@ func (s eventsNotifier) handleShareExpired(e events.ShareExpired) {
s.logger.Error().Err(err).Str("event", "ShareExpired").Msg("could not get render the email")
return
}
s.send(ownerCtx, recipientList)
s.send(ctx, recipientList)
}
+38 -14
View File
@@ -19,7 +19,7 @@ func (s eventsNotifier) handleSpaceShared(e events.SpaceShared) {
return
}
executantCtx, executant, err := utils.Impersonate(e.Executant, gatewayClient, s.machineAuthAPIKey)
ctx, err := utils.GetServiceUserContext(s.serviceAccountID, gatewayClient, s.serviceAccountSecret)
if err != nil {
logger.Error().
Err(err).
@@ -35,7 +35,7 @@ func (s eventsNotifier) handleSpaceShared(e events.SpaceShared) {
return
}
resourceInfo, err := s.getResourceInfo(executantCtx, &resourceID, nil)
resourceInfo, err := s.getResourceInfo(ctx, &resourceID, nil)
if err != nil {
logger.Error().
Err(err).
@@ -51,16 +51,24 @@ func (s eventsNotifier) handleSpaceShared(e events.SpaceShared) {
return
}
executant, err := utils.GetUser(e.Executant, gatewayClient)
if err != nil {
logger.Error().
Err(err).
Msg("could not get user")
return
}
// Note: We're using the 'executantCtx' (authenticated as the share executant) here for requesting
// the Grantees of the shares. Ideally the notfication service would use some kind of service
// user for this.
granteeList := s.ensureGranteeList(executantCtx, executant.GetId(), e.GranteeUserID, e.GranteeGroupID)
granteeList := s.ensureGranteeList(ctx, executant.GetId(), e.GranteeUserID, e.GranteeGroupID)
if granteeList == nil {
return
}
sharerDisplayName := executant.GetDisplayName()
recipientList, err := s.render(executantCtx, email.SharedSpace,
recipientList, err := s.render(ctx, email.SharedSpace,
"SpaceGrantee",
map[string]string{
"SpaceSharer": sharerDisplayName,
@@ -71,7 +79,7 @@ func (s eventsNotifier) handleSpaceShared(e events.SpaceShared) {
s.logger.Error().Err(err).Str("event", "SharedSpace").Msg("could not get render the email")
return
}
s.send(executantCtx, recipientList)
s.send(ctx, recipientList)
}
func (s eventsNotifier) handleSpaceUnshared(e events.SpaceUnshared) {
@@ -86,7 +94,7 @@ func (s eventsNotifier) handleSpaceUnshared(e events.SpaceUnshared) {
return
}
executantCtx, executant, err := utils.Impersonate(e.Executant, gatewayClient, s.machineAuthAPIKey)
ctx, err := utils.GetServiceUserContext(s.serviceAccountID, gatewayClient, s.serviceAccountSecret)
if err != nil {
logger.Error().Err(err).Msg("could not handle space unshared event")
return
@@ -100,7 +108,7 @@ func (s eventsNotifier) handleSpaceUnshared(e events.SpaceUnshared) {
return
}
resourceInfo, err := s.getResourceInfo(executantCtx, &resourceID, nil)
resourceInfo, err := s.getResourceInfo(ctx, &resourceID, nil)
if err != nil {
logger.Error().
Err(err).
@@ -116,16 +124,24 @@ func (s eventsNotifier) handleSpaceUnshared(e events.SpaceUnshared) {
return
}
executant, err := utils.GetUser(e.Executant, gatewayClient)
if err != nil {
logger.Error().
Err(err).
Msg("could not get user")
return
}
// Note: We're using the 'executantCtx' (authenticated as the share executant) here for requesting
// the Grantees of the shares. Ideally the notfication service would use some kind of service
// user for this.
granteeList := s.ensureGranteeList(executantCtx, executant.GetId(), e.GranteeUserID, e.GranteeGroupID)
granteeList := s.ensureGranteeList(ctx, executant.GetId(), e.GranteeUserID, e.GranteeGroupID)
if granteeList == nil {
return
}
sharerDisplayName := executant.GetDisplayName()
recipientList, err := s.render(executantCtx, email.UnsharedSpace,
recipientList, err := s.render(ctx, email.UnsharedSpace,
"SpaceGrantee",
map[string]string{
"SpaceSharer": sharerDisplayName,
@@ -136,7 +152,7 @@ func (s eventsNotifier) handleSpaceUnshared(e events.SpaceUnshared) {
s.logger.Error().Err(err).Str("event", "UnsharedSpace").Msg("Could not get render the email")
return
}
s.send(executantCtx, recipientList)
s.send(ctx, recipientList)
}
func (s eventsNotifier) handleSpaceMembershipExpired(e events.SpaceMembershipExpired) {
@@ -151,18 +167,26 @@ func (s eventsNotifier) handleSpaceMembershipExpired(e events.SpaceMembershipExp
return
}
ownerCtx, owner, err := utils.Impersonate(e.SpaceOwner, gatewayClient, s.machineAuthAPIKey)
ctx, err := utils.GetServiceUserContext(s.serviceAccountID, gatewayClient, s.serviceAccountSecret)
if err != nil {
logger.Error().Err(err).Msg("Could not impersonate sharer")
return
}
granteeList := s.ensureGranteeList(ownerCtx, owner.GetId(), e.GranteeUserID, e.GranteeGroupID)
owner, err := utils.GetUser(e.SpaceOwner, gatewayClient)
if err != nil {
logger.Error().
Err(err).
Msg("could not get user")
return
}
granteeList := s.ensureGranteeList(ctx, owner.GetId(), e.GranteeUserID, e.GranteeGroupID)
if granteeList == nil {
return
}
recipientList, err := s.render(ownerCtx, email.MembershipExpired,
recipientList, err := s.render(ctx, email.MembershipExpired,
"SpaceGrantee",
map[string]string{
"SpaceName": e.SpaceName,
@@ -172,5 +196,5 @@ func (s eventsNotifier) handleSpaceMembershipExpired(e events.SpaceMembershipExp
s.logger.Error().Err(err).Str("event", "SpaceUnshared").Msg("could not get render the email")
return
}
s.send(ownerCtx, recipientList)
s.send(ctx, recipientList)
}
+7 -1
View File
@@ -29,7 +29,13 @@ type Config struct {
Extractor Extractor `yaml:"extractor"`
ContentExtractionSizeLimit uint64 `yaml:"content_extraction_size_limit" env:"SEARCH_CONTENT_EXTRACTION_SIZE_LIMIT" desc:"Maximum file size in bytes that is allowed for content extraction."`
MachineAuthAPIKey string `yaml:"machine_auth_api_key" env:"OCIS_MACHINE_AUTH_API_KEY;SEARCH_MACHINE_AUTH_API_KEY" desc:"Machine auth API key used to validate internal requests necessary for the access to resources from other services."`
ServiceAccount ServiceAccount `yaml:"service_account"`
Context context.Context `yaml:"-"`
}
// ServiceAccount is the configuration for the used service account
type ServiceAccount struct {
ServiceAccountID string `yaml:"service_account_id" env:"OCIS_SERVICE_ACCOUNT_ID;SEARCH_SERVICE_ACCOUNT_ID" desc:"The ID of the service account the service should use. See the 'auth-service' service description for more details."`
ServiceAccountSecret string `yaml:"service_account_secret" env:"OCIS_SERVICE_ACCOUNT_SECRET;SEARCH_SERVICE_ACCOUNT_SECRET" desc:"The service account secret."`
}
@@ -54,7 +54,6 @@ func DefaultConfig() *config.Config {
EnableTLS: false,
},
ContentExtractionSizeLimit: 20 * 1024 * 1024, // Limit content extraction to <20MB files by default
MachineAuthAPIKey: "",
}
}
@@ -91,10 +90,6 @@ func EnsureDefaults(cfg *config.Config) {
cfg.TokenManager = &config.TokenManager{}
}
if cfg.MachineAuthAPIKey == "" && cfg.Commons != nil && cfg.Commons.MachineAuthAPIKey != "" {
cfg.MachineAuthAPIKey = cfg.Commons.MachineAuthAPIKey
}
if cfg.Reva == nil && cfg.Commons != nil {
cfg.Reva = structs.CopyOrZeroValue(cfg.Commons.Reva)
}
@@ -4,7 +4,6 @@ import (
"errors"
ociscfg "github.com/owncloud/ocis/v2/ocis-pkg/config"
"github.com/owncloud/ocis/v2/ocis-pkg/shared"
"github.com/owncloud/ocis/v2/services/search/pkg/config"
"github.com/owncloud/ocis/v2/services/search/pkg/config/defaults"
@@ -34,8 +33,5 @@ func ParseConfig(cfg *config.Config) error {
}
func Validate(cfg *config.Config) error {
if cfg.MachineAuthAPIKey == "" {
return shared.MissingMachineAuthApiKeyError(cfg.Service.Name)
}
return nil
}
+2 -22
View File
@@ -8,18 +8,14 @@ import (
"strings"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/v2/pkg/storagespace"
"github.com/cs3org/reva/v2/pkg/utils"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
searchmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/search/v0"
"github.com/owncloud/ocis/v2/services/search/pkg/engine"
"google.golang.org/grpc/metadata"
)
var scopeRegex = regexp.MustCompile(`scope:\s*([^" "\n\r]*)`)
@@ -71,30 +67,14 @@ func logDocCount(engine engine.Engine, logger log.Logger) {
logger.Debug().Interface("count", c).Msg("new document count")
}
func getAuthContext(owner *user.User, gatewaySelector pool.Selectable[gateway.GatewayAPIClient], secret string, logger log.Logger) (context.Context, error) {
func getAuthContext(serviceAccountID string, gatewaySelector pool.Selectable[gateway.GatewayAPIClient], secret string, logger log.Logger) (context.Context, error) {
gatewayClient, err := gatewaySelector.Next()
if err != nil {
logger.Error().Err(err).Msg("could not get reva gatewayClient")
return nil, err
}
ownerCtx := ctxpkg.ContextSetUser(context.Background(), owner)
authRes, err := gatewayClient.Authenticate(ownerCtx, &gateway.AuthenticateRequest{
Type: "machine",
ClientId: "userid:" + owner.GetId().GetOpaqueId(),
ClientSecret: secret,
})
if err == nil && authRes.GetStatus().GetCode() != rpc.Code_CODE_OK {
err = errtypes.NewErrtypeFromStatus(authRes.Status)
}
if err != nil {
logger.Error().Err(err).Interface("owner", owner).Interface("authRes", authRes).Msg("error using machine auth")
return nil, err
}
return metadata.AppendToOutgoingContext(ownerCtx, ctxpkg.TokenHeader, authRes.Token), nil
return utils.GetServiceUserContext(serviceAccountID, gatewayClient, secret)
}
func statResource(ctx context.Context, ref *provider.Reference, gatewaySelector pool.Selectable[gateway.GatewayAPIClient], logger log.Logger) (*provider.StatResponse, error) {
+12 -17
View File
@@ -57,7 +57,9 @@ type Service struct {
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
engine engine.Engine
extractor content.Extractor
secret string
serviceAccountID string
serviceAccountSecret string
}
var errSkipSpace error
@@ -67,9 +69,11 @@ func NewService(gatewaySelector pool.Selectable[gateway.GatewayAPIClient], eng e
var s = &Service{
gatewaySelector: gatewaySelector,
engine: eng,
secret: cfg.MachineAuthAPIKey,
logger: logger,
extractor: extractor,
serviceAccountID: cfg.ServiceAccount.ServiceAccountID,
serviceAccountSecret: cfg.ServiceAccount.ServiceAccountSecret,
}
return s
@@ -291,21 +295,12 @@ func (s *Service) searchIndex(ctx context.Context, req *searchsvc.SearchRequest,
return nil, err
}
var ownerCtx context.Context
if space.Owner.Id.Type == user.UserType_USER_TYPE_SPACE_OWNER {
// We can't impersonate SPACE_OWNER users and have to fall back to using the user auth instead,
// which will not resolve the absolute path of the share in the space but only the part the user
// is allowed to see.
// In the future this problem can be solved using service accounts.
ownerCtx = ctx
} else {
ownerCtx, err = getAuthContext(&user.User{Id: space.Owner.Id}, s.gatewaySelector, s.secret, s.logger)
if err != nil {
return nil, err
}
serviceCtx, err := getAuthContext(s.serviceAccountID, s.gatewaySelector, s.serviceAccountSecret, s.logger)
if err != nil {
return nil, err
}
gpRes, err := gatewayClient.GetPath(ownerCtx, &provider.GetPathRequest{
gpRes, err := gatewayClient.GetPath(serviceCtx, &provider.GetPathRequest{
ResourceId: space.Root,
})
if err != nil {
@@ -380,7 +375,7 @@ func (s *Service) searchIndex(ctx context.Context, req *searchsvc.SearchRequest,
// IndexSpace (re)indexes all resources of a given space.
func (s *Service) IndexSpace(spaceID *provider.StorageSpaceId, uID *user.UserId) error {
ownerCtx, err := getAuthContext(&user.User{Id: uID}, s.gatewaySelector, s.secret, s.logger)
ownerCtx, err := getAuthContext(s.serviceAccountID, s.gatewaySelector, s.serviceAccountSecret, s.logger)
if err != nil {
return err
}
@@ -510,7 +505,7 @@ func (s *Service) MoveItem(ref *provider.Reference, uID *user.UserId) {
}
func (s *Service) resInfo(uID *user.UserId, ref *provider.Reference) (context.Context, *provider.StatResponse, string) {
ownerCtx, err := getAuthContext(&user.User{Id: uID}, s.gatewaySelector, s.secret, s.logger)
ownerCtx, err := getAuthContext(s.serviceAccountID, s.gatewaySelector, s.serviceAccountSecret, s.logger)
if err != nil {
return nil, nil, ""
}
+5 -1
View File
@@ -8,7 +8,7 @@ The settings service is currently used for managing the:
* possible user roles and their respective permissions,
* assignment of roles to users.
As an example, user profile settings that can be changed in the Web UI must be persistent.
As an example, user profile settings that can be changed in the Web UI must be persistent.
The settings service supports two different backends for persisting the data. The backend can be set via the `SETTINGS_STORE_TYPE` environment variable. Supported values are:
@@ -67,3 +67,7 @@ Infinite Scale services can register *settings bundles* with the settings servic
## Settings Usage
Services can set or query ocis *setting values* of a user from settings bundles.
## Service Accounts
The settings service needs to know the ID's of service accounts but it doesn't need their secrets. Currently only one service account can be configured which has the admin role. This can be set with the `SETTINGS_SERVICE_ACCOUNT_ID_ADMIN` envvar, but it will also pick up the global `OCIS_SERVICE_ACCOUNT_ID` envvar. Also see the 'auth-service' service description for additional details.
+2
View File
@@ -37,6 +37,8 @@ type Config struct {
SetupDefaultAssignments bool `yaml:"set_default_assignments" env:"SETTINGS_SETUP_DEFAULT_ASSIGNMENTS;IDM_CREATE_DEMO_USERS" desc:"The default role assignments the demo users should be setup."`
ServiceAccountIDAdmin string `yaml:"service_account_id_admin" env:"OCIS_SERVICE_ACCOUNT_ID;SETTINGS_SERVICE_ACCOUNT_ID_ADMIN" desc:"The ID of the service account having the admin role. See the 'auth-service' service description for more details."`
Context context.Context `yaml:"-"`
}
@@ -64,8 +64,9 @@ func DefaultConfig() *config.Config {
TTL: time.Minute * 10,
},
},
BundlesPath: "",
Bundles: nil,
BundlesPath: "",
Bundles: nil,
ServiceAccountIDAdmin: "service-user-id",
}
}
@@ -822,6 +822,11 @@ func DefaultRoleAssignments(cfg *config.Config) []*settingsmsg.UserRoleAssignmen
AccountUuid: "534bb038-6f9d-4093-946f-133be61fa4e7",
RoleId: BundleUUIDRoleSpaceAdmin,
},
{
// service user
AccountUuid: "service-user-id",
RoleId: BundleUUIDRoleAdmin,
},
}
}
@@ -833,5 +838,12 @@ func DefaultRoleAssignments(cfg *config.Config) []*settingsmsg.UserRoleAssignmen
})
}
if cfg.ServiceAccountIDAdmin != "" {
assignments = append(assignments, &settingsmsg.UserRoleAssignment{
AccountUuid: cfg.ServiceAccountIDAdmin,
RoleId: BundleUUIDRoleAdmin,
})
}
return assignments
}
@@ -38,6 +38,7 @@ type Config struct {
ReadOnly bool `yaml:"readonly" env:"STORAGE_USERS_READ_ONLY" desc:"Set this storage to be read-only."`
UploadExpiration int64 `yaml:"upload_expiration" env:"STORAGE_USERS_UPLOAD_EXPIRATION" desc:"Duration in seconds after which uploads will expire. Note that when setting this to a low number, uploads could be cancelled before they are finished and return a 403 to the user."`
Tasks Tasks `yaml:"tasks"`
ServiceAccount ServiceAccount `yaml:"service_account"`
Supervised bool `yaml:"-"`
Context context.Context `yaml:"-"`
@@ -278,3 +279,9 @@ type PurgeTrashBin struct {
PersonalDeleteBefore time.Duration `yaml:"personal_delete_before" env:"STORAGE_USERS_PURGE_TRASH_BIN_PERSONAL_DELETE_BEFORE" desc:"Specifies the period of time in which items that have been in the personal trash-bin for longer than this value should be deleted. A value of 0 means no automatic deletion. The value is human-readable, valid values are '24h', '60m', '60s' etc."`
ProjectDeleteBefore time.Duration `yaml:"project_delete_before" env:"STORAGE_USERS_PURGE_TRASH_BIN_PROJECT_DELETE_BEFORE" desc:"Specifies the period of time in which items that have been in the project trash-bin for longer than this value should be deleted. A value of 0 means no automatic deletion. The value is human-readable, valid values are '24h', '60m', '60s' etc."`
}
// ServiceAccount is the configuration for the used service account
type ServiceAccount struct {
ServiceAccountID string `yaml:"service_account_id" env:"OCIS_SERVICE_ACCOUNT_ID;STORAGE_USERS_SERVICE_ACCOUNT_ID" desc:"The ID of the service account the service should use. See the 'auth-service' service description for more details."`
ServiceAccountSecret string `yaml:"service_account_secret" env:"OCIS_SERVICE_ACCOUNT_SECRET;STORAGE_USERS_SERVICE_ACCOUNT_SECRET" desc:"The service account secret."`
}
+1 -7
View File
@@ -4,7 +4,6 @@ import (
"time"
apiGateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
apiUser "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/cs3org/reva/v2/pkg/events"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
@@ -53,11 +52,6 @@ func (s Service) Run() error {
executionTime = time.Now()
}
executantID := ev.ExecutantID
if executantID == nil {
executantID = &apiUser.UserId{OpaqueId: s.config.Tasks.PurgeTrashBin.UserID}
}
tasks := map[task.SpaceType]time.Time{
task.Project: executionTime.Add(-s.config.Tasks.PurgeTrashBin.ProjectDeleteBefore),
task.Personal: executionTime.Add(-s.config.Tasks.PurgeTrashBin.PersonalDeleteBefore),
@@ -70,7 +64,7 @@ func (s Service) Run() error {
continue
}
if err = task.PurgeTrashBin(executantID, deleteBefore, spaceType, s.gatewaySelector, s.config.Commons.MachineAuthAPIKey); err != nil {
if err = task.PurgeTrashBin(s.config.ServiceAccount.ServiceAccountID, deleteBefore, spaceType, s.gatewaySelector, s.config.ServiceAccount.ServiceAccountSecret); err != nil {
errs = append(errs, err)
}
}
+9 -48
View File
@@ -1,11 +1,9 @@
package task
import (
"fmt"
"time"
apiGateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
apiUser "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
apiRpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
apiProvider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/cs3org/reva/v2/pkg/errtypes"
@@ -17,18 +15,18 @@ import (
// the provided executantID must have space access.
// removeBefore specifies how long an item must be in the trash-bin to be deleted,
// items that stay there for a shorter time are ignored and kept in place.
func PurgeTrashBin(executantID *apiUser.UserId, deleteBefore time.Time, spaceType SpaceType, gatewaySelector pool.Selectable[apiGateway.GatewayAPIClient], machineAuthAPIKey string) error {
func PurgeTrashBin(serviceAccountID string, deleteBefore time.Time, spaceType SpaceType, gatewaySelector pool.Selectable[apiGateway.GatewayAPIClient], serviceAccountSecret string) error {
gatewayClient, err := gatewaySelector.Next()
if err != nil {
return err
}
executantCtx, _, err := utils.Impersonate(executantID, gatewayClient, machineAuthAPIKey)
ctx, err := utils.GetServiceUserContext(serviceAccountID, gatewayClient, serviceAccountSecret)
if err != nil {
return err
}
listStorageSpacesResponse, err := gatewayClient.ListStorageSpaces(executantCtx, &apiProvider.ListStorageSpacesRequest{
listStorageSpacesResponse, err := gatewayClient.ListStorageSpaces(ctx, &apiProvider.ListStorageSpacesRequest{
Filters: []*apiProvider.ListStorageSpacesRequest_Filter{
{
Type: apiProvider.ListStorageSpacesRequest_Filter_TYPE_SPACE_TYPE,
@@ -43,52 +41,15 @@ func PurgeTrashBin(executantID *apiUser.UserId, deleteBefore time.Time, spaceTyp
}
for _, storageSpace := range listStorageSpacesResponse.StorageSpaces {
var (
err error
impersonationID *apiUser.UserId
storageSpaceReference = &apiProvider.Reference{
ResourceId: storageSpace.GetRoot(),
}
)
switch SpaceType(storageSpace.GetSpaceType()) {
case Personal:
impersonationID = storageSpace.GetOwner().GetId()
case Project:
var permissionsMap map[string]*apiProvider.ResourcePermissions
err := utils.ReadJSONFromOpaque(storageSpace.GetOpaque(), "grants", &permissionsMap)
if err != nil {
break
}
for id, permissions := range permissionsMap {
if !permissions.Delete {
continue
}
impersonationID = &apiUser.UserId{
OpaqueId: id,
}
break
}
default:
if typ := storageSpace.GetSpaceType(); typ != "personal" && typ != "project" {
// ignore spaces that are neither personal nor project
continue
}
if err != nil {
return err
storageSpaceReference := &apiProvider.Reference{
ResourceId: storageSpace.GetRoot(),
}
if impersonationID == nil {
return fmt.Errorf("can't impersonate space user for space: %s", storageSpace.GetId().GetOpaqueId())
}
impersonatedCtx, _, err := utils.Impersonate(impersonationID, gatewayClient, machineAuthAPIKey)
if err != nil {
return err
}
listRecycleResponse, err := gatewayClient.ListRecycle(impersonatedCtx, &apiProvider.ListRecycleRequest{Ref: storageSpaceReference})
listRecycleResponse, err := gatewayClient.ListRecycle(ctx, &apiProvider.ListRecycleRequest{Ref: storageSpaceReference})
if err != nil {
return err
}
@@ -99,7 +60,7 @@ func PurgeTrashBin(executantID *apiUser.UserId, deleteBefore time.Time, spaceTyp
continue
}
purgeRecycleResponse, err := gatewayClient.PurgeRecycle(impersonatedCtx, &apiProvider.PurgeRecycleRequest{
purgeRecycleResponse, err := gatewayClient.PurgeRecycle(ctx, &apiProvider.PurgeRecycleRequest{
Ref: storageSpaceReference,
Key: recycleItem.Key,
})
@@ -115,7 +115,7 @@ var _ = Describe("trash", func() {
gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil)
gatewayClient.On("Authenticate", mock.Anything, mock.Anything).Return(nil, genericError)
err := task.PurgeTrashBin(user.Id, now, task.Project, gatewaySelector, "")
err := task.PurgeTrashBin("service-user-id", now, task.Project, gatewaySelector, "")
Expect(err).To(HaveOccurred())
})
It("throws an error if space listing fails", func() {
@@ -123,45 +123,9 @@ var _ = Describe("trash", func() {
gatewayClient.On("Authenticate", mock.Anything, mock.Anything).Return(authenticateResponse, nil)
gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(nil, genericError)
err := task.PurgeTrashBin(user.Id, now, task.Project, gatewaySelector, "")
err := task.PurgeTrashBin("service-user-id", now, task.Project, gatewaySelector, "")
Expect(err).To(HaveOccurred())
})
It("throws an error if a personal space user can't be impersonated", func() {
listStorageSpacesResponse.StorageSpaces = []*apiProvider.StorageSpace{personalSpace}
gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil)
gatewayClient.On("Authenticate", mock.Anything, mock.Anything).Return(authenticateResponse, nil)
gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(listStorageSpacesResponse, nil)
err := task.PurgeTrashBin(user.Id, now, task.Project, gatewaySelector, "")
Expect(err).To(MatchError(errors.New("can't impersonate space user for space: personal")))
})
It("throws an error if a project space user can't be impersonated", func() {
listStorageSpacesResponse.StorageSpaces = []*apiProvider.StorageSpace{projectSpace}
gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil)
gatewayClient.On("Authenticate", mock.Anything, mock.Anything).Return(authenticateResponse, nil)
gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(listStorageSpacesResponse, nil)
err := task.PurgeTrashBin(user.Id, now, task.Project, gatewaySelector, "")
Expect(err).To(MatchError(errors.New("can't impersonate space user for space: project")))
})
It("throws an error if a project space has no user with delete permissions", func() {
listStorageSpacesResponse.StorageSpaces = []*apiProvider.StorageSpace{projectSpace}
projectSpace.Opaque.Map = map[string]*apiTypes.OpaqueEntry{
"grants": {
Value: MustMarshal(map[string]*apiProvider.ResourcePermissions{
"admin": {
Delete: false,
},
}),
},
}
gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil)
gatewayClient.On("Authenticate", mock.Anything, mock.Anything).Return(authenticateResponse, nil)
gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(listStorageSpacesResponse, nil)
err := task.PurgeTrashBin(user.Id, now, task.Project, gatewaySelector, "")
Expect(err).To(MatchError(errors.New("can't impersonate space user for space: project")))
})
It("only deletes items older than the specified period", func() {
var (
recycleItems = map[string][]*apiProvider.RecycleItem{
@@ -231,7 +195,7 @@ var _ = Describe("trash", func() {
}, nil,
)
err := task.PurgeTrashBin(user.Id, now, task.Project, gatewaySelector, "")
err := task.PurgeTrashBin("service-user-id", now, task.Project, gatewaySelector, "")
Expect(err).To(BeNil())
Expect(recycleItems["personal"]).To(HaveLen(2))
Expect(recycleItems["project"]).To(HaveLen(2))
+8
View File
@@ -32,6 +32,8 @@ type Config struct {
GlobalNotificationsSecret string `yaml:"global_notifications_secret" env:"USERLOG_GLOBAL_NOTIFICATIONS_SECRET" desc:"The secret to secure the global notifications endpoint. Only system admins and users knowing that secret can call the global notifications POST/DELETE endpoints."`
ServiceAccount ServiceAccount `yaml:"service_account"`
Context context.Context `yaml:"-"`
}
@@ -75,3 +77,9 @@ type HTTP struct {
type TokenManager struct {
JWTSecret string `yaml:"jwt_secret" env:"OCIS_JWT_SECRET;USERLOG_JWT_SECRET" desc:"The secret to mint and validate jwt tokens."`
}
// ServiceAccount is the configuration for the used service account
type ServiceAccount struct {
ServiceAccountID string `yaml:"service_account_id" env:"OCIS_SERVICE_ACCOUNT_ID;USERLOG_SERVICE_ACCOUNT_ID" desc:"The ID of the service account the service should use. See the 'auth-service' service description for more details."`
ServiceAccountSecret string `yaml:"service_account_secret" env:"OCIS_SERVICE_ACCOUNT_SECRET;USERLOG_SERVICE_ACCOUNT_SECRET" desc:"The service account secret."`
}
+32 -26
View File
@@ -52,31 +52,32 @@ type OC10Notification struct {
// Converter is responsible for converting eventhistory events to OC10Notifications
type Converter struct {
locale string
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
machineAuthAPIKey string
serviceName string
translationPath string
locale string
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
serviceAccountID string
serviceAccountSecret string
serviceName string
translationPath string
// cached within one request not to query other service too much
spaces map[string]*storageprovider.StorageSpace
users map[string]*user.User
resources map[string]*storageprovider.ResourceInfo
contexts map[string]context.Context
spaces map[string]*storageprovider.StorageSpace
users map[string]*user.User
resources map[string]*storageprovider.ResourceInfo
serviceAccountContext context.Context
}
// NewConverter returns a new Converter
func NewConverter(loc string, gatewaySelector pool.Selectable[gateway.GatewayAPIClient], machineAuthAPIKey string, name string, translationPath string) *Converter {
func NewConverter(loc string, gatewaySelector pool.Selectable[gateway.GatewayAPIClient], machineAuthAPIKey string, name string, translationPath string, serviceAccountID string, serviceAccountSecret string) *Converter {
return &Converter{
locale: loc,
gatewaySelector: gatewaySelector,
machineAuthAPIKey: machineAuthAPIKey,
serviceName: name,
translationPath: translationPath,
spaces: make(map[string]*storageprovider.StorageSpace),
users: make(map[string]*user.User),
resources: make(map[string]*storageprovider.ResourceInfo),
contexts: make(map[string]context.Context),
locale: loc,
gatewaySelector: gatewaySelector,
serviceAccountID: serviceAccountID,
serviceAccountSecret: serviceAccountSecret,
serviceName: name,
translationPath: translationPath,
spaces: make(map[string]*storageprovider.StorageSpace),
users: make(map[string]*user.User),
resources: make(map[string]*storageprovider.ResourceInfo),
}
}
@@ -171,7 +172,7 @@ func (c *Converter) spaceMessage(eventid string, nt NotificationTemplate, execut
return OC10Notification{}, err
}
ctx, err := c.authenticate(usr)
ctx, err := c.authenticate()
if err != nil {
return OC10Notification{}, err
}
@@ -210,7 +211,7 @@ func (c *Converter) shareMessage(eventid string, nt NotificationTemplate, execut
return OC10Notification{}, err
}
ctx, err := c.authenticate(usr)
ctx, err := c.authenticate()
if err != nil {
return OC10Notification{}, err
}
@@ -327,13 +328,18 @@ func (c *Converter) deprovisionMessage(nt NotificationTemplate, deproDate string
}, nil
}
func (c *Converter) authenticate(usr *user.User) (context.Context, error) {
if ctx, ok := c.contexts[usr.GetId().GetOpaqueId()]; ok {
return ctx, nil
func (c *Converter) authenticate() (context.Context, error) {
if c.serviceAccountContext != nil {
return c.serviceAccountContext, nil
}
ctx, err := authenticate(usr, c.gatewaySelector, c.machineAuthAPIKey)
gatewayClient, err := c.gatewaySelector.Next()
if err != nil {
return nil, err
}
ctx, err := utils.GetServiceUserContext(c.serviceAccountID, gatewayClient, c.serviceAccountSecret)
if err == nil {
c.contexts[usr.GetId().GetOpaqueId()] = ctx
c.serviceAccountContext = ctx
}
return ctx, err
}
+21 -46
View File
@@ -13,7 +13,6 @@ import (
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
revactx "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/events"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/v2/pkg/utils"
@@ -29,7 +28,6 @@ import (
micrometadata "go-micro.dev/v4/metadata"
"go-micro.dev/v4/store"
"go.opentelemetry.io/otel/trace"
"google.golang.org/grpc/metadata"
)
// UserlogService is the service responsible for user activities
@@ -137,12 +135,12 @@ func (ul *UserlogService) processEvent(event events.Event) {
users = append(users, e.ExecutingUser.GetId().GetOpaqueId())
default:
return
}
// space related // TODO: how to find spaceadmins?
case events.SpaceDisabled:
executant = e.Executant
users, err = ul.findSpaceMembers(ul.impersonate(e.Executant), e.ID.GetOpaqueId(), viewer)
users, err = ul.findSpaceMembers(ul.mustAuthenticate(), e.ID.GetOpaqueId(), viewer)
case events.SpaceDeleted:
executant = e.Executant
for u := range e.FinalMembers {
@@ -150,22 +148,22 @@ func (ul *UserlogService) processEvent(event events.Event) {
}
case events.SpaceShared:
executant = e.Executant
users, err = ul.resolveID(ul.impersonate(e.Executant), e.GranteeUserID, e.GranteeGroupID)
users, err = ul.resolveID(ul.mustAuthenticate(), e.GranteeUserID, e.GranteeGroupID)
case events.SpaceUnshared:
executant = e.Executant
users, err = ul.resolveID(ul.impersonate(e.Executant), e.GranteeUserID, e.GranteeGroupID)
users, err = ul.resolveID(ul.mustAuthenticate(), e.GranteeUserID, e.GranteeGroupID)
case events.SpaceMembershipExpired:
users, err = ul.resolveID(ul.impersonate(e.SpaceOwner), e.GranteeUserID, e.GranteeGroupID)
users, err = ul.resolveID(ul.mustAuthenticate(), e.GranteeUserID, e.GranteeGroupID)
// share related
case events.ShareCreated:
executant = e.Executant
users, err = ul.resolveID(ul.impersonate(e.Executant), e.GranteeUserID, e.GranteeGroupID)
users, err = ul.resolveID(ul.mustAuthenticate(), e.GranteeUserID, e.GranteeGroupID)
case events.ShareRemoved:
executant = e.Executant
users, err = ul.resolveID(ul.impersonate(e.Executant), e.GranteeUserID, e.GranteeGroupID)
users, err = ul.resolveID(ul.mustAuthenticate(), e.GranteeUserID, e.GranteeGroupID)
case events.ShareExpired:
users, err = ul.resolveID(ul.impersonate(e.ShareOwner), e.GranteeUserID, e.GranteeGroupID)
users, err = ul.resolveID(ul.mustAuthenticate(), e.GranteeUserID, e.GranteeGroupID)
}
if err != nil {
@@ -520,26 +518,6 @@ func (ul *UserlogService) resolveGroup(ctx context.Context, groupID string) ([]s
return userIDs, nil
}
func (ul *UserlogService) impersonate(uid *user.UserId) context.Context {
if uid == nil {
ul.log.Error().Msg("cannot impersonate nil user")
return nil
}
u, err := getUser(context.Background(), uid, ul.gatewaySelector)
if err != nil {
ul.log.Error().Err(err).Msg("cannot get user")
return nil
}
ctx, err := authenticate(u, ul.gatewaySelector, ul.cfg.MachineAuthAPIKey)
if err != nil {
ul.log.Error().Err(err).Str("userid", u.GetId().GetOpaqueId()).Msg("failed to impersonate user")
return nil
}
return ctx
}
func (ul *UserlogService) getUserLocale(userid string) string {
resp, err := ul.valueClient.GetValueByUniqueIdentifiers(
micrometadata.Set(context.Background(), middleware.AccountID, userid),
@@ -560,29 +538,25 @@ func (ul *UserlogService) getUserLocale(userid string) string {
}
func (ul *UserlogService) getConverter(locale string) *Converter {
return NewConverter(locale, ul.gatewaySelector, ul.cfg.MachineAuthAPIKey, ul.cfg.Service.Name, ul.cfg.TranslationPath)
return NewConverter(locale, ul.gatewaySelector, ul.cfg.MachineAuthAPIKey, ul.cfg.Service.Name, ul.cfg.TranslationPath, ul.cfg.ServiceAccount.ServiceAccountID, ul.cfg.ServiceAccount.ServiceAccountSecret)
}
func authenticate(usr *user.User, gatewaySelector pool.Selectable[gateway.GatewayAPIClient], machineAuthAPIKey string) (context.Context, error) {
func (ul *UserlogService) mustAuthenticate() context.Context {
ctx, err := authenticate(ul.cfg.ServiceAccount.ServiceAccountID, ul.gatewaySelector, ul.cfg.ServiceAccount.ServiceAccountSecret)
if err != nil {
ul.log.Error().Err(err).Str("accountid", ul.cfg.ServiceAccount.ServiceAccountID).Msg("failed to impersonate service account")
return nil
}
return ctx
}
func authenticate(serviceAccountID string, gatewaySelector pool.Selectable[gateway.GatewayAPIClient], serviceAccountSecret string) (context.Context, error) {
gatewayClient, err := gatewaySelector.Next()
if err != nil {
return nil, err
}
ctx := revactx.ContextSetUser(context.Background(), usr)
authRes, err := gatewayClient.Authenticate(ctx, &gateway.AuthenticateRequest{
Type: "machine",
ClientId: "userid:" + usr.GetId().GetOpaqueId(),
ClientSecret: machineAuthAPIKey,
})
if err != nil {
return nil, err
}
if authRes.GetStatus().GetCode() != rpc.Code_CODE_OK {
return nil, fmt.Errorf("error impersonating user: %s", authRes.Status.Message)
}
return metadata.AppendToOutgoingContext(ctx, revactx.TokenHeader, authRes.Token), nil
return utils.GetServiceUserContext(serviceAccountID, gatewayClient, serviceAccountSecret)
}
func getSpace(ctx context.Context, spaceID string, gatewaySelector pool.Selectable[gateway.GatewayAPIClient]) (*storageprovider.StorageSpace, error) {
@@ -665,6 +639,7 @@ func getResource(ctx context.Context, resourceid *storageprovider.ResourceId, ga
func listStorageSpaceRequest(spaceID string) *storageprovider.ListStorageSpacesRequest {
return &storageprovider.ListStorageSpacesRequest{
Opaque: utils.AppendPlainToOpaque(nil, "unrestricted", "true"),
Filters: []*storageprovider.ListStorageSpacesRequest_Filter{
{
Type: storageprovider.ListStorageSpacesRequest_Filter_TYPE_ID,
@@ -425,7 +425,5 @@ Feature: List and create spaces
And the json responded should not contain a space with name "Project Venus"
Examples:
| role |
| Admin |
| Space Admin |
| User |
| User Light |
+1
View File
@@ -30,5 +30,6 @@ import (
_ "github.com/cs3org/reva/v2/pkg/auth/manager/oidc"
_ "github.com/cs3org/reva/v2/pkg/auth/manager/owncloudsql"
_ "github.com/cs3org/reva/v2/pkg/auth/manager/publicshares"
_ "github.com/cs3org/reva/v2/pkg/auth/manager/serviceaccounts"
// Add your own here
)
+3 -1
View File
@@ -18,7 +18,9 @@
package registry
import "github.com/cs3org/reva/v2/pkg/auth"
import (
"github.com/cs3org/reva/v2/pkg/auth"
)
// NewFunc is the function that auth implementations
// should register to at init time.
@@ -0,0 +1,90 @@
package serviceaccounts
import (
"context"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/cs3org/reva/v2/pkg/auth"
"github.com/cs3org/reva/v2/pkg/auth/manager/registry"
"github.com/cs3org/reva/v2/pkg/auth/scope"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
)
type conf struct {
ServiceUsers []serviceuser `mapstructure:"service_accounts"`
}
type serviceuser struct {
ID string `mapstructure:"id"`
Secret string `mapstructure:"secret"`
}
type manager struct {
authenticate func(userID, secret string) error
}
func init() {
registry.Register("serviceaccounts", New)
}
// Configure parses the map conf
func (m *manager) Configure(config map[string]interface{}) error {
c := &conf{}
if err := mapstructure.Decode(config, c); err != nil {
return errors.Wrap(err, "error decoding conf")
}
// only inmem authenticator for now
a := &inmemAuthenticator{make(map[string]string)}
for _, s := range c.ServiceUsers {
a.m[s.ID] = s.Secret
}
m.authenticate = a.Authenticate
return nil
}
// New creates a new manager for the 'service' authentication
func New(conf map[string]interface{}) (auth.Manager, error) {
m := &manager{}
err := m.Configure(conf)
if err != nil {
return nil, err
}
return m, nil
}
// Authenticate authenticates the service account
func (m *manager) Authenticate(ctx context.Context, userID string, secret string) (*userpb.User, map[string]*authpb.Scope, error) {
if err := m.authenticate(userID, secret); err != nil {
return nil, nil, err
}
scope, err := scope.AddOwnerScope(nil)
if err != nil {
return nil, nil, err
}
return &userpb.User{
// TODO: more details for service users?
Id: &userpb.UserId{
OpaqueId: userID,
Type: userpb.UserType_USER_TYPE_SERVICE,
Idp: "none",
},
}, scope, nil
}
type inmemAuthenticator struct {
m map[string]string
}
func (a *inmemAuthenticator) Authenticate(userID string, secret string) error {
if secret == "" || a.m[userID] == "" {
return errors.New("unknown user")
}
if a.m[userID] == secret {
return nil
}
return errors.New("secrets do not match")
}
+1 -1
View File
@@ -481,7 +481,7 @@ func (r *registry) findProvidersForResource(ctx context.Context, id string, find
},
})
}
spaces, err := r.findStorageSpaceOnProvider(ctx, address, filters, false)
spaces, err := r.findStorageSpaceOnProvider(ctx, address, filters, unrestricted)
if err != nil {
appctx.GetLogger(ctx).Debug().Err(err).Interface("provider", provider).Msg("findStorageSpaceOnProvider by id failed, continuing")
continue
@@ -22,6 +22,7 @@ import (
"context"
"strings"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/cs3org/reva/v2/pkg/appctx"
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
@@ -84,6 +85,21 @@ func OwnerPermissions() provider.ResourcePermissions {
}
}
// ServiceAccountPermissions defines the permissions for nodes when requested by a service account
func ServiceAccountPermissions() provider.ResourcePermissions {
// TODO: Different permissions for different service accounts
return provider.ResourcePermissions{
Stat: true,
ListContainer: true,
GetPath: true, // for search index
InitiateFileUpload: true, // for personal data export
InitiateFileDownload: true, // for full-text-search
RemoveGrant: true, // for share expiry
ListRecycle: true, // for purge-trash-bin command
PurgeRecycle: true, // for purge-trash-bin command
}
}
// Permissions implements permission checks
type Permissions struct {
lu PathLookup
@@ -113,6 +129,10 @@ func (p *Permissions) assemblePermissions(ctx context.Context, n *Node, failOnTr
return NoPermissions(), nil
}
if u.GetId().GetType() == userpb.UserType_USER_TYPE_SERVICE {
return ServiceAccountPermissions(), nil
}
// are we reading a revision?
if strings.Contains(n.ID, RevisionIDDelimiter) {
// verify revision key format
+8 -19
View File
@@ -11,19 +11,8 @@ import (
"google.golang.org/grpc/metadata"
)
// Impersonate returns an authenticated reva context and the user it represents
func Impersonate(userID *user.UserId, gwc gateway.GatewayAPIClient, machineAuthAPIKey string) (context.Context, *user.User, error) {
usr, err := GetUser(userID, gwc, machineAuthAPIKey)
if err != nil {
return nil, nil, err
}
ctx, err := ImpersonateUser(usr, gwc, machineAuthAPIKey)
return ctx, usr, err
}
// GetUser gets the specified user
func GetUser(userID *user.UserId, gwc gateway.GatewayAPIClient, machineAuthAPIKey string) (*user.User, error) {
func GetUser(userID *user.UserId, gwc gateway.GatewayAPIClient) (*user.User, error) {
getUserResponse, err := gwc.GetUser(context.Background(), &user.GetUserRequest{UserId: userID})
if err != nil {
return nil, err
@@ -35,19 +24,19 @@ func GetUser(userID *user.UserId, gwc gateway.GatewayAPIClient, machineAuthAPIKe
return getUserResponse.GetUser(), nil
}
// ImpersonateUser impersonates the given user
func ImpersonateUser(usr *user.User, gwc gateway.GatewayAPIClient, machineAuthAPIKey string) (context.Context, error) {
ctx := revactx.ContextSetUser(context.Background(), usr)
// GetServiceUserContext returns an authenticated context of the given service user
func GetServiceUserContext(serviceUserID string, gwc gateway.GatewayAPIClient, serviceUserSecret string) (context.Context, error) {
ctx := context.Background()
authRes, err := gwc.Authenticate(ctx, &gateway.AuthenticateRequest{
Type: "machine",
ClientId: "userid:" + usr.GetId().GetOpaqueId(),
ClientSecret: machineAuthAPIKey,
Type: "serviceaccounts",
ClientId: serviceUserID,
ClientSecret: serviceUserSecret,
})
if err != nil {
return nil, err
}
if authRes.GetStatus().GetCode() != rpc.Code_CODE_OK {
return nil, fmt.Errorf("error impersonating user: %s", authRes.Status.Message)
return nil, fmt.Errorf("error authenticating service user: %s", authRes.Status.Message)
}
return metadata.AppendToOutgoingContext(ctx, revactx.TokenHeader, authRes.Token), nil
+2 -1
View File
@@ -354,7 +354,7 @@ github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1
github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1
github.com/cs3org/go-cs3apis/cs3/tx/v1beta1
github.com/cs3org/go-cs3apis/cs3/types/v1beta1
# github.com/cs3org/reva/v2 v2.16.1-0.20230828111521-594d4e103741
# github.com/cs3org/reva/v2 v2.16.1-0.20230829124655-8ba013d7a129
## explicit; go 1.20
github.com/cs3org/reva/v2/cmd/revad/internal/grace
github.com/cs3org/reva/v2/cmd/revad/runtime
@@ -473,6 +473,7 @@ github.com/cs3org/reva/v2/pkg/auth/manager/owncloudsql
github.com/cs3org/reva/v2/pkg/auth/manager/owncloudsql/accounts
github.com/cs3org/reva/v2/pkg/auth/manager/publicshares
github.com/cs3org/reva/v2/pkg/auth/manager/registry
github.com/cs3org/reva/v2/pkg/auth/manager/serviceaccounts
github.com/cs3org/reva/v2/pkg/auth/registry/loader
github.com/cs3org/reva/v2/pkg/auth/registry/registry
github.com/cs3org/reva/v2/pkg/auth/registry/static