feat(auth-app): secure create endpoint
Signed-off-by: jkoberg <jkoberg@owncloud.com>
This commit is contained in:
@@ -12,9 +12,11 @@ import (
|
|||||||
"github.com/oklog/run"
|
"github.com/oklog/run"
|
||||||
"github.com/owncloud/ocis/v2/ocis-pkg/config/configlog"
|
"github.com/owncloud/ocis/v2/ocis-pkg/config/configlog"
|
||||||
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
|
"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/sync"
|
||||||
"github.com/owncloud/ocis/v2/ocis-pkg/tracing"
|
"github.com/owncloud/ocis/v2/ocis-pkg/tracing"
|
||||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
"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"
|
||||||
"github.com/owncloud/ocis/v2/services/auth-app/pkg/config/parser"
|
"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/logging"
|
||||||
@@ -34,6 +36,10 @@ func Server(cfg *config.Config) *cli.Command {
|
|||||||
return configlog.ReturnFatal(parser.ParseConfig(cfg))
|
return configlog.ReturnFatal(parser.ParseConfig(cfg))
|
||||||
},
|
},
|
||||||
Action: func(c *cli.Context) error {
|
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)
|
logger := logging.Configure(cfg.Service.Name, cfg.Log)
|
||||||
traceProvider, err := tracing.GetServiceTraceProvider(cfg.Tracing, cfg.Service.Name)
|
traceProvider, err := tracing.GetServiceTraceProvider(cfg.Tracing, cfg.Service.Name)
|
||||||
if err != nil {
|
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")
|
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(
|
gatewaySelector, err := pool.GatewaySelector(
|
||||||
cfg.Reva.Address,
|
cfg.Reva.Address,
|
||||||
append(
|
append(
|
||||||
cfg.Reva.GetRevaOptions(),
|
cfg.Reva.GetRevaOptions(),
|
||||||
|
pool.WithTLSCACert(cfg.GRPCClientTLS.CACert),
|
||||||
|
pool.WithTLSMode(tm),
|
||||||
pool.WithRegistry(registry.GetRegistry()),
|
pool.WithRegistry(registry.GetRegistry()),
|
||||||
pool.WithTracerProvider(traceProvider),
|
pool.WithTracerProvider(traceProvider),
|
||||||
)...)
|
)...)
|
||||||
@@ -99,11 +111,20 @@ func Server(cfg *config.Config) *cli.Command {
|
|||||||
return err
|
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(
|
server, err := http.Server(
|
||||||
http.Logger(logger),
|
http.Logger(logger),
|
||||||
http.Context(ctx),
|
http.Context(ctx),
|
||||||
http.Config(cfg),
|
http.Config(cfg),
|
||||||
http.GatewaySelector(gatewaySelector),
|
http.GatewaySelector(gatewaySelector),
|
||||||
|
http.RoleClient(rClient),
|
||||||
http.TracerProvider(traceProvider),
|
http.TracerProvider(traceProvider),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ type Config struct {
|
|||||||
GRPC GRPCConfig `yaml:"grpc"`
|
GRPC GRPCConfig `yaml:"grpc"`
|
||||||
HTTP HTTP `yaml:"http"`
|
HTTP HTTP `yaml:"http"`
|
||||||
|
|
||||||
|
GRPCClientTLS *shared.GRPCClientTLS `yaml:"grpc_client_tls"`
|
||||||
|
|
||||||
TokenManager *TokenManager `yaml:"token_manager"`
|
TokenManager *TokenManager `yaml:"token_manager"`
|
||||||
Reva *shared.Reva `yaml:"reva"`
|
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%%"`
|
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:"-"`
|
Supervised bool `yaml:"-"`
|
||||||
Context context.Context `yaml:"-"`
|
Context context.Context `yaml:"-"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,6 +73,10 @@ func EnsureDefaults(cfg *config.Config) {
|
|||||||
cfg.Tracing = &config.Tracing{}
|
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 {
|
if cfg.Reva == nil && cfg.Commons != nil {
|
||||||
cfg.Reva = structs.CopyOrZeroValue(cfg.Commons.Reva)
|
cfg.Reva = structs.CopyOrZeroValue(cfg.Commons.Reva)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||||
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
|
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
|
||||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
"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/owncloud/ocis/v2/services/auth-app/pkg/config"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"go.opentelemetry.io/otel/trace"
|
"go.opentelemetry.io/otel/trace"
|
||||||
@@ -22,6 +23,7 @@ type Options struct {
|
|||||||
Flags []cli.Flag
|
Flags []cli.Flag
|
||||||
Namespace string
|
Namespace string
|
||||||
GatewaySelector pool.Selectable[gateway.GatewayAPIClient]
|
GatewaySelector pool.Selectable[gateway.GatewayAPIClient]
|
||||||
|
RoleClient settingssvc.RoleService
|
||||||
TracerProvider trace.TracerProvider
|
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
|
// TracerProvider provides a function to set the TracerProvider option
|
||||||
func TracerProvider(val trace.TracerProvider) Option {
|
func TracerProvider(val trace.TracerProvider) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ func Server(opts ...Option) (http.Service, error) {
|
|||||||
svc.Mux(mux),
|
svc.Mux(mux),
|
||||||
svc.Config(options.Config),
|
svc.Config(options.Config),
|
||||||
svc.GatewaySelector(options.GatewaySelector),
|
svc.GatewaySelector(options.GatewaySelector),
|
||||||
|
svc.RoleClient(options.RoleClient),
|
||||||
svc.TraceProvider(options.TracerProvider),
|
svc.TraceProvider(options.TracerProvider),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
|
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
"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/owncloud/ocis/v2/services/auth-app/pkg/config"
|
||||||
"go.opentelemetry.io/otel/trace"
|
"go.opentelemetry.io/otel/trace"
|
||||||
)
|
)
|
||||||
@@ -22,6 +23,7 @@ type Options struct {
|
|||||||
GatewaySelector pool.Selectable[gateway.GatewayAPIClient]
|
GatewaySelector pool.Selectable[gateway.GatewayAPIClient]
|
||||||
Mux *chi.Mux
|
Mux *chi.Mux
|
||||||
TracerProvider trace.TracerProvider
|
TracerProvider trace.TracerProvider
|
||||||
|
RoleClient settingssvc.RoleService
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logger provides a function to set the logger option.
|
// Logger provides a function to set the logger option.
|
||||||
@@ -65,3 +67,10 @@ func Mux(m *chi.Mux) Option {
|
|||||||
o.Mux = m
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,13 +12,16 @@ import (
|
|||||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||||
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/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"
|
"github.com/cs3org/reva/v2/pkg/auth/scope"
|
||||||
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
|
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
|
||||||
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
|
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
|
||||||
|
"github.com/cs3org/reva/v2/pkg/utils"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
"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"
|
"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"
|
"google.golang.org/grpc/metadata"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,6 +31,7 @@ type AuthAppService struct {
|
|||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
gws pool.Selectable[gateway.GatewayAPIClient]
|
gws pool.Selectable[gateway.GatewayAPIClient]
|
||||||
m *chi.Mux
|
m *chi.Mux
|
||||||
|
r *roles.Manager
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAuthAppService initializes a new AuthAppService.
|
// NewAuthAppService initializes a new AuthAppService.
|
||||||
@@ -36,11 +40,19 @@ func NewAuthAppService(opts ...Option) (*AuthAppService, error) {
|
|||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
opt(o)
|
opt(o)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
r := roles.NewManager(
|
||||||
|
// TODO: caching?
|
||||||
|
roles.Logger(o.Logger),
|
||||||
|
roles.RoleService(o.RoleClient),
|
||||||
|
)
|
||||||
|
|
||||||
a := &AuthAppService{
|
a := &AuthAppService{
|
||||||
log: o.Logger,
|
log: o.Logger,
|
||||||
cfg: o.Config,
|
cfg: o.Config,
|
||||||
gws: o.GatewaySelector,
|
gws: o.GatewaySelector,
|
||||||
m: o.Mux,
|
m: o.Mux,
|
||||||
|
r: &r,
|
||||||
}
|
}
|
||||||
|
|
||||||
a.m.Route("/auth-app/tokens", func(r chi.Router) {
|
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)
|
ctx := getContext(r)
|
||||||
|
|
||||||
q := r.URL.Query()
|
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"))
|
cid := buildClientID(q.Get("userID"), q.Get("userName"))
|
||||||
if cid != "" {
|
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)
|
ctx, err = a.authenticateUser(cid, gwc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.log.Error().Err(err).Msg("error authenticating user")
|
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{
|
res, err := gwc.GenerateAppPassword(ctx, &applications.GenerateAppPasswordRequest{
|
||||||
TokenScope: scopes,
|
TokenScope: scopes,
|
||||||
Label: "Generated via API",
|
Label: "Generated via API",
|
||||||
Expiration: &types.Timestamp{
|
Expiration: utils.TimeToTS(time.Now().Add(expiry)),
|
||||||
Seconds: uint64(time.Now().Add(time.Hour).Unix()),
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.log.Error().Err(err).Msg("error generating app password")
|
a.log.Error().Err(err).Msg("error generating app password")
|
||||||
@@ -225,3 +258,34 @@ func buildClientID(userID, userName string) string {
|
|||||||
return "username:" + 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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user