Merge pull request #9755 from kobergj/AppTokenApi

App Token API
This commit is contained in:
kobergj
2024-08-09 13:06:49 +02:00
committed by GitHub
10 changed files with 705 additions and 3 deletions

View 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

View File

@@ -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.

View File

@@ -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()
},
}

View File

@@ -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"`
}

View File

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

View 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
}
}

View 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
}

View 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
}
}

View 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(),
}
}

View File

@@ -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",
},
},
},
}