From d483234b653e6d4547ecbef28bb2a97c3936910f Mon Sep 17 00:00:00 2001 From: jkoberg Date: Wed, 7 Aug 2024 13:01:32 +0200 Subject: [PATCH] feat(auth-app): secure create endpoint Signed-off-by: jkoberg --- services/auth-app/pkg/command/server.go | 21 ++++++ services/auth-app/pkg/config/config.go | 4 ++ .../pkg/config/defaults/defaultconfig.go | 4 ++ services/auth-app/pkg/server/http/option.go | 9 +++ services/auth-app/pkg/server/http/server.go | 1 + services/auth-app/pkg/service/option.go | 9 +++ services/auth-app/pkg/service/service.go | 72 +++++++++++++++++-- 7 files changed, 116 insertions(+), 4 deletions(-) diff --git a/services/auth-app/pkg/command/server.go b/services/auth-app/pkg/command/server.go index a8a76f73ad..77cb36431a 100644 --- a/services/auth-app/pkg/command/server.go +++ b/services/auth-app/pkg/command/server.go @@ -12,9 +12,11 @@ import ( "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" @@ -34,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 { @@ -88,10 +94,16 @@ 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), )...) @@ -99,11 +111,20 @@ func Server(cfg *config.Config) *cli.Command { 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 { diff --git a/services/auth-app/pkg/config/config.go b/services/auth-app/pkg/config/config.go index 1fab75b7a2..175b74fd8a 100644 --- a/services/auth-app/pkg/config/config.go +++ b/services/auth-app/pkg/config/config.go @@ -17,6 +17,8 @@ type Config struct { 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"` @@ -24,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:"-"` } diff --git a/services/auth-app/pkg/config/defaults/defaultconfig.go b/services/auth-app/pkg/config/defaults/defaultconfig.go index 66b362f8ba..ffd7aef80e 100644 --- a/services/auth-app/pkg/config/defaults/defaultconfig.go +++ b/services/auth-app/pkg/config/defaults/defaultconfig.go @@ -73,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) } diff --git a/services/auth-app/pkg/server/http/option.go b/services/auth-app/pkg/server/http/option.go index 3f7a4779d3..1d141e9e34 100644 --- a/services/auth-app/pkg/server/http/option.go +++ b/services/auth-app/pkg/server/http/option.go @@ -6,6 +6,7 @@ import ( 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" @@ -22,6 +23,7 @@ type Options struct { Flags []cli.Flag Namespace string GatewaySelector pool.Selectable[gateway.GatewayAPIClient] + RoleClient settingssvc.RoleService TracerProvider trace.TracerProvider } @@ -78,6 +80,13 @@ func GatewaySelector(gatewaySelector pool.Selectable[gateway.GatewayAPIClient]) } } +// 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) { diff --git a/services/auth-app/pkg/server/http/server.go b/services/auth-app/pkg/server/http/server.go index 8ba4cec9a4..4c18d1e2fb 100644 --- a/services/auth-app/pkg/server/http/server.go +++ b/services/auth-app/pkg/server/http/server.go @@ -82,6 +82,7 @@ func Server(opts ...Option) (http.Service, error) { svc.Mux(mux), svc.Config(options.Config), svc.GatewaySelector(options.GatewaySelector), + svc.RoleClient(options.RoleClient), svc.TraceProvider(options.TracerProvider), ) if err != nil { diff --git a/services/auth-app/pkg/service/option.go b/services/auth-app/pkg/service/option.go index 3fc046192d..967001c7cd 100644 --- a/services/auth-app/pkg/service/option.go +++ b/services/auth-app/pkg/service/option.go @@ -7,6 +7,7 @@ import ( "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" ) @@ -22,6 +23,7 @@ type Options struct { GatewaySelector pool.Selectable[gateway.GatewayAPIClient] Mux *chi.Mux TracerProvider trace.TracerProvider + RoleClient settingssvc.RoleService } // Logger provides a function to set the logger option. @@ -65,3 +67,10 @@ func Mux(m *chi.Mux) Option { 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 + } +} diff --git a/services/auth-app/pkg/service/service.go b/services/auth-app/pkg/service/service.go index db16ababa7..bb9431b270 100644 --- a/services/auth-app/pkg/service/service.go +++ b/services/auth-app/pkg/service/service.go @@ -12,13 +12,16 @@ import ( 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" - types "github.com/cs3org/go-cs3apis/cs3/types/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" ) @@ -28,6 +31,7 @@ type AuthAppService struct { cfg *config.Config gws pool.Selectable[gateway.GatewayAPIClient] m *chi.Mux + r *roles.Manager } // NewAuthAppService initializes a new AuthAppService. @@ -36,11 +40,19 @@ func NewAuthAppService(opts ...Option) (*AuthAppService, error) { for _, opt := range opts { opt(o) } + + r := roles.NewManager( + // TODO: caching? + 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) { @@ -68,8 +80,31 @@ func (a *AuthAppService) HandleCreate(w http.ResponseWriter, r *http.Request) { ctx := getContext(r) q := r.URL.Query() + expiry, err := time.ParseDuration(q.Get("expiry")) + if err != nil { + a.log.Info().Err(err).Msg("error parsing expiry") + http.Error(w, "error parsing expiry. Use e.g. 30m or 72h", http.StatusBadRequest) + return + } + cid := buildClientID(q.Get("userID"), q.Get("userName")) if cid != "" { + if !a.cfg.AllowImpersonation { + a.log.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 { + a.log.Error().Err(err).Msg("error checking if user is admin") + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + if !ok { + a.log.Error().Msg("user is not admin") + http.Error(w, "forbidden", http.StatusForbidden) + return + } ctx, err = a.authenticateUser(cid, gwc) if err != nil { a.log.Error().Err(err).Msg("error authenticating user") @@ -88,9 +123,7 @@ func (a *AuthAppService) HandleCreate(w http.ResponseWriter, r *http.Request) { res, err := gwc.GenerateAppPassword(ctx, &applications.GenerateAppPasswordRequest{ TokenScope: scopes, Label: "Generated via API", - Expiration: &types.Timestamp{ - Seconds: uint64(time.Now().Add(time.Hour).Unix()), - }, + Expiration: utils.TimeToTS(time.Now().Add(expiry)), }) if err != nil { a.log.Error().Err(err).Msg("error generating app password") @@ -225,3 +258,34 @@ func buildClientID(userID, userName string) string { 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 +}