diff --git a/changelog/unreleased/enhancment-admin-pwreset.md b/changelog/unreleased/enhancment-admin-pwreset.md new file mode 100644 index 000000000..87931228b --- /dev/null +++ b/changelog/unreleased/enhancment-admin-pwreset.md @@ -0,0 +1,8 @@ +Enhancement: added command to reset administrator password + +The new command `ocis idm resetpassword` allows to reset the administrator +password when ocis is not running. So it is possible to recover setups +where the admin password was lost. + +https://github.com/owncloud/ocis/issues/4084 +https://github.com/owncloud/ocis/pull/4365 diff --git a/docs/services/idm/admin_password_reset.md b/docs/services/idm/admin_password_reset.md new file mode 100644 index 000000000..1e3065c37 --- /dev/null +++ b/docs/services/idm/admin_password_reset.md @@ -0,0 +1,24 @@ +--- +title: Resetting a lost administrator password +date: 2022-08-29:00:00+00:00 +weight: 10 +geekdocRepo: https://github.com/owncloud/ocis +geekdocEditPath: edit/master/docs/services/idm +geekdocFilePath: admin_password_reset.md +geekdocCollapseSection: true +--- + +## Resetting a lost administrator password +By default, when using oCIS with the builtin IDM an ad generates the +user `admin` (DN `uid=admin,ou=users,o=libregraph-idm`) if, for any +reason, the password of that user is lost, it can be reset using +the `resetpassword` sub-command: + +``` +ocis idm resetpassword +``` + +It will prompt for a new password and set the password of that user +accordingly. Note: As this command is accessing the idm database directly +will only work while ocis is not running and nothing else is accessing +database. diff --git a/go.mod b/go.mod index 4c8a52b58..64a5dbfa0 100644 --- a/go.mod +++ b/go.mod @@ -63,6 +63,7 @@ require ( github.com/urfave/cli/v2 v2.11.1 github.com/xhit/go-simple-mail/v2 v2.11.0 go-micro.dev/v4 v4.8.0 + go.etcd.io/bbolt v1.3.6 go.opencensus.io v0.23.0 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.34.0 go.opentelemetry.io/otel v1.9.0 @@ -74,6 +75,7 @@ require ( golang.org/x/image v0.0.0-20220321031419-a8550c1d254a golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e golang.org/x/oauth2 v0.0.0-20220722155238-128564f6959c + golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 google.golang.org/genproto v0.0.0-20220805133916-01dd62135a58 google.golang.org/grpc v1.48.0 google.golang.org/protobuf v1.28.1 @@ -254,7 +256,6 @@ require ( github.com/wk8/go-ordered-map v1.0.0 // indirect github.com/xanzy/ssh-agent v0.3.1 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect - go.etcd.io/bbolt v1.3.6 // indirect go.etcd.io/etcd/api/v3 v3.5.2 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.2 // indirect go.etcd.io/etcd/client/v3 v3.5.2 // indirect diff --git a/go.sum b/go.sum index 67022a4c8..fe2f21686 100644 --- a/go.sum +++ b/go.sum @@ -1568,6 +1568,7 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc= +golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/services/idm/pkg/command/resetpw.go b/services/idm/pkg/command/resetpw.go new file mode 100644 index 000000000..7fd5c52eb --- /dev/null +++ b/services/idm/pkg/command/resetpw.go @@ -0,0 +1,118 @@ +package command + +import ( + "context" + "errors" + "fmt" + "os" + "syscall" + "time" + + "github.com/go-ldap/ldap/v3" + "github.com/libregraph/idm/pkg/ldbbolt" + "github.com/libregraph/idm/server" + "github.com/owncloud/ocis/v2/ocis-pkg/log" + "github.com/owncloud/ocis/v2/services/idm/pkg/config" + "github.com/owncloud/ocis/v2/services/idm/pkg/config/parser" + "github.com/owncloud/ocis/v2/services/idm/pkg/logging" + "github.com/urfave/cli/v2" + bolt "go.etcd.io/bbolt" + "golang.org/x/term" +) + +// ResetPassword is the entrypoint for the resetpassword command +func ResetPassword(cfg *config.Config) *cli.Command { + return &cli.Command{ + Name: "resetpassword", + Usage: fmt.Sprintf("Reset admin password"), + Category: "password reset", + Before: func(c *cli.Context) error { + err := parser.ParseConfig(cfg) + if err != nil { + fmt.Printf("%v", err) + os.Exit(1) + } + return err + }, + Action: func(c *cli.Context) error { + logger := logging.Configure(cfg.Service.Name, cfg.Log) + ctx, cancel := func() (context.Context, context.CancelFunc) { + if cfg.Context == nil { + return context.WithCancel(context.Background()) + } + return context.WithCancel(cfg.Context) + }() + + defer cancel() + return resetPassword(ctx, logger, cfg) + }, + } +} + +func resetPassword(ctx context.Context, logger log.Logger, cfg *config.Config) error { + servercfg := server.Config{ + Logger: log.LogrusWrap(logger.Logger), + LDAPHandler: "boltdb", + LDAPBaseDN: "o=libregraph-idm", + + BoltDBFile: cfg.IDM.DatabasePath, + } + + adminUserDN := "uid=admin,ou=users," + servercfg.LDAPBaseDN + fmt.Printf("Resetting password for user '%s'.\n", adminUserDN) + if _, err := os.Stat(servercfg.BoltDBFile); errors.Is(err, os.ErrNotExist) { + fmt.Fprintf(os.Stderr, "IDM database does not exist.\n") + return err + } + + newPw, err := getPassword() + if err != nil { + fmt.Fprintf(os.Stderr, "Error reading password: %v\n", err) + return err + } + + bdb := &ldbbolt.LdbBolt{} + + opts := bolt.Options{ + Timeout: 1 * time.Millisecond, + } + if err := bdb.Configure(servercfg.Logger, servercfg.LDAPBaseDN, servercfg.BoltDBFile, &opts); err != nil { + fmt.Fprintf(os.Stderr, "Failed to open database: '%s'. Please stop any running ocis/idm instance, as this tool requires exclusive access to the database.\n", err) + return err + } + defer bdb.Close() + + if err := bdb.Initialize(); err != nil { + return err + } + + pwRequest := ldap.NewPasswordModifyRequest(adminUserDN, "", newPw) + if err := bdb.UpdatePassword(pwRequest); err != nil { + fmt.Fprintf(os.Stderr, "Failed to update admin password: %v\n", err) + } + fmt.Printf("Password for user '%s' updated.\n", adminUserDN) + return nil +} + +func getPassword() (string, error) { + fmt.Print("Enter new password: ") + bytePassword, err := term.ReadPassword(int(syscall.Stdin)) + if err != nil { + return "", err + } + fmt.Println("") + fmt.Print("Re-enter new password: ") + bytePasswordVerify, err := term.ReadPassword(int(syscall.Stdin)) + if err != nil { + return "", err + } + fmt.Println("") + + password := string(bytePassword) + passwordVerify := string(bytePasswordVerify) + + if password != passwordVerify { + return "", errors.New("Passwords do not match") + } + return password, nil +} diff --git a/services/idm/pkg/command/root.go b/services/idm/pkg/command/root.go index 84208cba8..ee1ccb95d 100644 --- a/services/idm/pkg/command/root.go +++ b/services/idm/pkg/command/root.go @@ -18,6 +18,7 @@ func GetCommands(cfg *config.Config) cli.Commands { Server(cfg), // interaction with this service + ResetPassword(cfg), // infos about this service Health(cfg),