refactor accounts

Signed-off-by: Christian Richter <crichter@owncloud.com>
This commit is contained in:
Christian Richter
2022-04-13 17:04:35 +02:00
parent 1c5d02da20
commit 2089ac5f7b
98 changed files with 95 additions and 11584 deletions
+50
View File
@@ -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
}}
}
+53
View File
@@ -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
}}
}
+70
View File
@@ -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
}
+104
View File
@@ -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
},
}
}
+85
View File
@@ -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."`
}
+9
View File
@@ -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)
}
+7
View File
@@ -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:"-"`
}
+18
View File
@@ -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"`
}
+9
View File
@@ -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"`
}
+241
View File
@@ -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 := &timestamppb.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
}
+110
View File
@@ -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"]
+331
View File
@@ -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 &notFoundErr{"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 &notFoundErr{"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 &notFoundErr{"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 &notFoundErr{"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)
//}
+202
View File
@@ -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 = &notFoundErr{"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 = &notFoundErr{"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 = &notFoundErr{"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 = &notFoundErr{"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
}
+19
View File
@@ -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
}
+24
View File
@@ -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
}