From 48af3fae3270181dd9f315879cde8ec3adda075c Mon Sep 17 00:00:00 2001 From: jkoberg Date: Wed, 7 Aug 2024 10:29:54 +0200 Subject: [PATCH 1/5] feat(auth-app): Add an API to generate AppTokens Signed-off-by: jkoberg --- services/auth-app/pkg/command/server.go | 28 ++++++ services/auth-app/pkg/config/config.go | 18 ++++ .../pkg/config/defaults/defaultconfig.go | 11 +++ services/auth-app/pkg/server/http/option.go | 86 +++++++++++++++++ services/auth-app/pkg/server/http/server.go | 96 +++++++++++++++++++ services/auth-app/pkg/service/option.go | 67 +++++++++++++ services/auth-app/pkg/service/service.go | 44 +++++++++ .../pkg/config/defaults/defaultconfig.go | 4 + 8 files changed, 354 insertions(+) create mode 100644 services/auth-app/pkg/server/http/option.go create mode 100644 services/auth-app/pkg/server/http/server.go create mode 100644 services/auth-app/pkg/service/option.go create mode 100644 services/auth-app/pkg/service/service.go diff --git a/services/auth-app/pkg/command/server.go b/services/auth-app/pkg/command/server.go index 323e203c73c..a8a76f73ad0 100644 --- a/services/auth-app/pkg/command/server.go +++ b/services/auth-app/pkg/command/server.go @@ -7,6 +7,7 @@ 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" @@ -19,6 +20,7 @@ import ( "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" ) @@ -86,6 +88,32 @@ func Server(cfg *config.Config) *cli.Command { logger.Fatal().Err(err).Msg("failed to register the grpc service") } + gatewaySelector, err := pool.GatewaySelector( + cfg.Reva.Address, + append( + cfg.Reva.GetRevaOptions(), + pool.WithRegistry(registry.GetRegistry()), + pool.WithTracerProvider(traceProvider), + )...) + if err != nil { + return err + } + + server, err := http.Server( + http.Logger(logger), + http.Context(ctx), + http.Config(cfg), + http.GatewaySelector(gatewaySelector), + 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() }, } diff --git a/services/auth-app/pkg/config/config.go b/services/auth-app/pkg/config/config.go index 01d58b81eb5..1fab75b7a20 100644 --- a/services/auth-app/pkg/config/config.go +++ b/services/auth-app/pkg/config/config.go @@ -15,6 +15,7 @@ type Config struct { Debug Debug `yaml:"debug"` GRPC GRPCConfig `yaml:"grpc"` + HTTP HTTP `yaml:"http"` TokenManager *TokenManager `yaml:"token_manager"` Reva *shared.Reva `yaml:"reva"` @@ -55,3 +56,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"` +} diff --git a/services/auth-app/pkg/config/defaults/defaultconfig.go b/services/auth-app/pkg/config/defaults/defaultconfig.go index 19953bcef28..cd845f7f4d6 100644 --- a/services/auth-app/pkg/config/defaults/defaultconfig.go +++ b/services/auth-app/pkg/config/defaults/defaultconfig.go @@ -28,6 +28,17 @@ func DefaultConfig() *config.Config { Namespace: "com.owncloud.api", Protocol: "tcp", }, + HTTP: config.HTTP{ + Addr: "127.0.0.1:0", + Namespace: "com.owncloud.api", + Root: "/", + CORS: config.CORS{ + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{"POST"}, + AllowedHeaders: []string{"Authorization", "Origin", "Content-Type", "Accept", "X-Requested-With", "X-Request-Id", "Ocs-Apirequest"}, + AllowCredentials: true, + }, + }, Service: config.Service{ Name: "auth-app", }, diff --git a/services/auth-app/pkg/server/http/option.go b/services/auth-app/pkg/server/http/option.go new file mode 100644 index 00000000000..3f7a4779d39 --- /dev/null +++ b/services/auth-app/pkg/server/http/option.go @@ -0,0 +1,86 @@ +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" + "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] + 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 + } +} + +// TracerProvider provides a function to set the TracerProvider option +func TracerProvider(val trace.TracerProvider) Option { + return func(o *Options) { + o.TracerProvider = val + } +} diff --git a/services/auth-app/pkg/server/http/server.go b/services/auth-app/pkg/server/http/server.go new file mode 100644 index 00000000000..8ba4cec9a42 --- /dev/null +++ b/services/auth-app/pkg/server/http/server.go @@ -0,0 +1,96 @@ +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.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 +} diff --git a/services/auth-app/pkg/service/option.go b/services/auth-app/pkg/service/option.go new file mode 100644 index 00000000000..3fc046192d9 --- /dev/null +++ b/services/auth-app/pkg/service/option.go @@ -0,0 +1,67 @@ +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" + "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 +} + +// 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 + } +} diff --git a/services/auth-app/pkg/service/service.go b/services/auth-app/pkg/service/service.go new file mode 100644 index 00000000000..37e67974b8c --- /dev/null +++ b/services/auth-app/pkg/service/service.go @@ -0,0 +1,44 @@ +package service + +import ( + "fmt" + "net/http" + + gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" + "github.com/go-chi/chi/v5" +) + +// AuthAppService defines the service interface. +type AuthAppService struct { + gws pool.Selectable[gateway.GatewayAPIClient] + m *chi.Mux +} + +// NewAuthAppService initializes a new AuthAppService. +func NewAuthAppService(opts ...Option) (*AuthAppService, error) { + o := &Options{} + for _, opt := range opts { + opt(o) + } + a := &AuthAppService{ + gws: o.GatewaySelector, + m: o.Mux, + } + + a.m.Route("/auth-app/tokens", func(r chi.Router) { + r.Post("/", a.HandleCreate) + }) + + 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 a new auth-token +func (a *AuthAppService) HandleCreate(w http.ResponseWriter, r *http.Request) { + fmt.Println("ALIVE") +} diff --git a/services/proxy/pkg/config/defaults/defaultconfig.go b/services/proxy/pkg/config/defaults/defaultconfig.go index d37c83ac9e3..f59255b396c 100644 --- a/services/proxy/pkg/config/defaults/defaultconfig.go +++ b/services/proxy/pkg/config/defaults/defaultconfig.go @@ -258,6 +258,10 @@ func DefaultPolicies() []config.Policy { Endpoint: "/api/v0/settings", Service: "com.owncloud.web.settings", }, + { + Endpoint: "/auth-app/tokens", + Service: "com.owncloud.api.auth-app", + }, }, }, } From 2a498daf073b0e511ae9cf3f45d3169fe5115047 Mon Sep 17 00:00:00 2001 From: jkoberg Date: Wed, 7 Aug 2024 10:47:20 +0200 Subject: [PATCH 2/5] feat(auth-app): list and delete endpoints Signed-off-by: jkoberg --- .../pkg/config/defaults/defaultconfig.go | 17 +- services/auth-app/pkg/service/service.go | 189 +++++++++++++++++- .../pkg/config/defaults/defaultconfig.go | 2 +- 3 files changed, 200 insertions(+), 8 deletions(-) diff --git a/services/auth-app/pkg/config/defaults/defaultconfig.go b/services/auth-app/pkg/config/defaults/defaultconfig.go index cd845f7f4d6..66b362f8ba4 100644 --- a/services/auth-app/pkg/config/defaults/defaultconfig.go +++ b/services/auth-app/pkg/config/defaults/defaultconfig.go @@ -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" @@ -29,8 +31,8 @@ func DefaultConfig() *config.Config { Protocol: "tcp", }, HTTP: config.HTTP{ - Addr: "127.0.0.1:0", - Namespace: "com.owncloud.api", + Addr: "127.0.0.1:9247", + Namespace: "com.owncloud.web", Root: "/", CORS: config.CORS{ AllowedOrigins: []string{"*"}, @@ -90,9 +92,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, "/") + } } diff --git a/services/auth-app/pkg/service/service.go b/services/auth-app/pkg/service/service.go index 37e67974b8c..db16ababa7a 100644 --- a/services/auth-app/pkg/service/service.go +++ b/services/auth-app/pkg/service/service.go @@ -1,16 +1,31 @@ package service import ( - "fmt" + "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" + types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + "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/go-chi/chi/v5" + "github.com/owncloud/ocis/v2/ocis-pkg/log" + "github.com/owncloud/ocis/v2/services/auth-app/pkg/config" + "google.golang.org/grpc/metadata" ) // AuthAppService defines the service interface. type AuthAppService struct { + log log.Logger + cfg *config.Config gws pool.Selectable[gateway.GatewayAPIClient] m *chi.Mux } @@ -22,12 +37,16 @@ func NewAuthAppService(opts ...Option) (*AuthAppService, error) { opt(o) } a := &AuthAppService{ + log: o.Logger, + cfg: o.Config, gws: o.GatewaySelector, m: o.Mux, } 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 @@ -38,7 +57,171 @@ func (a *AuthAppService) ServeHTTP(w http.ResponseWriter, r *http.Request) { a.m.ServeHTTP(w, r) } -// HandleCreate handles the creation of a new auth-token +// HandleCreate handles the creation of app tokens func (a *AuthAppService) HandleCreate(w http.ResponseWriter, r *http.Request) { - fmt.Println("ALIVE") + gwc, err := a.gws.Next() + if err != nil { + http.Error(w, "error getting gateway client", http.StatusInternalServerError) + return + } + + ctx := getContext(r) + + q := r.URL.Query() + cid := buildClientID(q.Get("userID"), q.Get("userName")) + if cid != "" { + ctx, err = a.authenticateUser(cid, gwc) + if err != nil { + a.log.Error().Err(err).Msg("error authenticating user") + http.Error(w, "error authenticating user", http.StatusInternalServerError) + return + } + } + + scopes, err := scope.AddOwnerScope(map[string]*authpb.Scope{}) + if err != nil { + a.log.Error().Err(err).Msg("error adding owner scope") + http.Error(w, "error adding owner scope", http.StatusInternalServerError) + return + } + + res, err := gwc.GenerateAppPassword(ctx, &applications.GenerateAppPasswordRequest{ + TokenScope: scopes, + Label: "Generated via API", + Expiration: &types.Timestamp{ + Seconds: uint64(time.Now().Add(time.Hour).Unix()), + }, + }) + if err != nil { + a.log.Error().Err(err).Msg("error generating app password") + http.Error(w, "error generating app password", http.StatusInternalServerError) + return + } + if res.GetStatus().GetCode() != rpc.Code_CODE_OK { + a.log.Error().Str("status", res.GetStatus().GetCode().String()).Msg("error generating app password") + http.Error(w, "error generating app password: "+res.GetStatus().GetMessage(), http.StatusInternalServerError) + return + } + + b, err := json.Marshal(res.GetAppPassword()) + if err != nil { + a.log.Error().Err(err).Msg("error marshaling app password") + http.Error(w, "error marshaling app password", http.StatusInternalServerError) + return + } + + if _, err := w.Write(b); err != nil { + a.log.Error().Err(err).Msg("error writing response") + http.Error(w, "error writing response", http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) +} + +// HandleList handles listing of app tokens +func (a *AuthAppService) HandleList(w http.ResponseWriter, r *http.Request) { + gwc, err := a.gws.Next() + if err != nil { + a.log.Error().Err(err).Msg("error getting gateway client") + http.Error(w, "error getting gateway client", http.StatusInternalServerError) + return + } + + ctx := getContext(r) + + res, err := gwc.ListAppPasswords(ctx, &applications.ListAppPasswordsRequest{}) + if err != nil { + a.log.Error().Err(err).Msg("error listing app passwords") + http.Error(w, "error listing app passwords", http.StatusInternalServerError) + return + } + + if res.GetStatus().GetCode() != rpc.Code_CODE_OK { + a.log.Error().Str("status", res.GetStatus().GetCode().String()).Msg("error listing app passwords") + http.Error(w, "error listing app passwords: "+res.GetStatus().GetMessage(), http.StatusInternalServerError) + return + } + + b, err := json.Marshal(res.GetAppPasswords()) + if err != nil { + a.log.Error().Err(err).Msg("error marshaling app passwords") + http.Error(w, "error marshaling app passwords", http.StatusInternalServerError) + return + } + + if _, err := w.Write(b); err != nil { + a.log.Error().Err(err).Msg("error writing response") + http.Error(w, "error writing response", http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) +} + +// HandleDelete handles deletion of app tokens +func (a *AuthAppService) HandleDelete(w http.ResponseWriter, r *http.Request) { + gwc, err := a.gws.Next() + if err != nil { + a.log.Error().Err(err).Msg("error getting gateway client") + http.Error(w, "error getting gateway client", http.StatusInternalServerError) + return + } + + ctx := getContext(r) + + pw := r.URL.Query().Get("token") + if pw == "" { + a.log.Info().Msg("missing token") + http.Error(w, "missing token", http.StatusBadRequest) + return + } + + res, err := gwc.InvalidateAppPassword(ctx, &applications.InvalidateAppPasswordRequest{Password: pw}) + if err != nil { + a.log.Error().Err(err).Msg("error invalidating app password") + http.Error(w, "error invalidating app password", http.StatusInternalServerError) + return + } + + if res.GetStatus().GetCode() != rpc.Code_CODE_OK { + a.log.Error().Str("status", res.GetStatus().GetCode().String()).Msg("error invalidating app password") + http.Error(w, "error invalidating app password: "+res.GetStatus().GetMessage(), 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 + } } diff --git a/services/proxy/pkg/config/defaults/defaultconfig.go b/services/proxy/pkg/config/defaults/defaultconfig.go index f59255b396c..40253045e58 100644 --- a/services/proxy/pkg/config/defaults/defaultconfig.go +++ b/services/proxy/pkg/config/defaults/defaultconfig.go @@ -260,7 +260,7 @@ func DefaultPolicies() []config.Policy { }, { Endpoint: "/auth-app/tokens", - Service: "com.owncloud.api.auth-app", + Service: "com.owncloud.web.auth-app", }, }, }, From d483234b653e6d4547ecbef28bb2a97c3936910f Mon Sep 17 00:00:00 2001 From: jkoberg Date: Wed, 7 Aug 2024 13:01:32 +0200 Subject: [PATCH 3/5] 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 a8a76f73ad0..77cb36431a2 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 1fab75b7a20..175b74fd8aa 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 66b362f8ba4..ffd7aef80e6 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 3f7a4779d39..1d141e9e34f 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 8ba4cec9a42..4c18d1e2fb2 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 3fc046192d9..967001c7cde 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 db16ababa7a..bb9431b2705 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 +} From e7e1839296826098f5cb550c10afdf58f60ecf17 Mon Sep 17 00:00:00 2001 From: jkoberg Date: Wed, 7 Aug 2024 13:28:10 +0200 Subject: [PATCH 4/5] feat(auth-app): fixed response format Signed-off-by: jkoberg --- changelog/unreleased/auth-app-api.md | 5 + services/auth-app/README.md | 15 ++- .../pkg/config/defaults/defaultconfig.go | 2 +- services/auth-app/pkg/service/service.go | 122 +++++++++++------- 4 files changed, 94 insertions(+), 50 deletions(-) create mode 100644 changelog/unreleased/auth-app-api.md diff --git a/changelog/unreleased/auth-app-api.md b/changelog/unreleased/auth-app-api.md new file mode 100644 index 00000000000..a98f9e4b945 --- /dev/null +++ b/changelog/unreleased/auth-app-api.md @@ -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 diff --git a/services/auth-app/README.md b/services/auth-app/README.md index 012490527e9..86896ba0557 100644 --- a/services/auth-app/README.md +++ b/services/auth-app/README.md @@ -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. diff --git a/services/auth-app/pkg/config/defaults/defaultconfig.go b/services/auth-app/pkg/config/defaults/defaultconfig.go index ffd7aef80e6..01215d1fe56 100644 --- a/services/auth-app/pkg/config/defaults/defaultconfig.go +++ b/services/auth-app/pkg/config/defaults/defaultconfig.go @@ -36,7 +36,7 @@ func DefaultConfig() *config.Config { Root: "/", CORS: config.CORS{ AllowedOrigins: []string{"*"}, - AllowedMethods: []string{"POST"}, + AllowedMethods: []string{"GET", "POST", "DELETE"}, AllowedHeaders: []string{"Authorization", "Origin", "Content-Type", "Accept", "X-Requested-With", "X-Request-Id", "Ocs-Apirequest"}, AllowCredentials: true, }, diff --git a/services/auth-app/pkg/service/service.go b/services/auth-app/pkg/service/service.go index bb9431b2705..486bafc1b8d 100644 --- a/services/auth-app/pkg/service/service.go +++ b/services/auth-app/pkg/service/service.go @@ -25,6 +25,14 @@ import ( "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 @@ -42,7 +50,6 @@ func NewAuthAppService(opts ...Option) (*AuthAppService, error) { } r := roles.NewManager( - // TODO: caching? roles.Logger(o.Logger), roles.RoleService(o.RoleClient), ) @@ -71,120 +78,129 @@ func (a *AuthAppService) ServeHTTP(w http.ResponseWriter, r *http.Request) { // 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 { - http.Error(w, "error getting gateway client", http.StatusInternalServerError) + sublog.Error().Err(err).Msg("error getting gateway client") + w.WriteHeader(http.StatusInternalServerError) return } - 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") + 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 { - a.log.Error().Msg("impersonation is not allowed") + 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 { - a.log.Error().Err(err).Msg("error checking if user is admin") - http.Error(w, "internal server error", http.StatusInternalServerError) + sublog.Error().Err(err).Msg("error checking if user is admin") + w.WriteHeader(http.StatusInternalServerError) return } if !ok { - a.log.Error().Msg("user is not admin") - http.Error(w, "forbidden", http.StatusForbidden) + sublog.Error().Msg("user is not admin") + w.WriteHeader(http.StatusForbidden) return } ctx, err = a.authenticateUser(cid, gwc) if err != nil { - a.log.Error().Err(err).Msg("error authenticating user") - http.Error(w, "error authenticating user", http.StatusInternalServerError) + 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 { - a.log.Error().Err(err).Msg("error adding owner scope") - http.Error(w, "error adding owner scope", http.StatusInternalServerError) + sublog.Error().Err(err).Msg("error adding owner scope") + w.WriteHeader(http.StatusInternalServerError) return } res, err := gwc.GenerateAppPassword(ctx, &applications.GenerateAppPasswordRequest{ TokenScope: scopes, - Label: "Generated via API", + Label: label, Expiration: utils.TimeToTS(time.Now().Add(expiry)), }) if err != nil { - a.log.Error().Err(err).Msg("error generating app password") - http.Error(w, "error generating app password", http.StatusInternalServerError) + sublog.Error().Err(err).Msg("error generating app password") + w.WriteHeader(http.StatusInternalServerError) return } if res.GetStatus().GetCode() != rpc.Code_CODE_OK { - a.log.Error().Str("status", res.GetStatus().GetCode().String()).Msg("error generating app password") - http.Error(w, "error generating app password: "+res.GetStatus().GetMessage(), http.StatusInternalServerError) + sublog.Error().Str("status", res.GetStatus().GetCode().String()).Msg("error generating app password") + w.WriteHeader(http.StatusInternalServerError) return } - b, err := json.Marshal(res.GetAppPassword()) + b, err := json.Marshal(convert(res.GetAppPassword())) if err != nil { - a.log.Error().Err(err).Msg("error marshaling app password") - http.Error(w, "error marshaling app password", http.StatusInternalServerError) + sublog.Error().Err(err).Msg("error marshaling app password") + w.WriteHeader(http.StatusInternalServerError) return } if _, err := w.Write(b); err != nil { - a.log.Error().Err(err).Msg("error writing response") - http.Error(w, "error writing response", http.StatusInternalServerError) - return + sublog.Error().Err(err).Msg("error writing response") } w.WriteHeader(http.StatusOK) } // 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 { - a.log.Error().Err(err).Msg("error getting gateway client") - http.Error(w, "error getting gateway client", http.StatusInternalServerError) + sublog.Error().Err(err).Msg("error getting gateway client") + w.WriteHeader(http.StatusInternalServerError) return } - ctx := getContext(r) - res, err := gwc.ListAppPasswords(ctx, &applications.ListAppPasswordsRequest{}) if err != nil { - a.log.Error().Err(err).Msg("error listing app passwords") - http.Error(w, "error listing app passwords", http.StatusInternalServerError) + sublog.Error().Err(err).Msg("error listing app passwords") + w.WriteHeader(http.StatusInternalServerError) return } if res.GetStatus().GetCode() != rpc.Code_CODE_OK { - a.log.Error().Str("status", res.GetStatus().GetCode().String()).Msg("error listing app passwords") - http.Error(w, "error listing app passwords: "+res.GetStatus().GetMessage(), http.StatusInternalServerError) + sublog.Error().Str("status", res.GetStatus().GetCode().String()).Msg("error listing app passwords") + w.WriteHeader(http.StatusInternalServerError) return } - b, err := json.Marshal(res.GetAppPasswords()) + 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 { - a.log.Error().Err(err).Msg("error marshaling app passwords") - http.Error(w, "error marshaling app passwords", http.StatusInternalServerError) + sublog.Error().Err(err).Msg("error marshaling app passwords") + w.WriteHeader(http.StatusInternalServerError) return } if _, err := w.Write(b); err != nil { - a.log.Error().Err(err).Msg("error writing response") - http.Error(w, "error writing response", http.StatusInternalServerError) + sublog.Error().Err(err).Msg("error writing response") + w.WriteHeader(http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) @@ -192,32 +208,33 @@ func (a *AuthAppService) HandleList(w http.ResponseWriter, r *http.Request) { // 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 { - a.log.Error().Err(err).Msg("error getting gateway client") - http.Error(w, "error getting gateway client", http.StatusInternalServerError) + sublog.Error().Err(err).Msg("error getting gateway client") + w.WriteHeader(http.StatusInternalServerError) return } - ctx := getContext(r) - pw := r.URL.Query().Get("token") if pw == "" { - a.log.Info().Msg("missing token") - http.Error(w, "missing token", http.StatusBadRequest) + 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 { - a.log.Error().Err(err).Msg("error invalidating app password") - http.Error(w, "error invalidating app password", http.StatusInternalServerError) + sublog.Error().Err(err).Msg("error invalidating app password") + w.WriteHeader(http.StatusInternalServerError) return } if res.GetStatus().GetCode() != rpc.Code_CODE_OK { - a.log.Error().Str("status", res.GetStatus().GetCode().String()).Msg("error invalidating app password") - http.Error(w, "error invalidating app password: "+res.GetStatus().GetMessage(), http.StatusInternalServerError) + sublog.Error().Str("status", res.GetStatus().GetCode().String()).Msg("error invalidating app password") + w.WriteHeader(http.StatusInternalServerError) return } @@ -289,3 +306,12 @@ func isAdmin(ctx context.Context, rm *roles.Manager) (bool, error) { // 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(), + } +} From 5413b8ece97d209f1ba6318231cc5271cf166a4f Mon Sep 17 00:00:00 2001 From: jkoberg Date: Fri, 9 Aug 2024 12:41:48 +0200 Subject: [PATCH 5/5] feat(auth-app): create should return 201 Signed-off-by: jkoberg --- services/auth-app/pkg/service/service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/auth-app/pkg/service/service.go b/services/auth-app/pkg/service/service.go index 486bafc1b8d..53de039fe8a 100644 --- a/services/auth-app/pkg/service/service.go +++ b/services/auth-app/pkg/service/service.go @@ -158,7 +158,7 @@ func (a *AuthAppService) HandleCreate(w http.ResponseWriter, r *http.Request) { if _, err := w.Write(b); err != nil { sublog.Error().Err(err).Msg("error writing response") } - w.WriteHeader(http.StatusOK) + w.WriteHeader(http.StatusCreated) } // HandleList handles listing of app tokens