Skip to content

Commit c77b09f

Browse files
authored
PBM-1213: Selective Logical Restore with included Users&Roles (#934)
* Add users&roles flag for restore cmd * Extract parse namespace into util * Add validation for restoring user&roles for specific collection * Extract user&roles validation for restore cmd * Add tests for ns helper funcs * Add logical selective restore with users and roles * Extract method for restoring users&roles * Add logic for config server selective restore * Add tests for resolving ns and users&roles opt
1 parent 364f7f6 commit c77b09f

File tree

8 files changed

+404
-52
lines changed

8 files changed

+404
-52
lines changed

cmd/pbm/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,8 @@ func main() {
170170
StringVar(&restore.pitrBase)
171171
restoreCmd.Flag("ns", `Namespaces to restore (e.g. "db1.*,db2.collection2"). If not set, restore all ("*.*")`).
172172
StringVar(&restore.ns)
173+
restoreCmd.Flag("with-users-and-roles", "Includes users and roles for selected database (--ns flag)").
174+
BoolVar(&restore.usersAndRoles)
173175
restoreCmd.Flag("wait", "Wait for the restore to finish.").
174176
Short('w').
175177
BoolVar(&restore.wait)

cmd/pbm/restore.go

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,16 @@ import (
2929
)
3030

3131
type restoreOpts struct {
32-
bcp string
33-
pitr string
34-
pitrBase string
35-
wait bool
36-
extern bool
37-
ns string
38-
rsMap string
39-
conf string
40-
ts string
32+
bcp string
33+
pitr string
34+
pitrBase string
35+
wait bool
36+
extern bool
37+
ns string
38+
usersAndRoles bool
39+
rsMap string
40+
conf string
41+
ts string
4142
}
4243

4344
type restoreRet struct {
@@ -101,6 +102,9 @@ func runRestore(ctx context.Context, conn connect.Client, o *restoreOpts, outf o
101102
if err != nil {
102103
return nil, errors.Wrap(err, "parse --ns option")
103104
}
105+
if err := validateRestoreUsersAndRoles(o.usersAndRoles, nss); err != nil {
106+
return nil, errors.Wrap(err, "parse --with-users-and-roles-option")
107+
}
104108

105109
rsMap, err := parseRSNamesMapping(o.rsMap)
106110
if err != nil {
@@ -315,11 +319,12 @@ func doRestore(
315319
cmd := ctrl.Cmd{
316320
Cmd: ctrl.CmdRestore,
317321
Restore: &ctrl.RestoreCmd{
318-
Name: name,
319-
BackupName: bcp,
320-
Namespaces: nss,
321-
RSMap: rsMapping,
322-
External: o.extern,
322+
Name: name,
323+
BackupName: bcp,
324+
Namespaces: nss,
325+
UsersAndRoles: o.usersAndRoles,
326+
RSMap: rsMapping,
327+
External: o.extern,
323328
},
324329
}
325330
if o.pitr != "" {
@@ -668,3 +673,16 @@ func describeRestore(ctx context.Context, conn connect.Client, o descrRestoreOpt
668673

669674
return res, nil
670675
}
676+
677+
func validateRestoreUsersAndRoles(usersAndRoles bool, nss []string) error {
678+
if !util.IsSelective(nss) && usersAndRoles {
679+
return errors.New("Including users and roles are only allowed for selected database " +
680+
"(use --ns flag for selective backup)")
681+
}
682+
if len(nss) >= 1 && util.ContainsSpecifiedColl(nss) && usersAndRoles {
683+
return errors.New("Including users and roles are not allowed for specific collection. " +
684+
"Use --ns='db.*' to specify the whole database instead.")
685+
}
686+
687+
return nil
688+
}

pbm/backup/logical.go

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func (b *Backup) doLogical(
4444
if inf.IsConfigSrv() {
4545
db = "config"
4646
} else {
47-
db, coll = parseNS(bcp.Namespaces[0])
47+
db, coll = util.ParseNS(bcp.Namespaces[0])
4848
}
4949
}
5050

@@ -445,16 +445,3 @@ func getNamespacesSize(ctx context.Context, m *mongo.Client, db, coll string) (m
445445
err = eg.Wait()
446446
return rv, err
447447
}
448-
449-
func parseNS(ns string) (string, string) {
450-
db, coll, _ := strings.Cut(ns, ".")
451-
452-
if db == "*" {
453-
db = ""
454-
}
455-
if coll == "*" {
456-
coll = ""
457-
}
458-
459-
return db, coll
460-
}

pbm/ctrl/cmd.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,11 @@ func (b BackupCmd) String() string {
126126
}
127127

128128
type RestoreCmd struct {
129-
Name string `bson:"name"`
130-
BackupName string `bson:"backupName"`
131-
Namespaces []string `bson:"nss,omitempty"`
132-
RSMap map[string]string `bson:"rsMap,omitempty"`
129+
Name string `bson:"name"`
130+
BackupName string `bson:"backupName"`
131+
Namespaces []string `bson:"nss,omitempty"`
132+
UsersAndRoles bool `bson:"usersAndRoles,omitempty"`
133+
RSMap map[string]string `bson:"rsMap,omitempty"`
133134

134135
OplogTS primitive.Timestamp `bson:"oplogTS,omitempty"`
135136

pbm/restore/logical.go

Lines changed: 97 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ type Restore struct {
6464
indexCatalog *idx.IndexCatalog
6565
}
6666

67+
// PBM restore from temp collections (pbmRUsers/pbmRRoles)should be used
68+
type restoreUsersAndRolesOption bool
69+
6770
// New creates a new restore object
6871
func New(leadConn connect.Client, nodeConn *mongo.Client, brief topo.NodeBrief, rsMap map[string]string) *Restore {
6972
if rsMap == nil {
@@ -100,6 +103,41 @@ func (r *Restore) exit(ctx context.Context, err error) {
100103
r.Close()
101104
}
102105

106+
// resolveNamespace resolves final namespace(s) based on the backup namespace,
107+
// restore namespace, and option whether we should restore users&roles
108+
func resolveNamespace(nssBackup, nssRestore []string, usingUsersAndRoles bool) []string {
109+
if util.IsSelective(nssRestore) {
110+
if usingUsersAndRoles {
111+
var nss []string
112+
nss = append(nss, nssRestore...)
113+
nss = append(nss,
114+
defs.DB+"."+defs.TmpUsersCollection,
115+
defs.DB+"."+defs.TmpRolesCollection,
116+
)
117+
return nss
118+
}
119+
120+
return nssRestore
121+
}
122+
if util.IsSelective(nssBackup) {
123+
return nssBackup
124+
}
125+
126+
return nssBackup
127+
}
128+
129+
// shouldRestoreUsersAndRoles determines whether user&roles should be restored from the backup
130+
func shouldRestoreUsersAndRoles(nssBackup, nssRestore []string, usingUsersAndRoles bool) restoreUsersAndRolesOption {
131+
if util.IsSelective(nssBackup) {
132+
return false
133+
}
134+
if util.IsSelective(nssRestore) {
135+
return restoreUsersAndRolesOption(usingUsersAndRoles)
136+
}
137+
138+
return true
139+
}
140+
103141
// Snapshot do the snapshot's (mongo dump) restore
104142
//
105143
//nolint:nonamedreturns
@@ -116,10 +154,8 @@ func (r *Restore) Snapshot(ctx context.Context, cmd *ctrl.RestoreCmd, opid ctrl.
116154
return err
117155
}
118156

119-
nss := cmd.Namespaces
120-
if !util.IsSelective(nss) {
121-
nss = bcp.Namespaces
122-
}
157+
nss := resolveNamespace(bcp.Namespaces, cmd.Namespaces, cmd.UsersAndRoles)
158+
usersAndRolesOpt := shouldRestoreUsersAndRoles(bcp.Namespaces, cmd.Namespaces, cmd.UsersAndRoles)
123159

124160
err = setRestoreBackup(ctx, r.leadConn, r.name, cmd.BackupName, nss)
125161
if err != nil {
@@ -150,7 +186,7 @@ func (r *Restore) Snapshot(ctx context.Context, cmd *ctrl.RestoreCmd, opid ctrl.
150186
return err
151187
}
152188

153-
err = r.RunSnapshot(ctx, dump, bcp, nss)
189+
err = r.RunSnapshot(ctx, dump, bcp, nss, usersAndRolesOpt)
154190
if err != nil {
155191
return err
156192
}
@@ -226,10 +262,8 @@ func (r *Restore) PITR(ctx context.Context, cmd *ctrl.RestoreCmd, opid ctrl.OPID
226262
"Try to set an earlier snapshot. Or leave the snapshot empty so PBM will choose one.")
227263
}
228264

229-
nss := cmd.Namespaces
230-
if len(nss) == 0 {
231-
nss = bcp.Namespaces
232-
}
265+
nss := resolveNamespace(bcp.Namespaces, cmd.Namespaces, cmd.UsersAndRoles)
266+
usersAndRolesOpt := shouldRestoreUsersAndRoles(bcp.Namespaces, cmd.Namespaces, cmd.UsersAndRoles)
233267

234268
if r.nodeInfo.IsLeader() {
235269
err = SetOplogTimestamps(ctx, r.leadConn, r.name, 0, int64(cmd.OplogTS.T))
@@ -281,7 +315,7 @@ func (r *Restore) PITR(ctx context.Context, cmd *ctrl.RestoreCmd, opid ctrl.OPID
281315
return err
282316
}
283317

284-
err = r.RunSnapshot(ctx, dump, bcp, nss)
318+
err = r.RunSnapshot(ctx, dump, bcp, nss, usersAndRolesOpt)
285319
if err != nil {
286320
return err
287321
}
@@ -647,7 +681,13 @@ func (r *Restore) toState(ctx context.Context, status defs.Status, wait *time.Du
647681
return toState(ctx, r.leadConn, status, r.name, r.nodeInfo, r.reconcileStatus, wait)
648682
}
649683

650-
func (r *Restore) RunSnapshot(ctx context.Context, dump string, bcp *backup.BackupMeta, nss []string) error {
684+
func (r *Restore) RunSnapshot(
685+
ctx context.Context,
686+
dump string,
687+
bcp *backup.BackupMeta,
688+
nss []string,
689+
usersAndRolesOpt restoreUsersAndRolesOption,
690+
) error {
651691
var rdr io.ReadCloser
652692

653693
var err error
@@ -673,7 +713,15 @@ func (r *Restore) RunSnapshot(ctx context.Context, dump string, bcp *backup.Back
673713
mapRS := util.MakeReverseRSMapFunc(r.rsMap)
674714
if r.nodeInfo.IsConfigSrv() && util.IsSelective(nss) {
675715
// restore cluster specific configs only
676-
return r.configsvrRestore(ctx, bcp, nss, mapRS)
716+
if err := r.configsvrRestore(ctx, bcp, nss, mapRS); err != nil {
717+
return err
718+
}
719+
if !usersAndRolesOpt {
720+
return nil
721+
}
722+
723+
// selective restore needs to process users and roles from the full backup,
724+
// so we'll continue with selective restore
677725
}
678726

679727
var cfg *config.Config
@@ -730,17 +778,23 @@ func (r *Restore) RunSnapshot(ctx context.Context, dump string, bcp *backup.Back
730778
return errors.Wrap(err, "mongorestore")
731779
}
732780

733-
if util.IsSelective(nss) {
734-
return nil
781+
if usersAndRolesOpt {
782+
if err := r.restoreUsersAndRoles(ctx, nss); err != nil {
783+
return errors.Wrap(err, "restoring users and roles")
784+
}
735785
}
736786

787+
return nil
788+
}
789+
790+
func (r *Restore) restoreUsersAndRoles(ctx context.Context, nss []string) error {
737791
r.log.Info("restoring users and roles")
738792
cusr, err := topo.CurrentUser(ctx, r.nodeConn)
739793
if err != nil {
740794
return errors.Wrap(err, "get current user")
741795
}
742796

743-
err = r.swapUsers(ctx, cusr)
797+
err = r.swapUsers(ctx, cusr, nss)
744798
if err != nil {
745799
return errors.Wrap(err, "swap users 'n' roles")
746800
}
@@ -1123,22 +1177,39 @@ func (r *Restore) Done(ctx context.Context) error {
11231177
return nil
11241178
}
11251179

1126-
func (r *Restore) swapUsers(ctx context.Context, exclude *topo.AuthInfo) error {
1180+
func (r *Restore) swapUsers(ctx context.Context, exclude *topo.AuthInfo, nss []string) error {
1181+
dbs := []string{}
1182+
for _, ns := range nss {
1183+
// ns can be "*.*" or "admin.pbmRUsers" or "admin.pbmRRoles"
1184+
db, _ := util.ParseNS(ns)
1185+
if len(db) == 0 || strings.HasPrefix(db, defs.DB) {
1186+
continue
1187+
}
1188+
dbs = append(dbs, db)
1189+
}
1190+
11271191
rolesC := r.nodeConn.Database("admin").Collection("system.roles")
11281192

11291193
eroles := []string{}
11301194
for _, r := range exclude.UserRoles {
11311195
eroles = append(eroles, r.DB+"."+r.Role)
11321196
}
11331197

1198+
rolesFilter := bson.M{
1199+
"_id": bson.M{"$nin": eroles},
1200+
}
1201+
if len(dbs) > 0 {
1202+
rolesFilter["db"] = bson.M{"$in": dbs}
1203+
}
1204+
11341205
curr, err := r.nodeConn.Database(defs.DB).Collection(defs.TmpRolesCollection).
1135-
Find(ctx, bson.M{"_id": bson.M{"$nin": eroles}})
1206+
Find(ctx, rolesFilter)
11361207
if err != nil {
11371208
return errors.Wrap(err, "create cursor for tmpRoles")
11381209
}
11391210
defer curr.Close(ctx)
11401211

1141-
_, err = rolesC.DeleteMany(ctx, bson.M{"_id": bson.M{"$nin": eroles}})
1212+
_, err = rolesC.DeleteMany(ctx, rolesFilter)
11421213
if err != nil {
11431214
return errors.Wrap(err, "delete current roles")
11441215
}
@@ -1159,15 +1230,21 @@ func (r *Restore) swapUsers(ctx context.Context, exclude *topo.AuthInfo) error {
11591230
if len(exclude.Users) > 0 {
11601231
user = exclude.Users[0].DB + "." + exclude.Users[0].User
11611232
}
1233+
filterUsers := bson.M{
1234+
"_id": bson.M{"$ne": user},
1235+
}
1236+
if len(dbs) > 0 {
1237+
filterUsers["db"] = bson.M{"$in": dbs}
1238+
}
11621239
cur, err := r.nodeConn.Database(defs.DB).Collection(defs.TmpUsersCollection).
1163-
Find(ctx, bson.M{"_id": bson.M{"$ne": user}})
1240+
Find(ctx, filterUsers)
11641241
if err != nil {
11651242
return errors.Wrap(err, "create cursor for tmpUsers")
11661243
}
11671244
defer cur.Close(ctx)
11681245

11691246
usersC := r.nodeConn.Database("admin").Collection("system.users")
1170-
_, err = usersC.DeleteMany(ctx, bson.M{"_id": bson.M{"$ne": user}})
1247+
_, err = usersC.DeleteMany(ctx, filterUsers)
11711248
if err != nil {
11721249
return errors.Wrap(err, "delete current users")
11731250
}

0 commit comments

Comments
 (0)