mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-02-05 03:41:30 -05:00
5
changelog/unreleased/auth-app-api.md
Normal file
5
changelog/unreleased/auth-app-api.md
Normal file
@@ -0,0 +1,5 @@
|
||||
Enhancement: Add an API to auth-app service
|
||||
|
||||
Adds an API to create, list and delete app tokens. Includes an impersonification feature for migration scenarios.
|
||||
|
||||
https://github.com/owncloud/ocis/pull/9755
|
||||
@@ -21,10 +21,23 @@ PROXY_ENABLE_APP_AUTH=true # mandatory, allow app authentication. In case o
|
||||
|
||||
## 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`.
|
||||
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. There are different options of creating a token.
|
||||
|
||||
### Via CLI (dev only)
|
||||
|
||||
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.
|
||||
|
||||
### Via API
|
||||
|
||||
The `auth-app` service provides an API to create (POST), list (GET) and delete (DELETE) tokens at `/auth-app/tokens`.
|
||||
|
||||
### Via Impersonation API
|
||||
|
||||
When setting the environment variable `AUTH_APP_ENABLE_IMPERSONATION` to `true`, admins will be able to use the `/auth-app/tokens` endpoint to create tokens for other users. This is crucial for migration scenarios,
|
||||
but should not be used on a productive system.
|
||||
|
||||
@@ -7,18 +7,22 @@ import (
|
||||
"path"
|
||||
|
||||
"github.com/cs3org/reva/v2/cmd/revad/runtime"
|
||||
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"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"
|
||||
ogrpc "github.com/owncloud/ocis/v2/ocis-pkg/service/grpc"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/sync"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/tracing"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
|
||||
"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/owncloud/ocis/v2/services/auth-app/pkg/server/http"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
@@ -32,6 +36,10 @@ func Server(cfg *config.Config) *cli.Command {
|
||||
return configlog.ReturnFatal(parser.ParseConfig(cfg))
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
if cfg.AllowImpersonation {
|
||||
fmt.Println("WARNING: Impersonation is enabled. Admins can impersonate all users.")
|
||||
}
|
||||
|
||||
logger := logging.Configure(cfg.Service.Name, cfg.Log)
|
||||
traceProvider, err := tracing.GetServiceTraceProvider(cfg.Tracing, cfg.Service.Name)
|
||||
if err != nil {
|
||||
@@ -86,6 +94,47 @@ func Server(cfg *config.Config) *cli.Command {
|
||||
logger.Fatal().Err(err).Msg("failed to register the grpc service")
|
||||
}
|
||||
|
||||
tm, err := pool.StringToTLSMode(cfg.GRPCClientTLS.Mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gatewaySelector, err := pool.GatewaySelector(
|
||||
cfg.Reva.Address,
|
||||
append(
|
||||
cfg.Reva.GetRevaOptions(),
|
||||
pool.WithTLSCACert(cfg.GRPCClientTLS.CACert),
|
||||
pool.WithTLSMode(tm),
|
||||
pool.WithRegistry(registry.GetRegistry()),
|
||||
pool.WithTracerProvider(traceProvider),
|
||||
)...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
grpcClient, err := ogrpc.NewClient(
|
||||
append(ogrpc.GetClientOptions(cfg.GRPCClientTLS), ogrpc.WithTraceProvider(traceProvider))...,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rClient := settingssvc.NewRoleService("com.owncloud.api.settings", grpcClient)
|
||||
server, err := http.Server(
|
||||
http.Logger(logger),
|
||||
http.Context(ctx),
|
||||
http.Config(cfg),
|
||||
http.GatewaySelector(gatewaySelector),
|
||||
http.RoleClient(rClient),
|
||||
http.TracerProvider(traceProvider),
|
||||
)
|
||||
if err != nil {
|
||||
logger.Fatal().Err(err).Msg("failed to initialize http server")
|
||||
}
|
||||
|
||||
gr.Add(server.Run, func(err error) {
|
||||
logger.Error().Err(err).Str("server", "http").Msg("shutting down server")
|
||||
})
|
||||
|
||||
return gr.Run()
|
||||
},
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ type Config struct {
|
||||
Debug Debug `yaml:"debug"`
|
||||
|
||||
GRPC GRPCConfig `yaml:"grpc"`
|
||||
HTTP HTTP `yaml:"http"`
|
||||
|
||||
GRPCClientTLS *shared.GRPCClientTLS `yaml:"grpc_client_tls"`
|
||||
|
||||
TokenManager *TokenManager `yaml:"token_manager"`
|
||||
Reva *shared.Reva `yaml:"reva"`
|
||||
@@ -23,6 +26,8 @@ type Config struct {
|
||||
|
||||
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%%"`
|
||||
|
||||
AllowImpersonation bool `yaml:"allow_impersonation" env:"AUTH_APP_ENABLE_IMPERSONATION" desc:"Allows admins to create app tokens for other users. Used for migration. Do NOT use in productive deployments." introductionVersion:"%%NEXT%%"`
|
||||
|
||||
Supervised bool `yaml:"-"`
|
||||
Context context.Context `yaml:"-"`
|
||||
}
|
||||
@@ -55,3 +60,20 @@ type GRPCConfig struct {
|
||||
Namespace string `yaml:"-"`
|
||||
Protocol string `yaml:"protocol" env:"AUTH_APP_GRPC_PROTOCOL" desc:"The transport protocol of the GRPC service." introductionVersion:"%%NEXT%%"`
|
||||
}
|
||||
|
||||
// HTTP defines the available http configuration.
|
||||
type HTTP struct {
|
||||
Addr string `yaml:"addr" env:"AUTH_APP_HTTP_ADDR" desc:"The bind address of the HTTP service." introductionVersion:"pre5.0"`
|
||||
Namespace string `yaml:"-"`
|
||||
Root string `yaml:"root" env:"AUTH_APP_HTTP_ROOT" desc:"Subdirectory that serves as the root for this HTTP service." introductionVersion:"pre5.0"`
|
||||
CORS CORS `yaml:"cors"`
|
||||
TLS shared.HTTPServiceTLS `yaml:"tls"`
|
||||
}
|
||||
|
||||
// CORS defines the available cors configuration.
|
||||
type CORS struct {
|
||||
AllowedOrigins []string `yaml:"allow_origins" env:"OCIS_CORS_ALLOW_ORIGINS;AUTH_APP_CORS_ALLOW_ORIGINS" desc:"A list of allowed CORS origins. See following chapter for more details: *Access-Control-Allow-Origin* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin. See the Environment Variable Types description for more details." introductionVersion:"pre5.0"`
|
||||
AllowedMethods []string `yaml:"allow_methods" env:"OCIS_CORS_ALLOW_METHODS;AUTH_APP_CORS_ALLOW_METHODS" desc:"A list of allowed CORS methods. See following chapter for more details: *Access-Control-Request-Method* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Method. See the Environment Variable Types description for more details." introductionVersion:"pre5.0"`
|
||||
AllowedHeaders []string `yaml:"allow_headers" env:"OCIS_CORS_ALLOW_HEADERS;AUTH_APP_CORS_ALLOW_HEADERS" desc:"A list of allowed CORS headers. See following chapter for more details: *Access-Control-Request-Headers* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Headers. See the Environment Variable Types description for more details." introductionVersion:"pre5.0"`
|
||||
AllowCredentials bool `yaml:"allow_credentials" env:"OCIS_CORS_ALLOW_CREDENTIALS;AUTH_APP_CORS_ALLOW_CREDENTIALS" desc:"Allow credentials for CORS.See following chapter for more details: *Access-Control-Allow-Credentials* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials." introductionVersion:"pre5.0"`
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package defaults
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
@@ -28,6 +30,17 @@ func DefaultConfig() *config.Config {
|
||||
Namespace: "com.owncloud.api",
|
||||
Protocol: "tcp",
|
||||
},
|
||||
HTTP: config.HTTP{
|
||||
Addr: "127.0.0.1:9247",
|
||||
Namespace: "com.owncloud.web",
|
||||
Root: "/",
|
||||
CORS: config.CORS{
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowedMethods: []string{"GET", "POST", "DELETE"},
|
||||
AllowedHeaders: []string{"Authorization", "Origin", "Content-Type", "Accept", "X-Requested-With", "X-Request-Id", "Ocs-Apirequest"},
|
||||
AllowCredentials: true,
|
||||
},
|
||||
},
|
||||
Service: config.Service{
|
||||
Name: "auth-app",
|
||||
},
|
||||
@@ -60,6 +73,10 @@ func EnsureDefaults(cfg *config.Config) {
|
||||
cfg.Tracing = &config.Tracing{}
|
||||
}
|
||||
|
||||
if cfg.GRPCClientTLS == nil && cfg.Commons != nil {
|
||||
cfg.GRPCClientTLS = structs.CopyOrZeroValue(cfg.Commons.GRPCClientTLS)
|
||||
}
|
||||
|
||||
if cfg.Reva == nil && cfg.Commons != nil {
|
||||
cfg.Reva = structs.CopyOrZeroValue(cfg.Commons.Reva)
|
||||
}
|
||||
@@ -79,9 +96,16 @@ func EnsureDefaults(cfg *config.Config) {
|
||||
if cfg.GRPC.TLS == nil && cfg.Commons != nil {
|
||||
cfg.GRPC.TLS = structs.CopyOrZeroValue(cfg.Commons.GRPCServiceTLS)
|
||||
}
|
||||
|
||||
if cfg.Commons != nil {
|
||||
cfg.HTTP.TLS = cfg.Commons.HTTPServiceTLS
|
||||
}
|
||||
}
|
||||
|
||||
// Sanitize sanitized the configuration
|
||||
func Sanitize(_ *config.Config) {
|
||||
// nothing to sanitize here atm
|
||||
func Sanitize(cfg *config.Config) {
|
||||
// sanitize config
|
||||
if cfg.HTTP.Root != "/" {
|
||||
cfg.HTTP.Root = strings.TrimSuffix(cfg.HTTP.Root, "/")
|
||||
}
|
||||
}
|
||||
|
||||
95
services/auth-app/pkg/server/http/option.go
Normal file
95
services/auth-app/pkg/server/http/option.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
|
||||
"github.com/owncloud/ocis/v2/services/auth-app/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
// 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
|
||||
Flags []cli.Flag
|
||||
Namespace string
|
||||
GatewaySelector pool.Selectable[gateway.GatewayAPIClient]
|
||||
RoleClient settingssvc.RoleService
|
||||
TracerProvider trace.TracerProvider
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// Flags provides a function to set the flags option.
|
||||
func Flags(val []cli.Flag) Option {
|
||||
return func(o *Options) {
|
||||
o.Flags = append(o.Flags, val...)
|
||||
}
|
||||
}
|
||||
|
||||
// Namespace provides a function to set the Namespace option.
|
||||
func Namespace(val string) Option {
|
||||
return func(o *Options) {
|
||||
o.Namespace = val
|
||||
}
|
||||
}
|
||||
|
||||
// GatewaySelector provides a function to configure the gateway client selector
|
||||
func GatewaySelector(gatewaySelector pool.Selectable[gateway.GatewayAPIClient]) Option {
|
||||
return func(o *Options) {
|
||||
o.GatewaySelector = gatewaySelector
|
||||
}
|
||||
}
|
||||
|
||||
// RoleClient adds a grpc client for the role service
|
||||
func RoleClient(rs settingssvc.RoleService) Option {
|
||||
return func(o *Options) {
|
||||
o.RoleClient = rs
|
||||
}
|
||||
}
|
||||
|
||||
// TracerProvider provides a function to set the TracerProvider option
|
||||
func TracerProvider(val trace.TracerProvider) Option {
|
||||
return func(o *Options) {
|
||||
o.TracerProvider = val
|
||||
}
|
||||
}
|
||||
97
services/auth-app/pkg/server/http/server.go
Normal file
97
services/auth-app/pkg/server/http/server.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
stdhttp "net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
chimiddleware "github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/account"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/cors"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/middleware"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/service/http"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/tracing"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
svc "github.com/owncloud/ocis/v2/services/auth-app/pkg/service"
|
||||
"github.com/riandyrn/otelchi"
|
||||
"go-micro.dev/v4"
|
||||
)
|
||||
|
||||
// Service is the service interface
|
||||
type Service interface{}
|
||||
|
||||
// Server initializes the http service and server.
|
||||
func Server(opts ...Option) (http.Service, error) {
|
||||
options := newOptions(opts...)
|
||||
|
||||
service, err := http.NewService(
|
||||
http.TLSConfig(options.Config.HTTP.TLS),
|
||||
http.Logger(options.Logger),
|
||||
http.Namespace(options.Config.HTTP.Namespace),
|
||||
http.Name(options.Config.Service.Name),
|
||||
http.Version(version.GetString()),
|
||||
http.Address(options.Config.HTTP.Addr),
|
||||
http.Context(options.Context),
|
||||
http.Flags(options.Flags...),
|
||||
http.TraceProvider(options.TracerProvider),
|
||||
)
|
||||
if err != nil {
|
||||
options.Logger.Error().
|
||||
Err(err).
|
||||
Msg("Error initializing http service")
|
||||
return http.Service{}, fmt.Errorf("could not initialize http service: %w", err)
|
||||
}
|
||||
|
||||
middlewares := []func(stdhttp.Handler) stdhttp.Handler{
|
||||
chimiddleware.RequestID,
|
||||
middleware.Version(
|
||||
options.Config.Service.Name,
|
||||
version.GetString(),
|
||||
),
|
||||
middleware.Logger(
|
||||
options.Logger,
|
||||
),
|
||||
middleware.ExtractAccountUUID(
|
||||
account.Logger(options.Logger),
|
||||
account.JWTSecret(options.Config.TokenManager.JWTSecret),
|
||||
),
|
||||
middleware.Cors(
|
||||
cors.Logger(options.Logger),
|
||||
cors.AllowedOrigins(options.Config.HTTP.CORS.AllowedOrigins),
|
||||
cors.AllowedMethods(options.Config.HTTP.CORS.AllowedMethods),
|
||||
cors.AllowedHeaders(options.Config.HTTP.CORS.AllowedHeaders),
|
||||
cors.AllowCredentials(options.Config.HTTP.CORS.AllowCredentials),
|
||||
),
|
||||
}
|
||||
|
||||
mux := chi.NewMux()
|
||||
mux.Use(middlewares...)
|
||||
|
||||
mux.Use(
|
||||
otelchi.Middleware(
|
||||
"auth-app",
|
||||
otelchi.WithChiRoutes(mux),
|
||||
otelchi.WithTracerProvider(options.TracerProvider),
|
||||
otelchi.WithPropagators(tracing.GetPropagator()),
|
||||
),
|
||||
)
|
||||
|
||||
handle, err := svc.NewAuthAppService(
|
||||
svc.Logger(options.Logger),
|
||||
svc.Mux(mux),
|
||||
svc.Config(options.Config),
|
||||
svc.GatewaySelector(options.GatewaySelector),
|
||||
svc.RoleClient(options.RoleClient),
|
||||
svc.TraceProvider(options.TracerProvider),
|
||||
)
|
||||
if err != nil {
|
||||
return http.Service{}, err
|
||||
}
|
||||
|
||||
if err := micro.RegisterHandler(service.Server(), handle); err != nil {
|
||||
return http.Service{}, err
|
||||
}
|
||||
|
||||
return service, nil
|
||||
}
|
||||
76
services/auth-app/pkg/service/option.go
Normal file
76
services/auth-app/pkg/service/option.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
|
||||
"github.com/owncloud/ocis/v2/services/auth-app/pkg/config"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
// 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
|
||||
GatewaySelector pool.Selectable[gateway.GatewayAPIClient]
|
||||
Mux *chi.Mux
|
||||
TracerProvider trace.TracerProvider
|
||||
RoleClient settingssvc.RoleService
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// GatewaySelector provides a function to configure the gateway client selector
|
||||
func GatewaySelector(gatewaySelector pool.Selectable[gateway.GatewayAPIClient]) Option {
|
||||
return func(o *Options) {
|
||||
o.GatewaySelector = gatewaySelector
|
||||
}
|
||||
}
|
||||
|
||||
// TraceProvider provides a function to set the TracerProvider option
|
||||
func TraceProvider(val trace.TracerProvider) Option {
|
||||
return func(o *Options) {
|
||||
o.TracerProvider = val
|
||||
}
|
||||
}
|
||||
|
||||
// Mux defines the muxer for the userlog service
|
||||
func Mux(m *chi.Mux) Option {
|
||||
return func(o *Options) {
|
||||
o.Mux = m
|
||||
}
|
||||
}
|
||||
|
||||
// RoleClient adds a grpc client for the role service
|
||||
func RoleClient(rs settingssvc.RoleService) Option {
|
||||
return func(o *Options) {
|
||||
o.RoleClient = rs
|
||||
}
|
||||
}
|
||||
317
services/auth-app/pkg/service/service.go
Normal file
317
services/auth-app/pkg/service/service.go
Normal file
@@ -0,0 +1,317 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
applications "github.com/cs3org/go-cs3apis/cs3/auth/applications/v1beta1"
|
||||
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
|
||||
gateway "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"
|
||||
"github.com/cs3org/reva/v2/pkg/appctx"
|
||||
"github.com/cs3org/reva/v2/pkg/auth/scope"
|
||||
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
|
||||
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/cs3org/reva/v2/pkg/utils"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/roles"
|
||||
"github.com/owncloud/ocis/v2/services/auth-app/pkg/config"
|
||||
settings "github.com/owncloud/ocis/v2/services/settings/pkg/service/v0"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
// AuthAppToken represents an app token.
|
||||
type AuthAppToken struct {
|
||||
Token string `json:"token"`
|
||||
ExpirationDate time.Time `json:"expiration_date"`
|
||||
CreatedDate time.Time `json:"created_date"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
// AuthAppService defines the service interface.
|
||||
type AuthAppService struct {
|
||||
log log.Logger
|
||||
cfg *config.Config
|
||||
gws pool.Selectable[gateway.GatewayAPIClient]
|
||||
m *chi.Mux
|
||||
r *roles.Manager
|
||||
}
|
||||
|
||||
// NewAuthAppService initializes a new AuthAppService.
|
||||
func NewAuthAppService(opts ...Option) (*AuthAppService, error) {
|
||||
o := &Options{}
|
||||
for _, opt := range opts {
|
||||
opt(o)
|
||||
}
|
||||
|
||||
r := roles.NewManager(
|
||||
roles.Logger(o.Logger),
|
||||
roles.RoleService(o.RoleClient),
|
||||
)
|
||||
|
||||
a := &AuthAppService{
|
||||
log: o.Logger,
|
||||
cfg: o.Config,
|
||||
gws: o.GatewaySelector,
|
||||
m: o.Mux,
|
||||
r: &r,
|
||||
}
|
||||
|
||||
a.m.Route("/auth-app/tokens", func(r chi.Router) {
|
||||
r.Get("/", a.HandleList)
|
||||
r.Post("/", a.HandleCreate)
|
||||
r.Delete("/", a.HandleDelete)
|
||||
})
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// ServeHTTP implements the http.Handler interface.
|
||||
func (a *AuthAppService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
a.m.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// HandleCreate handles the creation of app tokens
|
||||
func (a *AuthAppService) HandleCreate(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := getContext(r)
|
||||
sublog := a.log.With().Str("actor", ctxpkg.ContextMustGetUser(ctx).GetId().GetOpaqueId()).Logger()
|
||||
|
||||
gwc, err := a.gws.Next()
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error getting gateway client")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
q := r.URL.Query()
|
||||
expiry, err := time.ParseDuration(q.Get("expiry"))
|
||||
if err != nil {
|
||||
sublog.Info().Err(err).Str("duration", q.Get("expiry")).Msg("error parsing expiry")
|
||||
http.Error(w, "error parsing expiry. Use e.g. 30m or 72h", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
label := "Generated via API"
|
||||
cid := buildClientID(q.Get("userID"), q.Get("userName"))
|
||||
if cid != "" {
|
||||
if !a.cfg.AllowImpersonation {
|
||||
sublog.Error().Msg("impersonation is not allowed")
|
||||
http.Error(w, "impersonation is not allowed", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
ok, err := isAdmin(ctx, a.r)
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error checking if user is admin")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
sublog.Error().Msg("user is not admin")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
ctx, err = a.authenticateUser(cid, gwc)
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error authenticating user")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
label = "Generated via Impersonation API"
|
||||
}
|
||||
|
||||
scopes, err := scope.AddOwnerScope(map[string]*authpb.Scope{})
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error adding owner scope")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
res, err := gwc.GenerateAppPassword(ctx, &applications.GenerateAppPasswordRequest{
|
||||
TokenScope: scopes,
|
||||
Label: label,
|
||||
Expiration: utils.TimeToTS(time.Now().Add(expiry)),
|
||||
})
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error generating app password")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if res.GetStatus().GetCode() != rpc.Code_CODE_OK {
|
||||
sublog.Error().Str("status", res.GetStatus().GetCode().String()).Msg("error generating app password")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
b, err := json.Marshal(convert(res.GetAppPassword()))
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error marshaling app password")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := w.Write(b); err != nil {
|
||||
sublog.Error().Err(err).Msg("error writing response")
|
||||
}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
}
|
||||
|
||||
// HandleList handles listing of app tokens
|
||||
func (a *AuthAppService) HandleList(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := getContext(r)
|
||||
sublog := a.log.With().Str("actor", ctxpkg.ContextMustGetUser(ctx).GetId().GetOpaqueId()).Logger()
|
||||
|
||||
gwc, err := a.gws.Next()
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error getting gateway client")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
res, err := gwc.ListAppPasswords(ctx, &applications.ListAppPasswordsRequest{})
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error listing app passwords")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if res.GetStatus().GetCode() != rpc.Code_CODE_OK {
|
||||
sublog.Error().Str("status", res.GetStatus().GetCode().String()).Msg("error listing app passwords")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
tokens := make([]AuthAppToken, 0, len(res.GetAppPasswords()))
|
||||
for _, ap := range res.GetAppPasswords() {
|
||||
tokens = append(tokens, convert(ap))
|
||||
}
|
||||
|
||||
b, err := json.Marshal(tokens)
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error marshaling app passwords")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := w.Write(b); err != nil {
|
||||
sublog.Error().Err(err).Msg("error writing response")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// HandleDelete handles deletion of app tokens
|
||||
func (a *AuthAppService) HandleDelete(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := getContext(r)
|
||||
sublog := a.log.With().Str("actor", ctxpkg.ContextMustGetUser(ctx).GetId().GetOpaqueId()).Logger()
|
||||
|
||||
gwc, err := a.gws.Next()
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error getting gateway client")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
pw := r.URL.Query().Get("token")
|
||||
if pw == "" {
|
||||
sublog.Info().Msg("missing token")
|
||||
http.Error(w, "missing auth-app token. Set 'token' parameter", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
res, err := gwc.InvalidateAppPassword(ctx, &applications.InvalidateAppPasswordRequest{Password: pw})
|
||||
if err != nil {
|
||||
sublog.Error().Err(err).Msg("error invalidating app password")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if res.GetStatus().GetCode() != rpc.Code_CODE_OK {
|
||||
sublog.Error().Str("status", res.GetStatus().GetCode().String()).Msg("error invalidating app password")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (a *AuthAppService) authenticateUser(clientID string, gwc gateway.GatewayAPIClient) (context.Context, error) {
|
||||
ctx := context.Background()
|
||||
authRes, err := gwc.Authenticate(ctx, &gateway.AuthenticateRequest{
|
||||
Type: "machine",
|
||||
ClientId: clientID,
|
||||
ClientSecret: a.cfg.MachineAuthAPIKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if authRes.GetStatus().GetCode() != rpc.Code_CODE_OK {
|
||||
return nil, errors.New("error authenticating user: " + authRes.GetStatus().GetMessage())
|
||||
}
|
||||
|
||||
ctx = ctxpkg.ContextSetUser(ctx, &userpb.User{Id: authRes.GetUser().GetId()})
|
||||
return metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, authRes.GetToken()), nil
|
||||
}
|
||||
|
||||
func getContext(r *http.Request) context.Context {
|
||||
ctx := r.Context()
|
||||
return metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, r.Header.Get("X-Access-Token"))
|
||||
}
|
||||
|
||||
func buildClientID(userID, userName string) string {
|
||||
switch {
|
||||
default:
|
||||
return ""
|
||||
case userID != "":
|
||||
return "userid:" + userID
|
||||
case userName != "":
|
||||
return "username:" + userName
|
||||
}
|
||||
}
|
||||
|
||||
// isAdmin determines if the user in the context is an admin / has account management permissions
|
||||
func isAdmin(ctx context.Context, rm *roles.Manager) (bool, error) {
|
||||
logger := appctx.GetLogger(ctx)
|
||||
|
||||
u, ok := ctxpkg.ContextGetUser(ctx)
|
||||
uid := u.GetId().GetOpaqueId()
|
||||
if !ok || uid == "" {
|
||||
logger.Error().Str("userid", uid).Msg("user not in context")
|
||||
return false, errors.New("no user in context")
|
||||
}
|
||||
// get roles from context
|
||||
roleIDs, ok := roles.ReadRoleIDsFromContext(ctx)
|
||||
if !ok {
|
||||
logger.Debug().Str("userid", uid).Msg("No roles in context, contacting settings service")
|
||||
var err error
|
||||
roleIDs, err = rm.FindRoleIDsForUser(ctx, uid)
|
||||
if err != nil {
|
||||
logger.Err(err).Str("userid", uid).Msg("failed to get roles for user")
|
||||
return false, err
|
||||
}
|
||||
|
||||
if len(roleIDs) == 0 {
|
||||
logger.Err(err).Str("userid", uid).Msg("user has no roles")
|
||||
return false, errors.New("user has no roles")
|
||||
}
|
||||
}
|
||||
|
||||
// check if permission is present in roles of the authenticated account
|
||||
return rm.FindPermissionByID(ctx, roleIDs, settings.AccountManagementPermissionID) != nil, nil
|
||||
}
|
||||
|
||||
func convert(ap *applications.AppPassword) AuthAppToken {
|
||||
return AuthAppToken{
|
||||
Token: ap.GetPassword(),
|
||||
ExpirationDate: utils.TSToTime(ap.GetExpiration()),
|
||||
CreatedDate: utils.TSToTime(ap.GetCtime()),
|
||||
Label: ap.GetLabel(),
|
||||
}
|
||||
}
|
||||
@@ -258,6 +258,10 @@ func DefaultPolicies() []config.Policy {
|
||||
Endpoint: "/api/v0/settings",
|
||||
Service: "com.owncloud.web.settings",
|
||||
},
|
||||
{
|
||||
Endpoint: "/auth-app/tokens",
|
||||
Service: "com.owncloud.web.auth-app",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user