package upgrade import ( "context" "errors" "fmt" "reflect" "sort" "testing" "unsafe" "github.com/google/go-cmp/cmp" "github.com/influxdata/influxdb/v2" "github.com/influxdata/influxdb/v2/authorization" "github.com/influxdata/influxdb/v2/inmem" "github.com/influxdata/influxdb/v2/kit/platform" "github.com/influxdata/influxdb/v2/kv/migration" "github.com/influxdata/influxdb/v2/kv/migration/all" "github.com/influxdata/influxdb/v2/pkg/testing/assert" "github.com/influxdata/influxdb/v2/tenant" authv1 "github.com/influxdata/influxdb/v2/v1/authorization" "github.com/influxdata/influxdb/v2/v1/services/meta" "github.com/influxdata/influxql" "github.com/stretchr/testify/require" "go.uber.org/zap" "go.uber.org/zap/zaptest" "golang.org/x/crypto/bcrypt" ) func TestUpgradeSecurity(t *testing.T) { type testCase struct { name string users []meta.UserInfo db2ids map[string][]platform.ID wantErr error want []*influxdb.Authorization } hash := func(password string) string { hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) require.NoError(t, err) return string(hash) } var testCases = []testCase{ { name: "ordinary", users: []meta.UserInfo{ { // not upgraded because admin Name: "superman", Admin: true, Hash: hash("superman@123"), }, { // not upgraded because no privileges Name: "loser", Admin: false, Hash: hash("loser@123"), }, { Name: "weatherman", Admin: false, Hash: hash("weatherman@123"), Privileges: map[string]influxql.Privilege{ "water": influxql.AllPrivileges, "air": influxql.AllPrivileges, }, }, { Name: "hitgirl", Admin: false, Hash: hash("hitgirl@123"), Privileges: map[string]influxql.Privilege{ "hits": influxql.WritePrivilege, }, }, { Name: "boss@hits.org", // special name Admin: false, Hash: hash("boss@123"), Privileges: map[string]influxql.Privilege{ "hits": influxql.AllPrivileges, }, }, { Name: "viewer", Admin: false, Hash: hash("viewer@123"), Privileges: map[string]influxql.Privilege{ "water": influxql.ReadPrivilege, "air": influxql.ReadPrivilege, }, }, }, db2ids: map[string][]platform.ID{ "water": {0x33f9d67bc9cbc5b7, 0x33f9d67bc9cbc5b8, 0x33f9d67bc9cbc5b9}, "air": {0x43f9d67bc9cbc5b7, 0x43f9d67bc9cbc5b8, 0x43f9d67bc9cbc5b9}, "hits": {0x53f9d67bc9cbc5b7}, }, want: []*influxdb.Authorization{ { Token: "boss@hits.org", Status: "active", Description: "boss@hits.org's Legacy Token", }, { Token: "hitgirl", Status: "active", Description: "hitgirl's Legacy Token", }, { Token: "viewer", Status: "active", Description: "viewer's Legacy Token", }, { Token: "weatherman", Status: "active", Description: "weatherman's Legacy Token", }, }, }, { name: "missing buckets", users: []meta.UserInfo{ { Name: "weatherman", Admin: false, Hash: hash("weatherman@123"), Privileges: map[string]influxql.Privilege{ "water": influxql.AllPrivileges, "air": influxql.AllPrivileges, }, }, }, db2ids: nil, wantErr: errors.New("upgrade: there were errors/warnings, please fix them and run the command again"), }, { name: "no users", users: []meta.UserInfo{}, }, } for _, tc := range testCases { tc := tc t.Run(tc.name, func(t *testing.T) { // better do not run in parallel ctx := context.Background() log := zaptest.NewLogger(t) // mock v1 meta v1 := &influxDBv1{ meta: &meta.Client{}, } data := &meta.Data{ Users: tc.users, } f := reflect.ValueOf(v1.meta).Elem().Field(4) f = reflect.NewAt(f.Type(), unsafe.Pointer(f.UnsafeAddr())).Elem() f.Set(reflect.ValueOf(data)) // mock v2 meta kvStore := inmem.NewKVStore() migrator, err := migration.NewMigrator(zap.NewNop(), kvStore, all.Migrations[:]...) require.NoError(t, err) err = migrator.Up(ctx) require.NoError(t, err) authStoreV1, err := authv1.NewStore(kvStore) require.NoError(t, err) tenantStore := tenant.NewStore(kvStore) tenantSvc := tenant.NewService(tenantStore) authStoreV2, err := authorization.NewStore(kvStore) require.NoError(t, err) v2 := &influxDBv2{ authSvc: authv1.NewService(authStoreV1, tenantSvc), onboardSvc: tenant.NewOnboardService( tenantSvc, authorization.NewService(authStoreV2, tenantSvc), ), } // onboard admin oReq := &influxdb.OnboardingRequest{ User: "admin", Password: "12345678", Org: "testers", Bucket: "def", RetentionPeriodSeconds: influxdb.InfiniteRetention, } oResp, err := setupAdmin(ctx, v2, oReq) require.NoError(t, err) // target options targetOptions := optionsV2{ userName: oReq.User, orgName: oReq.Org, token: oResp.Auth.Token, orgID: oResp.Auth.OrgID, userID: oResp.Auth.UserID, } for k, v := range tc.db2ids { for i, id := range v { b := &influxdb.Bucket{ ID: id, Name: fmt.Sprintf("%s_%d", k, id), OrgID: targetOptions.orgID, } err := tenantSvc.CreateBucket(context.Background(), b) require.NoError(t, err) tc.db2ids[k][i] = b.ID } } // fill in expected permissions now that we know IDs for _, want := range tc.want { for _, user := range tc.users { if want.Token == user.Name { // v1 username is v2 token var permissions []influxdb.Permission for db, privilege := range user.Privileges { ids, ok := tc.db2ids[db] require.True(t, ok) for _, id := range ids { id := id resource := influxdb.Resource{ Type: influxdb.BucketsResourceType, OrgID: &targetOptions.orgID, ID: &id, } switch privilege { case influxql.ReadPrivilege: permissions = append(permissions, influxdb.Permission{ Action: influxdb.ReadAction, Resource: resource, }) case influxql.WritePrivilege: permissions = append(permissions, influxdb.Permission{ Action: influxdb.WriteAction, Resource: resource, }) case influxql.AllPrivileges: permissions = append(permissions, influxdb.Permission{ Action: influxdb.ReadAction, Resource: resource, }) permissions = append(permissions, influxdb.Permission{ Action: influxdb.WriteAction, Resource: resource, }) } } } want.Permissions = permissions } } } // command execution n, err := upgradeUsers(ctx, v1, v2, &targetOptions, tc.db2ids, log) assert.Equal(t, len(tc.want), n, "Upgraded count must match") if err != nil { if tc.wantErr != nil { if diff := cmp.Diff(tc.wantErr.Error(), err.Error()); diff != "" { t.Fatal(diff) } } else { t.Fatal(err) } } else if tc.wantErr != nil { t.Fatalf("should have failed with %v", tc.wantErr) } for _, want := range tc.want { actual, err := v2.authSvc.FindAuthorizationByToken(ctx, want.Token) require.NoError(t, err) if diff := cmp.Diff(targetOptions.orgID, actual.OrgID); diff != "" { t.Fatal(diff) } if diff := cmp.Diff(targetOptions.userID, actual.UserID); diff != "" { t.Fatal(diff) } if diff := cmp.Diff(want.Token, actual.Token); diff != "" { t.Fatal(diff) } if diff := cmp.Diff(want.Description, actual.Description); diff != "" { t.Fatal(diff) } if diff := cmp.Diff(want.Status, actual.Status); diff != "" { t.Fatal(diff) } sort.Slice(want.Permissions, func(i, j int) bool { return *(want.Permissions[i].Resource.ID) < *(want.Permissions[j].Resource.ID) }) sort.Slice(actual.Permissions, func(i, j int) bool { return *(actual.Permissions[i].Resource.ID) < *(actual.Permissions[j].Resource.ID) }) if diff := cmp.Diff(want.Permissions, actual.Permissions); diff != "" { t.Logf("permissions mismatch for user %s", want.Token) t.Fatal(diff) } } }) } }