Merge pull request #9079 from owncloud/feat/reva-app-auth

feat: reva app auth
This commit is contained in:
kobergj
2024-07-23 10:37:35 +02:00
committed by GitHub
36 changed files with 1013 additions and 4 deletions

View File

@@ -25,6 +25,7 @@ OCIS_MODULES = \
services/app-provider \
services/app-registry \
services/audit \
services/auth-app \
services/auth-basic \
services/auth-bearer \
services/auth-machine \

View File

@@ -0,0 +1,5 @@
Enhancement: Introduce auth-app service
Introduce a new service, auth-app, that provides authentication and authorization services for applications.
https://github.com/owncloud/ocis/pull/9079

View File

@@ -60,7 +60,7 @@ We also suggest using the last port in your extensions' range as a debug/metrics
| 9230-9234 | [nats]({{< ref "../nats/_index.md" >}}) |
| 9235-9239 | [idm]({{< ref "../idm/_index.md" >}}) |
| 9240-9244 | [app-registry]({{< ref "../app-registry/_index.md" >}}) |
| 9245-9249 | FREE |
| 9245-9249 | [auth-app]({{< ref "../auth-app/_index.md" >}}) |
| 9250-9254 | [ocis server (runtime)](https://github.com/owncloud/ocis/tree/master/ocis/pkg/runtime) |
| 9255-9259 | [postprocessing]({{< ref "../postprocessing/_index.md" >}}) |
| 9260-9264 | [clientlog]({{< ref "../clientlog/_index.md" >}}) |

View File

@@ -7,6 +7,7 @@ import (
appProvider "github.com/owncloud/ocis/v2/services/app-provider/pkg/config"
appRegistry "github.com/owncloud/ocis/v2/services/app-registry/pkg/config"
audit "github.com/owncloud/ocis/v2/services/audit/pkg/config"
authapp "github.com/owncloud/ocis/v2/services/auth-app/pkg/config"
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"
@@ -86,6 +87,7 @@ type Config struct {
AppProvider *appProvider.Config `yaml:"app_provider"`
AppRegistry *appRegistry.Config `yaml:"app_registry"`
Audit *audit.Config `yaml:"audit"`
AuthApp *authapp.Config `yaml:"auth_app"`
AuthBasic *authbasic.Config `yaml:"auth_basic"`
AuthBearer *authbearer.Config `yaml:"auth_bearer"`
AuthMachine *authmachine.Config `yaml:"auth_machine"`

View File

@@ -6,6 +6,7 @@ import (
appProvider "github.com/owncloud/ocis/v2/services/app-provider/pkg/config/defaults"
appRegistry "github.com/owncloud/ocis/v2/services/app-registry/pkg/config/defaults"
audit "github.com/owncloud/ocis/v2/services/audit/pkg/config/defaults"
authapp "github.com/owncloud/ocis/v2/services/auth-app/pkg/config/defaults"
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"
@@ -58,6 +59,7 @@ func DefaultConfig() *Config {
AppProvider: appProvider.DefaultConfig(),
AppRegistry: appRegistry.DefaultConfig(),
Audit: audit.DefaultConfig(),
AuthApp: authapp.DefaultConfig(),
AuthBasic: authbasic.DefaultConfig(),
AuthBearer: authbearer.DefaultConfig(),
AuthMachine: authmachine.DefaultConfig(),

View File

@@ -13,6 +13,7 @@ import (
appprovider "github.com/owncloud/ocis/v2/services/app-provider/pkg/command"
appregistry "github.com/owncloud/ocis/v2/services/app-registry/pkg/command"
audit "github.com/owncloud/ocis/v2/services/audit/pkg/command"
authapp "github.com/owncloud/ocis/v2/services/auth-app/pkg/command"
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"
@@ -78,6 +79,11 @@ var svccmds = []register.Command{
cfg.Audit.Commons = cfg.Commons
})
},
func(cfg *config.Config) *cli.Command {
return ServiceCommand(cfg, cfg.AuthApp.Service.Name, authapp.GetCommands(cfg.AuthApp), func(_ *config.Config) {
cfg.AuthApp.Commons = cfg.Commons
})
},
func(cfg *config.Config) *cli.Command {
return ServiceCommand(cfg, cfg.AuthBasic.Service.Name, authbasic.GetCommands(cfg.AuthBasic), func(c *config.Config) {
cfg.AuthBasic.Commons = cfg.Commons
@@ -266,10 +272,10 @@ var svccmds = []register.Command{
}
// ServiceCommand is the entry point for the all service commands.
func ServiceCommand(cfg *config.Config, servicename string, subcommands []*cli.Command, f func(*config.Config)) *cli.Command {
func ServiceCommand(cfg *config.Config, serviceName string, subcommands []*cli.Command, f func(*config.Config)) *cli.Command {
return &cli.Command{
Name: servicename,
Usage: helper.SubcommandDescription(servicename),
Name: serviceName,
Usage: helper.SubcommandDescription(serviceName),
Category: "services",
Before: func(c *cli.Context) error {
configlog.Error(parser.ParseConfig(cfg, true))

View File

@@ -13,6 +13,8 @@ import (
"syscall"
"time"
authapp "github.com/owncloud/ocis/v2/services/auth-app/pkg/command"
"github.com/cenkalti/backoff"
"github.com/cs3org/reva/v2/pkg/events/stream"
"github.com/cs3org/reva/v2/pkg/logger"
@@ -324,6 +326,11 @@ func NewService(options ...Option) (*Service, error) {
cfg.Audit.Commons = cfg.Commons
return audit.Execute(cfg.Audit)
})
areg(opts.Config.AuthApp.Service.Name, func(ctx context.Context, cfg *ociscfg.Config) error {
cfg.AuthApp.Context = ctx
cfg.AuthApp.Commons = cfg.Commons
return authapp.Execute(cfg.AuthApp)
})
areg(opts.Config.Policies.Service.Name, func(ctx context.Context, cfg *ociscfg.Config) error {
cfg.Policies.Context = ctx
cfg.Policies.Commons = cfg.Commons

View File

@@ -0,0 +1,37 @@
SHELL := bash
NAME := auth-app
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:

View File

@@ -0,0 +1,30 @@
# Auth-App
The auth-app service provides authentication for 3rd party apps.
## 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-app` handles authentication of external 3rd party apps
- `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 Startup
Because this service is not started automatically, a manual start needs to be initiated which can be done in several ways. To configure the service usage, an environment variable for the proxy service needs to be set to allow app authentication.
```bash
OCIS_ADD_RUN_SERVICES=auth-app # deployment specific. Add the service to the manual startup list, use with binary deployments. Alternatively you can start the service explicitly via the command line.
PROXY_ENABLE_APP_AUTH=true # mandatory, allow app authentication. In case of a distributed environment, this envvar needs to be set in the proxy service.
```
## App Tokens
App Tokens are used to authenticate 3rd party access via https like when using curl (apps) to access an API endpoint. These apps need to authenticate themselves as no logged in user authenticates the request. To be able to use an app token, one must first create a token via the cli. Replace the `user-name` with an existing user. For the `token-expiration`, you can use any time abbreviation from the following list: `h, m, s`. Examples: `72h` or `1h` or `1m` or `1s.` Default is `72h`.
```bash
ocis auth-app create --user-name={user-name} --expiration={token-expiration}
```
Once generated, these tokens can be used to authenticate requests to ocis. They are passed as part of the request as `Basic Auth` header.

View File

@@ -0,0 +1,14 @@
package main
import (
"os"
"github.com/owncloud/ocis/v2/services/auth-app/pkg/command"
"github.com/owncloud/ocis/v2/services/auth-app/pkg/config/defaults"
)
func main() {
if err := command.Execute(defaults.DefaultConfig()); err != nil {
os.Exit(1)
}
}

View File

@@ -0,0 +1,124 @@
package command
import (
"context"
"fmt"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
"github.com/cs3org/reva/v2/pkg/auth/scope"
"time"
applicationsv1beta1 "github.com/cs3org/go-cs3apis/cs3/auth/applications/v1beta1"
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/owncloud/ocis/v2/ocis-pkg/config/configlog"
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
"github.com/owncloud/ocis/v2/ocis-pkg/tracing"
"github.com/owncloud/ocis/v2/services/auth-app/pkg/config"
"github.com/owncloud/ocis/v2/services/auth-app/pkg/config/parser"
"github.com/urfave/cli/v2"
"google.golang.org/grpc/metadata"
)
// Create is the entrypoint for the app auth create command
func Create(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "create",
Usage: "create an app auth token for a user",
Category: "maintenance",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "user-name",
Value: "",
Usage: "user to create the app-token for",
},
&cli.StringFlag{
Name: "expiration",
Value: "72h",
Usage: "expiration of the app password, e.g. 72h, 1h, 1m, 1s. Default is 72h.",
},
},
Before: func(_ *cli.Context) error {
return configlog.ReturnError(parser.ParseConfig(cfg))
},
Action: func(c *cli.Context) error {
traceProvider, err := tracing.GetServiceTraceProvider(cfg.Tracing, cfg.Service.Name)
if err != nil {
return err
}
gatewaySelector, err := pool.GatewaySelector(
cfg.Reva.Address,
append(
cfg.Reva.GetRevaOptions(),
pool.WithRegistry(registry.GetRegistry()),
pool.WithTracerProvider(traceProvider),
)...)
if err != nil {
return err
}
next, err := gatewaySelector.Next()
if err != nil {
return err
}
userName := c.String("user-name")
if userName == "" {
fmt.Printf("Username to create app token for: ")
if _, err := fmt.Scanln(&userName); err != nil {
return err
}
}
ctx := context.Background()
authRes, err := next.Authenticate(ctx, &gatewayv1beta1.AuthenticateRequest{
Type: "machine",
ClientId: "username:" + userName,
ClientSecret: cfg.MachineAuthAPIKey,
})
if err != nil {
return err
}
if authRes.GetStatus().GetCode() != rpc.Code_CODE_OK {
return fmt.Errorf("error authenticating user: %s", authRes.GetStatus().GetMessage())
}
granteeCtx := ctxpkg.ContextSetUser(context.Background(), &userpb.User{Id: authRes.GetUser().GetId()})
granteeCtx = metadata.AppendToOutgoingContext(granteeCtx, ctxpkg.TokenHeader, authRes.GetToken())
scopes, err := scope.AddOwnerScope(map[string]*authpb.Scope{})
if err != nil {
return err
}
expiry, err := time.ParseDuration(c.String("expiration"))
if err != nil {
return err
}
appPassword, err := next.GenerateAppPassword(granteeCtx, &applicationsv1beta1.GenerateAppPasswordRequest{
TokenScope: scopes,
Label: "Generated via CLI",
Expiration: &typesv1beta1.Timestamp{
Seconds: uint64(time.Now().Add(expiry).Unix()),
},
})
if err != nil {
return err
}
fmt.Printf("App token created for %s", authRes.GetUser().GetUsername())
fmt.Println()
fmt.Printf(" token: %s", appPassword.GetAppPassword().GetPassword())
fmt.Println()
return nil
},
}
}

View File

@@ -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-app/pkg/config"
"github.com/owncloud/ocis/v2/services/auth-app/pkg/config/parser"
"github.com/owncloud/ocis/v2/services/auth-app/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(_ *cli.Context) error {
return configlog.ReturnError(parser.ParseConfig(cfg))
},
Action: func(_ *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
},
}
}

View File

@@ -0,0 +1,35 @@
package command
import (
"os"
"github.com/owncloud/ocis/v2/ocis-pkg/clihelper"
"github.com/owncloud/ocis/v2/services/auth-app/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
Create(cfg),
// infos about this service
Health(cfg),
Version(cfg),
}
}
// Execute is the entry point for the ocis-auth-app command.
func Execute(cfg *config.Config) error {
app := clihelper.DefaultApp(&cli.App{
Name: "auth-app",
Usage: "Provide app authentication for oCIS",
Commands: GetCommands(cfg),
})
return app.Run(os.Args)
}

View File

@@ -0,0 +1,104 @@
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/tracing"
"github.com/owncloud/ocis/v2/ocis-pkg/version"
"github.com/owncloud/ocis/v2/services/auth-app/pkg/config"
"github.com/owncloud/ocis/v2/services/auth-app/pkg/config/parser"
"github.com/owncloud/ocis/v2/services/auth-app/pkg/logging"
"github.com/owncloud/ocis/v2/services/auth-app/pkg/revaconfig"
"github.com/owncloud/ocis/v2/services/auth-app/pkg/server/debug"
"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(_ *cli.Context) error {
return configlog.ReturnFatal(parser.ParseConfig(cfg))
},
Action: func(_ *cli.Context) error {
logger := logging.Configure(cfg.Service.Name, cfg.Log)
traceProvider, err := tracing.GetServiceTraceProvider(cfg.Tracing, cfg.Service.Name)
if err != nil {
return err
}
gr := run.Group{}
ctx, cancel := defineContext(cfg)
defer cancel()
gr.Add(func() error {
pidFile := path.Join(os.TempDir(), "revad-"+cfg.Service.Name+"-"+uuid.Must(uuid.NewV4()).String()+".pid")
rCfg := revaconfig.AuthAppConfigFromStruct(cfg)
reg := registry.GetRegistry()
runtime.RunWithOptions(rCfg, pidFile,
runtime.WithLogger(&logger.Logger),
runtime.WithRegistry(reg),
runtime.WithTraceProvider(traceProvider),
)
return nil
}, func(err error) {
logger.Error().
Str("server", cfg.Service.Name).
Err(err).
Msg("Shutting down server")
cancel()
os.Exit(1)
})
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)
}()
}

View File

@@ -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-app/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
},
}
}

View File

@@ -0,0 +1,57 @@
package config
import (
"context"
"github.com/owncloud/ocis/v2/ocis-pkg/shared"
)
// Config defines the root config structure
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"`
SkipUserGroupsInToken bool `yaml:"skip_user_groups_in_token" env:"AUTH_APP_SKIP_USER_GROUPS_IN_TOKEN" desc:"Disables the encoding of the user's group memberships in the access token. This reduces the token size, especially when users are members of a large number of groups." introductionVersion:"%%NEXT%%"`
MachineAuthAPIKey string `yaml:"machine_auth_api_key" env:"OCIS_MACHINE_AUTH_API_KEY;AUTH_APP_MACHINE_AUTH_API_KEY" desc:"The machine auth API key used to validate internal requests necessary to access resources from other services." introductionVersion:"%%NEXT%%"`
Supervised bool `yaml:"-"`
Context context.Context `yaml:"-"`
}
// Log defines the loging configuration
type Log struct {
Level string `yaml:"level" env:"OCIS_LOG_LEVEL;AUTH_APP_LOG_LEVEL" desc:"The log level. Valid values are: 'panic', 'fatal', 'error', 'warn', 'info', 'debug', 'trace'." introductionVersion:"%%NEXT%%"`
Pretty bool `yaml:"pretty" env:"OCIS_LOG_PRETTY;AUTH_APP_LOG_PRETTY" desc:"Activates pretty log output." introductionVersion:"%%NEXT%%"`
Color bool `yaml:"color" env:"OCIS_LOG_COLOR;AUTH_APP_LOG_COLOR" desc:"Activates colorized log output." introductionVersion:"%%NEXT%%"`
File string `yaml:"file" env:"OCIS_LOG_FILE;AUTH_APP_LOG_FILE" desc:"The path to the log file. Activates logging to this file if set." introductionVersion:"%%NEXT%%"`
}
// Service defines the service configuration
type Service struct {
Name string `yaml:"-"`
}
// Debug defines the debug configuration
type Debug struct {
Addr string `yaml:"addr" env:"AUTH_APP_DEBUG_ADDR" desc:"Bind address of the debug server, where metrics, health, config and debug endpoints will be exposed." introductionVersion:"%%NEXT%%"`
Token string `yaml:"token" env:"AUTH_APP_DEBUG_TOKEN" desc:"Token to secure the metrics endpoint." introductionVersion:"%%NEXT%%"`
Pprof bool `yaml:"pprof" env:"AUTH_APP_DEBUG_PPROF" desc:"Enables pprof, which can be used for profiling." introductionVersion:"%%NEXT%%"`
Zpages bool `yaml:"zpages" env:"AUTH_APP_DEBUG_ZPAGES" desc:"Enables zpages, which can be used for collecting and viewing traces in-memory." introductionVersion:"%%NEXT%%"`
}
// GRPCConfig defines the GRPC configuration
type GRPCConfig struct {
Addr string `yaml:"addr" env:"AUTH_APP_GRPC_ADDR" desc:"The bind address of the GRPC service." introductionVersion:"%%NEXT%%"`
TLS *shared.GRPCServiceTLS `yaml:"tls"`
Namespace string `yaml:"-"`
Protocol string `yaml:"protocol" env:"AUTH_APP_GRPC_PROTOCOL" desc:"The transport protocol of the GRPC service." introductionVersion:"%%NEXT%%"`
}

View File

@@ -0,0 +1,87 @@
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-app/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:9245",
Token: "",
Pprof: false,
Zpages: false,
},
GRPC: config.GRPCConfig{
Addr: "127.0.0.1:9246",
Namespace: "com.owncloud.api",
Protocol: "tcp",
},
Service: config.Service{
Name: "auth-app",
},
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.MachineAuthAPIKey == "" && cfg.Commons != nil && cfg.Commons.MachineAuthAPIKey != "" {
cfg.MachineAuthAPIKey = cfg.Commons.MachineAuthAPIKey
}
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(_ *config.Config) {
// nothing to sanitize here atm
}

View File

@@ -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-app/pkg/config"
"github.com/owncloud/ocis/v2/services/auth-app/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
}

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_APP_JWT_SECRET" desc:"The secret to mint and validate jwt tokens." introductionVersion:"%%NEXT%%"`
}

View File

@@ -0,0 +1,21 @@
package config
import "github.com/owncloud/ocis/v2/ocis-pkg/tracing"
// Tracing defines the tracing configuration.
type Tracing struct {
Enabled bool `yaml:"enabled" env:"OCIS_TRACING_ENABLED;AUTH_APP_TRACING_ENABLED" desc:"Activates tracing." introductionVersion:"%%NEXT%%"`
Type string `yaml:"type" env:"OCIS_TRACING_TYPE;AUTH_APP_TRACING_TYPE" desc:"The type of tracing. Defaults to '', which is the same as 'jaeger'. Allowed tracing types are 'jaeger' and '' as of now." introductionVersion:"%%NEXT%%"`
Endpoint string `yaml:"endpoint" env:"OCIS_TRACING_ENDPOINT;AUTH_APP_TRACING_ENDPOINT" desc:"The endpoint of the tracing agent." introductionVersion:"%%NEXT%%"`
Collector string `yaml:"collector" env:"OCIS_TRACING_COLLECTOR;AUTH_APP_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." introductionVersion:"%%NEXT%%"`
}
// Convert Tracing to the tracing package's Config struct.
func (t Tracing) Convert() tracing.Config {
return tracing.Config{
Enabled: t.Enabled,
Type: t.Type,
Endpoint: t.Endpoint,
Collector: t.Collector,
}
}

View File

@@ -0,0 +1,17 @@
package logging
import (
"github.com/owncloud/ocis/v2/ocis-pkg/log"
"github.com/owncloud/ocis/v2/services/auth-app/pkg/config"
)
// Configure 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),
)
}

View File

@@ -0,0 +1,56 @@
package revaconfig
import (
"path/filepath"
"github.com/owncloud/ocis/v2/ocis-pkg/config/defaults"
"github.com/owncloud/ocis/v2/services/auth-app/pkg/config"
)
// AuthAppConfigFromStruct will adapt an oCIS config struct into a reva mapstructure to start a reva service.
func AuthAppConfigFromStruct(cfg *config.Config) map[string]interface{} {
appAuthJSON := filepath.Join(defaults.BaseDataPath(), "appauth.json")
rcfg := map[string]interface{}{
"shared": map[string]interface{}{
"jwt_secret": cfg.TokenManager.JWTSecret,
"gatewaysvc": cfg.Reva.Address,
"skip_user_groups_in_token": cfg.SkipUserGroupsInToken,
"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": "appauth",
"auth_managers": map[string]interface{}{
"appauth": map[string]interface{}{
"gateway_addr": cfg.Reva.Address,
},
},
},
"applicationauth": map[string]interface{}{
"driver": "json",
"drivers": map[string]interface{}{
"json": map[string]interface{}{
"file": appAuthJSON,
},
},
},
},
"interceptors": map[string]interface{}{
"prometheus": map[string]interface{}{
"namespace": "ocis",
"subsystem": "auth_app",
},
},
},
}
return rcfg
}

View File

@@ -0,0 +1,50 @@
package debug
import (
"context"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
"github.com/owncloud/ocis/v2/services/auth-app/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
}
}

View File

@@ -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-app/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(_ *config.Config) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, _ *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(_ *config.Config) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, _ *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)
}
}
}

View File

@@ -9,6 +9,7 @@ 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-app` handles authentication of external 3rd party apps
- `auth-basic` handles basic authentication
- `auth-bearer` handles oidc authentication
- `auth-machine` handles interservice authentication when a user is impersonated

View File

@@ -5,6 +5,7 @@ The oCIS Auth Bearer service communicates with the configured OpenID Connect ide
## 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-app` handles authentication of external 3rd party apps
- `auth-basic` handles basic authentication
- `auth-bearer` handles oidc authentication
- `auth-machine` handles interservice authentication when a user is impersonated

View File

@@ -3,6 +3,7 @@
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-app` handles authentication of external 3rd party apps
- `auth-basic` handles basic authentication
- `auth-bearer` handles oidc authentication
- `auth-machine` handles interservice authentication when a user is impersonated

View File

@@ -5,6 +5,7 @@ The ocis Auth Service is used to authenticate service accounts. Compared to norm
## 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-app` handles authentication of external 3rd party apps
- `auth-basic` handles basic authentication
- `auth-bearer` handles oidc authentication
- `auth-machine` handles interservice authentication when a user is impersonated

View File

@@ -35,6 +35,7 @@ type Config struct {
GroupsEndpoint string `yaml:"-"`
PermissionsEndpoint string `yaml:"-"`
SharingEndpoint string `yaml:"-"`
AuthAppEndpoint string `yaml:"-"`
AuthBasicEndpoint string `yaml:"-"`
AuthBearerEndpoint string `yaml:"-"`
AuthMachineEndpoint string `yaml:"-"`

View File

@@ -52,6 +52,7 @@ func DefaultConfig() *config.Config {
FrontendPublicURL: "https://localhost:9200",
AppRegistryEndpoint: "com.owncloud.api.app-registry",
AuthAppEndpoint: "com.owncloud.api.auth-app",
AuthBasicEndpoint: "com.owncloud.api.auth-basic",
AuthMachineEndpoint: "com.owncloud.api.auth-machine",
AuthServiceEndpoint: "com.owncloud.api.auth-service",

View File

@@ -37,6 +37,7 @@ func GatewayConfigFromStruct(cfg *config.Config, logger log.Logger) map[string]i
// TODO build services dynamically
"services": map[string]interface{}{
"gateway": map[string]interface{}{
"applicationauthsvc": cfg.AuthAppEndpoint,
// registries are located on the gateway
"authregistrysvc": cfg.Reva.Address,
"storageregistrysvc": cfg.Reva.Address,
@@ -89,6 +90,7 @@ func GatewayConfigFromStruct(cfg *config.Config, logger log.Logger) map[string]i
"drivers": map[string]interface{}{
"static": map[string]interface{}{
"rules": map[string]interface{}{
"appauth": cfg.AuthAppEndpoint,
"basic": cfg.AuthBasicEndpoint,
"machine": cfg.AuthMachineEndpoint,
"publicshares": cfg.StoragePublicLinkEndpoint,

View File

@@ -194,6 +194,7 @@ func Server(cfg *config.Config) *cli.Command {
{
middlewares := loadMiddlewares(logger, cfg, userInfoCache, signingKeyStore, traceProvider, *m, userProvider, gatewaySelector)
server, err := proxyHTTP.Server(
proxyHTTP.Handler(lh.Handler()),
proxyHTTP.Logger(logger),
@@ -293,6 +294,12 @@ func loadMiddlewares(logger log.Logger, cfg *config.Config,
})
}
if cfg.AuthMiddleware.AllowAppAuth {
authenticators = append(authenticators, middleware.AppAuthAuthenticator{
Logger: logger,
RevaGatewaySelector: gatewaySelector,
})
}
authenticators = append(authenticators, middleware.NewOIDCAuthenticator(
middleware.Logger(logger),
middleware.UserInfoCache(userInfoCache),

View File

@@ -91,6 +91,7 @@ var (
// AuthMiddleware configures the proxy http auth middleware.
type AuthMiddleware struct {
CredentialsByUserAgent map[string]string `yaml:"credentials_by_user_agent"`
AllowAppAuth bool `yaml:"allow_app_auth" env:"PROXY_ENABLE_APP_AUTH" desc:"Allow app authentication. This can be used to authenticate 3rd party applications. Note that auth-app service must be running for this feature to work." introductionVersion:"%%NEXT%%"`
}
// PoliciesMiddleware configures the proxy's policies middleware.

View File

@@ -0,0 +1,53 @@
package middleware
import (
"net/http"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
revactx "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
)
// AppAuthAuthenticator defines the app auth authenticator
type AppAuthAuthenticator struct {
Logger log.Logger
RevaGatewaySelector pool.Selectable[gateway.GatewayAPIClient]
}
// Authenticate implements the authenticator interface to authenticate requests via app auth.
func (m AppAuthAuthenticator) Authenticate(r *http.Request) (*http.Request, bool) {
if isPublicPath(r.URL.Path) {
// The authentication of public path requests is handled by another authenticator.
// Since we can't guarantee the order of execution of the authenticators, we better
// implement an early return here for paths we can't authenticate in this authenticator.
return nil, false
}
username, password, ok := r.BasicAuth()
if !ok {
return nil, false
}
next, err := m.RevaGatewaySelector.Next()
if err != nil {
return nil, false
}
authenticateResponse, err := next.Authenticate(r.Context(), &gateway.AuthenticateRequest{
Type: "appauth",
ClientId: username,
ClientSecret: password,
})
if err != nil {
return nil, false
}
if authenticateResponse.GetStatus().GetCode() != cs3rpc.Code_CODE_OK {
// TODO: log???
return nil, false
}
r.Header.Set(revactx.TokenHeader, authenticateResponse.GetToken())
return r, true
}

View File

@@ -0,0 +1,69 @@
package middleware
import (
"net/http"
"net/http/httptest"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"google.golang.org/grpc"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
)
var _ = Describe("Authenticating requests", Label("AppAuthAuthenticator"), func() {
var authenticator Authenticator
BeforeEach(func() {
pool.RemoveSelector("GatewaySelector" + "com.owncloud.api.gateway")
authenticator = AppAuthAuthenticator{
Logger: log.NewLogger(),
RevaGatewaySelector: pool.GetSelector[gateway.GatewayAPIClient](
"GatewaySelector",
"com.owncloud.api.gateway",
func(cc grpc.ClientConnInterface) gateway.GatewayAPIClient {
return mockGatewayClient{
AuthenticateFunc: func(authType, clientID, clientSecret string) (string, rpcv1beta1.Code) {
if authType != "appauth" {
return "", rpcv1beta1.Code_CODE_NOT_FOUND
}
if clientID == "test-user" && clientSecret == "AppPassword" {
return "reva-token", rpcv1beta1.Code_CODE_OK
}
return "", rpcv1beta1.Code_CODE_NOT_FOUND
},
}
},
),
}
})
When("the request contains correct data", func() {
It("should successfully authenticate", func() {
req := httptest.NewRequest(http.MethodGet, "http://example.com/example/path", http.NoBody)
req.SetBasicAuth("test-user", "AppPassword")
req2, valid := authenticator.Authenticate(req)
Expect(valid).To(Equal(true))
Expect(req2).ToNot(BeNil())
Expect(req2.Header.Get("x-access-token")).To(Equal("reva-token"))
})
})
When("the request contains incorrect data", func() {
It("should not successfully authenticate", func() {
req := httptest.NewRequest(http.MethodGet, "http://example.com/example/path", http.NoBody)
req.SetBasicAuth("test-user", "WrongAppPassword")
req2, valid := authenticator.Authenticate(req)
Expect(valid).To(Equal(false))
Expect(req2).To(BeNil())
})
})
})

View File

@@ -18,6 +18,7 @@ import (
var _ = Describe("Authenticating requests", Label("PublicShareAuthenticator"), func() {
var authenticator Authenticator
BeforeEach(func() {
pool.RemoveSelector("GatewaySelector" + "com.owncloud.api.gateway")
authenticator = PublicShareAuthenticator{
Logger: log.NewLogger(),
RevaGatewaySelector: pool.GetSelector[gateway.GatewayAPIClient](