diff --git a/user/manager.go b/user/manager.go index f2f4875d..09db145e 100644 --- a/user/manager.go +++ b/user/manager.go @@ -316,7 +316,7 @@ const ( // Schema management queries const ( - currentSchemaVersion = 5 + currentSchemaVersion = 6 insertSchemaVersion = `INSERT INTO schemaVersion VALUES (1, ?)` updateSchemaVersion = `UPDATE schemaVersion SET version = ? WHERE id = 1` selectSchemaVersionQuery = `SELECT version FROM schemaVersion WHERE id = 1` @@ -434,11 +434,78 @@ const ( // 5 -> 6 migrate5To6UpdateQueries = ` - ALTER TABLE user ADD COLUMN provisioned INT NOT NULL DEFAULT (0); - ALTER TABLE user ALTER COLUMN provisioned DROP DEFAULT; + PRAGMA foreign_keys=off; - ALTER TABLE user_access ADD COLUMN provisioned INT NOT NULL DEFAULT (0); - ALTER TABLE user_access ALTER COLUMN provisioned DROP DEFAULT; + -- Alter user table: Add provisioned column + ALTER TABLE user RENAME TO user_old; + CREATE TABLE IF NOT EXISTS user ( + id TEXT PRIMARY KEY, + tier_id TEXT, + user TEXT NOT NULL, + pass TEXT NOT NULL, + role TEXT CHECK (role IN ('anonymous', 'admin', 'user')) NOT NULL, + prefs JSON NOT NULL DEFAULT '{}', + sync_topic TEXT NOT NULL, + provisioned INT NOT NULL, + stats_messages INT NOT NULL DEFAULT (0), + stats_emails INT NOT NULL DEFAULT (0), + stats_calls INT NOT NULL DEFAULT (0), + stripe_customer_id TEXT, + stripe_subscription_id TEXT, + stripe_subscription_status TEXT, + stripe_subscription_interval TEXT, + stripe_subscription_paid_until INT, + stripe_subscription_cancel_at INT, + created INT NOT NULL, + deleted INT, + FOREIGN KEY (tier_id) REFERENCES tier (id) + ); + INSERT INTO user + SELECT + id, + tier_id, + user, + pass, + role, + prefs, + sync_topic, + 0, + stats_messages, + stats_emails, + stats_calls, + stripe_customer_id, + stripe_subscription_id, + stripe_subscription_status, + stripe_subscription_interval, + stripe_subscription_paid_until, + stripe_subscription_cancel_at, + created, deleted + FROM user_old; + DROP TABLE user_old; + + -- Alter user_access table: Add provisioned column + ALTER TABLE user_access RENAME TO user_access_old; + CREATE TABLE user_access ( + user_id TEXT NOT NULL, + topic TEXT NOT NULL, + read INT NOT NULL, + write INT NOT NULL, + owner_user_id INT, + provisioned INTEGER NOT NULL, + PRIMARY KEY (user_id, topic), + FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE, + FOREIGN KEY (owner_user_id) REFERENCES user (id) ON DELETE CASCADE + ); + INSERT INTO user_access SELECT *, 0 FROM user_access_old; + DROP TABLE user_access_old; + + -- Recreate indices + CREATE UNIQUE INDEX idx_user ON user (user); + CREATE UNIQUE INDEX idx_user_stripe_customer_id ON user (stripe_customer_id); + CREATE UNIQUE INDEX idx_user_stripe_subscription_id ON user (stripe_subscription_id); + + -- Re-enable foreign keys + PRAGMA foreign_keys=on; ` ) @@ -1422,10 +1489,10 @@ func (a *Manager) AddReservation(username string, topic string, everyone Permiss return err } defer tx.Rollback() - if _, err := tx.Exec(upsertUserAccessQuery, username, escapeUnderscore(topic), true, true, username, username); err != nil { + if _, err := tx.Exec(upsertUserAccessQuery, username, escapeUnderscore(topic), true, true, username, username, false); err != nil { return err } - if _, err := tx.Exec(upsertUserAccessQuery, Everyone, escapeUnderscore(topic), everyone.IsRead(), everyone.IsWrite(), username, username); err != nil { + if _, err := tx.Exec(upsertUserAccessQuery, Everyone, escapeUnderscore(topic), everyone.IsRead(), everyone.IsWrite(), username, username, false); err != nil { return err } return tx.Commit() diff --git a/user/manager_test.go b/user/manager_test.go index 42def63f..c2887ff3 100644 --- a/user/manager_test.go +++ b/user/manager_test.go @@ -52,10 +52,10 @@ func TestManager_FullScenario_Default_DenyAll(t *testing.T) { benGrants, err := a.Grants("ben") require.Nil(t, err) require.Equal(t, []Grant{ - {"everyonewrite", PermissionDenyAll}, - {"mytopic", PermissionReadWrite}, - {"writeme", PermissionWrite}, - {"readme", PermissionRead}, + {"everyonewrite", PermissionDenyAll, false}, + {"mytopic", PermissionReadWrite, false}, + {"writeme", PermissionWrite, false}, + {"readme", PermissionRead, false}, }, benGrants) john, err := a.Authenticate("john", "john") @@ -67,10 +67,10 @@ func TestManager_FullScenario_Default_DenyAll(t *testing.T) { johnGrants, err := a.Grants("john") require.Nil(t, err) require.Equal(t, []Grant{ - {"mytopic_deny*", PermissionDenyAll}, - {"mytopic_ro*", PermissionRead}, - {"mytopic*", PermissionReadWrite}, - {"*", PermissionRead}, + {"mytopic_deny*", PermissionDenyAll, false}, + {"mytopic_ro*", PermissionRead, false}, + {"mytopic*", PermissionReadWrite, false}, + {"*", PermissionRead, false}, }, johnGrants) notben, err := a.Authenticate("ben", "this is wrong") @@ -277,10 +277,10 @@ func TestManager_UserManagement(t *testing.T) { benGrants, err := a.Grants("ben") require.Nil(t, err) require.Equal(t, []Grant{ - {"everyonewrite", PermissionDenyAll}, - {"mytopic", PermissionReadWrite}, - {"writeme", PermissionWrite}, - {"readme", PermissionRead}, + {"everyonewrite", PermissionDenyAll, false}, + {"mytopic", PermissionReadWrite, false}, + {"writeme", PermissionWrite, false}, + {"readme", PermissionRead, false}, }, benGrants) everyone, err := a.User(Everyone) @@ -292,8 +292,8 @@ func TestManager_UserManagement(t *testing.T) { everyoneGrants, err := a.Grants(Everyone) require.Nil(t, err) require.Equal(t, []Grant{ - {"everyonewrite", PermissionReadWrite}, - {"announcements", PermissionRead}, + {"everyonewrite", PermissionReadWrite, false}, + {"announcements", PermissionRead, false}, }, everyoneGrants) // Ben: Before revoking @@ -1099,19 +1099,98 @@ func TestManager_Topic_Wildcard_With_Underscore(t *testing.T) { func TestManager_WithProvisionedUsers(t *testing.T) { f := filepath.Join(t.TempDir(), "user.db") conf := &Config{ - Filename: f, - DefaultAccess: PermissionReadWrite, - ProvisionedUsers: []*User{ - {Name: "phil", Hash: "$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C", Role: RoleAdmin}, + Filename: f, + DefaultAccess: PermissionReadWrite, + ProvisionEnabled: true, + ProvisionUsers: []*User{ + {Name: "philuser", Hash: "$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C", Role: RoleUser}, + {Name: "philadmin", Hash: "$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C", Role: RoleAdmin}, + }, + ProvisionAccess: map[string][]*Grant{ + "philuser": { + {TopicPattern: "stats", Permission: PermissionReadWrite}, + {TopicPattern: "secret", Permission: PermissionRead}, + }, }, } a, err := NewManager(conf) require.Nil(t, err) + + // Manually add user + require.Nil(t, a.AddUser("philmanual", "manual", RoleUser, false)) + + // Check that the provisioned users are there users, err := a.Users() require.Nil(t, err) - for _, u := range users { - fmt.Println(u.ID, u.Name, u.Role) + require.Len(t, users, 4) + + require.Equal(t, "philadmin", users[0].Name) + require.Equal(t, RoleAdmin, users[0].Role) + + require.Equal(t, "philmanual", users[1].Name) + require.Equal(t, RoleUser, users[1].Role) + + grants, err := a.Grants("philuser") + require.Nil(t, err) + require.Equal(t, "philuser", users[2].Name) + require.Equal(t, RoleUser, users[2].Role) + require.Equal(t, 2, len(grants)) + require.Equal(t, "secret", grants[0].TopicPattern) + require.Equal(t, PermissionRead, grants[0].Permission) + require.Equal(t, "stats", grants[1].TopicPattern) + require.Equal(t, PermissionReadWrite, grants[1].Permission) + + require.Equal(t, "*", users[3].Name) + + // Re-open the DB (second app start) + require.Nil(t, a.db.Close()) + conf.ProvisionUsers = []*User{ + {Name: "philuser", Hash: "$2a$10$AAAU21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C", Role: RoleUser}, } + conf.ProvisionAccess = map[string][]*Grant{ + "philuser": { + {TopicPattern: "stats12", Permission: PermissionReadWrite}, + {TopicPattern: "secret12", Permission: PermissionRead}, + }, + } + a, err = NewManager(conf) + require.Nil(t, err) + + // Check that the provisioned users are there + users, err = a.Users() + require.Nil(t, err) + require.Len(t, users, 3) + + require.Equal(t, "philmanual", users[0].Name) + require.Equal(t, RoleUser, users[0].Role) + + grants, err = a.Grants("philuser") + require.Nil(t, err) + require.Equal(t, "philuser", users[1].Name) + require.Equal(t, RoleUser, users[1].Role) + require.Equal(t, 2, len(grants)) + require.Equal(t, "secret12", grants[0].TopicPattern) + require.Equal(t, PermissionRead, grants[0].Permission) + require.Equal(t, "stats12", grants[1].TopicPattern) + require.Equal(t, PermissionReadWrite, grants[1].Permission) + + require.Equal(t, "*", users[2].Name) + + // Re-open the DB again (third app start) + require.Nil(t, a.db.Close()) + conf.ProvisionUsers = []*User{} + conf.ProvisionAccess = map[string][]*Grant{} + a, err = NewManager(conf) + require.Nil(t, err) + + // Check that the provisioned users are there + users, err = a.Users() + require.Nil(t, err) + require.Len(t, users, 2) + + require.Equal(t, "philmanual", users[0].Name) + require.Equal(t, RoleUser, users[0].Role) + require.Equal(t, "*", users[1].Name) } func TestToFromSQLWildcard(t *testing.T) {