refactor accounts
Signed-off-by: Christian Richter <crichter@owncloud.com>
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
package assets
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/owncloud/ocis/extensions/accounts"
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/config"
|
||||
"github.com/owncloud/ocis/ocis-pkg/assetsfs"
|
||||
"github.com/owncloud/ocis/ocis-pkg/log"
|
||||
)
|
||||
|
||||
// New returns a new http filesystem to serve assets.
|
||||
func New(opts ...Option) http.FileSystem {
|
||||
options := newOptions(opts...)
|
||||
return assetsfs.New(accounts.Assets, options.Config.Asset.Path, options.Logger)
|
||||
}
|
||||
|
||||
// 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
|
||||
Config *config.Config
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// Config provides a function to set the config option.
|
||||
func Config(val *config.Config) Option {
|
||||
return func(o *Options) {
|
||||
o.Config = val
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
accountsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/accounts/v0"
|
||||
accountssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/accounts/v0"
|
||||
|
||||
"github.com/asim/go-micro/plugins/client/grpc/v4"
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/config"
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/flagset"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// AddAccount command creates a new account
|
||||
func AddAccount(cfg *config.Config) *cli.Command {
|
||||
a := &accountsmsg.Account{
|
||||
PasswordProfile: &accountsmsg.PasswordProfile{},
|
||||
}
|
||||
return &cli.Command{
|
||||
Name: "add",
|
||||
Usage: "create a new account",
|
||||
Category: "account management",
|
||||
Aliases: []string{"create", "a"},
|
||||
Flags: flagset.AddAccountWithConfig(cfg, a),
|
||||
Before: func(c *cli.Context) error {
|
||||
// Write value of username to the flags beneath, as preferred name
|
||||
// and on-premises-sam-account-name is probably confusing for users.
|
||||
if username := c.String("username"); username != "" {
|
||||
if !c.IsSet("on-premises-sam-account-name") {
|
||||
if err := c.Set("on-premises-sam-account-name", username); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !c.IsSet("preferred-name") {
|
||||
if err := c.Set("preferred-name", username); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
accSvcID := cfg.GRPC.Namespace + "." + cfg.Service.Name
|
||||
accSvc := accountssvc.NewAccountsService(accSvcID, grpc.NewClient())
|
||||
_, err := accSvc.CreateAccount(c.Context, &accountssvc.CreateAccountRequest{
|
||||
Account: a,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(fmt.Errorf("could not create account %w", err))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/config"
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/config/parser"
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/logging"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// Health is the entrypoint for the health command.
|
||||
func Health(cfg *config.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "health",
|
||||
Usage: "check health status",
|
||||
Category: "info",
|
||||
Before: func(c *cli.Context) error {
|
||||
return parser.ParseConfig(cfg)
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
logger := logging.Configure(cfg.Service.Name, cfg.Log)
|
||||
|
||||
resp, err := http.Get(
|
||||
fmt.Sprintf(
|
||||
"http://%s/healthz",
|
||||
cfg.Debug.Addr,
|
||||
),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
logger.Fatal().
|
||||
Err(err).
|
||||
Msg("Failed to request health check")
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
logger.Fatal().
|
||||
Int("code", resp.StatusCode).
|
||||
Msg("Health seems to be in bad state")
|
||||
}
|
||||
|
||||
logger.Debug().
|
||||
Int("code", resp.StatusCode).
|
||||
Msg("Health got a good state")
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
accountsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/accounts/v0"
|
||||
accountssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/accounts/v0"
|
||||
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/flagset"
|
||||
|
||||
"github.com/asim/go-micro/plugins/client/grpc/v4"
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// InspectAccount command shows detailed information about a specific account.
|
||||
func InspectAccount(cfg *config.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "inspect",
|
||||
Usage: "show detailed data on an existing account",
|
||||
Category: "account management",
|
||||
ArgsUsage: "id",
|
||||
Flags: flagset.InspectAccountWithConfig(cfg),
|
||||
Action: func(c *cli.Context) error {
|
||||
accServiceID := cfg.GRPC.Namespace + "." + cfg.Service.Name
|
||||
if c.NArg() != 1 {
|
||||
fmt.Println("Please provide a user-id")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
uid := c.Args().First()
|
||||
accSvc := accountssvc.NewAccountsService(accServiceID, grpc.NewClient())
|
||||
acc, err := accSvc.GetAccount(c.Context, &accountssvc.GetAccountRequest{
|
||||
Id: uid,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(fmt.Errorf("could not view account %w", err))
|
||||
return err
|
||||
}
|
||||
|
||||
buildAccountInspectTable(acc).Render()
|
||||
return nil
|
||||
}}
|
||||
}
|
||||
|
||||
func buildAccountInspectTable(acc *accountsmsg.Account) *tw.Table {
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetAutoMergeCells(true)
|
||||
table.AppendBulk([][]string{
|
||||
{"ID", acc.Id},
|
||||
{"Mail", acc.Mail},
|
||||
{"DisplayName", acc.DisplayName},
|
||||
{"PreferredName", acc.PreferredName},
|
||||
{"AccountEnabled", strconv.FormatBool(acc.AccountEnabled)},
|
||||
{"CreationType", acc.CreationType},
|
||||
{"CreatedDateTime", acc.CreatedDateTime.String()},
|
||||
{"Description", acc.Description},
|
||||
{"ExternalUserState", acc.ExternalUserState},
|
||||
{"UidNumber", fmt.Sprintf("%+d", acc.UidNumber)},
|
||||
{"GidNumber", fmt.Sprintf("%+d", acc.GidNumber)},
|
||||
{"IsResourceAccount", strconv.FormatBool(acc.IsResourceAccount)},
|
||||
{"OnPremisesDistinguishedName", acc.OnPremisesDistinguishedName},
|
||||
{"OnPremisesDomainName", acc.OnPremisesDomainName},
|
||||
{"OnPremisesImmutableId", acc.OnPremisesImmutableId},
|
||||
{"OnPremisesSamAccountName", acc.OnPremisesSamAccountName},
|
||||
{"OnPremisesSecurityIdentifier", acc.OnPremisesSecurityIdentifier},
|
||||
{"OnPremisesUserPrincipalName", acc.OnPremisesUserPrincipalName},
|
||||
{"RefreshTokenValidFromDateTime", acc.RefreshTokensValidFromDateTime.String()},
|
||||
})
|
||||
|
||||
// Merged cell with group memberships
|
||||
for k := range acc.MemberOf {
|
||||
table.Append([]string{"MemberOf", acc.MemberOf[k].DisplayName})
|
||||
}
|
||||
|
||||
return table
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
accountsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/accounts/v0"
|
||||
accountssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/accounts/v0"
|
||||
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/flagset"
|
||||
|
||||
"github.com/asim/go-micro/plugins/client/grpc/v4"
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// ListAccounts command lists all accounts
|
||||
func ListAccounts(cfg *config.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "list",
|
||||
Usage: "list existing accounts",
|
||||
Category: "account management",
|
||||
Aliases: []string{"ls"},
|
||||
Flags: flagset.ListAccountsWithConfig(cfg),
|
||||
Action: func(c *cli.Context) error {
|
||||
accSvcID := cfg.GRPC.Namespace + "." + cfg.Service.Name
|
||||
accSvc := accountssvc.NewAccountsService(accSvcID, grpc.NewClient())
|
||||
resp, err := accSvc.ListAccounts(c.Context, &accountssvc.ListAccountsRequest{})
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(fmt.Errorf("could not list accounts %w", err))
|
||||
return err
|
||||
}
|
||||
|
||||
buildAccountsListTable(resp.Accounts).Render()
|
||||
return nil
|
||||
}}
|
||||
}
|
||||
|
||||
// buildAccountsListTable creates an ascii table for printing on the cli
|
||||
func buildAccountsListTable(accs []*accountsmsg.Account) *tw.Table {
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Id", "DisplayName", "Mail", "AccountEnabled"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
for _, acc := range accs {
|
||||
table.Append([]string{
|
||||
acc.Id,
|
||||
acc.DisplayName,
|
||||
acc.Mail,
|
||||
strconv.FormatBool(acc.AccountEnabled)})
|
||||
}
|
||||
return table
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
accountssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/accounts/v0"
|
||||
|
||||
"github.com/asim/go-micro/plugins/client/grpc/v4"
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
merrors "go-micro.dev/v4/errors"
|
||||
)
|
||||
|
||||
// RebuildIndex rebuilds the entire configured index.
|
||||
func RebuildIndex(cdf *config.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "rebuildIndex",
|
||||
Usage: "rebuilds the service's index, i.e. deleting and then re-adding all existing documents",
|
||||
Category: "account management",
|
||||
Aliases: []string{"rebuild", "ri"},
|
||||
Action: func(ctx *cli.Context) error {
|
||||
idxSvcID := "com.owncloud.api.accounts"
|
||||
idxSvc := accountssvc.NewIndexService(idxSvcID, grpc.NewClient())
|
||||
|
||||
_, err := idxSvc.RebuildIndex(context.Background(), &accountssvc.RebuildIndexRequest{})
|
||||
if err != nil {
|
||||
fmt.Println(merrors.FromError(err).Detail)
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("index rebuilt successfully")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
accountssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/accounts/v0"
|
||||
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/flagset"
|
||||
|
||||
"github.com/asim/go-micro/plugins/client/grpc/v4"
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// RemoveAccount command deletes an existing account.
|
||||
func RemoveAccount(cfg *config.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "remove",
|
||||
Usage: "removes an existing account",
|
||||
Category: "account management",
|
||||
ArgsUsage: "id",
|
||||
Aliases: []string{"rm"},
|
||||
Flags: flagset.RemoveAccountWithConfig(cfg),
|
||||
Action: func(c *cli.Context) error {
|
||||
accServiceID := cfg.GRPC.Namespace + "." + cfg.Service.Name
|
||||
if c.NArg() != 1 {
|
||||
fmt.Println("Please provide a user-id")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
uid := c.Args().First()
|
||||
accSvc := accountssvc.NewAccountsService(accServiceID, grpc.NewClient())
|
||||
_, err := accSvc.DeleteAccount(c.Context, &accountssvc.DeleteAccountRequest{Id: uid})
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(fmt.Errorf("could not delete account %w", err))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/config"
|
||||
"github.com/owncloud/ocis/ocis-pkg/clihelper"
|
||||
ociscfg "github.com/owncloud/ocis/ocis-pkg/config"
|
||||
"github.com/thejerf/suture/v4"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// GetCommands provides all commands for this service
|
||||
func GetCommands(cfg *config.Config) cli.Commands {
|
||||
return []*cli.Command{
|
||||
// start this service
|
||||
Server(cfg),
|
||||
|
||||
// interaction with this service
|
||||
AddAccount(cfg),
|
||||
UpdateAccount(cfg),
|
||||
ListAccounts(cfg),
|
||||
InspectAccount(cfg),
|
||||
RemoveAccount(cfg),
|
||||
RebuildIndex(cfg),
|
||||
|
||||
// infos about this service
|
||||
Health(cfg),
|
||||
Version(cfg),
|
||||
}
|
||||
}
|
||||
|
||||
// Execute is the entry point for the ocis-accounts command.
|
||||
func Execute(cfg *config.Config) error {
|
||||
app := clihelper.DefaultApp(&cli.App{
|
||||
Name: "ocis-accounts",
|
||||
Usage: "Provide accounts and groups for oCIS",
|
||||
Commands: GetCommands(cfg),
|
||||
})
|
||||
|
||||
cli.HelpFlag = &cli.BoolFlag{
|
||||
Name: "help,h",
|
||||
Usage: "Show the help",
|
||||
}
|
||||
|
||||
return app.Run(os.Args)
|
||||
}
|
||||
|
||||
// SutureService allows for the accounts command to be embedded and supervised by a suture supervisor tree.
|
||||
type SutureService struct {
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
// NewSutureService creates a new accounts.SutureService
|
||||
func NewSutureService(cfg *ociscfg.Config) suture.Service {
|
||||
cfg.Accounts.Commons = cfg.Commons
|
||||
return SutureService{
|
||||
cfg: cfg.Accounts,
|
||||
}
|
||||
}
|
||||
|
||||
func (s SutureService) Serve(ctx context.Context) error {
|
||||
s.cfg.Context = ctx
|
||||
if err := Execute(s.cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/oklog/run"
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/config"
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/config/parser"
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/logging"
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/metrics"
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/server/debug"
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/server/grpc"
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/server/http"
|
||||
svc "github.com/owncloud/ocis/extensions/accounts/pkg/service/v0"
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/tracing"
|
||||
"github.com/owncloud/ocis/ocis-pkg/version"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// Server is the entry point for the server command.
|
||||
func Server(cfg *config.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "server",
|
||||
Usage: fmt.Sprintf("start %s extension without runtime (unsupervised mode)", cfg.Service.Name),
|
||||
Category: "server",
|
||||
Before: func(c *cli.Context) error {
|
||||
return parser.ParseConfig(cfg)
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
logger := logging.Configure(cfg.Service.Name, cfg.Log)
|
||||
err := tracing.Configure(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gr := run.Group{}
|
||||
ctx, cancel := defineContext(cfg)
|
||||
mtrcs := metrics.New()
|
||||
|
||||
defer cancel()
|
||||
|
||||
mtrcs.BuildInfo.WithLabelValues(version.String).Set(1)
|
||||
|
||||
handler, err := svc.New(svc.Logger(logger), svc.Config(cfg))
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("handler init")
|
||||
return err
|
||||
}
|
||||
|
||||
httpServer := http.Server(
|
||||
http.Config(cfg),
|
||||
http.Logger(logger),
|
||||
http.Name(cfg.Service.Name),
|
||||
http.Context(ctx),
|
||||
http.Metrics(mtrcs),
|
||||
http.Handler(handler),
|
||||
)
|
||||
|
||||
gr.Add(httpServer.Run, func(_ error) {
|
||||
logger.Info().Str("server", "http").Msg("shutting down server")
|
||||
cancel()
|
||||
})
|
||||
|
||||
grpcServer := grpc.Server(
|
||||
grpc.Config(cfg),
|
||||
grpc.Logger(logger),
|
||||
grpc.Name(cfg.Service.Name),
|
||||
grpc.Context(ctx),
|
||||
grpc.Metrics(mtrcs),
|
||||
grpc.Handler(handler),
|
||||
)
|
||||
|
||||
gr.Add(grpcServer.Run, func(_ error) {
|
||||
logger.Info().Str("server", "grpc").Msg("shutting down server")
|
||||
cancel()
|
||||
})
|
||||
|
||||
// prepare a debug server and add it to the group run.
|
||||
debugServer, err := debug.Server(debug.Logger(logger), debug.Context(ctx), debug.Config(cfg))
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Str("server", "debug").Msg("Failed to initialize server")
|
||||
return err
|
||||
}
|
||||
|
||||
gr.Add(debugServer.ListenAndServe, func(_ error) {
|
||||
_ = debugServer.Shutdown(ctx)
|
||||
cancel()
|
||||
})
|
||||
|
||||
return gr.Run()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// defineContext sets the context for the extension. If there is a context configured it will create a new child from it,
|
||||
// if not, it will create a root context that can be cancelled.
|
||||
func defineContext(cfg *config.Config) (context.Context, context.CancelFunc) {
|
||||
return func() (context.Context, context.CancelFunc) {
|
||||
if cfg.Context == nil {
|
||||
return context.WithCancel(context.Background())
|
||||
}
|
||||
return context.WithCancel(cfg.Context)
|
||||
}()
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
accountsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/accounts/v0"
|
||||
accountssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/accounts/v0"
|
||||
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/flagset"
|
||||
|
||||
"github.com/asim/go-micro/plugins/client/grpc/v4"
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
"google.golang.org/genproto/protobuf/field_mask"
|
||||
)
|
||||
|
||||
// UpdateAccount command for modifying accounts including password policies
|
||||
func UpdateAccount(cfg *config.Config) *cli.Command {
|
||||
a := &accountsmsg.Account{
|
||||
PasswordProfile: &accountsmsg.PasswordProfile{},
|
||||
}
|
||||
return &cli.Command{
|
||||
Name: "update",
|
||||
Usage: "Make changes to an existing account",
|
||||
Category: "account management",
|
||||
ArgsUsage: "id",
|
||||
Flags: flagset.UpdateAccountWithConfig(cfg, a),
|
||||
Before: func(c *cli.Context) error {
|
||||
if len(c.StringSlice("password_policies")) > 0 {
|
||||
a.PasswordProfile.PasswordPolicies = c.StringSlice("password_policies")
|
||||
}
|
||||
|
||||
if c.NArg() != 1 {
|
||||
return errors.New("missing account-id")
|
||||
}
|
||||
|
||||
if c.NumFlags() == 0 {
|
||||
return errors.New("missing attribute-flags for update")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
a.Id = c.Args().First()
|
||||
accSvcID := cfg.GRPC.Namespace + "." + cfg.Service.Name
|
||||
accSvc := accountssvc.NewAccountsService(accSvcID, grpc.NewClient())
|
||||
_, err := accSvc.UpdateAccount(c.Context, &accountssvc.UpdateAccountRequest{
|
||||
Account: a,
|
||||
UpdateMask: buildAccUpdateMask(c.FlagNames()),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(fmt.Errorf("could not update account %w", err))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}}
|
||||
}
|
||||
|
||||
// buildAccUpdateMask by mapping passed update flags to account fieldNames.
|
||||
//
|
||||
// The UpdateMask is passed with the update-request to the server so that
|
||||
// only the modified values are transferred.
|
||||
func buildAccUpdateMask(setFlags []string) *field_mask.FieldMask {
|
||||
var flagToPath = map[string]string{
|
||||
"enabled": "AccountEnabled",
|
||||
"displayname": "DisplayName",
|
||||
"preferred-name": "PreferredName",
|
||||
"uidnumber": "UidNumber",
|
||||
"gidnumber": "GidNumber",
|
||||
"mail": "Mail",
|
||||
"description": "Description",
|
||||
"password": "PasswordProfile.Password",
|
||||
"password-policies": "PasswordProfile.PasswordPolicies",
|
||||
"force-password-change": "PasswordProfile.ForceChangePasswordNextSignIn",
|
||||
"force-password-change-mfa": "PasswordProfile.ForceChangePasswordNextSignInWithMfa",
|
||||
"on-premises-sam-account-name": "OnPremisesSamAccountName",
|
||||
}
|
||||
|
||||
updatedPaths := make([]string, 0)
|
||||
|
||||
for _, v := range setFlags {
|
||||
if _, ok := flagToPath[v]; ok {
|
||||
updatedPaths = append(updatedPaths, flagToPath[v])
|
||||
}
|
||||
}
|
||||
|
||||
return &field_mask.FieldMask{Paths: updatedPaths}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/owncloud/ocis/ocis-pkg/registry"
|
||||
"github.com/owncloud/ocis/ocis-pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// Version prints the service versions of all running instances.
|
||||
func Version(cfg *config.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "version",
|
||||
Usage: "print the version of this binary and the running extension instances",
|
||||
Category: "info",
|
||||
Action: func(c *cli.Context) error {
|
||||
fmt.Println("Version: " + version.String)
|
||||
fmt.Printf("Compiled: %s\n", version.Compiled())
|
||||
fmt.Println("")
|
||||
|
||||
reg := registry.GetRegistry()
|
||||
services, err := reg.GetService(cfg.GRPC.Namespace + "." + cfg.Service.Name)
|
||||
if err != nil {
|
||||
fmt.Println(fmt.Errorf("could not get %s services from the registry: %v", cfg.Service.Name, err))
|
||||
return err
|
||||
}
|
||||
|
||||
if len(services) == 0 {
|
||||
fmt.Println("No running " + cfg.Service.Name + " service found.")
|
||||
return nil
|
||||
}
|
||||
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Version", "Address", "Id"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
}
|
||||
}
|
||||
table.Render()
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/owncloud/ocis/ocis-pkg/shared"
|
||||
)
|
||||
|
||||
// Config combines all available configuration parts.
|
||||
type Config struct {
|
||||
*shared.Commons `yaml:"-"`
|
||||
|
||||
Service Service `yaml:"-"`
|
||||
|
||||
Tracing *Tracing `yaml:"tracing"`
|
||||
Log *Log `yaml:"log"`
|
||||
Debug Debug `yaml:"debug"`
|
||||
|
||||
HTTP HTTP `yaml:"http"`
|
||||
GRPC GRPC `yaml:"grpc"`
|
||||
|
||||
TokenManager TokenManager `yaml:"token_manager"`
|
||||
|
||||
Asset Asset `yaml:"asset"`
|
||||
Repo Repo `yaml:"repo"`
|
||||
Index Index `yaml:"index"`
|
||||
ServiceUser ServiceUser `yaml:"service_user"`
|
||||
HashDifficulty int `yaml:"hash_difficulty" env:"ACCOUNTS_HASH_DIFFICULTY" desc:"The hash difficulty makes sure that validating a password takes at least a certain amount of time."`
|
||||
DemoUsersAndGroups bool `yaml:"demo_users_and_groups" env:"ACCOUNTS_DEMO_USERS_AND_GROUPS" desc:"If this flag is set the service will setup the demo users and groups."`
|
||||
|
||||
Context context.Context `yaml:"-"`
|
||||
}
|
||||
|
||||
// Asset defines the available asset configuration.
|
||||
type Asset struct {
|
||||
Path string `yaml:"path" env:"ACCOUNTS_ASSET_PATH" desc:"The path to the ui assets."`
|
||||
}
|
||||
|
||||
// TokenManager is the config for using the reva token manager
|
||||
type TokenManager struct {
|
||||
JWTSecret string `yaml:"jwt_secret" env:"OCIS_JWT_SECRET;ACCOUNTS_JWT_SECRET" desc:"The secret to mint jwt tokens."`
|
||||
}
|
||||
|
||||
// Repo defines which storage implementation is to be used.
|
||||
type Repo struct {
|
||||
Backend string `yaml:"backend" env:"ACCOUNTS_STORAGE_BACKEND" desc:"Defines which storage implementation is to be used"`
|
||||
Disk Disk `yaml:"disk"`
|
||||
CS3 CS3 `yaml:"cs3"`
|
||||
}
|
||||
|
||||
// Disk is the local disk implementation of the storage.
|
||||
type Disk struct {
|
||||
Path string `yaml:"path" env:"ACCOUNTS_STORAGE_DISK_PATH" desc:"The path where the accounts data is stored."`
|
||||
}
|
||||
|
||||
// CS3 is the cs3 implementation of the storage.
|
||||
type CS3 struct {
|
||||
ProviderAddr string `yaml:"provider_addr" env:"ACCOUNTS_STORAGE_CS3_PROVIDER_ADDR" desc:"The address to the storage provider."`
|
||||
}
|
||||
|
||||
// ServiceUser defines the user required for EOS.
|
||||
type ServiceUser struct {
|
||||
UUID string `yaml:"uuid" env:"ACCOUNTS_SERVICE_USER_UUID" desc:"The id of the accounts service user."`
|
||||
Username string `yaml:"username" env:"ACCOUNTS_SERVICE_USER_USERNAME" desc:"The username of the accounts service user."`
|
||||
UID int64 `yaml:"uid" env:"ACCOUNTS_SERVICE_USER_UID" desc:"The uid of the accounts service user."`
|
||||
GID int64 `yaml:"gid" env:"ACCOUNTS_SERVICE_USER_GID" desc:"The gid of the accounts service user."`
|
||||
}
|
||||
|
||||
// Index defines config for indexes.
|
||||
type Index struct {
|
||||
UID UIDBound `yaml:"uid"`
|
||||
GID GIDBound `yaml:"gid"`
|
||||
}
|
||||
|
||||
// GIDBound defines a lower and upper bound.
|
||||
type GIDBound struct {
|
||||
Lower int64 `yaml:"lower" env:"ACCOUNTS_GID_INDEX_LOWER_BOUND" desc:"The lowest possible gid value for the indexer."`
|
||||
Upper int64 `yaml:"upper" env:"ACCOUNTS_GID_INDEX_UPPER_BOUND" desc:"The highest possible gid value for the indexer."`
|
||||
}
|
||||
|
||||
// UIDBound defines a lower and upper bound.
|
||||
type UIDBound struct {
|
||||
Lower int64 `yaml:"lower" env:"ACCOUNTS_UID_INDEX_LOWER_BOUND" desc:"The lowest possible uid value for the indexer."`
|
||||
Upper int64 `yaml:"upper" env:"ACCOUNTS_UID_INDEX_UPPER_BOUND" desc:"The highest possible uid value for the indexer."`
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package config
|
||||
|
||||
// Debug defines the available debug configuration.
|
||||
type Debug struct {
|
||||
Addr string `yaml:"addr" env:"ACCOUNTS_DEBUG_ADDR"`
|
||||
Token string `yaml:"token" env:"ACCOUNTS_DEBUG_TOKEN"`
|
||||
Pprof bool `yaml:"pprof" env:"ACCOUNTS_DEBUG_PPROF"`
|
||||
Zpages bool `yaml:"zpages" env:"ACCOUNTS_DEBUG_ZPAGES"`
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package defaults
|
||||
|
||||
import (
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/config"
|
||||
"github.com/owncloud/ocis/ocis-pkg/config/defaults"
|
||||
)
|
||||
|
||||
func FullDefaultConfig() *config.Config {
|
||||
cfg := DefaultConfig()
|
||||
|
||||
EnsureDefaults(cfg)
|
||||
Sanitize(cfg)
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func DefaultConfig() *config.Config {
|
||||
return &config.Config{
|
||||
Debug: config.Debug{
|
||||
Addr: "127.0.0.1:9182",
|
||||
Token: "",
|
||||
Pprof: false,
|
||||
Zpages: false,
|
||||
},
|
||||
HTTP: config.HTTP{
|
||||
Addr: "127.0.0.1:9181",
|
||||
Namespace: "com.owncloud.web",
|
||||
Root: "/",
|
||||
CacheTTL: 604800, // 7 days
|
||||
CORS: config.CORS{
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
|
||||
AllowedHeaders: []string{"Authorization", "Origin", "Content-Type", "Accept", "X-Requested-With"},
|
||||
AllowCredentials: true,
|
||||
},
|
||||
},
|
||||
GRPC: config.GRPC{
|
||||
Addr: "127.0.0.1:9180",
|
||||
Namespace: "com.owncloud.api",
|
||||
},
|
||||
Service: config.Service{
|
||||
Name: "accounts",
|
||||
},
|
||||
Asset: config.Asset{},
|
||||
TokenManager: config.TokenManager{
|
||||
JWTSecret: "Pive-Fumkiu4",
|
||||
},
|
||||
HashDifficulty: 11,
|
||||
DemoUsersAndGroups: false,
|
||||
Repo: config.Repo{
|
||||
Backend: "CS3",
|
||||
Disk: config.Disk{
|
||||
Path: path.Join(defaults.BaseDataPath(), "accounts"),
|
||||
},
|
||||
CS3: config.CS3{
|
||||
ProviderAddr: "localhost:9215",
|
||||
},
|
||||
},
|
||||
Index: config.Index{
|
||||
UID: config.UIDBound{
|
||||
Lower: 0,
|
||||
Upper: 1000,
|
||||
},
|
||||
GID: config.GIDBound{
|
||||
Lower: 0,
|
||||
Upper: 1000,
|
||||
},
|
||||
},
|
||||
ServiceUser: config.ServiceUser{
|
||||
UUID: "95cb8724-03b2-11eb-a0a6-c33ef8ef53ad",
|
||||
Username: "95cb8724-03b2-11eb-a0a6-c33ef8ef53ad",
|
||||
UID: 0,
|
||||
GID: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func EnsureDefaults(cfg *config.Config) {
|
||||
// provide with defaults for shared logging, since we need a valid destination address for BindEnv.
|
||||
if cfg.Log == nil && cfg.Commons != nil && cfg.Commons.Log != nil {
|
||||
cfg.Log = &config.Log{
|
||||
Level: cfg.Commons.Log.Level,
|
||||
Pretty: cfg.Commons.Log.Pretty,
|
||||
Color: cfg.Commons.Log.Color,
|
||||
File: cfg.Commons.Log.File,
|
||||
}
|
||||
} else if cfg.Log == nil {
|
||||
cfg.Log = &config.Log{}
|
||||
}
|
||||
// provide with defaults for shared tracing, since we need a valid destination address for BindEnv.
|
||||
if cfg.Tracing == nil && cfg.Commons != nil && cfg.Commons.Tracing != nil {
|
||||
cfg.Tracing = &config.Tracing{
|
||||
Enabled: cfg.Commons.Tracing.Enabled,
|
||||
Type: cfg.Commons.Tracing.Type,
|
||||
Endpoint: cfg.Commons.Tracing.Endpoint,
|
||||
Collector: cfg.Commons.Tracing.Collector,
|
||||
}
|
||||
} else if cfg.Tracing == nil {
|
||||
cfg.Tracing = &config.Tracing{}
|
||||
}
|
||||
}
|
||||
|
||||
func Sanitize(cfg *config.Config) {
|
||||
// sanitize config
|
||||
if cfg.HTTP.Root != "/" {
|
||||
cfg.HTTP.Root = strings.TrimSuffix(cfg.HTTP.Root, "/")
|
||||
}
|
||||
cfg.Repo.Backend = strings.ToLower(cfg.Repo.Backend)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package config
|
||||
|
||||
// GRPC defines the available grpc configuration.
|
||||
type GRPC struct {
|
||||
Addr string `yaml:"addr" env:"ACCOUNTS_GRPC_ADDR" desc:"The address of the grpc service."`
|
||||
Namespace string `yaml:"-"`
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package config
|
||||
|
||||
// HTTP defines the available http configuration.
|
||||
type HTTP struct {
|
||||
Addr string `yaml:"addr" env:"ACCOUNTS_HTTP_ADDR" desc:"The address of the http service."`
|
||||
Namespace string `yaml:"-"`
|
||||
Root string `yaml:"root" env:"ACCOUNTS_HTTP_ROOT" desc:"The root path of the http service."`
|
||||
CacheTTL int `yaml:"cache_ttl" env:"ACCOUNTS_CACHE_TTL" desc:"The cache time for the static assets."`
|
||||
CORS CORS `yaml:"cors"`
|
||||
}
|
||||
|
||||
// CORS defines the available cors configuration.
|
||||
type CORS struct {
|
||||
AllowedOrigins []string `yaml:"allowed_origins"`
|
||||
AllowedMethods []string `yaml:"allowed_methods"`
|
||||
AllowedHeaders []string `yaml:"allowed_headers"`
|
||||
AllowCredentials bool `yaml:"allowed_credentials"`
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package config
|
||||
|
||||
// Log defines the available log configuration.
|
||||
type Log struct {
|
||||
Level string `yaml:"level" env:"OCIS_LOG_LEVEL;ACCOUNTS_LOG_LEVEL" desc:"The log level."`
|
||||
Pretty bool `yaml:"pretty" env:"OCIS_LOG_PRETTY;ACCOUNTS_LOG_PRETTY" desc:"Activates pretty log output."`
|
||||
Color bool `yaml:"color" env:"OCIS_LOG_COLOR;ACCOUNTS_LOG_COLOR" desc:"Activates colorized log output."`
|
||||
File string `yaml:"file" env:"OCIS_LOG_FILE;ACCOUNTS_LOG_FILE" desc:"The target log file."`
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/config"
|
||||
defaults "github.com/owncloud/ocis/extensions/accounts/pkg/config/defaults"
|
||||
ociscfg "github.com/owncloud/ocis/ocis-pkg/config"
|
||||
|
||||
"github.com/owncloud/ocis/ocis-pkg/config/envdecode"
|
||||
)
|
||||
|
||||
// ParseConfig loads accounts configuration from known paths.
|
||||
func ParseConfig(cfg *config.Config) error {
|
||||
_, err := ociscfg.BindSourcesToStructs(cfg.Service.Name, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defaults.EnsureDefaults(cfg)
|
||||
|
||||
// load all env variables relevant to the config in the current context.
|
||||
if err := envdecode.Decode(cfg); err != nil {
|
||||
// no environment variable set for this config is an expected "error"
|
||||
if !errors.Is(err, envdecode.ErrNoTargetFieldsAreSet) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
defaults.Sanitize(cfg)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package config
|
||||
|
||||
// Service defines the available service configuration.
|
||||
type Service struct {
|
||||
Name string `yaml:"-"`
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package config
|
||||
|
||||
// Tracing defines the available tracing configuration.
|
||||
type Tracing struct {
|
||||
Enabled bool `yaml:"enabled" env:"OCIS_TRACING_ENABLED;ACCOUNTS_TRACING_ENABLED" desc:"Activates tracing."`
|
||||
Type string `yaml:"type" env:"OCIS_TRACING_TYPE;ACCOUNTS_TRACING_TYPE"`
|
||||
Endpoint string `yaml:"endpoint" env:"OCIS_TRACING_ENDPOINT;ACCOUNTS_TRACING_ENDPOINT" desc:"The endpoint to the tracing collector."`
|
||||
Collector string `yaml:"collector" env:"OCIS_TRACING_COLLECTOR;ACCOUNTS_TRACING_COLLECTOR"`
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
package flagset
|
||||
|
||||
import (
|
||||
accountsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/accounts/v0"
|
||||
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/config"
|
||||
"github.com/owncloud/ocis/ocis-pkg/flags"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// UpdateAccountWithConfig applies update command flags to cfg
|
||||
func UpdateAccountWithConfig(cfg *config.Config, a *accountsmsg.Account) []cli.Flag {
|
||||
if a.PasswordProfile == nil {
|
||||
a.PasswordProfile = &accountsmsg.PasswordProfile{}
|
||||
}
|
||||
|
||||
return []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "grpc-namespace",
|
||||
Value: flags.OverrideDefaultString(cfg.GRPC.Namespace, "com.owncloud.api"),
|
||||
Usage: "Set the base namespace for the grpc namespace",
|
||||
EnvVars: []string{"ACCOUNTS_GRPC_NAMESPACE"},
|
||||
Destination: &cfg.GRPC.Namespace,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Value: flags.OverrideDefaultString(cfg.Service.Name, "accounts"),
|
||||
Usage: "service name",
|
||||
EnvVars: []string{"ACCOUNTS_NAME"},
|
||||
Destination: &cfg.Service.Name,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "enabled",
|
||||
Usage: "Enable the account",
|
||||
Destination: &a.AccountEnabled,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "displayname",
|
||||
Usage: "Set the displayname for the account",
|
||||
Destination: &a.DisplayName,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "preferred-name",
|
||||
Usage: "Set the preferred-name for the account",
|
||||
Destination: &a.PreferredName,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "on-premises-sam-account-name",
|
||||
Usage: "Set the on-premises-sam-account-name",
|
||||
Destination: &a.OnPremisesSamAccountName,
|
||||
},
|
||||
&cli.Int64Flag{
|
||||
Name: "uidnumber",
|
||||
Usage: "Set the uidnumber for the account",
|
||||
Destination: &a.UidNumber,
|
||||
},
|
||||
&cli.Int64Flag{
|
||||
Name: "gidnumber",
|
||||
Usage: "Set the gidnumber for the account",
|
||||
Destination: &a.GidNumber,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "mail",
|
||||
Usage: "Set the mail for the account",
|
||||
Destination: &a.Mail,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "description",
|
||||
Usage: "Set the description for the account",
|
||||
Destination: &a.Description,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "password",
|
||||
Usage: "Set the password for the account",
|
||||
Destination: &a.PasswordProfile.Password,
|
||||
// TODO read password from ENV?
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "password-policies",
|
||||
Usage: "Possible policies: DisableStrongPassword, DisablePasswordExpiration",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "force-password-change",
|
||||
Usage: "Force password change on next sign-in",
|
||||
Destination: &a.PasswordProfile.ForceChangePasswordNextSignIn,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "force-password-change-mfa",
|
||||
Usage: "Force password change on next sign-in with mfa",
|
||||
Destination: &a.PasswordProfile.ForceChangePasswordNextSignInWithMfa,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// AddAccountWithConfig applies create command flags to cfg
|
||||
func AddAccountWithConfig(cfg *config.Config, a *accountsmsg.Account) []cli.Flag {
|
||||
if a.PasswordProfile == nil {
|
||||
a.PasswordProfile = &accountsmsg.PasswordProfile{}
|
||||
}
|
||||
|
||||
return []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "grpc-namespace",
|
||||
Value: flags.OverrideDefaultString(cfg.GRPC.Namespace, "com.owncloud.api"),
|
||||
Usage: "Set the base namespace for the grpc namespace",
|
||||
EnvVars: []string{"ACCOUNTS_GRPC_NAMESPACE"},
|
||||
Destination: &cfg.GRPC.Namespace,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Value: flags.OverrideDefaultString(cfg.Service.Name, "accounts"),
|
||||
Usage: "service name",
|
||||
EnvVars: []string{"ACCOUNTS_NAME"},
|
||||
Destination: &cfg.Service.Name,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "enabled",
|
||||
Usage: "Enable the account",
|
||||
Destination: &a.AccountEnabled,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "displayname",
|
||||
Usage: "Set the displayname for the account",
|
||||
Destination: &a.DisplayName,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
Usage: "Username will be written to preferred-name and on_premises_sam_account_name",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "preferred-name",
|
||||
Usage: "Set the preferred-name for the account",
|
||||
Destination: &a.PreferredName,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "on-premises-sam-account-name",
|
||||
Usage: "Set the on-premises-sam-account-name",
|
||||
Destination: &a.OnPremisesSamAccountName,
|
||||
},
|
||||
&cli.Int64Flag{
|
||||
Name: "uidnumber",
|
||||
Usage: "Set the uidnumber for the account",
|
||||
Destination: &a.UidNumber,
|
||||
},
|
||||
&cli.Int64Flag{
|
||||
Name: "gidnumber",
|
||||
Usage: "Set the gidnumber for the account",
|
||||
Destination: &a.GidNumber,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "mail",
|
||||
Usage: "Set the mail for the account",
|
||||
Destination: &a.Mail,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "description",
|
||||
Usage: "Set the description for the account",
|
||||
Destination: &a.Description,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "password",
|
||||
Usage: "Set the password for the account",
|
||||
Destination: &a.PasswordProfile.Password,
|
||||
// TODO read password from ENV?
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "password-policies",
|
||||
Usage: "Possible policies: DisableStrongPassword, DisablePasswordExpiration",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "force-password-change",
|
||||
Usage: "Force password change on next sign-in",
|
||||
Destination: &a.PasswordProfile.ForceChangePasswordNextSignIn,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "force-password-change-mfa",
|
||||
Usage: "Force password change on next sign-in with mfa",
|
||||
Destination: &a.PasswordProfile.ForceChangePasswordNextSignInWithMfa,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ListAccountsWithConfig applies list command flags to cfg
|
||||
func ListAccountsWithConfig(cfg *config.Config) []cli.Flag {
|
||||
return []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "grpc-namespace",
|
||||
Value: flags.OverrideDefaultString(cfg.GRPC.Namespace, "com.owncloud.api"),
|
||||
Usage: "Set the base namespace for the grpc namespace",
|
||||
EnvVars: []string{"ACCOUNTS_GRPC_NAMESPACE"},
|
||||
Destination: &cfg.GRPC.Namespace,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Value: flags.OverrideDefaultString(cfg.Service.Name, "accounts"),
|
||||
Usage: "service name",
|
||||
EnvVars: []string{"ACCOUNTS_NAME"},
|
||||
Destination: &cfg.Service.Name,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveAccountWithConfig applies remove command flags to cfg
|
||||
func RemoveAccountWithConfig(cfg *config.Config) []cli.Flag {
|
||||
return []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "grpc-namespace",
|
||||
Value: flags.OverrideDefaultString(cfg.GRPC.Namespace, "com.owncloud.api"),
|
||||
Usage: "Set the base namespace for the grpc namespace",
|
||||
EnvVars: []string{"ACCOUNTS_GRPC_NAMESPACE"},
|
||||
Destination: &cfg.GRPC.Namespace,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Value: flags.OverrideDefaultString(cfg.Service.Name, "accounts"),
|
||||
Usage: "service name",
|
||||
EnvVars: []string{"ACCOUNTS_NAME"},
|
||||
Destination: &cfg.Service.Name,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// InspectAccountWithConfig applies inspect command flags to cfg
|
||||
func InspectAccountWithConfig(cfg *config.Config) []cli.Flag {
|
||||
return []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "grpc-namespace",
|
||||
Value: flags.OverrideDefaultString(cfg.GRPC.Namespace, "com.owncloud.api"),
|
||||
Usage: "Set the base namespace for the grpc namespace",
|
||||
EnvVars: []string{"ACCOUNTS_GRPC_NAMESPACE"},
|
||||
Destination: &cfg.GRPC.Namespace,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Value: flags.OverrideDefaultString(cfg.Service.Name, "accounts"),
|
||||
Usage: "service name",
|
||||
EnvVars: []string{"ACCOUNTS_NAME"},
|
||||
Destination: &cfg.Service.Name,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/config"
|
||||
"github.com/owncloud/ocis/ocis-pkg/log"
|
||||
)
|
||||
|
||||
// LoggerFromConfig initializes a service-specific logger instance.
|
||||
func Configure(name string, cfg *config.Log) log.Logger {
|
||||
return log.NewLogger(
|
||||
log.Name(name),
|
||||
log.Level(cfg.Level),
|
||||
log.Pretty(cfg.Pretty),
|
||||
log.Color(cfg.Color),
|
||||
log.File(cfg.File),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package metrics
|
||||
|
||||
import "github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
var (
|
||||
// Namespace defines the namespace for the defines metrics.
|
||||
Namespace = "ocis"
|
||||
|
||||
// Subsystem defines the subsystem for the defines metrics.
|
||||
Subsystem = "accounts"
|
||||
)
|
||||
|
||||
// Metrics defines the available metrics of this service.
|
||||
type Metrics struct {
|
||||
// Counter *prometheus.CounterVec
|
||||
BuildInfo *prometheus.GaugeVec
|
||||
}
|
||||
|
||||
// New initializes the available metrics.
|
||||
func New() *Metrics {
|
||||
m := &Metrics{
|
||||
BuildInfo: prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: Namespace,
|
||||
Subsystem: Subsystem,
|
||||
Name: "build_info",
|
||||
Help: "Build information",
|
||||
}, []string{"version"}),
|
||||
}
|
||||
|
||||
_ = prometheus.Register(m.BuildInfo)
|
||||
// TODO: implement metrics
|
||||
return m
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package debug
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/config"
|
||||
"github.com/owncloud/ocis/ocis-pkg/log"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package debug
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/config"
|
||||
"github.com/owncloud/ocis/ocis-pkg/service/debug"
|
||||
"github.com/owncloud/ocis/ocis-pkg/version"
|
||||
)
|
||||
|
||||
// Server initializes the debug service and server.
|
||||
func Server(opts ...Option) (*http.Server, error) {
|
||||
options := newOptions(opts...)
|
||||
|
||||
return debug.NewService(
|
||||
debug.Logger(options.Logger),
|
||||
debug.Name(options.Config.Service.Name),
|
||||
debug.Version(version.String),
|
||||
debug.Address(options.Config.Debug.Addr),
|
||||
debug.Token(options.Config.Debug.Token),
|
||||
debug.Pprof(options.Config.Debug.Pprof),
|
||||
debug.Zpages(options.Config.Debug.Zpages),
|
||||
debug.Health(health(options.Config)),
|
||||
debug.Ready(ready(options.Config)),
|
||||
debug.CorsAllowedOrigins(options.Config.HTTP.CORS.AllowedOrigins),
|
||||
debug.CorsAllowedMethods(options.Config.HTTP.CORS.AllowedMethods),
|
||||
debug.CorsAllowedHeaders(options.Config.HTTP.CORS.AllowedHeaders),
|
||||
debug.CorsAllowCredentials(options.Config.HTTP.CORS.AllowCredentials),
|
||||
), nil
|
||||
}
|
||||
|
||||
// health implements the health check.
|
||||
func health(cfg *config.Config) func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
// TODO: check if services are up and running
|
||||
|
||||
_, err := io.WriteString(w, http.StatusText(http.StatusOK))
|
||||
// io.WriteString should not fail but if it does we want to know.
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ready implements the ready check.
|
||||
func ready(cfg *config.Config) func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
// TODO: check if services are up and running
|
||||
|
||||
_, err := io.WriteString(w, http.StatusText(http.StatusOK))
|
||||
// io.WriteString should not fail but if it does we want to know.
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/config"
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/metrics"
|
||||
svc "github.com/owncloud/ocis/extensions/accounts/pkg/service/v0"
|
||||
"github.com/owncloud/ocis/ocis-pkg/log"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// Option defines a single option function.
|
||||
type Option func(o *Options)
|
||||
|
||||
// Options defines the available options for this package.
|
||||
type Options struct {
|
||||
Name string
|
||||
Logger log.Logger
|
||||
Context context.Context
|
||||
Config *config.Config
|
||||
Metrics *metrics.Metrics
|
||||
Flags []cli.Flag
|
||||
Handler *svc.Service
|
||||
}
|
||||
|
||||
// newOptions initializes the available default options.
|
||||
func newOptions(opts ...Option) Options {
|
||||
opt := Options{}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
// Name provides a name for the service.
|
||||
func Name(val string) Option {
|
||||
return func(o *Options) {
|
||||
o.Name = val
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// Metrics provides a function to set the metrics option.
|
||||
func Metrics(val *metrics.Metrics) Option {
|
||||
return func(o *Options) {
|
||||
o.Metrics = 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...)
|
||||
}
|
||||
}
|
||||
|
||||
// Handler provides a function to set the handler option.
|
||||
func Handler(val *svc.Service) Option {
|
||||
return func(o *Options) {
|
||||
o.Handler = val
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
accountssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/accounts/v0"
|
||||
|
||||
"github.com/owncloud/ocis/ocis-pkg/service/grpc"
|
||||
"github.com/owncloud/ocis/ocis-pkg/version"
|
||||
)
|
||||
|
||||
// Server initializes a new go-micro service ready to run
|
||||
func Server(opts ...Option) grpc.Service {
|
||||
options := newOptions(opts...)
|
||||
handler := options.Handler
|
||||
|
||||
service := grpc.NewService(
|
||||
grpc.Name(options.Config.Service.Name),
|
||||
grpc.Context(options.Context),
|
||||
grpc.Address(options.Config.GRPC.Addr),
|
||||
grpc.Namespace(options.Config.GRPC.Namespace),
|
||||
grpc.Logger(options.Logger),
|
||||
grpc.Flags(options.Flags...),
|
||||
grpc.Version(version.String),
|
||||
)
|
||||
|
||||
if err := accountssvc.RegisterAccountsServiceHandler(service.Server(), handler); err != nil {
|
||||
options.Logger.Fatal().Err(err).Msg("could not register service handler")
|
||||
}
|
||||
if err := accountssvc.RegisterGroupsServiceHandler(service.Server(), handler); err != nil {
|
||||
options.Logger.Fatal().Err(err).Msg("could not register groups handler")
|
||||
}
|
||||
if err := accountssvc.RegisterIndexServiceHandler(service.Server(), handler); err != nil {
|
||||
options.Logger.Fatal().Err(err).Msg("could not register index handler")
|
||||
}
|
||||
|
||||
return service
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/config"
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/metrics"
|
||||
svc "github.com/owncloud/ocis/extensions/accounts/pkg/service/v0"
|
||||
"github.com/owncloud/ocis/ocis-pkg/log"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// Option defines a single option function.
|
||||
type Option func(o *Options)
|
||||
|
||||
// Options defines the available options for this package.
|
||||
type Options struct {
|
||||
Name string
|
||||
Logger log.Logger
|
||||
Context context.Context
|
||||
Config *config.Config
|
||||
Metrics *metrics.Metrics
|
||||
Flags []cli.Flag
|
||||
Handler *svc.Service
|
||||
}
|
||||
|
||||
// newOptions initializes the available default options.
|
||||
func newOptions(opts ...Option) Options {
|
||||
opt := Options{}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
// Name provides a name for the service.
|
||||
func Name(val string) Option {
|
||||
return func(o *Options) {
|
||||
o.Name = val
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// Metrics provides a function to set the metrics option.
|
||||
func Metrics(val *metrics.Metrics) Option {
|
||||
return func(o *Options) {
|
||||
o.Metrics = 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...)
|
||||
}
|
||||
}
|
||||
|
||||
// Handler provides a function to set the handler option.
|
||||
func Handler(val *svc.Service) Option {
|
||||
return func(o *Options) {
|
||||
o.Handler = val
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
accountssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/accounts/v0"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
chimiddleware "github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/assets"
|
||||
"github.com/owncloud/ocis/ocis-pkg/account"
|
||||
"github.com/owncloud/ocis/ocis-pkg/cors"
|
||||
"github.com/owncloud/ocis/ocis-pkg/middleware"
|
||||
"github.com/owncloud/ocis/ocis-pkg/service/http"
|
||||
"github.com/owncloud/ocis/ocis-pkg/version"
|
||||
"go-micro.dev/v4"
|
||||
)
|
||||
|
||||
// Server initializes the http service and server.
|
||||
func Server(opts ...Option) http.Service {
|
||||
options := newOptions(opts...)
|
||||
handler := options.Handler
|
||||
|
||||
service := http.NewService(
|
||||
http.Logger(options.Logger),
|
||||
http.Name(options.Name),
|
||||
http.Version(version.String),
|
||||
http.Address(options.Config.HTTP.Addr),
|
||||
http.Namespace(options.Config.HTTP.Namespace),
|
||||
http.Context(options.Context),
|
||||
http.Flags(options.Flags...),
|
||||
)
|
||||
|
||||
mux := chi.NewMux()
|
||||
|
||||
mux.Use(chimiddleware.RealIP)
|
||||
mux.Use(chimiddleware.RequestID)
|
||||
mux.Use(middleware.TraceContext)
|
||||
mux.Use(middleware.NoCache)
|
||||
mux.Use(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.Use(middleware.Secure)
|
||||
mux.Use(middleware.ExtractAccountUUID(
|
||||
account.Logger(options.Logger),
|
||||
account.JWTSecret(options.Config.TokenManager.JWTSecret)),
|
||||
)
|
||||
|
||||
mux.Use(middleware.Version(
|
||||
options.Name,
|
||||
version.String,
|
||||
))
|
||||
|
||||
mux.Use(middleware.Logger(
|
||||
options.Logger,
|
||||
))
|
||||
|
||||
mux.Use(middleware.Static(
|
||||
options.Config.HTTP.Root,
|
||||
assets.New(
|
||||
assets.Logger(options.Logger),
|
||||
assets.Config(options.Config),
|
||||
),
|
||||
options.Config.HTTP.CacheTTL,
|
||||
))
|
||||
|
||||
mux.Route(options.Config.HTTP.Root, func(r chi.Router) {
|
||||
accountssvc.RegisterAccountsServiceWeb(r, handler)
|
||||
accountssvc.RegisterGroupsServiceWeb(r, handler)
|
||||
})
|
||||
|
||||
err := micro.RegisterHandler(service.Server(), mux)
|
||||
if err != nil {
|
||||
options.Logger.Fatal().Err(err).Msg("failed to register the handler")
|
||||
}
|
||||
|
||||
return service
|
||||
}
|
||||
@@ -0,0 +1,864 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
|
||||
accountsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/accounts/v0"
|
||||
accountssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/accounts/v0"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/golang/protobuf/ptypes/empty"
|
||||
fieldmask_utils "github.com/mennanov/fieldmask-utils"
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/storage"
|
||||
accTracing "github.com/owncloud/ocis/extensions/accounts/pkg/tracing"
|
||||
"github.com/owncloud/ocis/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/ocis-pkg/middleware"
|
||||
"github.com/owncloud/ocis/ocis-pkg/roles"
|
||||
"github.com/owncloud/ocis/ocis-pkg/sync"
|
||||
settingssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/settings/v0"
|
||||
settings_svc "github.com/owncloud/ocis/settings/pkg/service/v0"
|
||||
"github.com/rs/zerolog"
|
||||
merrors "go-micro.dev/v4/errors"
|
||||
"go-micro.dev/v4/metadata"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"google.golang.org/genproto/protobuf/field_mask"
|
||||
p "google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
// passwordValidCache caches basic auth password validations
|
||||
var passwordValidCache = sync.NewCache(1024)
|
||||
|
||||
// passwordValidCacheExpiration defines the entry lifetime
|
||||
const passwordValidCacheExpiration = 10 * time.Minute
|
||||
|
||||
// an auth request is currently hardcoded and has to match this regex
|
||||
// login eq \"teddy\" and password eq \"F&1!b90t111!\"
|
||||
var authQuery = regexp.MustCompile(`^login eq '(.*)' and password eq '(.*)'$`) // TODO how is ' escaped in the password?
|
||||
|
||||
func (s Service) expandMemberOf(a *accountsmsg.Account) {
|
||||
if a == nil {
|
||||
return
|
||||
}
|
||||
expanded := []*accountsmsg.Group{}
|
||||
for i := range a.MemberOf {
|
||||
g := &accountsmsg.Group{}
|
||||
// TODO resolve by name, when a create or update is issued they may not have an id? fall back to searching the group id in the index?
|
||||
if err := s.repo.LoadGroup(context.Background(), a.MemberOf[i].Id, g); err == nil {
|
||||
g.Members = nil // always hide members when expanding
|
||||
expanded = append(expanded, g)
|
||||
} else {
|
||||
// log errors but continue execution for now
|
||||
s.log.Error().Err(err).Str("id", a.MemberOf[i].Id).Msg("could not load group")
|
||||
}
|
||||
}
|
||||
a.MemberOf = expanded
|
||||
}
|
||||
|
||||
func (s Service) hasAccountManagementPermissions(ctx context.Context) bool {
|
||||
// get roles from context
|
||||
roleIDs, ok := roles.ReadRoleIDsFromContext(ctx)
|
||||
if !ok {
|
||||
/**
|
||||
* FIXME: with this we are skipping permission checks on all requests that are coming in without roleIDs in the
|
||||
* metadata context. This is a huge security impairment, as that's the case not only for grpc requests but also
|
||||
* for unauthenticated http requests and http requests coming in without hitting the ocis-proxy first.
|
||||
*/
|
||||
// TODO add system role for internal requests.
|
||||
// - at least the proxy needs to look up account info
|
||||
// - glauth needs to make bind requests
|
||||
// tracked as OCIS-454
|
||||
return true
|
||||
}
|
||||
|
||||
// check if permission is present in roles of the authenticated account
|
||||
return s.RoleManager.FindPermissionByID(ctx, roleIDs, AccountManagementPermissionID) != nil
|
||||
}
|
||||
|
||||
func (s Service) hasSelfManagementPermissions(ctx context.Context) bool {
|
||||
// get roles from context
|
||||
roleIDs, ok := roles.ReadRoleIDsFromContext(ctx)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
// check if permission is present in roles of the authenticated account
|
||||
return s.RoleManager.FindPermissionByID(ctx, roleIDs, SelfManagementPermissionID) != nil
|
||||
}
|
||||
|
||||
// ListAccounts implements the AccountsServiceHandler interface
|
||||
// the query contains account properties
|
||||
func (s Service) ListAccounts(ctx context.Context, in *accountssvc.ListAccountsRequest, out *accountssvc.ListAccountsResponse) (err error) {
|
||||
var span trace.Span
|
||||
ctx, span = accTracing.TraceProvider.Tracer("accounts").Start(ctx, "Accounts.ListAccounts")
|
||||
defer span.End()
|
||||
|
||||
span.SetAttributes(
|
||||
attribute.KeyValue{Key: "page_size", Value: attribute.Int64Value(int64(in.PageSize))},
|
||||
attribute.KeyValue{Key: "page_token", Value: attribute.StringValue(in.PageToken)},
|
||||
)
|
||||
|
||||
hasSelf := s.hasSelfManagementPermissions(ctx)
|
||||
hasManagement := s.hasAccountManagementPermissions(ctx)
|
||||
if !hasSelf && !hasManagement {
|
||||
return merrors.Forbidden(s.id, "no permission for ListAccounts")
|
||||
}
|
||||
onlySelf := hasSelf && !hasManagement
|
||||
|
||||
match, authRequest := getAuthQueryMatch(in.Query)
|
||||
if authRequest {
|
||||
password := match[2]
|
||||
if len(password) == 0 {
|
||||
return merrors.Unauthorized(s.id, "account not found or invalid credentials")
|
||||
}
|
||||
|
||||
ids, err := s.index.FindBy(&accountsmsg.Account{}, "OnPremisesSamAccountName", match[1])
|
||||
if err != nil || len(ids) > 1 {
|
||||
return merrors.Unauthorized(s.id, "account not found or invalid credentials")
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
ids, err = s.index.FindBy(&accountsmsg.Account{}, "Mail", match[1])
|
||||
if err != nil || len(ids) != 1 {
|
||||
return merrors.Unauthorized(s.id, "account not found or invalid credentials")
|
||||
}
|
||||
}
|
||||
|
||||
a := &accountsmsg.Account{}
|
||||
err = s.repo.LoadAccount(ctx, ids[0], a)
|
||||
if err != nil || a.PasswordProfile == nil || len(a.PasswordProfile.Password) == 0 {
|
||||
return merrors.Unauthorized(s.id, "account not found or invalid credentials")
|
||||
}
|
||||
|
||||
// isPasswordValid uses bcrypt.CompareHashAndPassword which is slow by design.
|
||||
// if every request that matches authQuery regex needs to do this step over and over again,
|
||||
// this is secure but also slow. In this implementation we keep it same secure but increase the speed.
|
||||
//
|
||||
// flow:
|
||||
// - request comes in
|
||||
// - it creates a sha256 based on found account PasswordProfile.LastPasswordChangeDateTime and requested password (v)
|
||||
// - it checks if the cache already contains an entry that matches found account Id // account PasswordProfile.LastPasswordChangeDateTime (k)
|
||||
// - if no entry exists it runs the bcrypt.CompareHashAndPassword as before and if everything is ok it stores the
|
||||
// result by the (k) as key and (v) as value. If not it errors
|
||||
// - if a entry is found it checks if the given value matches (v). If it doesnt match, the cache entry gets removed
|
||||
// and it errors.
|
||||
{
|
||||
var suspicious bool
|
||||
|
||||
kh := sha256.New()
|
||||
mustWrite(kh, []byte(a.Id))
|
||||
k := hex.EncodeToString(kh.Sum([]byte(a.PasswordProfile.LastPasswordChangeDateTime.String())))
|
||||
|
||||
vh := sha256.New()
|
||||
mustWrite(vh, []byte(a.PasswordProfile.Password))
|
||||
v := vh.Sum([]byte(password))
|
||||
|
||||
e := passwordValidCache.Load(k)
|
||||
|
||||
if e == nil {
|
||||
suspicious = !isPasswordValid(s.log, a.PasswordProfile.Password, password)
|
||||
} else if !bytes.Equal(e.V.([]byte), v) {
|
||||
suspicious = true
|
||||
}
|
||||
|
||||
if suspicious {
|
||||
passwordValidCache.Delete(k)
|
||||
return merrors.Unauthorized(s.id, "account not found or invalid credentials")
|
||||
}
|
||||
|
||||
if e == nil {
|
||||
passwordValidCache.Store(k, v, time.Now().Add(passwordValidCacheExpiration))
|
||||
}
|
||||
}
|
||||
|
||||
a.PasswordProfile.Password = ""
|
||||
out.Accounts = []*accountsmsg.Account{a}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if onlySelf {
|
||||
// limit list to own account id
|
||||
if aid, ok := metadata.Get(ctx, middleware.AccountID); ok {
|
||||
in.Query = "id eq '" + aid + "'"
|
||||
} else {
|
||||
return merrors.InternalServerError(s.id, "account id not in context")
|
||||
}
|
||||
}
|
||||
|
||||
if in.Query == "" {
|
||||
err = s.repo.LoadAccounts(ctx, &out.Accounts)
|
||||
if err != nil {
|
||||
s.log.Err(err).Msg("failed to load all accounts from storage")
|
||||
return merrors.InternalServerError(s.id, "failed to load all accounts")
|
||||
}
|
||||
for i := range out.Accounts {
|
||||
a := out.Accounts[i]
|
||||
|
||||
// TODO add groups only if requested
|
||||
// if in.FieldMask ...
|
||||
s.expandMemberOf(a)
|
||||
|
||||
if a.PasswordProfile != nil {
|
||||
a.PasswordProfile.Password = ""
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
searchResults, err := s.findAccountsByQuery(ctx, in.Query)
|
||||
out.Accounts = make([]*accountsmsg.Account, 0, len(searchResults))
|
||||
|
||||
for _, hit := range searchResults {
|
||||
a := &accountsmsg.Account{}
|
||||
if hit == s.Config.ServiceUser.UUID {
|
||||
acc := s.getInMemoryServiceUser()
|
||||
a = &acc
|
||||
} else if err = s.repo.LoadAccount(ctx, hit, a); err != nil {
|
||||
s.log.Error().Err(err).Str("account", hit).Msg("could not load account, skipping")
|
||||
continue
|
||||
}
|
||||
|
||||
s.debugLogAccount(a).Msg("found account")
|
||||
|
||||
// TODO add groups if requested
|
||||
// if in.FieldMask ...
|
||||
s.expandMemberOf(a)
|
||||
|
||||
// remove password before returning
|
||||
if a.PasswordProfile != nil {
|
||||
a.PasswordProfile.Password = ""
|
||||
}
|
||||
|
||||
out.Accounts = append(out.Accounts, a)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s Service) findAccountsByQuery(ctx context.Context, query string) ([]string, error) {
|
||||
return s.index.Query(ctx, &accountsmsg.Account{}, query)
|
||||
}
|
||||
|
||||
// GetAccount implements the AccountsServiceHandler interface
|
||||
func (s Service) GetAccount(ctx context.Context, in *accountssvc.GetAccountRequest, out *accountsmsg.Account) (err error) {
|
||||
var span trace.Span
|
||||
|
||||
ctx, span = accTracing.TraceProvider.Tracer("accounts").Start(ctx, "Accounts.GetAccount")
|
||||
defer span.End()
|
||||
|
||||
span.SetAttributes(
|
||||
attribute.KeyValue{Key: "account_id", Value: attribute.StringValue(in.Id)},
|
||||
)
|
||||
|
||||
hasSelf := s.hasSelfManagementPermissions(ctx)
|
||||
hasManagement := s.hasAccountManagementPermissions(ctx)
|
||||
if !hasSelf && !hasManagement {
|
||||
return merrors.Forbidden(s.id, "no permission for GetAccount")
|
||||
}
|
||||
onlySelf := hasSelf && !hasManagement
|
||||
|
||||
var id string
|
||||
if id, err = cleanupID(in.Id); err != nil {
|
||||
return merrors.InternalServerError(s.id, "could not clean up account id: %v", err.Error())
|
||||
}
|
||||
|
||||
if onlySelf {
|
||||
// limit get to own account id
|
||||
if aid, ok := metadata.Get(ctx, middleware.AccountID); ok {
|
||||
if id != aid {
|
||||
return merrors.Forbidden(s.id, "no permission for GetAccount of another user")
|
||||
}
|
||||
} else {
|
||||
return merrors.InternalServerError(s.id, "account id not in context")
|
||||
}
|
||||
}
|
||||
|
||||
if err = s.repo.LoadAccount(ctx, id, out); err != nil {
|
||||
if storage.IsNotFoundErr(err) {
|
||||
return merrors.NotFound(s.id, "account not found: %v", err.Error())
|
||||
}
|
||||
|
||||
s.log.Error().Err(err).Str("id", id).Msg("could not load account")
|
||||
return merrors.InternalServerError(s.id, "could not load account: %v", err.Error())
|
||||
}
|
||||
|
||||
s.debugLogAccount(out).Msg("found account")
|
||||
|
||||
// TODO add groups if requested
|
||||
// if in.FieldMask ...
|
||||
s.expandMemberOf(out)
|
||||
|
||||
// remove password
|
||||
if out.PasswordProfile != nil {
|
||||
out.PasswordProfile.Password = ""
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// CreateAccount implements the AccountsServiceHandler interface
|
||||
func (s Service) CreateAccount(ctx context.Context, in *accountssvc.CreateAccountRequest, out *accountsmsg.Account) (err error) {
|
||||
var span trace.Span
|
||||
|
||||
ctx, span = accTracing.TraceProvider.Tracer("accounts").Start(ctx, "Accounts.CreateAccount")
|
||||
defer span.End()
|
||||
|
||||
span.SetAttributes(
|
||||
attribute.KeyValue{Key: "account", Value: attribute.StringValue(in.Account.String())},
|
||||
)
|
||||
|
||||
if !s.hasAccountManagementPermissions(ctx) {
|
||||
return merrors.Forbidden(s.id, "no permission for CreateAccount")
|
||||
}
|
||||
|
||||
var id string
|
||||
|
||||
if in.Account == nil {
|
||||
return merrors.InternalServerError(s.id, "invalid account: empty")
|
||||
}
|
||||
|
||||
p.Merge(out, in.Account)
|
||||
|
||||
if out.Id == "" {
|
||||
out.Id = uuid.Must(uuid.NewV4()).String()
|
||||
}
|
||||
if err = validateAccount(s.id, out); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if id, err = cleanupID(out.Id); err != nil {
|
||||
return merrors.InternalServerError(s.id, "could not clean up account id: %v", err.Error())
|
||||
}
|
||||
|
||||
exists, err := s.accountExists(ctx, out.PreferredName, out.Mail, out.Id)
|
||||
if err != nil {
|
||||
return merrors.InternalServerError(s.id, "could not check if account exists: %v", err.Error())
|
||||
}
|
||||
if exists {
|
||||
return merrors.Conflict(s.id, "account already exists")
|
||||
}
|
||||
|
||||
if out.PasswordProfile != nil {
|
||||
if out.PasswordProfile.Password != "" {
|
||||
// encrypt password
|
||||
hashed, err := bcrypt.GenerateFromPassword([]byte(in.Account.PasswordProfile.Password), s.Config.HashDifficulty)
|
||||
if err != nil {
|
||||
s.log.Error().Err(err).Str("id", id).Msg("could not hash password")
|
||||
return merrors.InternalServerError(s.id, "could not hash password: %v", err.Error())
|
||||
}
|
||||
out.PasswordProfile.Password = string(hashed)
|
||||
in.Account.PasswordProfile.Password = ""
|
||||
}
|
||||
|
||||
if err := passwordPoliciesValid(out.PasswordProfile.PasswordPolicies); err != nil {
|
||||
return merrors.BadRequest(s.id, "%s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// extract group id
|
||||
// TODO groups should be ignored during create, use groups.AddMember? return error?
|
||||
|
||||
// write and index account - note: don't do anything else in between!
|
||||
if err = s.repo.WriteAccount(ctx, out); err != nil {
|
||||
s.log.Error().Err(err).Str("id", id).Msg("could not persist new account")
|
||||
s.debugLogAccount(out).Msg("could not persist new account")
|
||||
return merrors.InternalServerError(s.id, "could not persist new account: %v", err.Error())
|
||||
}
|
||||
indexResults, err := s.index.Add(out)
|
||||
if err != nil {
|
||||
s.rollbackCreateAccount(ctx, out)
|
||||
return merrors.Conflict(s.id, "Account already exists %v", err.Error())
|
||||
|
||||
}
|
||||
s.log.Debug().Interface("account", out).Msg("account after indexing")
|
||||
|
||||
for _, r := range indexResults {
|
||||
if r.Field == "UidNumber" {
|
||||
id, err := strconv.Atoi(path.Base(r.Value))
|
||||
if err != nil {
|
||||
s.rollbackCreateAccount(ctx, out)
|
||||
return err
|
||||
}
|
||||
out.UidNumber = int64(id)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if out.GidNumber == 0 {
|
||||
out.GidNumber = userDefaultGID
|
||||
}
|
||||
|
||||
r := accountssvc.ListGroupsResponse{}
|
||||
err = s.ListGroups(ctx, &accountssvc.ListGroupsRequest{}, &r)
|
||||
if err != nil {
|
||||
// rollback account creation
|
||||
return err
|
||||
}
|
||||
|
||||
for _, group := range r.Groups {
|
||||
if group.GidNumber == out.GidNumber {
|
||||
out.MemberOf = append(out.MemberOf, group)
|
||||
}
|
||||
}
|
||||
//acc.MemberOf = append(acc.MemberOf, &group)
|
||||
if err := s.repo.WriteAccount(context.Background(), out); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if out.PasswordProfile != nil {
|
||||
out.PasswordProfile.Password = ""
|
||||
}
|
||||
|
||||
// TODO: assign user role to all new users for now, as create Account request does not have any role field
|
||||
if s.RoleService == nil {
|
||||
return merrors.InternalServerError(s.id, "could not assign role to account: roleService not configured")
|
||||
}
|
||||
if _, err = s.RoleService.AssignRoleToUser(ctx, &settingssvc.AssignRoleToUserRequest{
|
||||
AccountUuid: out.Id,
|
||||
RoleId: settings_svc.BundleUUIDRoleUser,
|
||||
}); err != nil {
|
||||
return merrors.InternalServerError(s.id, "could not assign role to account: %v", err.Error())
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// rollbackCreateAccount tries to rollback changes made by `CreateAccount` if parts of it failed.
|
||||
func (s Service) rollbackCreateAccount(ctx context.Context, acc *accountsmsg.Account) {
|
||||
err := s.index.Delete(acc)
|
||||
if err != nil {
|
||||
s.log.Err(err).Msg("failed to rollback account from indices")
|
||||
}
|
||||
err = s.repo.DeleteAccount(ctx, acc.Id)
|
||||
if err != nil {
|
||||
s.log.Err(err).Msg("failed to rollback account from repo")
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateAccount implements the AccountsServiceHandler interface
|
||||
// read only fields are ignored
|
||||
// TODO how can we unset specific values? using the update mask
|
||||
func (s Service) UpdateAccount(ctx context.Context, in *accountssvc.UpdateAccountRequest, out *accountsmsg.Account) (err error) {
|
||||
var span trace.Span
|
||||
|
||||
ctx, span = accTracing.TraceProvider.Tracer("accounts").Start(ctx, "Accounts.UpdateAccount")
|
||||
defer span.End()
|
||||
|
||||
span.SetAttributes(
|
||||
attribute.KeyValue{Key: "account", Value: attribute.StringValue(in.Account.String())},
|
||||
)
|
||||
|
||||
hasSelf := s.hasSelfManagementPermissions(ctx)
|
||||
hasManagement := s.hasAccountManagementPermissions(ctx)
|
||||
if !hasSelf && !hasManagement {
|
||||
return merrors.Forbidden(s.id, "no permission for UpdateAccount")
|
||||
}
|
||||
onlySelf := hasSelf && !hasManagement
|
||||
|
||||
var id string
|
||||
if in.Account == nil {
|
||||
return merrors.BadRequest(s.id, "account missing")
|
||||
}
|
||||
if in.Account.Id == "" {
|
||||
return merrors.BadRequest(s.id, "account id missing")
|
||||
}
|
||||
|
||||
if id, err = cleanupID(in.Account.Id); err != nil {
|
||||
return merrors.InternalServerError(s.id, "could not clean up account id: %v", err.Error())
|
||||
}
|
||||
|
||||
if onlySelf {
|
||||
// limit update to own account id
|
||||
if aid, ok := metadata.Get(ctx, middleware.AccountID); ok {
|
||||
if id != aid {
|
||||
return merrors.Forbidden(s.id, "no permission to UpdateAccount of another user")
|
||||
}
|
||||
} else {
|
||||
return merrors.InternalServerError(s.id, "account id not in context")
|
||||
}
|
||||
}
|
||||
|
||||
if err = s.repo.LoadAccount(ctx, id, out); err != nil {
|
||||
if storage.IsNotFoundErr(err) {
|
||||
return merrors.NotFound(s.id, "account not found: %v", err.Error())
|
||||
}
|
||||
|
||||
s.log.Error().Err(err).Str("id", id).Msg("could not load account")
|
||||
return merrors.InternalServerError(s.id, "could not load account: %v", err.Error())
|
||||
}
|
||||
|
||||
t := time.Now()
|
||||
tsnow := ×tamppb.Timestamp{
|
||||
Seconds: t.Unix(),
|
||||
Nanos: int32(t.Nanosecond()),
|
||||
}
|
||||
|
||||
var validMask fieldmask_utils.FieldFilterContainer
|
||||
if onlySelf {
|
||||
if validMask, err = validateUpdate(in.UpdateMask, selfUpdatableAccountPaths); err != nil {
|
||||
return merrors.BadRequest(s.id, "%s", err)
|
||||
}
|
||||
} else {
|
||||
if validMask, err = validateUpdate(in.UpdateMask, updatableAccountPaths); err != nil {
|
||||
return merrors.BadRequest(s.id, "%s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if _, exists := validMask.Filter("PreferredName"); exists {
|
||||
if err = validateAccountPreferredName(s.id, in.Account); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, exists := validMask.Filter("OnPremisesSamAccountName"); exists {
|
||||
if err = validateAccountOnPremisesSamAccountName(s.id, in.Account); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, exists := validMask.Filter("Mail"); exists {
|
||||
if in.Account.Mail != "" {
|
||||
if err = validateAccountEmail(s.id, in.Account); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := fieldmask_utils.StructToStruct(validMask, in.Account, out); err != nil {
|
||||
return merrors.InternalServerError(s.id, "%s", err)
|
||||
}
|
||||
|
||||
if in.Account.PasswordProfile != nil {
|
||||
if out.PasswordProfile == nil {
|
||||
out.PasswordProfile = &accountsmsg.PasswordProfile{}
|
||||
}
|
||||
if in.Account.PasswordProfile.Password != "" {
|
||||
// encrypt password
|
||||
hashed, err := bcrypt.GenerateFromPassword([]byte(in.Account.PasswordProfile.Password), s.Config.HashDifficulty)
|
||||
if err != nil {
|
||||
in.Account.PasswordProfile.Password = ""
|
||||
s.log.Error().Err(err).Str("id", id).Msg("could not hash password")
|
||||
return merrors.InternalServerError(s.id, "could not hash password: %v", err.Error())
|
||||
}
|
||||
out.PasswordProfile.Password = string(hashed)
|
||||
in.Account.PasswordProfile.Password = ""
|
||||
}
|
||||
|
||||
if err := passwordPoliciesValid(in.Account.PasswordProfile.PasswordPolicies); err != nil {
|
||||
return merrors.BadRequest(s.id, "%s", err)
|
||||
}
|
||||
|
||||
// lastPasswordChangeDateTime calculated, see password
|
||||
out.PasswordProfile.LastPasswordChangeDateTime = tsnow
|
||||
}
|
||||
|
||||
// out.RefreshTokensValidFromDateTime TODO use to invalidate all existing sessions
|
||||
// out.SignInSessionsValidFromDateTime TODO use to invalidate all existing sessions
|
||||
|
||||
// ... TODO on prem for sync
|
||||
|
||||
if out.ExternalUserState != in.Account.ExternalUserState {
|
||||
out.ExternalUserState = in.Account.ExternalUserState
|
||||
out.ExternalUserStateChangeDateTime = tsnow
|
||||
}
|
||||
|
||||
// We need to reload the old account state to be able to compute the update
|
||||
old := &accountsmsg.Account{}
|
||||
if err = s.repo.LoadAccount(ctx, id, old); err != nil {
|
||||
s.log.Error().Err(err).Str("id", out.Id).Msg("could not load old account representation during update, maybe the account got deleted meanwhile?")
|
||||
return merrors.InternalServerError(s.id, "could not load current account for update: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = s.repo.WriteAccount(ctx, out); err != nil {
|
||||
s.log.Error().Err(err).Str("id", out.Id).Msg("could not persist updated account")
|
||||
return merrors.InternalServerError(s.id, "could not persist updated account: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = s.index.Update(old, out); err != nil {
|
||||
s.log.Error().Err(err).Str("id", id).Msg("could not index new account")
|
||||
return merrors.InternalServerError(s.id, "could not index updated account: %v", err.Error())
|
||||
}
|
||||
|
||||
// remove password
|
||||
if out.PasswordProfile != nil {
|
||||
out.PasswordProfile.Password = ""
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// whitelist of all paths/fields which can be updated by users themselves
|
||||
var selfUpdatableAccountPaths = map[string]struct{}{
|
||||
"DisplayName": {},
|
||||
"Description": {},
|
||||
"Mail": {}, // read only?,
|
||||
"PasswordProfile.Password": {},
|
||||
}
|
||||
|
||||
// whitelist of all paths/fields which can be updated by clients
|
||||
var updatableAccountPaths = map[string]struct{}{
|
||||
"AccountEnabled": {},
|
||||
"IsResourceAccount": {},
|
||||
"Identities": {},
|
||||
"DisplayName": {},
|
||||
"PreferredName": {},
|
||||
"UidNumber": {},
|
||||
"GidNumber": {},
|
||||
"Description": {},
|
||||
"Mail": {}, // read only?,
|
||||
"PasswordProfile.Password": {},
|
||||
"PasswordProfile.PasswordPolicies": {},
|
||||
"PasswordProfile.ForceChangePasswordNextSignIn": {},
|
||||
"PasswordProfile.ForceChangePasswordNextSignInWithMfa": {},
|
||||
"OnPremisesSyncEnabled": {},
|
||||
"OnPremisesSamAccountName": {},
|
||||
}
|
||||
|
||||
// DeleteAccount implements the AccountsServiceHandler interface
|
||||
func (s Service) DeleteAccount(ctx context.Context, in *accountssvc.DeleteAccountRequest, out *empty.Empty) (err error) {
|
||||
var span trace.Span
|
||||
|
||||
ctx, span = accTracing.TraceProvider.Tracer("accounts").Start(ctx, "Accounts.DeleteAccount")
|
||||
defer span.End()
|
||||
|
||||
if !s.hasAccountManagementPermissions(ctx) {
|
||||
return merrors.Forbidden(s.id, "no permission for DeleteAccount")
|
||||
}
|
||||
|
||||
var id string
|
||||
if id, err = cleanupID(in.Id); err != nil {
|
||||
return merrors.InternalServerError(s.id, "could not clean up account id: %v", err.Error())
|
||||
}
|
||||
|
||||
a := &accountsmsg.Account{}
|
||||
if err = s.repo.LoadAccount(ctx, id, a); err != nil {
|
||||
if storage.IsNotFoundErr(err) {
|
||||
return merrors.NotFound(s.id, "account not found: %v", err.Error())
|
||||
}
|
||||
|
||||
s.log.Error().Err(err).Str("id", id).Msg("could not load account")
|
||||
return merrors.InternalServerError(s.id, "could not load account: %v", err.Error())
|
||||
}
|
||||
|
||||
// delete member relationship in groups
|
||||
for i := range a.MemberOf {
|
||||
err = s.RemoveMember(ctx, &accountssvc.RemoveMemberRequest{
|
||||
GroupId: a.MemberOf[i].Id,
|
||||
AccountId: id,
|
||||
}, a.MemberOf[i])
|
||||
if err != nil {
|
||||
s.log.Error().Err(err).Str("accountid", id).Str("groupid", a.MemberOf[i].Id).Msg("could not remove group member, skipping")
|
||||
}
|
||||
}
|
||||
|
||||
if err = s.repo.DeleteAccount(ctx, id); err != nil {
|
||||
if storage.IsNotFoundErr(err) {
|
||||
return merrors.NotFound(s.id, "account not found: %v", err.Error())
|
||||
}
|
||||
|
||||
s.log.Error().Err(err).Str("id", id).Str("accountId", id).Msg("could not remove account")
|
||||
return merrors.InternalServerError(s.id, "could not remove account: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = s.index.Delete(a); err != nil {
|
||||
s.log.Error().Err(err).Str("id", id).Str("accountId", id).Msg("could not remove account from index")
|
||||
return merrors.InternalServerError(s.id, "could not remove account from index: %v", err.Error())
|
||||
}
|
||||
|
||||
s.log.Info().Str("id", id).Msg("deleted account")
|
||||
return
|
||||
}
|
||||
|
||||
func validateAccount(serviceID string, a *accountsmsg.Account) error {
|
||||
if err := validateAccountPreferredName(serviceID, a); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateAccountOnPremisesSamAccountName(serviceID, a); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateAccountEmail(serviceID, a); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateAccountPreferredName(serviceID string, a *accountsmsg.Account) error {
|
||||
if !isValidUsername(a.PreferredName) {
|
||||
return merrors.BadRequest(serviceID, "preferred_name '%s' must be at least the local part of an email", a.PreferredName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateAccountOnPremisesSamAccountName(serviceID string, a *accountsmsg.Account) error {
|
||||
if !isValidUsername(a.OnPremisesSamAccountName) {
|
||||
return merrors.BadRequest(serviceID, "on_premises_sam_account_name '%s' must be at least the local part of an email", a.OnPremisesSamAccountName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateAccountEmail(serviceID string, a *accountsmsg.Account) error {
|
||||
if !isValidEmail(a.Mail) {
|
||||
return merrors.BadRequest(serviceID, "mail '%s' must be a valid email", a.Mail)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// We want to allow email addresses as usernames so they show up when using them in ACLs on storages that allow integration with our glauth LDAP service
|
||||
// so we are adding a few restrictions from https://stackoverflow.com/questions/6949667/what-are-the-real-rules-for-linux-usernames-on-centos-6-and-rhel-6
|
||||
// names should not start with numbers
|
||||
var usernameRegex = regexp.MustCompile("^[a-zA-Z_][a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]*(@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)*$")
|
||||
|
||||
func isValidUsername(e string) bool {
|
||||
if len(e) < 1 && len(e) > 254 {
|
||||
return false
|
||||
}
|
||||
return usernameRegex.MatchString(e)
|
||||
}
|
||||
|
||||
// regex from https://www.w3.org/TR/2016/REC-html51-20161101/sec-forms.html#valid-e-mail-address
|
||||
var emailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
|
||||
|
||||
func isValidEmail(e string) bool {
|
||||
if len(e) < 3 && len(e) > 254 {
|
||||
return false
|
||||
}
|
||||
return emailRegex.MatchString(e)
|
||||
}
|
||||
|
||||
const (
|
||||
policyDisableStrongPassword = "DisableStrongPassword"
|
||||
policyDisablePasswordExpiration = "DisablePasswordExpiration"
|
||||
)
|
||||
|
||||
func passwordPoliciesValid(policies []string) error {
|
||||
for _, v := range policies {
|
||||
if v != policyDisableStrongPassword && v != policyDisablePasswordExpiration {
|
||||
return fmt.Errorf("invalid password-policy %s", v)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateUpdate takes a update field-mask and validates it against a whitelist of updatable paths.
|
||||
// Returns a FieldFilter on success which can be passed to the fieldmask_utils..StructToStruct. An error is returned
|
||||
// if the mask tries to update no whitelisted fields.
|
||||
//
|
||||
// Given an empty or nil mask we assume that the client wants to update all whitelisted fields.
|
||||
//
|
||||
func validateUpdate(mask *field_mask.FieldMask, updatablePaths map[string]struct{}) (fieldmask_utils.FieldFilterContainer, error) {
|
||||
nop := func(s string) string { return s }
|
||||
// Assume that the client wants to update all updatable path if
|
||||
// no field-mask is given, so we create a mask with all paths
|
||||
if mask == nil || len(mask.Paths) == 0 {
|
||||
paths := make([]string, 0, len(updatablePaths))
|
||||
for fieldName := range updatablePaths {
|
||||
paths = append(paths, fieldName)
|
||||
}
|
||||
|
||||
return fieldmask_utils.MaskFromPaths(paths, nop)
|
||||
}
|
||||
|
||||
// Check that only allowed fields are updated
|
||||
for _, v := range mask.Paths {
|
||||
if _, ok := updatablePaths[v]; !ok {
|
||||
return nil, fmt.Errorf("can not update field %s, either unknown or readonly", v)
|
||||
}
|
||||
}
|
||||
|
||||
return fieldmask_utils.MaskFromPaths(mask.Paths, nop)
|
||||
}
|
||||
|
||||
// debugLogAccount returns a debug-log event with detailed account-info, and filtered password data
|
||||
func (s Service) debugLogAccount(a *accountsmsg.Account) *zerolog.Event {
|
||||
return s.log.Debug().Fields(map[string]interface{}{
|
||||
"Id": a.Id,
|
||||
"Mail": a.Mail,
|
||||
"DisplayName": a.DisplayName,
|
||||
"AccountEnabled": a.AccountEnabled,
|
||||
"IsResourceAccount": a.IsResourceAccount,
|
||||
"Identities": a.Identities,
|
||||
"PreferredName": a.PreferredName,
|
||||
"UidNumber": a.UidNumber,
|
||||
"GidNumber": a.GidNumber,
|
||||
"Description": a.Description,
|
||||
"OnPremisesSyncEnabled": a.OnPremisesSyncEnabled,
|
||||
"OnPremisesSamAccountName": a.OnPremisesSamAccountName,
|
||||
"OnPremisesUserPrincipalName": a.OnPremisesUserPrincipalName,
|
||||
"OnPremisesSecurityIdentifier": a.OnPremisesSecurityIdentifier,
|
||||
"OnPremisesDistinguishedName": a.OnPremisesDistinguishedName,
|
||||
"OnPremisesLastSyncDateTime": a.OnPremisesLastSyncDateTime,
|
||||
"MemberOf": a.MemberOf,
|
||||
"CreatedDateTime": a.CreatedDateTime,
|
||||
"DeletedDateTime": a.DeletedDateTime,
|
||||
})
|
||||
}
|
||||
|
||||
func (s Service) accountExists(ctx context.Context, username, mail, id string) (exists bool, err error) {
|
||||
var ids []string
|
||||
ids, err = s.index.FindBy(&accountsmsg.Account{}, "preferred_name", username)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(ids) > 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
ids, err = s.index.FindBy(&accountsmsg.Account{}, "on_premises_sam_account_name", username)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(ids) > 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
ids, err = s.index.FindBy(&accountsmsg.Account{}, "mail", mail)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(ids) > 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
a := &accountsmsg.Account{}
|
||||
err = s.repo.LoadAccount(ctx, id, a)
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
if !storage.IsNotFoundErr(err) {
|
||||
return true, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func getAuthQueryMatch(query string) (match []string, authRequest bool) {
|
||||
match = authQuery.FindStringSubmatch(query)
|
||||
return match, len(match) == 3
|
||||
}
|
||||
|
||||
func isPasswordValid(logger log.Logger, hash string, pwd string) (ok bool) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.Error().Err(fmt.Errorf("%s", r)).Str("hash", hash).Msg("password lib panicked")
|
||||
}
|
||||
}()
|
||||
|
||||
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(pwd)) == nil
|
||||
}
|
||||
|
||||
func mustWrite(w io.Writer, val []byte) {
|
||||
if _, err := w.Write(val); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
accountsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/accounts/v0"
|
||||
accountssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/accounts/v0"
|
||||
|
||||
"github.com/golang/protobuf/ptypes/empty"
|
||||
config "github.com/owncloud/ocis/extensions/accounts/pkg/config/defaults"
|
||||
olog "github.com/owncloud/ocis/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/ocis-pkg/middleware"
|
||||
"github.com/owncloud/ocis/ocis-pkg/roles"
|
||||
settingsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/settings/v0"
|
||||
settingssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/settings/v0"
|
||||
ssvc "github.com/owncloud/ocis/settings/pkg/service/v0"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go-micro.dev/v4/client"
|
||||
merrors "go-micro.dev/v4/errors"
|
||||
"go-micro.dev/v4/metadata"
|
||||
)
|
||||
|
||||
const dataPath = "/tmp/ocis-accounts-tests"
|
||||
|
||||
var (
|
||||
roleServiceMock settingssvc.RoleService
|
||||
s *Service
|
||||
)
|
||||
|
||||
func init() {
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Repo.Backend = "disk"
|
||||
cfg.Repo.Disk.Path = dataPath
|
||||
logger := olog.NewLogger(olog.Color(true), olog.Pretty(true))
|
||||
roleServiceMock = buildRoleServiceMock()
|
||||
roleManager := roles.NewManager(
|
||||
roles.Logger(logger),
|
||||
roles.RoleService(roleServiceMock),
|
||||
roles.CacheTTL(time.Hour),
|
||||
roles.CacheSize(1024),
|
||||
)
|
||||
s, _ = New(
|
||||
Logger(logger),
|
||||
Config(cfg),
|
||||
RoleService(roleServiceMock),
|
||||
RoleManager(&roleManager),
|
||||
)
|
||||
}
|
||||
|
||||
func setup() (teardown func()) {
|
||||
return func() {
|
||||
if err := os.RemoveAll(dataPath); err != nil {
|
||||
log.Printf("could not delete data root: %s", dataPath)
|
||||
} else {
|
||||
log.Println("data root deleted")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestPermissionsListAccounts checks permission handling on ListAccounts
|
||||
func TestPermissionsListAccounts(t *testing.T) {
|
||||
var scenarios = []struct {
|
||||
name string
|
||||
roleIDs []string
|
||||
query string
|
||||
permissionError error
|
||||
}{
|
||||
// TODO: remove this test when https://github.com/owncloud/ocis/accounts/pull/111 is merged
|
||||
// replace with two tests:
|
||||
// 1: "ListAccounts fails with 403 when roleIDs don't exist in context"
|
||||
// 2: "ListAccounts fails with 403 when ('no admin role in context' AND 'empty query')"
|
||||
{
|
||||
"ListAccounts succeeds when no roleIDs in context",
|
||||
nil,
|
||||
"",
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"ListAccounts fails when no admin roleID in context",
|
||||
[]string{ssvc.BundleUUIDRoleUser, ssvc.BundleUUIDRoleGuest},
|
||||
"",
|
||||
merrors.Forbidden(s.id, "no permission for ListAccounts"),
|
||||
},
|
||||
{
|
||||
"ListAccounts succeeds when admin roleID in context",
|
||||
[]string{ssvc.BundleUUIDRoleAdmin},
|
||||
"",
|
||||
nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.name, func(t *testing.T) {
|
||||
teardown := setup()
|
||||
defer teardown()
|
||||
|
||||
ctx := buildTestCtx(t, scenario.roleIDs)
|
||||
request := &accountssvc.ListAccountsRequest{
|
||||
Query: scenario.query,
|
||||
}
|
||||
response := &accountssvc.ListAccountsResponse{}
|
||||
err := s.ListAccounts(ctx, request, response)
|
||||
if scenario.permissionError != nil {
|
||||
assert.Equal(t, scenario.permissionError, err)
|
||||
} else if err != nil {
|
||||
// we are only checking permissions here, so just check that the error code is not 403
|
||||
merr := merrors.FromError(err)
|
||||
assert.NotEqual(t, http.StatusForbidden, merr.GetCode())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPermissionsGetAccount checks permission handling on GetAccount
|
||||
// TODO: remove this test function entirely, when https://github.com/owncloud/ocis/accounts/pull/111 is merged. GetAccount will not have permission checks for the time being.
|
||||
func TestPermissionsGetAccount(t *testing.T) {
|
||||
var scenarios = []struct {
|
||||
name string
|
||||
roleIDs []string
|
||||
permissionError error
|
||||
}{
|
||||
{
|
||||
"GetAccount succeeds when no role IDs in context",
|
||||
nil,
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"GetAccount fails when no admin roleID in context",
|
||||
[]string{ssvc.BundleUUIDRoleUser, ssvc.BundleUUIDRoleGuest},
|
||||
merrors.Forbidden(s.id, "no permission for GetAccount"),
|
||||
},
|
||||
{
|
||||
"GetAccount succeeds when admin roleID in context",
|
||||
[]string{ssvc.BundleUUIDRoleAdmin},
|
||||
nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.name, func(t *testing.T) {
|
||||
teardown := setup()
|
||||
defer teardown()
|
||||
|
||||
ctx := buildTestCtx(t, scenario.roleIDs)
|
||||
request := &accountssvc.GetAccountRequest{}
|
||||
response := &accountsmsg.Account{}
|
||||
err := s.GetAccount(ctx, request, response)
|
||||
if scenario.permissionError != nil {
|
||||
assert.Equal(t, scenario.permissionError, err)
|
||||
} else if err != nil {
|
||||
// we are only checking permissions here, so just check that the error code is not 403
|
||||
merr := merrors.FromError(err)
|
||||
assert.NotEqual(t, http.StatusForbidden, merr.GetCode())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPermissionsCreateAccount checks permission handling on CreateAccount
|
||||
func TestPermissionsCreateAccount(t *testing.T) {
|
||||
var scenarios = []struct {
|
||||
name string
|
||||
roleIDs []string
|
||||
permissionError error
|
||||
}{
|
||||
// TODO: remove this test when https://github.com/owncloud/ocis/accounts/pull/111 is merged
|
||||
// replace with two tests:
|
||||
// 1: "CreateAccount fails with 403 when roleIDs don't exist in context"
|
||||
// 2: "CreateAccount fails with 403 when no admin role in context"
|
||||
{
|
||||
"CreateAccount succeeds when no role IDs in context",
|
||||
nil,
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"CreateAccount fails when no admin roleID in context",
|
||||
[]string{ssvc.BundleUUIDRoleUser, ssvc.BundleUUIDRoleGuest},
|
||||
merrors.Forbidden(s.id, "no permission for CreateAccount"),
|
||||
},
|
||||
{
|
||||
"CreateAccount succeeds when admin roleID in context",
|
||||
[]string{ssvc.BundleUUIDRoleAdmin},
|
||||
nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.name, func(t *testing.T) {
|
||||
teardown := setup()
|
||||
defer teardown()
|
||||
|
||||
ctx := buildTestCtx(t, scenario.roleIDs)
|
||||
request := &accountssvc.CreateAccountRequest{}
|
||||
response := &accountsmsg.Account{}
|
||||
err := s.CreateAccount(ctx, request, response)
|
||||
if scenario.permissionError != nil {
|
||||
assert.Equal(t, scenario.permissionError, err)
|
||||
} else if err != nil {
|
||||
// we are only checking permissions here, so just check that the error code is not 403
|
||||
merr := merrors.FromError(err)
|
||||
assert.NotEqual(t, http.StatusForbidden, merr.GetCode())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPermissionsUpdateAccount checks permission handling on UpdateAccount
|
||||
func TestPermissionsUpdateAccount(t *testing.T) {
|
||||
var scenarios = []struct {
|
||||
name string
|
||||
roleIDs []string
|
||||
permissionError error
|
||||
}{
|
||||
// TODO: remove this test when https://github.com/owncloud/ocis/accounts/pull/111 is merged
|
||||
// replace with two tests:
|
||||
// 1: "UpdateAccount fails with 403 when roleIDs don't exist in context"
|
||||
// 2: "UpdateAccount fails with 403 when no admin role in context"
|
||||
{
|
||||
"UpdateAccount succeeds when no role IDs in context",
|
||||
nil,
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"UpdateAccount fails when no admin roleID in context",
|
||||
[]string{ssvc.BundleUUIDRoleUser, ssvc.BundleUUIDRoleGuest},
|
||||
merrors.Forbidden(s.id, "no permission for UpdateAccount"),
|
||||
},
|
||||
{
|
||||
"UpdateAccount succeeds when admin roleID in context",
|
||||
[]string{ssvc.BundleUUIDRoleAdmin},
|
||||
nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.name, func(t *testing.T) {
|
||||
teardown := setup()
|
||||
defer teardown()
|
||||
|
||||
ctx := buildTestCtx(t, scenario.roleIDs)
|
||||
request := &accountssvc.UpdateAccountRequest{}
|
||||
response := &accountsmsg.Account{}
|
||||
err := s.UpdateAccount(ctx, request, response)
|
||||
if scenario.permissionError != nil {
|
||||
assert.Equal(t, scenario.permissionError, err)
|
||||
} else if err != nil {
|
||||
// we are only checking permissions here, so just check that the error code is not 403
|
||||
merr := merrors.FromError(err)
|
||||
assert.NotEqual(t, http.StatusForbidden, merr.GetCode())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPermissionsDeleteAccount checks permission handling on DeleteAccount
|
||||
func TestPermissionsDeleteAccount(t *testing.T) {
|
||||
var scenarios = []struct {
|
||||
name string
|
||||
roleIDs []string
|
||||
permissionError error
|
||||
}{
|
||||
// TODO: remove this test when https://github.com/owncloud/ocis/accounts/pull/111 is merged
|
||||
// replace with two tests:
|
||||
// 1: "DeleteAccount fails with 403 when roleIDs don't exist in context"
|
||||
// 2: "DeleteAccount fails with 403 when no admin role in context"
|
||||
{
|
||||
"DeleteAccount succeeds when no role IDs in context",
|
||||
nil,
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"DeleteAccount fails when no admin roleID in context",
|
||||
[]string{ssvc.BundleUUIDRoleUser, ssvc.BundleUUIDRoleGuest},
|
||||
merrors.Forbidden(s.id, "no permission for DeleteAccount"),
|
||||
},
|
||||
{
|
||||
"DeleteAccount succeeds when admin roleID in context",
|
||||
[]string{ssvc.BundleUUIDRoleAdmin},
|
||||
nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.name, func(t *testing.T) {
|
||||
teardown := setup()
|
||||
defer teardown()
|
||||
|
||||
ctx := buildTestCtx(t, scenario.roleIDs)
|
||||
request := &accountssvc.DeleteAccountRequest{}
|
||||
response := &empty.Empty{}
|
||||
err := s.DeleteAccount(ctx, request, response)
|
||||
if scenario.permissionError != nil {
|
||||
assert.Equal(t, scenario.permissionError, err)
|
||||
} else if err != nil {
|
||||
// we are only checking permissions here, so just check that the error code is not 403
|
||||
merr := merrors.FromError(err)
|
||||
assert.NotEqual(t, http.StatusForbidden, merr.GetCode())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func buildTestCtx(t *testing.T, roleIDs []string) context.Context {
|
||||
ctx := context.Background()
|
||||
if roleIDs != nil {
|
||||
roleIDs, err := json.Marshal(roleIDs)
|
||||
assert.NoError(t, err)
|
||||
ctx = metadata.Set(ctx, middleware.RoleIDs, string(roleIDs))
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
func buildRoleServiceMock() settingssvc.RoleService {
|
||||
defaultRoles := map[string]*settingsmsg.Bundle{
|
||||
ssvc.BundleUUIDRoleAdmin: {
|
||||
Id: ssvc.BundleUUIDRoleAdmin,
|
||||
Type: settingsmsg.Bundle_TYPE_ROLE,
|
||||
Resource: &settingsmsg.Resource{
|
||||
Type: settingsmsg.Resource_TYPE_SYSTEM,
|
||||
},
|
||||
Settings: []*settingsmsg.Setting{
|
||||
{
|
||||
Id: AccountManagementPermissionID,
|
||||
},
|
||||
},
|
||||
},
|
||||
ssvc.BundleUUIDRoleUser: {
|
||||
Id: ssvc.BundleUUIDRoleUser,
|
||||
Type: settingsmsg.Bundle_TYPE_ROLE,
|
||||
Resource: &settingsmsg.Resource{
|
||||
Type: settingsmsg.Resource_TYPE_SYSTEM,
|
||||
},
|
||||
Settings: []*settingsmsg.Setting{},
|
||||
},
|
||||
ssvc.BundleUUIDRoleGuest: {
|
||||
Id: ssvc.BundleUUIDRoleGuest,
|
||||
Type: settingsmsg.Bundle_TYPE_ROLE,
|
||||
Resource: &settingsmsg.Resource{
|
||||
Type: settingsmsg.Resource_TYPE_SYSTEM,
|
||||
},
|
||||
Settings: []*settingsmsg.Setting{},
|
||||
},
|
||||
}
|
||||
return settingssvc.MockRoleService{
|
||||
ListRolesFunc: func(ctx context.Context, req *settingssvc.ListBundlesRequest, opts ...client.CallOption) (res *settingssvc.ListBundlesResponse, err error) {
|
||||
payload := make([]*settingsmsg.Bundle, 0)
|
||||
for _, roleID := range req.BundleIds {
|
||||
if defaultRoles[roleID] != nil {
|
||||
payload = append(payload, defaultRoles[roleID])
|
||||
}
|
||||
}
|
||||
return &settingssvc.ListBundlesResponse{
|
||||
Bundles: payload,
|
||||
}, nil
|
||||
},
|
||||
AssignRoleToUserFunc: func(ctx context.Context, req *settingssvc.AssignRoleToUserRequest, opts ...client.CallOption) (res *settingssvc.AssignRoleToUserResponse, err error) {
|
||||
// mock can be empty. function is called during service start. actual role assignments not needed for the tests.
|
||||
return &settingssvc.AssignRoleToUserResponse{
|
||||
Assignment: &settingsmsg.UserRoleAssignment{},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
accountsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/accounts/v0"
|
||||
accountssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/accounts/v0"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/golang/protobuf/ptypes/empty"
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/storage"
|
||||
merrors "go-micro.dev/v4/errors"
|
||||
p "google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func (s Service) expandMembers(g *accountsmsg.Group) {
|
||||
if g == nil {
|
||||
return
|
||||
}
|
||||
expanded := []*accountsmsg.Account{}
|
||||
for i := range g.Members {
|
||||
// TODO resolve by name, when a create or update is issued they may not have an id? fall back to searching the group id in the index?
|
||||
a := &accountsmsg.Account{}
|
||||
if err := s.repo.LoadAccount(context.Background(), g.Members[i].Id, a); err == nil {
|
||||
expanded = append(expanded, a)
|
||||
} else {
|
||||
// log errors but con/var/tmp/ocis-accounts-store-408341811tinue execution for now
|
||||
s.log.Error().Err(err).Str("id", g.Members[i].Id).Msg("could not load account")
|
||||
}
|
||||
}
|
||||
g.Members = expanded
|
||||
}
|
||||
|
||||
// deflateMembers replaces the users of a group with an instance that only contains the id
|
||||
func (s Service) deflateMembers(g *accountsmsg.Group) {
|
||||
if g == nil {
|
||||
return
|
||||
}
|
||||
deflated := []*accountsmsg.Account{}
|
||||
for i := range g.Members {
|
||||
if g.Members[i].Id != "" {
|
||||
deflated = append(deflated, &accountsmsg.Account{Id: g.Members[i].Id})
|
||||
} else {
|
||||
// TODO fetch and use an id when group only has a name but no id
|
||||
s.log.Error().Str("id", g.Id).Interface("account", g.Members[i]).Msg("resolving members by name is not implemented yet")
|
||||
}
|
||||
}
|
||||
g.Members = deflated
|
||||
}
|
||||
|
||||
// ListGroups implements the GroupsServiceHandler interface
|
||||
func (s Service) ListGroups(ctx context.Context, in *accountssvc.ListGroupsRequest, out *accountssvc.ListGroupsResponse) (err error) {
|
||||
if in.Query == "" {
|
||||
err = s.repo.LoadGroups(ctx, &out.Groups)
|
||||
if err != nil {
|
||||
s.log.Err(err).Msg("failed to load all groups from storage")
|
||||
return merrors.InternalServerError(s.id, "failed to load all groups")
|
||||
}
|
||||
for i := range out.Groups {
|
||||
a := out.Groups[i]
|
||||
|
||||
// TODO add accounts only if requested
|
||||
// if in.FieldMask ...
|
||||
s.expandMembers(a)
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
searchResults, err := s.findGroupsByQuery(ctx, in.Query)
|
||||
out.Groups = make([]*accountsmsg.Group, 0, len(searchResults))
|
||||
|
||||
for _, hit := range searchResults {
|
||||
g := &accountsmsg.Group{}
|
||||
if err = s.repo.LoadGroup(ctx, hit, g); err != nil {
|
||||
s.log.Error().Err(err).Str("group", hit).Msg("could not load group, skipping")
|
||||
continue
|
||||
}
|
||||
s.log.Debug().Interface("group", g).Msg("found group")
|
||||
|
||||
// TODO add accounts if requested
|
||||
// if in.FieldMask ...
|
||||
s.expandMembers(g)
|
||||
|
||||
out.Groups = append(out.Groups, g)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
func (s Service) findGroupsByQuery(ctx context.Context, query string) ([]string, error) {
|
||||
return s.index.Query(ctx, &accountsmsg.Group{}, query)
|
||||
}
|
||||
|
||||
// GetGroup implements the GroupsServiceHandler interface
|
||||
func (s Service) GetGroup(c context.Context, in *accountssvc.GetGroupRequest, out *accountsmsg.Group) (err error) {
|
||||
var id string
|
||||
if id, err = cleanupID(in.Id); err != nil {
|
||||
return merrors.InternalServerError(s.id, "could not clean up group id: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = s.repo.LoadGroup(c, id, out); err != nil {
|
||||
if storage.IsNotFoundErr(err) {
|
||||
return merrors.NotFound(s.id, "group not found: %v", err.Error())
|
||||
}
|
||||
|
||||
s.log.Error().Err(err).Str("id", id).Msg("could not load group")
|
||||
return merrors.InternalServerError(s.id, "could not load group: %v", err.Error())
|
||||
}
|
||||
s.log.Debug().Interface("group", out).Msg("found group")
|
||||
|
||||
// TODO only add accounts if requested
|
||||
// if in.FieldMask ...
|
||||
s.expandMembers(out)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// CreateGroup implements the GroupsServiceHandler interface
|
||||
func (s Service) CreateGroup(c context.Context, in *accountssvc.CreateGroupRequest, out *accountsmsg.Group) (err error) {
|
||||
if in.Group == nil {
|
||||
return merrors.InternalServerError(s.id, "invalid group: empty")
|
||||
}
|
||||
p.Merge(out, in.Group)
|
||||
|
||||
if out.Id == "" {
|
||||
out.Id = uuid.Must(uuid.NewV4()).String()
|
||||
}
|
||||
|
||||
if _, err = cleanupID(out.Id); err != nil {
|
||||
return merrors.InternalServerError(s.id, "could not clean up account id: %v", err.Error())
|
||||
}
|
||||
|
||||
s.deflateMembers(out)
|
||||
|
||||
if err = s.repo.WriteGroup(c, out); err != nil {
|
||||
s.log.Error().Err(err).Interface("group", out).Msg("could not persist new group")
|
||||
return merrors.InternalServerError(s.id, "could not persist new group: %v", err.Error())
|
||||
}
|
||||
|
||||
indexResults, err := s.index.Add(out)
|
||||
if err != nil {
|
||||
s.rollbackCreateGroup(c, out)
|
||||
return merrors.InternalServerError(s.id, "could not index new group: %v", err.Error())
|
||||
}
|
||||
|
||||
for _, r := range indexResults {
|
||||
if r.Field == "GidNumber" {
|
||||
gid, err := strconv.Atoi(path.Base(r.Value))
|
||||
if err != nil {
|
||||
s.rollbackCreateGroup(c, out)
|
||||
return err
|
||||
}
|
||||
out.GidNumber = int64(gid)
|
||||
return s.repo.WriteGroup(context.Background(), out)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// rollbackCreateGroup tries to rollback changes made by `CreateGroup` if parts of it failed.
|
||||
func (s Service) rollbackCreateGroup(ctx context.Context, group *accountsmsg.Group) {
|
||||
err := s.index.Delete(group)
|
||||
if err != nil {
|
||||
s.log.Err(err).Msg("failed to rollback group from indices")
|
||||
}
|
||||
err = s.repo.DeleteGroup(ctx, group.Id)
|
||||
if err != nil {
|
||||
s.log.Err(err).Msg("failed to rollback group from repo")
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateGroup implements the GroupsServiceHandler interface
|
||||
func (s Service) UpdateGroup(c context.Context, in *accountssvc.UpdateGroupRequest, out *accountsmsg.Group) (err error) {
|
||||
return merrors.InternalServerError(s.id, "not implemented")
|
||||
}
|
||||
|
||||
// DeleteGroup implements the GroupsServiceHandler interface
|
||||
func (s Service) DeleteGroup(c context.Context, in *accountssvc.DeleteGroupRequest, out *empty.Empty) (err error) {
|
||||
var id string
|
||||
if id, err = cleanupID(in.Id); err != nil {
|
||||
return merrors.InternalServerError(s.id, "could not clean up group id: %v", err.Error())
|
||||
}
|
||||
|
||||
g := &accountsmsg.Group{}
|
||||
if err = s.repo.LoadGroup(c, id, g); err != nil {
|
||||
if storage.IsNotFoundErr(err) {
|
||||
return merrors.NotFound(s.id, "group not found: %v", err.Error())
|
||||
}
|
||||
return merrors.InternalServerError(s.id, "could not load group: %v", err.Error())
|
||||
}
|
||||
|
||||
// delete memberof relationship in users
|
||||
for i := range g.Members {
|
||||
err = s.RemoveMember(c, &accountssvc.RemoveMemberRequest{
|
||||
AccountId: g.Members[i].Id,
|
||||
GroupId: id,
|
||||
}, g)
|
||||
if err != nil {
|
||||
s.log.Error().Err(err).Str("groupid", id).Str("accountid", g.Members[i].Id).Msg("could not remove account memberof, skipping")
|
||||
}
|
||||
}
|
||||
|
||||
if err = s.repo.DeleteGroup(c, id); err != nil {
|
||||
if storage.IsNotFoundErr(err) {
|
||||
return merrors.NotFound(s.id, "group not found: %v", err.Error())
|
||||
}
|
||||
|
||||
return merrors.InternalServerError(s.id, "could not load group: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = s.index.Delete(g); err != nil {
|
||||
s.log.Error().Err(err).Str("id", id).Msg("could not remove group from index")
|
||||
return merrors.InternalServerError(s.id, "could not remove group from index: %v", err.Error())
|
||||
}
|
||||
|
||||
s.log.Info().Str("id", id).Msg("deleted group")
|
||||
return
|
||||
}
|
||||
|
||||
// AddMember implements the GroupsServiceHandler interface
|
||||
func (s Service) AddMember(c context.Context, in *accountssvc.AddMemberRequest, out *accountsmsg.Group) (err error) {
|
||||
// cleanup ids
|
||||
var groupID string
|
||||
if groupID, err = cleanupID(in.GroupId); err != nil {
|
||||
return merrors.InternalServerError(s.id, "could not clean up group id: %v", err.Error())
|
||||
}
|
||||
|
||||
var accountID string
|
||||
if accountID, err = cleanupID(in.AccountId); err != nil {
|
||||
return merrors.InternalServerError(s.id, "could not clean up account id: %v", err.Error())
|
||||
}
|
||||
|
||||
// load structs
|
||||
a := &accountsmsg.Account{}
|
||||
if err = s.repo.LoadAccount(c, accountID, a); err != nil {
|
||||
if storage.IsNotFoundErr(err) {
|
||||
return merrors.NotFound(s.id, "group not found: %v", err.Error())
|
||||
}
|
||||
return merrors.InternalServerError(s.id, "could not load group: %v", err.Error())
|
||||
}
|
||||
|
||||
g := &accountsmsg.Group{}
|
||||
if err = s.repo.LoadGroup(c, groupID, g); err != nil {
|
||||
if storage.IsNotFoundErr(err) {
|
||||
return merrors.NotFound(s.id, "could not load group: %v", err.Error())
|
||||
}
|
||||
return merrors.InternalServerError(s.id, "could not load group: %v", err.Error())
|
||||
}
|
||||
|
||||
// check if we need to add the account to the group
|
||||
alreadyRelated := false
|
||||
for i := range g.Members {
|
||||
if g.Members[i].Id == a.Id {
|
||||
alreadyRelated = true
|
||||
}
|
||||
}
|
||||
aref := &accountsmsg.Account{
|
||||
Id: a.Id,
|
||||
}
|
||||
if !alreadyRelated {
|
||||
g.Members = append(g.Members, aref)
|
||||
}
|
||||
|
||||
// check if we need to add the group to the account
|
||||
alreadyRelated = false
|
||||
for i := range a.MemberOf {
|
||||
if a.MemberOf[i].Id == g.Id {
|
||||
alreadyRelated = true
|
||||
break
|
||||
}
|
||||
}
|
||||
// only store the reference to prevent recursion when marshaling json
|
||||
gref := &accountsmsg.Group{
|
||||
Id: g.Id,
|
||||
}
|
||||
if !alreadyRelated {
|
||||
a.MemberOf = append(a.MemberOf, gref)
|
||||
}
|
||||
|
||||
if err = s.repo.WriteAccount(c, a); err != nil {
|
||||
return merrors.InternalServerError(s.id, "could not persist account: %v", err.Error())
|
||||
}
|
||||
if err = s.repo.WriteGroup(c, g); err != nil {
|
||||
return merrors.InternalServerError(s.id, "could not persist group: %v", err.Error())
|
||||
}
|
||||
// FIXME update index!
|
||||
// TODO rollback changes when only one of them failed?
|
||||
// TODO store relation in another file?
|
||||
// TODO return error if they are already related?
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveMember implements the GroupsServiceHandler interface
|
||||
func (s Service) RemoveMember(c context.Context, in *accountssvc.RemoveMemberRequest, out *accountsmsg.Group) (err error) {
|
||||
|
||||
// cleanup ids
|
||||
var groupID string
|
||||
if groupID, err = cleanupID(in.GroupId); err != nil {
|
||||
return merrors.InternalServerError(s.id, "could not clean up group id: %v", err.Error())
|
||||
}
|
||||
|
||||
var accountID string
|
||||
if accountID, err = cleanupID(in.AccountId); err != nil {
|
||||
return merrors.InternalServerError(s.id, "could not clean up account id: %v", err.Error())
|
||||
}
|
||||
|
||||
// load structs
|
||||
a := &accountsmsg.Account{}
|
||||
if err = s.repo.LoadAccount(c, accountID, a); err != nil {
|
||||
if storage.IsNotFoundErr(err) {
|
||||
return merrors.NotFound(s.id, "could not load account: %v", err.Error())
|
||||
}
|
||||
s.log.Error().Err(err).Str("id", accountID).Msg("could not load account")
|
||||
return merrors.InternalServerError(s.id, "could not load account: %v", err.Error())
|
||||
}
|
||||
|
||||
g := &accountsmsg.Group{}
|
||||
if err = s.repo.LoadGroup(c, groupID, g); err != nil {
|
||||
if storage.IsNotFoundErr(err) {
|
||||
return merrors.NotFound(s.id, "could not load group: %v", err.Error())
|
||||
}
|
||||
s.log.Error().Err(err).Str("id", groupID).Msg("could not load group")
|
||||
return merrors.InternalServerError(s.id, "could not load group: %v", err.Error())
|
||||
}
|
||||
|
||||
//remove the account from the group if it exists
|
||||
newMembers := []*accountsmsg.Account{}
|
||||
for i := range g.Members {
|
||||
if g.Members[i].Id != a.Id {
|
||||
newMembers = append(newMembers, g.Members[i])
|
||||
}
|
||||
}
|
||||
g.Members = newMembers
|
||||
|
||||
// remove the group from the account if it exists
|
||||
newGroups := []*accountsmsg.Group{}
|
||||
for i := range a.MemberOf {
|
||||
if a.MemberOf[i].Id != g.Id {
|
||||
newGroups = append(newGroups, a.MemberOf[i])
|
||||
}
|
||||
}
|
||||
a.MemberOf = newGroups
|
||||
|
||||
if err = s.repo.WriteAccount(c, a); err != nil {
|
||||
s.log.Error().Err(err).Interface("account", a).Msg("could not persist account")
|
||||
return merrors.InternalServerError(s.id, "could not persist account: %v", err.Error())
|
||||
}
|
||||
if err = s.repo.WriteGroup(c, g); err != nil {
|
||||
s.log.Error().Err(err).Interface("group", g).Msg("could not persist group")
|
||||
return merrors.InternalServerError(s.id, "could not persist group: %v", err.Error())
|
||||
}
|
||||
// FIXME update index!
|
||||
// TODO rollback changes when only one of them failed?
|
||||
// TODO store relation in another file?
|
||||
// TODO return error if they are not related?
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListMembers implements the GroupsServiceHandler interface
|
||||
func (s Service) ListMembers(c context.Context, in *accountssvc.ListMembersRequest, out *accountssvc.ListMembersResponse) (err error) {
|
||||
// cleanup ids
|
||||
var groupID string
|
||||
if groupID, err = cleanupID(in.Id); err != nil {
|
||||
return merrors.InternalServerError(s.id, "could not clean up group id: %v", err.Error())
|
||||
}
|
||||
|
||||
g := &accountsmsg.Group{}
|
||||
if err = s.repo.LoadGroup(c, groupID, g); err != nil {
|
||||
if storage.IsNotFoundErr(err) {
|
||||
return merrors.NotFound(s.id, "group not found: %v", err.Error())
|
||||
}
|
||||
s.log.Error().Err(err).Str("id", groupID).Msg("could not load group")
|
||||
return merrors.InternalServerError(s.id, "could not load group: %v", err.Error())
|
||||
}
|
||||
|
||||
// TODO only expand accounts if requested
|
||||
// if in.FieldMask ...
|
||||
s.expandMembers(g)
|
||||
out.Members = g.Members
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
accountsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/accounts/v0"
|
||||
accountssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/accounts/v0"
|
||||
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/storage"
|
||||
|
||||
"github.com/owncloud/ocis/ocis-pkg/indexer"
|
||||
"github.com/owncloud/ocis/ocis-pkg/indexer/config"
|
||||
"github.com/owncloud/ocis/ocis-pkg/indexer/option"
|
||||
)
|
||||
|
||||
// RebuildIndex deletes all indices (in memory and on storage) and rebuilds them from scratch.
|
||||
func (s Service) RebuildIndex(ctx context.Context, request *accountssvc.RebuildIndexRequest, response *accountssvc.RebuildIndexResponse) error {
|
||||
if err := s.index.Reset(); err != nil {
|
||||
return fmt.Errorf("failed to delete index containers: %w", err)
|
||||
}
|
||||
|
||||
c, err := configFromSvc(s.Config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := recreateContainers(s.index, c); err != nil {
|
||||
return fmt.Errorf("failed to recreate index containers: %w", err)
|
||||
}
|
||||
|
||||
if err := reindexDocuments(ctx, s.repo, s.index); err != nil {
|
||||
return fmt.Errorf("failed to reindex documents: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// recreateContainers adds all indices to the indexer that we have for this service.
|
||||
func recreateContainers(idx *indexer.Indexer, cfg *config.Config) error {
|
||||
// Accounts
|
||||
if err := idx.AddIndex(&accountsmsg.Account{}, "Id", "Id", "accounts", "non_unique", nil, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := idx.AddIndex(&accountsmsg.Account{}, "DisplayName", "Id", "accounts", "non_unique", nil, true); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := idx.AddIndex(&accountsmsg.Account{}, "Mail", "Id", "accounts", "unique", nil, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := idx.AddIndex(&accountsmsg.Account{}, "OnPremisesSamAccountName", "Id", "accounts", "unique", nil, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := idx.AddIndex(&accountsmsg.Account{}, "PreferredName", "Id", "accounts", "unique", nil, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := idx.AddIndex(&accountsmsg.Account{}, "UidNumber", "Id", "accounts", "autoincrement", &option.Bound{
|
||||
Lower: cfg.Index.UID.Lower,
|
||||
Upper: cfg.Index.UID.Upper,
|
||||
}, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Groups
|
||||
if err := idx.AddIndex(&accountsmsg.Group{}, "OnPremisesSamAccountName", "Id", "groups", "unique", nil, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := idx.AddIndex(&accountsmsg.Group{}, "DisplayName", "Id", "groups", "non_unique", nil, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := idx.AddIndex(&accountsmsg.Group{}, "GidNumber", "Id", "groups", "autoincrement", &option.Bound{
|
||||
Lower: cfg.Index.GID.Lower,
|
||||
Upper: cfg.Index.GID.Upper,
|
||||
}, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// reindexDocuments loads all existing documents and adds them to the index.
|
||||
func reindexDocuments(ctx context.Context, repo storage.Repo, index *indexer.Indexer) error {
|
||||
accounts := make([]*accountsmsg.Account, 0)
|
||||
if err := repo.LoadAccounts(ctx, &accounts); err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range accounts {
|
||||
_, err := index.Add(accounts[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
groups := make([]*accountsmsg.Group, 0)
|
||||
if err := repo.LoadGroups(ctx, &groups); err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range groups {
|
||||
_, err := index.Add(groups[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/config"
|
||||
"github.com/owncloud/ocis/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/ocis-pkg/roles"
|
||||
settingssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/settings/v0"
|
||||
)
|
||||
|
||||
// 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
|
||||
Config *config.Config
|
||||
RoleService settingssvc.RoleService
|
||||
RoleManager *roles.Manager
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// Config provides a function to set the Config option.
|
||||
func Config(val *config.Config) Option {
|
||||
return func(o *Options) {
|
||||
o.Config = val
|
||||
}
|
||||
}
|
||||
|
||||
// RoleService provides a function to set the RoleService option.
|
||||
func RoleService(val settingssvc.RoleService) Option {
|
||||
return func(o *Options) {
|
||||
o.RoleService = val
|
||||
}
|
||||
}
|
||||
|
||||
// RoleManager provides a function to set the RoleManager option.
|
||||
func RoleManager(val *roles.Manager) Option {
|
||||
return func(o *Options) {
|
||||
o.RoleManager = val
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/owncloud/ocis/ocis-pkg/service/grpc"
|
||||
|
||||
olog "github.com/owncloud/ocis/ocis-pkg/log"
|
||||
settingsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/settings/v0"
|
||||
settingssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/settings/v0"
|
||||
ssvc "github.com/owncloud/ocis/settings/pkg/service/v0"
|
||||
)
|
||||
|
||||
const (
|
||||
// AccountManagementPermissionID is the hardcoded setting UUID for the account management permission
|
||||
AccountManagementPermissionID string = "8e587774-d929-4215-910b-a317b1e80f73"
|
||||
// AccountManagementPermissionName is the hardcoded setting name for the account management permission
|
||||
AccountManagementPermissionName string = "account-management"
|
||||
// GroupManagementPermissionID is the hardcoded setting UUID for the group management permission
|
||||
GroupManagementPermissionID string = "522adfbe-5908-45b4-b135-41979de73245"
|
||||
// GroupManagementPermissionName is the hardcoded setting name for the group management permission
|
||||
GroupManagementPermissionName string = "group-management"
|
||||
// SelfManagementPermissionID is the hardcoded setting UUID for the self management permission
|
||||
SelfManagementPermissionID string = "e03070e9-4362-4cc6-a872-1c7cb2eb2b8e"
|
||||
// SelfManagementPermissionName is the hardcoded setting name for the self management permission
|
||||
SelfManagementPermissionName string = "self-management"
|
||||
)
|
||||
|
||||
// RegisterPermissions registers permissions for account management and group management with the settings service.
|
||||
func RegisterPermissions(l *olog.Logger) {
|
||||
service := settingssvc.NewBundleService("com.owncloud.api.settings", grpc.DefaultClient)
|
||||
|
||||
permissionRequests := generateAccountManagementPermissionsRequests()
|
||||
for i := range permissionRequests {
|
||||
res, err := service.AddSettingToBundle(context.Background(), &permissionRequests[i])
|
||||
bundleID := permissionRequests[i].BundleId
|
||||
if err != nil {
|
||||
l.Err(err).Str("bundle", bundleID).Str("setting", permissionRequests[i].Setting.Id).Msg("error adding permission to bundle")
|
||||
} else {
|
||||
l.Info().Str("bundle", bundleID).Str("setting", res.Setting.Id).Msg("successfully added permission to bundle")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func generateAccountManagementPermissionsRequests() []settingssvc.AddSettingToBundleRequest {
|
||||
return []settingssvc.AddSettingToBundleRequest{
|
||||
{
|
||||
BundleId: ssvc.BundleUUIDRoleAdmin,
|
||||
Setting: &settingsmsg.Setting{
|
||||
Id: AccountManagementPermissionID,
|
||||
Name: AccountManagementPermissionName,
|
||||
DisplayName: "Account Management",
|
||||
Description: "This permission gives full access to everything that is related to account management.",
|
||||
Resource: &settingsmsg.Resource{
|
||||
Type: settingsmsg.Resource_TYPE_USER,
|
||||
Id: "all",
|
||||
},
|
||||
Value: &settingsmsg.Setting_PermissionValue{
|
||||
PermissionValue: &settingsmsg.Permission{
|
||||
Operation: settingsmsg.Permission_OPERATION_READWRITE,
|
||||
Constraint: settingsmsg.Permission_CONSTRAINT_ALL,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
BundleId: ssvc.BundleUUIDRoleAdmin,
|
||||
Setting: &settingsmsg.Setting{
|
||||
Id: GroupManagementPermissionID,
|
||||
Name: GroupManagementPermissionName,
|
||||
DisplayName: "Group Management",
|
||||
Description: "This permission gives full access to everything that is related to group management.",
|
||||
Resource: &settingsmsg.Resource{
|
||||
Type: settingsmsg.Resource_TYPE_GROUP,
|
||||
Id: "all",
|
||||
},
|
||||
Value: &settingsmsg.Setting_PermissionValue{
|
||||
PermissionValue: &settingsmsg.Permission{
|
||||
Operation: settingsmsg.Permission_OPERATION_READWRITE,
|
||||
Constraint: settingsmsg.Permission_CONSTRAINT_ALL,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
BundleId: ssvc.BundleUUIDRoleUser,
|
||||
Setting: &settingsmsg.Setting{
|
||||
Id: SelfManagementPermissionID,
|
||||
Name: SelfManagementPermissionName,
|
||||
DisplayName: "Self Management",
|
||||
Description: "This permission gives access to self management.",
|
||||
Resource: &settingsmsg.Resource{
|
||||
Type: settingsmsg.Resource_TYPE_USER,
|
||||
Id: "me",
|
||||
},
|
||||
Value: &settingsmsg.Setting_PermissionValue{
|
||||
PermissionValue: &settingsmsg.Permission{
|
||||
Operation: settingsmsg.Permission_OPERATION_READWRITE,
|
||||
Constraint: settingsmsg.Permission_CONSTRAINT_OWN,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,508 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
accountsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/accounts/v0"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/owncloud/ocis/ocis-pkg/service/grpc"
|
||||
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/storage"
|
||||
"github.com/owncloud/ocis/ocis-pkg/indexer"
|
||||
idxcfg "github.com/owncloud/ocis/ocis-pkg/indexer/config"
|
||||
idxerrs "github.com/owncloud/ocis/ocis-pkg/indexer/errors"
|
||||
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/config"
|
||||
"github.com/owncloud/ocis/ocis-pkg/log"
|
||||
oreg "github.com/owncloud/ocis/ocis-pkg/registry"
|
||||
"github.com/owncloud/ocis/ocis-pkg/roles"
|
||||
settingssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/settings/v0"
|
||||
)
|
||||
|
||||
// userDefaultGID is the default integer representing the "users" group.
|
||||
const userDefaultGID = 30000
|
||||
|
||||
// New returns a new instance of Service
|
||||
func New(opts ...Option) (s *Service, err error) {
|
||||
options := newOptions(opts...)
|
||||
logger := options.Logger
|
||||
cfg := options.Config
|
||||
|
||||
roleService := options.RoleService
|
||||
if roleService == nil {
|
||||
roleService = settingssvc.NewRoleService("com.owncloud.api.settings", grpc.DefaultClient)
|
||||
}
|
||||
roleManager := options.RoleManager
|
||||
if roleManager == nil {
|
||||
m := roles.NewManager(
|
||||
roles.CacheSize(1024),
|
||||
roles.CacheTTL(time.Hour*24*7),
|
||||
roles.Logger(options.Logger),
|
||||
roles.RoleService(roleService),
|
||||
)
|
||||
roleManager = &m
|
||||
}
|
||||
|
||||
storage, err := createMetadataStorage(cfg, logger)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not create metadata storage")
|
||||
}
|
||||
|
||||
s = &Service{
|
||||
id: cfg.GRPC.Namespace + "." + cfg.Service.Name,
|
||||
log: logger,
|
||||
Config: cfg,
|
||||
RoleService: roleService,
|
||||
RoleManager: roleManager,
|
||||
repo: storage,
|
||||
}
|
||||
|
||||
r := oreg.GetRegistry()
|
||||
if cfg.Repo.Backend == "cs3" {
|
||||
if _, err := r.GetService("com.owncloud.storage.metadata"); err != nil {
|
||||
logger.Error().Err(err).Msg("index: storage-metadata service not present")
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// we want to wait anyway. If it depends on a reva service it could be the case that the entry on the registry
|
||||
// happens prior to the reva service being up and running
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
if s.index, err = s.buildIndex(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = s.createDefaultAccounts(cfg.DemoUsersAndGroups); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = s.createDefaultGroups(cfg.DemoUsersAndGroups); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.serviceUserToIndex()
|
||||
return
|
||||
}
|
||||
|
||||
// serviceUserToIndex temporarily adds a service user to the index, which is supposed to be removed before the lock on the handler function is released
|
||||
func (s Service) serviceUserToIndex() {
|
||||
if s.Config.ServiceUser.Username != "" && s.Config.ServiceUser.UUID != "" {
|
||||
_, err := s.index.Add(s.getInMemoryServiceUser())
|
||||
if err != nil {
|
||||
s.log.Logger.Err(err).Msg("service user was configured but failed to be added to the index")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s Service) getInMemoryServiceUser() accountsmsg.Account {
|
||||
return accountsmsg.Account{
|
||||
AccountEnabled: true,
|
||||
Id: s.Config.ServiceUser.UUID,
|
||||
PreferredName: s.Config.ServiceUser.Username,
|
||||
OnPremisesSamAccountName: s.Config.ServiceUser.Username,
|
||||
DisplayName: s.Config.ServiceUser.Username,
|
||||
UidNumber: s.Config.ServiceUser.UID,
|
||||
GidNumber: s.Config.ServiceUser.GID,
|
||||
}
|
||||
}
|
||||
|
||||
func (s Service) buildIndex() (*indexer.Indexer, error) {
|
||||
var indexcfg *idxcfg.Config
|
||||
|
||||
indexcfg, err := configFromSvc(s.Config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
idx := indexer.CreateIndexer(indexcfg)
|
||||
|
||||
if err := recreateContainers(idx, indexcfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return idx, nil
|
||||
}
|
||||
|
||||
// configFromSvc creates an index config out of a service configuration. This intermediate step exists
|
||||
// because the index config was mapped after the service config.
|
||||
func configFromSvc(cfg *config.Config) (*idxcfg.Config, error) {
|
||||
c := idxcfg.New()
|
||||
|
||||
if cfg.Log == nil {
|
||||
cfg.Log = &config.Log{}
|
||||
}
|
||||
|
||||
defer func(cfg *config.Config) {
|
||||
l := log.NewLogger(log.Color(cfg.Log.Color), log.Pretty(cfg.Log.Pretty), log.Level(cfg.Log.Level))
|
||||
if r := recover(); r != nil {
|
||||
l.Error().
|
||||
Str("panic", "recovered from panic while parsing index config from service configuration").
|
||||
Interface("svc_config", cfg).
|
||||
Msg("recovered from panic")
|
||||
}
|
||||
}(cfg)
|
||||
|
||||
switch cfg.Repo.Backend {
|
||||
case "disk":
|
||||
c.Repo = idxcfg.Repo{
|
||||
Backend: cfg.Repo.Backend,
|
||||
Disk: idxcfg.Disk{
|
||||
Path: cfg.Repo.Disk.Path,
|
||||
},
|
||||
}
|
||||
case "cs3":
|
||||
c.Repo = idxcfg.Repo{
|
||||
Backend: cfg.Repo.Backend,
|
||||
CS3: idxcfg.CS3{
|
||||
ProviderAddr: cfg.Repo.CS3.ProviderAddr,
|
||||
JWTSecret: cfg.TokenManager.JWTSecret,
|
||||
},
|
||||
}
|
||||
default:
|
||||
return nil, errors.New("index backend " + cfg.Repo.Backend + " is not supported")
|
||||
}
|
||||
|
||||
if (config.Index{}) != cfg.Index {
|
||||
c.Index = idxcfg.Index{
|
||||
UID: idxcfg.Bound{
|
||||
Lower: cfg.Index.UID.Lower,
|
||||
},
|
||||
GID: idxcfg.Bound{
|
||||
Lower: cfg.Index.GID.Lower,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (config.ServiceUser{}) != cfg.ServiceUser {
|
||||
c.ServiceUser = cfg.ServiceUser
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (s Service) createDefaultAccounts(withDemoAccounts bool) (err error) {
|
||||
accounts := []accountsmsg.Account{
|
||||
{
|
||||
Id: "4c510ada-c86b-4815-8820-42cdf82c3d51",
|
||||
PreferredName: "einstein",
|
||||
OnPremisesSamAccountName: "einstein",
|
||||
Mail: "einstein@example.org",
|
||||
DisplayName: "Albert Einstein",
|
||||
UidNumber: 20000,
|
||||
GidNumber: 30000,
|
||||
PasswordProfile: &accountsmsg.PasswordProfile{
|
||||
Password: "$2a$04$L.Rkpa0/nOhF3SsFo.QY9uzjMG8zB9a8dZP./LZBCDgsiuI8w10Em",
|
||||
},
|
||||
AccountEnabled: true,
|
||||
MemberOf: []*accountsmsg.Group{
|
||||
{Id: "509a9dcd-bb37-4f4f-a01a-19dca27d9cfa"}, // users
|
||||
{Id: "6040aa17-9c64-4fef-9bd0-77234d71bad0"}, // sailing-lovers
|
||||
{Id: "dd58e5ec-842e-498b-8800-61f2ec6f911f"}, // violin-haters
|
||||
{Id: "262982c1-2362-4afa-bfdf-8cbfef64a06e"}, // physics-lovers
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c",
|
||||
PreferredName: "marie",
|
||||
OnPremisesSamAccountName: "marie",
|
||||
Mail: "marie@example.org",
|
||||
DisplayName: "Marie Curie",
|
||||
UidNumber: 20001,
|
||||
GidNumber: 30000,
|
||||
PasswordProfile: &accountsmsg.PasswordProfile{
|
||||
Password: "$2a$04$AZd1k6OVpzP7E4hw5.ysFuuL2.XjjgakAuRs2zdBvIMizF0KaZkNG",
|
||||
},
|
||||
AccountEnabled: true,
|
||||
MemberOf: []*accountsmsg.Group{
|
||||
{Id: "509a9dcd-bb37-4f4f-a01a-19dca27d9cfa"}, // users
|
||||
{Id: "7b87fd49-286e-4a5f-bafd-c535d5dd997a"}, // radium-lovers
|
||||
{Id: "cedc21aa-4072-4614-8676-fa9165f598ff"}, // polonium-lovers
|
||||
{Id: "262982c1-2362-4afa-bfdf-8cbfef64a06e"}, // physics-lovers
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "932b4540-8d16-481e-8ef4-588e4b6b151c",
|
||||
PreferredName: "richard",
|
||||
OnPremisesSamAccountName: "richard",
|
||||
Mail: "richard@example.org",
|
||||
DisplayName: "Richard Feynman",
|
||||
UidNumber: 20002,
|
||||
GidNumber: 30000,
|
||||
PasswordProfile: &accountsmsg.PasswordProfile{
|
||||
Password: "$2a$04$aeVYaBH3LCTj9DviV6Y4xO2reoEzY9vnc7a5/0mhJWQUDtPqPINme",
|
||||
},
|
||||
AccountEnabled: true,
|
||||
MemberOf: []*accountsmsg.Group{
|
||||
{Id: "509a9dcd-bb37-4f4f-a01a-19dca27d9cfa"}, // users
|
||||
{Id: "a1726108-01f8-4c30-88df-2b1a9d1cba1a"}, // quantum-lovers
|
||||
{Id: "167cbee2-0518-455a-bfb2-031fe0621e5d"}, // philosophy-haters
|
||||
{Id: "262982c1-2362-4afa-bfdf-8cbfef64a06e"}, // physics-lovers
|
||||
},
|
||||
},
|
||||
// admin user(s)
|
||||
{
|
||||
Id: "058bff95-6708-4fe5-91e4-9ea3d377588b",
|
||||
PreferredName: "moss",
|
||||
OnPremisesSamAccountName: "moss",
|
||||
Mail: "moss@example.org",
|
||||
DisplayName: "Maurice Moss",
|
||||
UidNumber: 20003,
|
||||
GidNumber: 30000,
|
||||
PasswordProfile: &accountsmsg.PasswordProfile{
|
||||
Password: "$2a$04$la2yFV6N.pPySwHnLIxyAuBCJ2t/DxWfXJGnIooA9Ebb3.lSTKXby",
|
||||
},
|
||||
AccountEnabled: true,
|
||||
MemberOf: []*accountsmsg.Group{
|
||||
{Id: "509a9dcd-bb37-4f4f-a01a-19dca27d9cfa"}, // users
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "ddc2004c-0977-11eb-9d3f-a793888cd0f8",
|
||||
PreferredName: "admin",
|
||||
OnPremisesSamAccountName: "admin",
|
||||
Mail: "admin@example.org",
|
||||
DisplayName: "Admin",
|
||||
UidNumber: 20004,
|
||||
GidNumber: 30000,
|
||||
PasswordProfile: &accountsmsg.PasswordProfile{
|
||||
Password: "$2a$04$zqpfwdtBUDg89cpltxd.9ef7ZMzsor1BLCJyTEcdoitmEuS3Hr/Q6",
|
||||
},
|
||||
AccountEnabled: true,
|
||||
MemberOf: []*accountsmsg.Group{
|
||||
{Id: "509a9dcd-bb37-4f4f-a01a-19dca27d9cfa"}, // users
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "534bb038-6f9d-4093-946f-133be61fa4e7",
|
||||
PreferredName: "katherine",
|
||||
OnPremisesSamAccountName: "katherine",
|
||||
Mail: "katherine@example.org",
|
||||
DisplayName: "Katherine Johnson",
|
||||
UidNumber: 20005,
|
||||
GidNumber: 30000,
|
||||
PasswordProfile: &accountsmsg.PasswordProfile{
|
||||
Password: "$2a$04$j0//gOyZ3xg/WtMOk4XUaOMJ1r5niD3paPcFh1O/PNr8pL7yC8rhG",
|
||||
},
|
||||
AccountEnabled: true,
|
||||
MemberOf: []*accountsmsg.Group{
|
||||
{Id: "509a9dcd-bb37-4f4f-a01a-19dca27d9cfa"}, // users
|
||||
{Id: "6040aa17-9c64-4fef-9bd0-77234d71bad0"}, // sailing-lovers
|
||||
{Id: "a1726108-01f8-4c30-88df-2b1a9d1cba1a"}, // quantum-lovers
|
||||
{Id: "262982c1-2362-4afa-bfdf-8cbfef64a06e"}, // physics-lovers
|
||||
},
|
||||
},
|
||||
// technical users for kopano and reva
|
||||
{
|
||||
Id: "820ba2a1-3f54-4538-80a4-2d73007e30bf",
|
||||
PreferredName: "idp",
|
||||
OnPremisesSamAccountName: "idp",
|
||||
Mail: "idp@example.org",
|
||||
DisplayName: "Kopano IDP",
|
||||
UidNumber: 10000,
|
||||
GidNumber: 15000,
|
||||
PasswordProfile: &accountsmsg.PasswordProfile{
|
||||
Password: "$2a$04$TiuPj61Lkwt9hPOj4UUdwO.fupKBO3gpMv1EoXo0XF8Z8L9rFN8Nm",
|
||||
},
|
||||
AccountEnabled: true,
|
||||
MemberOf: []*accountsmsg.Group{
|
||||
{Id: "34f38767-c937-4eb6-b847-1c175829a2a0"}, // sysusers
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "bc596f3c-c955-4328-80a0-60d018b4ad57",
|
||||
PreferredName: "reva",
|
||||
OnPremisesSamAccountName: "reva",
|
||||
Mail: "storage@example.org",
|
||||
DisplayName: "Reva Inter Operability Platform",
|
||||
UidNumber: 10001,
|
||||
GidNumber: 15000,
|
||||
PasswordProfile: &accountsmsg.PasswordProfile{
|
||||
Password: "$2a$04$.cYhDMMXsvoCJzH9rX0eKev7fsLZwUv.VsRn66iaCXj2KlgpzHu3a",
|
||||
},
|
||||
AccountEnabled: true,
|
||||
MemberOf: []*accountsmsg.Group{
|
||||
{Id: "34f38767-c937-4eb6-b847-1c175829a2a0"}, // sysusers
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mustHaveAccounts := map[string]bool{
|
||||
"bc596f3c-c955-4328-80a0-60d018b4ad57": true, // Reva IOP
|
||||
"820ba2a1-3f54-4538-80a4-2d73007e30bf": true, // Kopano IDP
|
||||
"ddc2004c-0977-11eb-9d3f-a793888cd0f8": true, // admin
|
||||
}
|
||||
|
||||
// this only deals with the metadata service.
|
||||
for i := range accounts {
|
||||
if !withDemoAccounts && !mustHaveAccounts[accounts[i].Id] {
|
||||
continue
|
||||
}
|
||||
|
||||
a := &accountsmsg.Account{}
|
||||
err := s.repo.LoadAccount(context.Background(), accounts[i].Id, a)
|
||||
if !storage.IsNotFoundErr(err) {
|
||||
continue // account already exists -> do not overwrite
|
||||
}
|
||||
|
||||
if err := s.repo.WriteAccount(context.Background(), &accounts[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
results, err := s.index.Add(&accounts[i])
|
||||
if err != nil {
|
||||
if idxerrs.IsAlreadyExistsErr(err) {
|
||||
continue
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
changed := false
|
||||
for _, r := range results {
|
||||
if r.Field == "UidNumber" || r.Field == "GidNumber" {
|
||||
id, err := strconv.ParseInt(path.Base(r.Value), 10, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if r.Field == "UidNumber" {
|
||||
accounts[i].UidNumber = id
|
||||
} else {
|
||||
accounts[i].GidNumber = id
|
||||
}
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
if err := s.repo.WriteAccount(context.Background(), &accounts[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s Service) createDefaultGroups(withDemoGroups bool) (err error) {
|
||||
groups := []accountsmsg.Group{
|
||||
{Id: "34f38767-c937-4eb6-b847-1c175829a2a0", GidNumber: 15000, OnPremisesSamAccountName: "sysusers", DisplayName: "Technical users", Description: "A group for technical users. They should not show up in sharing dialogs.", Members: []*accountsmsg.Account{
|
||||
{Id: "820ba2a1-3f54-4538-80a4-2d73007e30bf"}, // idp
|
||||
{Id: "bc596f3c-c955-4328-80a0-60d018b4ad57"}, // reva
|
||||
}},
|
||||
{Id: "509a9dcd-bb37-4f4f-a01a-19dca27d9cfa", GidNumber: 30000, OnPremisesSamAccountName: "users", DisplayName: "Users", Description: "A group every normal user belongs to.", Members: []*accountsmsg.Account{
|
||||
{Id: "4c510ada-c86b-4815-8820-42cdf82c3d51"}, // einstein
|
||||
{Id: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c"}, // marie
|
||||
{Id: "932b4540-8d16-481e-8ef4-588e4b6b151c"}, // feynman
|
||||
{Id: "534bb038-6f9d-4093-946f-133be61fa4e7"}, // katherine
|
||||
}},
|
||||
{Id: "6040aa17-9c64-4fef-9bd0-77234d71bad0", GidNumber: 30001, OnPremisesSamAccountName: "sailing-lovers", DisplayName: "Sailing lovers", Members: []*accountsmsg.Account{
|
||||
{Id: "4c510ada-c86b-4815-8820-42cdf82c3d51"}, // einstein
|
||||
{Id: "534bb038-6f9d-4093-946f-133be61fa4e7"}, // katherine
|
||||
}},
|
||||
{Id: "dd58e5ec-842e-498b-8800-61f2ec6f911f", GidNumber: 30002, OnPremisesSamAccountName: "violin-haters", DisplayName: "Violin haters", Members: []*accountsmsg.Account{
|
||||
{Id: "4c510ada-c86b-4815-8820-42cdf82c3d51"}, // einstein
|
||||
}},
|
||||
{Id: "7b87fd49-286e-4a5f-bafd-c535d5dd997a", GidNumber: 30003, OnPremisesSamAccountName: "radium-lovers", DisplayName: "Radium lovers", Members: []*accountsmsg.Account{
|
||||
{Id: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c"}, // marie
|
||||
}},
|
||||
{Id: "cedc21aa-4072-4614-8676-fa9165f598ff", GidNumber: 30004, OnPremisesSamAccountName: "polonium-lovers", DisplayName: "Polonium lovers", Members: []*accountsmsg.Account{
|
||||
{Id: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c"}, // marie
|
||||
}},
|
||||
{Id: "a1726108-01f8-4c30-88df-2b1a9d1cba1a", GidNumber: 30005, OnPremisesSamAccountName: "quantum-lovers", DisplayName: "Quantum lovers", Members: []*accountsmsg.Account{
|
||||
{Id: "932b4540-8d16-481e-8ef4-588e4b6b151c"}, // feynman
|
||||
{Id: "534bb038-6f9d-4093-946f-133be61fa4e7"}, // katherine
|
||||
}},
|
||||
{Id: "167cbee2-0518-455a-bfb2-031fe0621e5d", GidNumber: 30006, OnPremisesSamAccountName: "philosophy-haters", DisplayName: "Philosophy haters", Members: []*accountsmsg.Account{
|
||||
{Id: "932b4540-8d16-481e-8ef4-588e4b6b151c"}, // feynman
|
||||
}},
|
||||
{Id: "262982c1-2362-4afa-bfdf-8cbfef64a06e", GidNumber: 30007, OnPremisesSamAccountName: "physics-lovers", DisplayName: "Physics lovers", Members: []*accountsmsg.Account{
|
||||
{Id: "4c510ada-c86b-4815-8820-42cdf82c3d51"}, // einstein
|
||||
{Id: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c"}, // marie
|
||||
{Id: "932b4540-8d16-481e-8ef4-588e4b6b151c"}, // feynman
|
||||
{Id: "534bb038-6f9d-4093-946f-133be61fa4e7"}, // katherine
|
||||
}},
|
||||
}
|
||||
|
||||
mustHaveGroups := map[string]bool{
|
||||
"34f38767-c937-4eb6-b847-1c175829a2a0": true, // sysusers
|
||||
"509a9dcd-bb37-4f4f-a01a-19dca27d9cfa": true, // users
|
||||
}
|
||||
|
||||
for i := range groups {
|
||||
if !withDemoGroups && !mustHaveGroups[groups[i].Id] {
|
||||
continue
|
||||
}
|
||||
|
||||
g := &accountsmsg.Group{}
|
||||
err := s.repo.LoadGroup(context.Background(), groups[i].Id, g)
|
||||
if !storage.IsNotFoundErr(err) {
|
||||
continue // group already exists -> do not overwrite
|
||||
}
|
||||
|
||||
if err := s.repo.WriteGroup(context.Background(), &groups[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
results, err := s.index.Add(&groups[i])
|
||||
if err != nil {
|
||||
if idxerrs.IsAlreadyExistsErr(err) {
|
||||
continue
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: can be removed again as soon as we respect the predefined GIDs from the group. Then no autoincrement is happening, therefore we don't need to update groups.
|
||||
for _, r := range results {
|
||||
if r.Field == "GidNumber" {
|
||||
gid, err := strconv.ParseInt(path.Base(r.Value), 10, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
groups[i].GidNumber = gid
|
||||
if err := s.repo.WriteGroup(context.Background(), &groups[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func createMetadataStorage(cfg *config.Config, logger log.Logger) (storage.Repo, error) {
|
||||
switch cfg.Repo.Backend {
|
||||
case "disk":
|
||||
return storage.NewDiskRepo(cfg, logger), nil
|
||||
case "cs3":
|
||||
repo, err := storage.NewCS3Repo(cfg)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cs3 backend was configured but failed to start")
|
||||
}
|
||||
return repo, nil
|
||||
default:
|
||||
return nil, errors.New("backend type " + cfg.Repo.Backend + " is not supported")
|
||||
}
|
||||
}
|
||||
|
||||
// Service implements the AccountsServiceHandler interface
|
||||
type Service struct {
|
||||
id string
|
||||
log log.Logger
|
||||
Config *config.Config
|
||||
index *indexer.Indexer
|
||||
RoleService settingssvc.RoleService
|
||||
RoleManager *roles.Manager
|
||||
repo storage.Repo
|
||||
}
|
||||
|
||||
func cleanupID(id string) (string, error) {
|
||||
id = filepath.Clean(id)
|
||||
if id == "." || strings.Contains(id, "/") {
|
||||
return "", errors.New("invalid id " + id)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
checks = ["all", "-ST1003", "-ST1000", "-SA1019"]
|
||||
@@ -0,0 +1,331 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
v1beta11 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/cs3org/reva/v2/pkg/auth/scope"
|
||||
revactx "github.com/cs3org/reva/v2/pkg/ctx"
|
||||
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/cs3org/reva/v2/pkg/token"
|
||||
"github.com/cs3org/reva/v2/pkg/token/manager/jwt"
|
||||
"github.com/cs3org/reva/v2/pkg/utils"
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/config"
|
||||
olog "github.com/owncloud/ocis/ocis-pkg/log"
|
||||
metadatastorage "github.com/owncloud/ocis/ocis-pkg/metadata_storage"
|
||||
accountsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/accounts/v0"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
// CS3Repo provides a cs3 implementation of the Repo interface
|
||||
type CS3Repo struct {
|
||||
cfg *config.Config
|
||||
tm token.Manager
|
||||
storageProvider provider.ProviderAPIClient
|
||||
metadataStorage *metadatastorage.MetadataStorage
|
||||
}
|
||||
|
||||
// NewCS3Repo creates a new cs3 repo
|
||||
func NewCS3Repo(cfg *config.Config) (Repo, error) {
|
||||
tokenManager, err := jwt.New(map[string]interface{}{
|
||||
"secret": cfg.TokenManager.JWTSecret,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := pool.GetStorageProviderServiceClient(cfg.Repo.CS3.ProviderAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ms, err := metadatastorage.NewMetadataStorage(cfg.Repo.CS3.ProviderAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r := CS3Repo{
|
||||
cfg: cfg,
|
||||
tm: tokenManager,
|
||||
storageProvider: client,
|
||||
metadataStorage: &ms,
|
||||
}
|
||||
|
||||
ctx, err := r.getAuthenticatedContext(context.Background())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ms.Init(ctx, cfg.ServiceUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// WriteAccount writes an account via cs3 and modifies the provided account (e.g. with a generated id).
|
||||
func (r CS3Repo) WriteAccount(ctx context.Context, a *accountsmsg.Account) (err error) {
|
||||
ctx, err = r.getAuthenticatedContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := r.makeRootDirIfNotExist(ctx, accountsFolder); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var by []byte
|
||||
if by, err = json.Marshal(a); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = r.metadataStorage.SimpleUpload(ctx, r.accountURL(a.Id), by)
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
// LoadAccount loads an account via cs3 by id and writes it to the provided account
|
||||
func (r CS3Repo) LoadAccount(ctx context.Context, id string, a *accountsmsg.Account) (err error) {
|
||||
ctx, err = r.getAuthenticatedContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return r.loadAccount(ctx, id, a)
|
||||
}
|
||||
|
||||
// LoadAccounts loads all the accounts from the cs3 api
|
||||
func (r CS3Repo) LoadAccounts(ctx context.Context, a *[]*accountsmsg.Account) (err error) {
|
||||
ctx, err = r.getAuthenticatedContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := r.storageProvider.ListContainer(ctx, &provider.ListContainerRequest{
|
||||
Ref: &provider.Reference{
|
||||
ResourceId: r.metadataStorage.SpaceRoot,
|
||||
Path: utils.MakeRelativePath(accountsFolder),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log := olog.NewLogger(olog.Pretty(r.cfg.Log.Pretty), olog.Color(r.cfg.Log.Color), olog.Level(r.cfg.Log.Level))
|
||||
for i := range res.Infos {
|
||||
acc := &accountsmsg.Account{}
|
||||
err := r.loadAccount(ctx, filepath.Base(res.Infos[i].Path), acc)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("could not load account")
|
||||
continue
|
||||
}
|
||||
*a = append(*a, acc)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r CS3Repo) loadAccount(ctx context.Context, id string, a *accountsmsg.Account) error {
|
||||
account, err := r.metadataStorage.SimpleDownload(ctx, r.accountURL(id))
|
||||
if err != nil {
|
||||
if metadatastorage.IsNotFoundErr(err) {
|
||||
return ¬FoundErr{"account", id}
|
||||
}
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(account, &a)
|
||||
}
|
||||
|
||||
// DeleteAccount deletes an account via cs3 by id
|
||||
func (r CS3Repo) DeleteAccount(ctx context.Context, id string) (err error) {
|
||||
ctx, err = r.getAuthenticatedContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := r.storageProvider.Delete(ctx, &provider.DeleteRequest{
|
||||
Ref: &provider.Reference{
|
||||
ResourceId: r.metadataStorage.SpaceRoot,
|
||||
Path: utils.MakeRelativePath(filepath.Join("/", accountsFolder, id)),
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO Handle other error codes?
|
||||
if resp.Status.Code == v1beta11.Code_CODE_NOT_FOUND {
|
||||
return ¬FoundErr{"account", id}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteGroup writes a group via cs3 and modifies the provided group (e.g. with a generated id).
|
||||
func (r CS3Repo) WriteGroup(ctx context.Context, g *accountsmsg.Group) (err error) {
|
||||
ctx, err = r.getAuthenticatedContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := r.makeRootDirIfNotExist(ctx, groupsFolder); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var by []byte
|
||||
if by, err = json.Marshal(g); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = r.metadataStorage.SimpleUpload(ctx, r.groupURL(g.Id), by)
|
||||
return err
|
||||
}
|
||||
|
||||
// LoadGroup loads a group via cs3 by id and writes it to the provided group
|
||||
func (r CS3Repo) LoadGroup(ctx context.Context, id string, g *accountsmsg.Group) (err error) {
|
||||
ctx, err = r.getAuthenticatedContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return r.loadGroup(ctx, id, g)
|
||||
}
|
||||
|
||||
// LoadGroups loads all the groups from the cs3 api
|
||||
func (r CS3Repo) LoadGroups(ctx context.Context, g *[]*accountsmsg.Group) (err error) {
|
||||
ctx, err = r.getAuthenticatedContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := r.storageProvider.ListContainer(ctx, &provider.ListContainerRequest{
|
||||
Ref: &provider.Reference{
|
||||
ResourceId: r.metadataStorage.SpaceRoot,
|
||||
Path: utils.MakeRelativePath(groupsFolder),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log := olog.NewLogger(olog.Pretty(r.cfg.Log.Pretty), olog.Color(r.cfg.Log.Color), olog.Level(r.cfg.Log.Level))
|
||||
for i := range res.Infos {
|
||||
grp := &accountsmsg.Group{}
|
||||
err := r.loadGroup(ctx, filepath.Base(res.Infos[i].Path), grp)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("could not load account")
|
||||
continue
|
||||
}
|
||||
*g = append(*g, grp)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r CS3Repo) loadGroup(ctx context.Context, id string, g *accountsmsg.Group) error {
|
||||
group, err := r.metadataStorage.SimpleDownload(ctx, r.groupURL(id))
|
||||
if err != nil {
|
||||
if metadatastorage.IsNotFoundErr(err) {
|
||||
return ¬FoundErr{"group", id}
|
||||
}
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(group, &g)
|
||||
}
|
||||
|
||||
// DeleteGroup deletes a group via cs3 by id
|
||||
func (r CS3Repo) DeleteGroup(ctx context.Context, id string) (err error) {
|
||||
ctx, err = r.getAuthenticatedContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := r.storageProvider.Delete(ctx, &provider.DeleteRequest{
|
||||
Ref: &provider.Reference{
|
||||
ResourceId: r.metadataStorage.SpaceRoot,
|
||||
Path: utils.MakeRelativePath(filepath.Join(groupsFolder, id)),
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO Handle other error codes?
|
||||
if resp.Status.Code == v1beta11.Code_CODE_NOT_FOUND {
|
||||
return ¬FoundErr{"group", id}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (r CS3Repo) getAuthenticatedContext(ctx context.Context) (context.Context, error) {
|
||||
t, err := AuthenticateCS3(ctx, r.cfg.ServiceUser, r.tm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, revactx.TokenHeader, t)
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
// AuthenticateCS3 mints an auth token for communicating with cs3 storage based on a service user from config
|
||||
func AuthenticateCS3(ctx context.Context, su config.ServiceUser, tm token.Manager) (token string, err error) {
|
||||
u := &user.User{
|
||||
Id: &user.UserId{
|
||||
OpaqueId: su.UUID,
|
||||
Type: user.UserType_USER_TYPE_APPLICATION,
|
||||
},
|
||||
Groups: []string{},
|
||||
UidNumber: su.UID,
|
||||
GidNumber: su.GID,
|
||||
}
|
||||
s, err := scope.AddOwnerScope(nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return tm.MintToken(ctx, u, s)
|
||||
}
|
||||
|
||||
func (r CS3Repo) accountURL(id string) string {
|
||||
return path.Join(accountsFolder, id)
|
||||
}
|
||||
|
||||
func (r CS3Repo) groupURL(id string) string {
|
||||
return path.Join(groupsFolder, id)
|
||||
}
|
||||
|
||||
func (r CS3Repo) makeRootDirIfNotExist(ctx context.Context, folder string) error {
|
||||
return MakeDirIfNotExist(ctx, r.storageProvider, r.metadataStorage.SpaceRoot, folder)
|
||||
}
|
||||
|
||||
// MakeDirIfNotExist will create a root node in the metadata storage. Requires an authenticated context.
|
||||
func MakeDirIfNotExist(ctx context.Context, sp provider.ProviderAPIClient, root *provider.ResourceId, folder string) error {
|
||||
var rootPathRef = &provider.Reference{
|
||||
ResourceId: root,
|
||||
Path: utils.MakeRelativePath(folder),
|
||||
}
|
||||
|
||||
resp, err := sp.Stat(ctx, &provider.StatRequest{
|
||||
Ref: rootPathRef,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.Status.Code == v1beta11.Code_CODE_NOT_FOUND {
|
||||
_, err := sp.CreateContainer(ctx, &provider.CreateContainerRequest{
|
||||
Ref: rootPathRef,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package storage
|
||||
|
||||
// Uncomment to test locally, requires started metadata-storage for now
|
||||
|
||||
//import (
|
||||
// "context"
|
||||
// accountsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/accounts/v0"
|
||||
// "github.com/owncloud/ocis/accounts/pkg/config"
|
||||
// "github.com/stretchr/testify/assert"
|
||||
// "testing"
|
||||
//)
|
||||
//
|
||||
//var cfg = &config.Config{
|
||||
// TokenManager: config.TokenManager{
|
||||
// JWTSecret: "Pive-Fumkiu4",
|
||||
// },
|
||||
// Repo: config.Repo{
|
||||
// CS3: config.CS3{
|
||||
// ProviderAddr: "0.0.0.0:9215",
|
||||
// },
|
||||
// },
|
||||
//}
|
||||
//
|
||||
//func TestCS3Repo_WriteAccount(t *testing.T) {
|
||||
// r, err := NewCS3Repo("hello", cfg)
|
||||
// assert.NoError(t, err)
|
||||
//
|
||||
// err = r.WriteAccount(context.Background(), &accountsmsg.Account{
|
||||
// Id: "fefef-egegweg-gegeg",
|
||||
// AccountEnabled: true,
|
||||
// DisplayName: "Mike Jones",
|
||||
// Mail: "mike@example.com",
|
||||
// })
|
||||
//
|
||||
// assert.NoError(t, err)
|
||||
//}
|
||||
//
|
||||
//func TestCS3Repo_LoadAccount(t *testing.T) {
|
||||
// r, err := NewCS3Repo("hello", cfg)
|
||||
// assert.NoError(t, err)
|
||||
//
|
||||
// err = r.WriteAccount(context.Background(), &accountsmsg.Account{
|
||||
// Id: "fefef-egegweg-gegeg",
|
||||
// AccountEnabled: true,
|
||||
// DisplayName: "Mike Jones",
|
||||
// Mail: "mike@example.com",
|
||||
// })
|
||||
//
|
||||
// acc := &accountsmsg.Account{}
|
||||
// err = r.LoadAccount(context.Background(), "fefef-egegweg-gegeg", acc)
|
||||
//
|
||||
// assert.NoError(t, err)
|
||||
// assert.Equal(t, "fefef-egegweg-gegeg", acc.Id)
|
||||
// assert.Equal(t, "Mike Jones", acc.DisplayName)
|
||||
// assert.Equal(t, "mike@example.com", acc.Mail)
|
||||
//}
|
||||
//
|
||||
//func TestCS3Repo_DeleteAccount(t *testing.T) {
|
||||
// r, err := NewCS3Repo("hello", cfg)
|
||||
// assert.NoError(t, err)
|
||||
//
|
||||
// err = r.WriteAccount(context.Background(), &accountsmsg.Account{
|
||||
// Id: "delete-me-id",
|
||||
// AccountEnabled: true,
|
||||
// DisplayName: "Mike Jones",
|
||||
// Mail: "mike@example.com",
|
||||
// })
|
||||
//
|
||||
// err = r.DeleteAccount(context.Background(), "delete-me-id")
|
||||
//
|
||||
// assert.NoError(t, err)
|
||||
//}
|
||||
@@ -0,0 +1,202 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
accountsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/accounts/v0"
|
||||
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/config"
|
||||
olog "github.com/owncloud/ocis/ocis-pkg/log"
|
||||
)
|
||||
|
||||
var groupLock sync.Mutex
|
||||
|
||||
// DiskRepo provides a local filesystem implementation of the Repo interface
|
||||
type DiskRepo struct {
|
||||
cfg *config.Config
|
||||
log olog.Logger
|
||||
}
|
||||
|
||||
// NewDiskRepo creates a new disk repo
|
||||
func NewDiskRepo(cfg *config.Config, log olog.Logger) DiskRepo {
|
||||
paths := []string{
|
||||
filepath.Join(cfg.Repo.Disk.Path, accountsFolder),
|
||||
filepath.Join(cfg.Repo.Disk.Path, groupsFolder),
|
||||
}
|
||||
for i := range paths {
|
||||
if _, err := os.Stat(paths[i]); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if err = os.MkdirAll(paths[i], 0700); err != nil {
|
||||
log.Fatal().Err(err).Msgf("could not create data folder %v", paths[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return DiskRepo{
|
||||
cfg: cfg,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
// WriteAccount to the local filesystem
|
||||
func (r DiskRepo) WriteAccount(ctx context.Context, a *accountsmsg.Account) (err error) {
|
||||
// leave only the group id
|
||||
r.deflateMemberOf(a)
|
||||
|
||||
var bytes []byte
|
||||
if bytes, err = json.Marshal(a); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
path := filepath.Join(r.cfg.Repo.Disk.Path, accountsFolder, a.Id)
|
||||
return ioutil.WriteFile(path, bytes, 0600)
|
||||
}
|
||||
|
||||
// LoadAccount from the local filesystem
|
||||
func (r DiskRepo) LoadAccount(ctx context.Context, id string, a *accountsmsg.Account) (err error) {
|
||||
path := filepath.Join(r.cfg.Repo.Disk.Path, accountsFolder, id)
|
||||
var data []byte
|
||||
if data, err = ioutil.ReadFile(path); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
err = ¬FoundErr{"account", id}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
return json.Unmarshal(data, a)
|
||||
}
|
||||
|
||||
// LoadAccounts loads all the accounts from the local filesystem
|
||||
func (r DiskRepo) LoadAccounts(ctx context.Context, a *[]*accountsmsg.Account) (err error) {
|
||||
root := filepath.Join(r.cfg.Repo.Disk.Path, accountsFolder)
|
||||
infos, err := ioutil.ReadDir(root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range infos {
|
||||
acc := &accountsmsg.Account{}
|
||||
if e := r.LoadAccount(ctx, infos[i].Name(), acc); e != nil {
|
||||
r.log.Err(e).Msg("could not load account")
|
||||
continue
|
||||
}
|
||||
*a = append(*a, acc)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteAccount from the local filesystem
|
||||
func (r DiskRepo) DeleteAccount(ctx context.Context, id string) (err error) {
|
||||
path := filepath.Join(r.cfg.Repo.Disk.Path, accountsFolder, id)
|
||||
if err = os.Remove(path); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
err = ¬FoundErr{"account", id}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// WriteGroup to the local filesystem
|
||||
func (r DiskRepo) WriteGroup(ctx context.Context, g *accountsmsg.Group) (err error) {
|
||||
// leave only the member id
|
||||
r.deflateMembers(g)
|
||||
|
||||
var bytes []byte
|
||||
if bytes, err = json.Marshal(g); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
path := filepath.Join(r.cfg.Repo.Disk.Path, groupsFolder, g.Id)
|
||||
|
||||
groupLock.Lock()
|
||||
defer groupLock.Unlock()
|
||||
|
||||
return ioutil.WriteFile(path, bytes, 0600)
|
||||
}
|
||||
|
||||
// LoadGroup from the local filesystem
|
||||
func (r DiskRepo) LoadGroup(ctx context.Context, id string, g *accountsmsg.Group) (err error) {
|
||||
path := filepath.Join(r.cfg.Repo.Disk.Path, groupsFolder, id)
|
||||
|
||||
groupLock.Lock()
|
||||
defer groupLock.Unlock()
|
||||
var data []byte
|
||||
if data, err = ioutil.ReadFile(path); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
err = ¬FoundErr{"group", id}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
return json.Unmarshal(data, g)
|
||||
}
|
||||
|
||||
// LoadGroups loads all the groups from the local filesystem
|
||||
func (r DiskRepo) LoadGroups(ctx context.Context, g *[]*accountsmsg.Group) (err error) {
|
||||
root := filepath.Join(r.cfg.Repo.Disk.Path, groupsFolder)
|
||||
infos, err := ioutil.ReadDir(root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range infos {
|
||||
grp := &accountsmsg.Group{}
|
||||
if e := r.LoadGroup(ctx, infos[i].Name(), grp); e != nil {
|
||||
r.log.Err(e).Msg("could not load group")
|
||||
continue
|
||||
}
|
||||
*g = append(*g, grp)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteGroup from the local filesystem
|
||||
func (r DiskRepo) DeleteGroup(ctx context.Context, id string) (err error) {
|
||||
path := filepath.Join(r.cfg.Repo.Disk.Path, groupsFolder, id)
|
||||
if err = os.Remove(path); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
err = ¬FoundErr{"account", id}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// deflateMemberOf replaces the groups of a user with an instance that only contains the id
|
||||
func (r DiskRepo) deflateMemberOf(a *accountsmsg.Account) {
|
||||
if a == nil {
|
||||
return
|
||||
}
|
||||
var deflated []*accountsmsg.Group
|
||||
for i := range a.MemberOf {
|
||||
if a.MemberOf[i].Id != "" {
|
||||
deflated = append(deflated, &accountsmsg.Group{Id: a.MemberOf[i].Id})
|
||||
} else {
|
||||
// TODO fetch and use an id when group only has a name but no id
|
||||
r.log.Error().Str("id", a.Id).Interface("group", a.MemberOf[i]).Msg("resolving groups by name is not implemented yet")
|
||||
}
|
||||
}
|
||||
a.MemberOf = deflated
|
||||
}
|
||||
|
||||
// deflateMembers replaces the users of a group with an instance that only contains the id
|
||||
func (r DiskRepo) deflateMembers(g *accountsmsg.Group) {
|
||||
if g == nil {
|
||||
return
|
||||
}
|
||||
var deflated []*accountsmsg.Account
|
||||
for i := range g.Members {
|
||||
if g.Members[i].Id != "" {
|
||||
deflated = append(deflated, &accountsmsg.Account{Id: g.Members[i].Id})
|
||||
} else {
|
||||
// TODO fetch and use an id when group only has a name but no id
|
||||
r.log.Error().Str("id", g.Id).Interface("account", g.Members[i]).Msg("resolving members by name is not implemented yet")
|
||||
}
|
||||
}
|
||||
g.Members = deflated
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type notFoundErr struct {
|
||||
typ, id string
|
||||
}
|
||||
|
||||
func (e notFoundErr) Error() string {
|
||||
return fmt.Sprintf("%s with id %s not found", e.typ, e.id)
|
||||
}
|
||||
|
||||
// IsNotFoundErr can be returned by repo Load and Delete operations
|
||||
func IsNotFoundErr(e error) bool {
|
||||
_, ok := e.(*notFoundErr)
|
||||
return ok
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
accountsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/accounts/v0"
|
||||
)
|
||||
|
||||
const (
|
||||
accountsFolder = "accounts"
|
||||
groupsFolder = "groups"
|
||||
)
|
||||
|
||||
// Repo defines the storage operations
|
||||
type Repo interface {
|
||||
WriteAccount(ctx context.Context, a *accountsmsg.Account) (err error)
|
||||
LoadAccount(ctx context.Context, id string, a *accountsmsg.Account) (err error)
|
||||
LoadAccounts(ctx context.Context, a *[]*accountsmsg.Account) (err error)
|
||||
DeleteAccount(ctx context.Context, id string) (err error)
|
||||
WriteGroup(ctx context.Context, g *accountsmsg.Group) (err error)
|
||||
LoadGroup(ctx context.Context, id string, g *accountsmsg.Group) (err error)
|
||||
LoadGroups(ctx context.Context, g *[]*accountsmsg.Group) (err error)
|
||||
DeleteGroup(ctx context.Context, id string) (err error)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package tracing
|
||||
|
||||
import (
|
||||
"github.com/owncloud/ocis/extensions/accounts/pkg/config"
|
||||
pkgtrace "github.com/owncloud/ocis/ocis-pkg/tracing"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
var (
|
||||
// TraceProvider is the global trace provider for the proxy service.
|
||||
TraceProvider = trace.NewNoopTracerProvider()
|
||||
)
|
||||
|
||||
func Configure(cfg *config.Config) error {
|
||||
var err error
|
||||
if cfg.Tracing.Enabled {
|
||||
if TraceProvider, err = pkgtrace.GetTraceProvider(cfg.Tracing.Endpoint, cfg.Tracing.Collector, cfg.Service.Name, cfg.Tracing.Type); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user