refactor(kv): delete deprecated kv service code

This includes removal of a lot of kv.Service responsibilities. However,
it does not finish the re-wiring. It removes documents, telegrafs,
notification rules + endpoints, checks, orgs, users, buckets, passwords,
urms, labels and authorizations. There are some oustanding pieces that
are needed to get kv service compiling (dashboard service urm
dependency). Then all the call sites for kv service need updating and
the new implementations of telegraf and notification rules + endpoints
needed installing (along with any necessary migrations).
pull/20053/head
George MacRorie 2020-10-20 14:25:36 +01:00 committed by George
parent aa1cefa9e2
commit 16d916a952
111 changed files with 2840 additions and 14302 deletions

View File

@ -6,23 +6,26 @@ import (
"time"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/authorization"
"github.com/influxdata/influxdb/v2/authorizer"
pctx "github.com/influxdata/influxdb/v2/context"
_ "github.com/influxdata/influxdb/v2/fluxinit/static"
"github.com/influxdata/influxdb/v2/http"
"github.com/influxdata/influxdb/v2/inmem"
"github.com/influxdata/influxdb/v2/kv"
"github.com/influxdata/influxdb/v2/kv/migration/all"
"github.com/influxdata/influxdb/v2/mock"
_ "github.com/influxdata/influxdb/v2/fluxinit/static"
"github.com/influxdata/influxdb/v2/tenant"
"github.com/pkg/errors"
"go.uber.org/zap/zaptest"
)
func TestOnboardingValidation(t *testing.T) {
svc := newKVSVC(t)
_, onboard := setup(t)
ts := authorizer.NewTaskService(zaptest.NewLogger(t), mockTaskService(3, 2, 1))
r, err := svc.OnboardInitialUser(context.Background(), &influxdb.OnboardingRequest{
r, err := onboard.OnboardInitialUser(context.Background(), &influxdb.OnboardingRequest{
User: "Setec Astronomy",
Password: "too many secrets",
Org: "thing",
@ -120,9 +123,9 @@ func TestValidations(t *testing.T) {
otherOrg = &influxdb.Organization{Name: "other_org"}
)
svc := newKVSVC(t)
svc, onboard := setup(t)
r, err := svc.OnboardInitialUser(context.Background(), &influxdb.OnboardingRequest{
r, err := onboard.OnboardInitialUser(context.Background(), &influxdb.OnboardingRequest{
User: "Setec Astronomy",
Password: "too many secrets",
Org: "thing",
@ -572,7 +575,26 @@ from(bucket:"holder") |> range(start:-5m) |> to(bucket:"holder", org:"thing")`
}
}
func newKVSVC(t *testing.T) *kv.Service {
func setup(t *testing.T) (*tenant.Service, influxdb.OnboardingService) {
t.Helper()
store := newStore(t)
svc := tenant.NewService(tenant.NewStore(store))
authStore, err := authorization.NewStore(store)
if err != nil {
t.Fatal(err)
}
authSvc := authorization.NewService(authStore, svc)
onboard := tenant.NewOnboardService(svc, authSvc)
return svc, onboard
}
func newStore(t *testing.T) kv.Store {
t.Helper()
store := inmem.NewKVStore()
@ -581,5 +603,5 @@ func newKVSVC(t *testing.T) *kv.Service {
t.Fatal(err)
}
return kv.NewService(zaptest.NewLogger(t), store)
return store
}

View File

@ -174,13 +174,12 @@ var taskCmpOptions = cmp.Options{
// CheckFields will include the IDGenerator, and checks
type CheckFields struct {
IDGenerator influxdb.IDGenerator
TimeGenerator influxdb.TimeGenerator
TaskService influxdb.TaskService
Checks []influxdb.Check
Organizations []*influxdb.Organization
UserResourceMappings []*influxdb.UserResourceMapping
Tasks []influxdb.TaskCreate
IDGenerator influxdb.IDGenerator
TimeGenerator influxdb.TimeGenerator
TaskService influxdb.TaskService
Checks []influxdb.Check
Organizations []*influxdb.Organization
Tasks []influxdb.TaskCreate
}
type checkServiceFactory func(CheckFields, *testing.T) (influxdb.CheckService, influxdb.TaskService, string, func())
@ -268,26 +267,6 @@ func CreateCheck(
ID: MustIDBase16(orgOneID),
},
},
UserResourceMappings: []*influxdb.UserResourceMapping{
{
ResourceID: MustIDBase16(orgOneID),
ResourceType: influxdb.OrgsResourceType,
UserID: MustIDBase16(fiveID),
UserType: influxdb.Owner,
},
{
ResourceID: MustIDBase16(orgOneID),
ResourceType: influxdb.OrgsResourceType,
UserID: MustIDBase16(sixID),
UserType: influxdb.Owner,
},
{
ResourceID: MustIDBase16(orgOneID),
ResourceType: influxdb.OrgsResourceType,
UserID: MustIDBase16(twoID),
UserType: influxdb.Member,
},
},
},
args: args{
userID: MustIDBase16(twoID),
@ -394,26 +373,6 @@ func CreateCheck(
return MustIDBase16(checkTwoID)
},
},
UserResourceMappings: []*influxdb.UserResourceMapping{
{
ResourceID: MustIDBase16(orgTwoID),
ResourceType: influxdb.OrgsResourceType,
UserID: MustIDBase16(sixID),
UserType: influxdb.Owner,
},
{
ResourceID: MustIDBase16(orgOneID),
ResourceType: influxdb.OrgsResourceType,
UserID: MustIDBase16(sixID),
UserType: influxdb.Owner,
},
{
ResourceID: MustIDBase16(checkOneID),
ResourceType: influxdb.ChecksResourceType,
UserID: MustIDBase16(sixID),
UserType: influxdb.Owner,
},
},
TimeGenerator: mock.TimeGenerator{FakeValue: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC)},
Checks: []influxdb.Check{
deadman1,
@ -493,26 +452,6 @@ func CreateCheck(
{
name: "names should be unique within an organization",
fields: CheckFields{
UserResourceMappings: []*influxdb.UserResourceMapping{
{
ResourceID: MustIDBase16(orgTwoID),
ResourceType: influxdb.OrgsResourceType,
UserID: MustIDBase16(sixID),
UserType: influxdb.Owner,
},
{
ResourceID: MustIDBase16(orgOneID),
ResourceType: influxdb.OrgsResourceType,
UserID: MustIDBase16(sixID),
UserType: influxdb.Owner,
},
{
ResourceID: MustIDBase16(checkOneID),
ResourceType: influxdb.ChecksResourceType,
UserID: MustIDBase16(sixID),
UserType: influxdb.Owner,
},
},
IDGenerator: &mock.IDGenerator{
IDFn: func() influxdb.ID {
return MustIDBase16(checkTwoID)
@ -585,26 +524,6 @@ func CreateCheck(
},
},
TimeGenerator: mock.TimeGenerator{FakeValue: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC)},
UserResourceMappings: []*influxdb.UserResourceMapping{
{
ResourceID: MustIDBase16(orgTwoID),
ResourceType: influxdb.OrgsResourceType,
UserID: MustIDBase16(twoID),
UserType: influxdb.Owner,
},
{
ResourceID: MustIDBase16(orgOneID),
ResourceType: influxdb.OrgsResourceType,
UserID: MustIDBase16(sixID),
UserType: influxdb.Owner,
},
{
ResourceID: MustIDBase16(checkOneID),
ResourceType: influxdb.ChecksResourceType,
UserID: MustIDBase16(sixID),
UserType: influxdb.Owner,
},
},
Organizations: []*influxdb.Organization{
{
Name: "theorg",
@ -901,20 +820,6 @@ func FindChecks(
{
name: "find all checks",
fields: CheckFields{
UserResourceMappings: []*influxdb.UserResourceMapping{
{
ResourceID: MustIDBase16(oneID),
UserID: MustIDBase16(sixID),
UserType: influxdb.Owner,
ResourceType: influxdb.ChecksResourceType,
},
{
ResourceID: MustIDBase16(twoID),
UserID: MustIDBase16(sixID),
UserType: influxdb.Member,
ResourceType: influxdb.ChecksResourceType,
},
},
Organizations: []*influxdb.Organization{
{
Name: "theorg",
@ -943,20 +848,6 @@ func FindChecks(
{
name: "find all checks by offset and limit",
fields: CheckFields{
UserResourceMappings: []*influxdb.UserResourceMapping{
{
ResourceID: MustIDBase16(oneID),
UserID: MustIDBase16(sixID),
UserType: influxdb.Owner,
ResourceType: influxdb.ChecksResourceType,
},
{
ResourceID: MustIDBase16(twoID),
UserID: MustIDBase16(sixID),
UserType: influxdb.Member,
ResourceType: influxdb.ChecksResourceType,
},
},
Organizations: []*influxdb.Organization{
{
Name: "theorg",
@ -983,20 +874,6 @@ func FindChecks(
{
name: "find all checks by descending",
fields: CheckFields{
UserResourceMappings: []*influxdb.UserResourceMapping{
{
ResourceID: MustIDBase16(oneID),
UserID: MustIDBase16(sixID),
UserType: influxdb.Owner,
ResourceType: influxdb.ChecksResourceType,
},
{
ResourceID: MustIDBase16(twoID),
UserID: MustIDBase16(sixID),
UserType: influxdb.Member,
ResourceType: influxdb.ChecksResourceType,
},
},
Organizations: []*influxdb.Organization{
{
Name: "theorg",
@ -1024,20 +901,6 @@ func FindChecks(
{
name: "find checks by organization name",
fields: CheckFields{
UserResourceMappings: []*influxdb.UserResourceMapping{
{
ResourceID: MustIDBase16(oneID),
UserID: MustIDBase16(sixID),
UserType: influxdb.Owner,
ResourceType: influxdb.ChecksResourceType,
},
{
ResourceID: MustIDBase16(twoID),
UserID: MustIDBase16(sixID),
UserType: influxdb.Member,
ResourceType: influxdb.ChecksResourceType,
},
},
Organizations: []*influxdb.Organization{
{
Name: "theorg",
@ -1066,20 +929,6 @@ func FindChecks(
{
name: "find checks by organization id",
fields: CheckFields{
UserResourceMappings: []*influxdb.UserResourceMapping{
{
ResourceID: MustIDBase16(oneID),
UserID: MustIDBase16(sixID),
UserType: influxdb.Owner,
ResourceType: influxdb.ChecksResourceType,
},
{
ResourceID: MustIDBase16(twoID),
UserID: MustIDBase16(sixID),
UserType: influxdb.Member,
ResourceType: influxdb.ChecksResourceType,
},
},
Organizations: []*influxdb.Organization{
{
Name: "theorg",
@ -1114,20 +963,6 @@ func FindChecks(
ID: MustIDBase16(orgOneID),
},
},
UserResourceMappings: []*influxdb.UserResourceMapping{
{
ResourceID: MustIDBase16(oneID),
UserID: MustIDBase16(sixID),
UserType: influxdb.Owner,
ResourceType: influxdb.ChecksResourceType,
},
{
ResourceID: MustIDBase16(twoID),
UserID: MustIDBase16(sixID),
UserType: influxdb.Member,
ResourceType: influxdb.ChecksResourceType,
},
},
Checks: []influxdb.Check{
deadman1,
threshold1,
@ -1168,12 +1003,7 @@ func FindChecks(
defer done()
ctx := context.Background()
filter := influxdb.CheckFilter{
UserResourceMappingFilter: influxdb.UserResourceMappingFilter{
UserID: tt.args.userID,
ResourceType: influxdb.ChecksResourceType,
},
}
filter := influxdb.CheckFilter{}
if tt.args.ID.Valid() {
filter.ID = &tt.args.ID
}
@ -1227,20 +1057,6 @@ func DeleteCheck(
ID: MustIDBase16(orgOneID),
},
},
UserResourceMappings: []*influxdb.UserResourceMapping{
{
ResourceID: MustIDBase16(oneID),
UserID: MustIDBase16(sixID),
UserType: influxdb.Owner,
ResourceType: influxdb.ChecksResourceType,
},
{
ResourceID: MustIDBase16(twoID),
UserID: MustIDBase16(sixID),
UserType: influxdb.Member,
ResourceType: influxdb.ChecksResourceType,
},
},
Tasks: []influxdb.TaskCreate{
{
Flux: `option task = { every: 10s, name: "foo" }
@ -1274,20 +1090,6 @@ data = from(bucket: "telegraf") |> range(start: -1m)`,
ID: MustIDBase16(orgOneID),
},
},
UserResourceMappings: []*influxdb.UserResourceMapping{
{
ResourceID: MustIDBase16(oneID),
UserID: MustIDBase16(sixID),
UserType: influxdb.Owner,
ResourceType: influxdb.ChecksResourceType,
},
{
ResourceID: MustIDBase16(twoID),
UserID: MustIDBase16(sixID),
UserType: influxdb.Member,
ResourceType: influxdb.ChecksResourceType,
},
},
Tasks: []influxdb.TaskCreate{
{
Flux: `option task = { every: 10s, name: "foo" }
@ -1327,12 +1129,7 @@ data = from(bucket: "telegraf") |> range(start: -1m)`,
err := s.DeleteCheck(ctx, MustIDBase16(tt.args.ID))
influxErrsEqual(t, tt.wants.err, err)
filter := influxdb.CheckFilter{
UserResourceMappingFilter: influxdb.UserResourceMappingFilter{
UserID: tt.args.userID,
ResourceType: influxdb.ChecksResourceType,
},
}
filter := influxdb.CheckFilter{}
checks, _, err := s.FindChecks(ctx, filter)
if err != nil {
t.Fatalf("failed to retrieve checks: %v", err)

View File

@ -5,11 +5,13 @@ import (
"testing"
"github.com/influxdata/influxdb/v2"
_ "github.com/influxdata/influxdb/v2/fluxinit/static"
"github.com/influxdata/influxdb/v2/inmem"
"github.com/influxdata/influxdb/v2/kv"
"github.com/influxdata/influxdb/v2/kv/migration/all"
_ "github.com/influxdata/influxdb/v2/fluxinit/static"
"github.com/influxdata/influxdb/v2/mock"
"github.com/influxdata/influxdb/v2/query/fluxlang"
"github.com/influxdata/influxdb/v2/tenant"
"go.uber.org/zap/zaptest"
)
@ -32,7 +34,11 @@ func TestCheckService(t *testing.T) {
func initCheckService(f CheckFields, t *testing.T) (influxdb.CheckService, influxdb.TaskService, string, func()) {
store, closeKVStore := NewKVTestStore(t)
logger := zaptest.NewLogger(t)
svc := kv.NewService(logger, store, kv.ServiceConfig{
tenantStore := tenant.NewStore(store)
tenantSvc := tenant.NewService(tenantStore)
svc := kv.NewService(logger, store, tenantSvc, kv.ServiceConfig{
FluxLanguageService: fluxlang.DefaultService,
})
svc.IDGenerator = f.IDGenerator
@ -41,22 +47,19 @@ func initCheckService(f CheckFields, t *testing.T) (influxdb.CheckService, influ
svc.TimeGenerator = influxdb.RealTimeGenerator{}
}
checkService := NewService(logger, store, svc, svc)
checkService := NewService(logger, store, tenantSvc, svc)
checkService.idGenerator = f.IDGenerator
if f.TimeGenerator != nil {
checkService.timeGenerator = f.TimeGenerator
}
ctx := context.Background()
for _, m := range f.UserResourceMappings {
if err := svc.CreateUserResourceMapping(ctx, m); err != nil {
t.Fatalf("failed to populate user resource mapping: %v", err)
}
}
for _, o := range f.Organizations {
if err := svc.PutOrganization(ctx, o); err != nil {
t.Fatalf("failed to populate organizations")
}
mock.SetIDForFunc(&tenantStore.OrgIDGen, o.ID, func() {
if err := tenantSvc.CreateOrganization(ctx, o); err != nil {
t.Fatalf("failed to populate organizations")
}
})
}
for _, c := range f.Checks {
if err := checkService.PutCheck(ctx, c); err != nil {
@ -70,15 +73,10 @@ func initCheckService(f CheckFields, t *testing.T) (influxdb.CheckService, influ
}
return checkService, svc, kv.OpPrefix, func() {
for _, o := range f.Organizations {
if err := svc.DeleteOrganization(ctx, o.ID); err != nil {
if err := tenantSvc.DeleteOrganization(ctx, o.ID); err != nil {
t.Logf("failed to remove organization: %v", err)
}
}
for _, urm := range f.UserResourceMappings {
if err := svc.DeleteUserResourceMapping(ctx, urm.ResourceID, urm.UserID); err != nil && influxdb.ErrorCode(err) != influxdb.ENotFound {
t.Logf("failed to remove urm rule: %v", err)
}
}
for _, c := range f.Checks {
if err := checkService.DeleteCheck(ctx, c.GetID()); err != nil {
t.Logf("failed to remove check: %v", err)

View File

@ -15,6 +15,7 @@ import (
"github.com/influxdata/influxdb/v2/http"
"github.com/influxdata/influxdb/v2/kv"
influxlogger "github.com/influxdata/influxdb/v2/logger"
"github.com/influxdata/influxdb/v2/tenant"
"github.com/influxdata/influxdb/v2/v1/services/meta"
"github.com/spf13/cobra"
"go.uber.org/zap"
@ -39,6 +40,7 @@ type cmdBackupBuilder struct {
backupService *http.BackupService
kvStore *bolt.KVStore
kvService *kv.Service
tenantService *tenant.Service
metaClient *meta.Client
logger *zap.Logger
@ -129,7 +131,11 @@ func (b *cmdBackupBuilder) backupRunE(cmd *cobra.Command, args []string) (err er
// Open meta store so we can iterate over meta data.
b.kvStore = bolt.NewKVStore(b.logger, filepath.Join(b.path, b.kvPath()))
b.kvStore.WithDB(boltClient.DB())
b.kvService = kv.NewService(b.logger, b.kvStore, kv.ServiceConfig{})
tenantStore := tenant.NewStore(b.kvStore)
b.tenantService = tenant.NewService(tenantStore)
b.kvService = kv.NewService(b.logger, b.kvStore, b.tenantService, kv.ServiceConfig{})
b.metaClient = meta.NewClient(meta.NewConfig(), b.kvStore)
if err := b.metaClient.Open(); err != nil {
@ -196,7 +202,7 @@ func (b *cmdBackupBuilder) backupOrganizations(ctx context.Context) (err error)
}
// Retrieve a list of all matching organizations.
orgs, _, err := b.kvService.FindOrganizations(ctx, filter)
orgs, _, err := b.tenantService.FindOrganizations(ctx, filter)
if err != nil {
return err
}
@ -224,7 +230,7 @@ func (b *cmdBackupBuilder) backupBuckets(ctx context.Context, org *influxdb.Orga
}
// Retrieve a list of all matching organizations.
buckets, _, err := b.kvService.FindBuckets(ctx, filter)
buckets, _, err := b.tenantService.FindBuckets(ctx, filter)
if err != nil {
return err
}

View File

@ -6,7 +6,7 @@ import (
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/cmd/internal"
"github.com/influxdata/influxdb/v2/http"
"github.com/influxdata/influxdb/v2/tenant"
"github.com/spf13/cobra"
)
@ -352,7 +352,7 @@ func newBucketSVCs() (influxdb.BucketService, influxdb.OrganizationService, erro
return nil, nil, err
}
orgSvc := &http.OrganizationService{Client: httpClient}
orgSvc := &tenant.OrgClientService{Client: httpClient}
return &http.BucketService{Client: httpClient}, orgSvc, nil
return &tenant.BucketClientService{Client: httpClient}, orgSvc, nil
}

View File

@ -20,6 +20,7 @@ import (
"github.com/influxdata/influxdb/v2/internal/fs"
"github.com/influxdata/influxdb/v2/kit/cli"
"github.com/influxdata/influxdb/v2/pkg/httpc"
"github.com/influxdata/influxdb/v2/tenant"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@ -441,11 +442,13 @@ func writeConfigToPath(tok, org, path, dir string) error {
}
func checkSetup(host string, skipVerify bool) error {
s := &http.SetupService{
Addr: host,
InsecureSkipVerify: skipVerify,
httpClient, err := newHTTPClient()
if err != nil {
return err
}
s := &tenant.OnboardClientService{Client: httpClient}
isOnboarding, err := s.IsOnboarding(context.Background())
if err != nil {
return err

View File

@ -6,7 +6,7 @@ import (
"io"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/http"
"github.com/influxdata/influxdb/v2/tenant"
"github.com/spf13/cobra"
)
@ -525,9 +525,9 @@ func newOrgServices() (influxdb.OrganizationService, influxdb.UserResourceMappin
return nil, nil, nil, err
}
orgSVC := &http.OrganizationService{Client: client}
urmSVC := &http.UserResourceMappingService{Client: client}
userSVC := &http.UserService{Client: client}
orgSVC := &tenant.OrgClientService{Client: client}
urmSVC := &tenant.UserResourceMappingClient{Client: client}
userSVC := &tenant.UserClientService{Client: client}
return orgSVC, urmSVC, userSVC, nil
}
@ -538,7 +538,7 @@ func newOrganizationService() (influxdb.OrganizationService, error) {
return nil, err
}
return &http.OrganizationService{
return &tenant.OrgClientService{
Client: client,
}, nil
}

View File

@ -14,8 +14,8 @@ import (
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/bolt"
"github.com/influxdata/influxdb/v2/http"
"github.com/influxdata/influxdb/v2/kv"
influxlogger "github.com/influxdata/influxdb/v2/logger"
"github.com/influxdata/influxdb/v2/tenant"
"github.com/influxdata/influxdb/v2/v1/services/meta"
"github.com/spf13/cobra"
"go.uber.org/zap"
@ -40,10 +40,10 @@ type cmdRestoreBuilder struct {
kvEntry *influxdb.ManifestKVEntry
shardEntries map[uint64]*influxdb.ManifestEntry
orgService *http.OrganizationService
bucketService *http.BucketService
orgService *tenant.OrgClientService
bucketService *tenant.BucketClientService
restoreService *http.RestoreService
kvService *kv.Service
tenantService *tenant.Service
metaClient *meta.Client
logger *zap.Logger
@ -123,8 +123,8 @@ func (b *cmdRestoreBuilder) restoreRunE(cmd *cobra.Command, args []string) (err
return err
}
b.orgService = &http.OrganizationService{Client: client}
b.bucketService = &http.BucketService{Client: client}
b.orgService = &tenant.OrgClientService{Client: client}
b.bucketService = &tenant.BucketClientService{Client: client}
if !b.full {
return b.restorePartial(ctx)
@ -177,7 +177,9 @@ func (b *cmdRestoreBuilder) restorePartial(ctx context.Context) (err error) {
// Open meta store so we can iterate over meta data.
kvStore := bolt.NewKVStore(b.logger, boltClient.Path)
kvStore.WithDB(boltClient.DB())
b.kvService = kv.NewService(b.logger, kvStore, kv.ServiceConfig{})
tenantStore := tenant.NewStore(kvStore)
b.tenantService = tenant.NewService(tenantStore)
b.metaClient = meta.NewClient(meta.NewConfig(), kvStore)
if err := b.metaClient.Open(); err != nil {
@ -206,7 +208,7 @@ func (b *cmdRestoreBuilder) restoreOrganizations(ctx context.Context) (err error
}
// Retrieve a list of all matching organizations.
orgs, _, err := b.kvService.FindOrganizations(ctx, filter)
orgs, _, err := b.tenantService.FindOrganizations(ctx, filter)
if err != nil {
return err
}
@ -251,7 +253,7 @@ func (b *cmdRestoreBuilder) restoreOrganization(ctx context.Context, org *influx
}
// Retrieve a list of all buckets for the organization in the local backup.
buckets, _, err := b.kvService.FindBuckets(ctx, filter)
buckets, _, err := b.tenantService.FindBuckets(ctx, filter)
if err != nil {
return err
}

View File

@ -6,7 +6,8 @@ import (
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/cmd/internal"
"github.com/influxdata/influxdb/v2/http"
isecret "github.com/influxdata/influxdb/v2/secret"
"github.com/influxdata/influxdb/v2/tenant"
"github.com/spf13/cobra"
"github.com/tcnksm/go-input"
)
@ -244,7 +245,8 @@ func newSecretSVCs() (influxdb.SecretService, influxdb.OrganizationService, func
if err != nil {
return nil, nil, nil, err
}
orgSvc := &http.OrganizationService{Client: httpClient}
return &http.SecretService{Client: httpClient}, orgSvc, internal.GetSecret, nil
orgSvc := &tenant.OrgClientService{Client: httpClient}
return &isecret.Client{Client: httpClient}, orgSvc, internal.GetSecret, nil
}

View File

@ -21,9 +21,9 @@ import (
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/cmd/influx/internal"
internal2 "github.com/influxdata/influxdb/v2/cmd/internal"
ihttp "github.com/influxdata/influxdb/v2/http"
ierror "github.com/influxdata/influxdb/v2/kit/errors"
"github.com/influxdata/influxdb/v2/pkger"
"github.com/influxdata/influxdb/v2/tenant"
"github.com/olekukonko/tablewriter"
"github.com/spf13/cobra"
input "github.com/tcnksm/go-input"
@ -1235,7 +1235,7 @@ func newPkgerSVC() (pkger.SVC, influxdb.OrganizationService, error) {
return nil, nil, err
}
orgSvc := &ihttp.OrganizationService{
orgSvc := &tenant.OrgClientService{
Client: httpClient,
}

View File

@ -8,6 +8,7 @@ import (
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/cmd/internal"
"github.com/influxdata/influxdb/v2/http"
"github.com/influxdata/influxdb/v2/tenant"
"github.com/spf13/cobra"
"github.com/tcnksm/go-input"
)
@ -362,10 +363,10 @@ func newUserSVC() (cmdUserDeps, error) {
if err != nil {
return cmdUserDeps{}, err
}
userSvc := &http.UserService{Client: httpClient}
orgSvc := &http.OrganizationService{Client: httpClient}
passSvc := &http.PasswordService{Client: httpClient}
urmSvc := &http.UserResourceMappingService{Client: httpClient}
userSvc := &tenant.UserClientService{Client: httpClient}
orgSvc := &tenant.OrgClientService{Client: httpClient}
passSvc := &tenant.PasswordClientService{Client: httpClient}
urmSvc := &tenant.UserResourceMappingClient{Client: httpClient}
getPassFn := internal.GetPassword
return cmdUserDeps{

View File

@ -735,28 +735,23 @@ func (m *Launcher) run(ctx context.Context) (err error) {
return err
}
serviceConfig := kv.ServiceConfig{
SessionLength: time.Duration(m.sessionLength) * time.Minute,
FluxLanguageService: fluxlang.DefaultService,
}
flushers := flushers{}
var flushers flushers
switch m.storeType {
case BoltStore:
store := bolt.NewKVStore(m.log.With(zap.String("service", "kvstore-bolt")), m.boltPath)
store.WithDB(m.boltClient.DB())
m.kvStore = store
m.kvService = kv.NewService(m.log.With(zap.String("store", "kv")), store, serviceConfig)
if m.testing {
flushers = append(flushers, store)
}
case MemoryStore:
store := inmem.NewKVStore()
m.kvStore = store
m.kvService = kv.NewService(m.log.With(zap.String("store", "kv")), store, serviceConfig)
if m.testing {
flushers = append(flushers, store)
}
default:
err := fmt.Errorf("unknown store type %s; expected bolt or memory", m.storeType)
m.log.Error("Failed opening bolt", zap.Error(err))
@ -786,18 +781,27 @@ func (m *Launcher) run(ctx context.Context) (err error) {
)
m.reg.MustRegister(m.boltClient)
var (
variableSvc platform.VariableService = m.kvService
sourceSvc platform.SourceService = m.kvService
userLogSvc platform.UserOperationLogService = m.kvService
bucketLogSvc platform.BucketOperationLogService = m.kvService
orgLogSvc platform.OrganizationOperationLogService = m.kvService
scraperTargetSvc platform.ScraperTargetStoreService = m.kvService
)
tenantStore := tenant.NewStore(m.kvStore)
ts := tenant.NewSystem(tenantStore, m.log.With(zap.String("store", "new")), m.reg, metric.WithSuffix("new"))
serviceConfig := kv.ServiceConfig{
FluxLanguageService: fluxlang.DefaultService,
}
m.kvService = kv.NewService(m.log.With(zap.String("store", "kv")), m.kvStore, ts, serviceConfig)
var (
opLogSvc = tenant.NewOpLogService(m.kvStore, m.kvService)
userLogSvc platform.UserOperationLogService = opLogSvc
bucketLogSvc platform.BucketOperationLogService = opLogSvc
orgLogSvc platform.OrganizationOperationLogService = opLogSvc
)
var (
variableSvc platform.VariableService = m.kvService
sourceSvc platform.SourceService = m.kvService
scraperTargetSvc platform.ScraperTargetStoreService = m.kvService
)
var authSvc platform.AuthorizationService
{
authStore, err := authorization.NewStore(m.kvStore)
@ -914,7 +918,14 @@ func (m *Launcher) run(ctx context.Context) (err error) {
var taskSvc platform.TaskService
{
// create the task stack
combinedTaskService := taskbackend.NewAnalyticalStorage(m.log.With(zap.String("service", "task-analytical-store")), m.kvService, m.kvService, m.kvService, pointsWriter, query.QueryServiceBridge{AsyncQueryService: m.queryController})
combinedTaskService := taskbackend.NewAnalyticalStorage(
m.log.With(zap.String("service", "task-analytical-store")),
m.kvService,
ts.BucketService,
m.kvService,
pointsWriter,
query.QueryServiceBridge{AsyncQueryService: m.queryController},
)
executor, executorMetrics := executor.NewExecutor(
m.log.With(zap.String("service", "task-executor")),
@ -1007,7 +1018,7 @@ func (m *Launcher) run(ctx context.Context) (err error) {
var checkSvc platform.CheckService
{
coordinator := coordinator.NewCoordinator(m.log, m.scheduler, m.executor)
checkSvc = checks.NewService(m.log.With(zap.String("svc", "checks")), m.kvStore, m.kvService, m.kvService)
checkSvc = checks.NewService(m.log.With(zap.String("svc", "checks")), m.kvStore, ts.OrganizationService, m.kvService)
checkSvc = middleware.NewCheckService(checkSvc, m.kvService, coordinator)
}
@ -1111,8 +1122,7 @@ func (m *Launcher) run(ctx context.Context) (err error) {
m.log.Error("Failed creating new labels store", zap.Error(err))
return err
}
ls := label.NewService(labelsStore)
labelSvc = label.NewLabelController(m.flagger, m.kvService, ls)
labelSvc = label.NewService(labelsStore)
}
ts.BucketService = storage.NewBucketService(m.log, ts.BucketService, m.engine)
@ -1228,7 +1238,6 @@ func (m *Launcher) run(ctx context.Context) (err error) {
ChronografService: chronografSvc,
SecretService: secretSvc,
LookupService: resourceResolver,
DocumentService: m.kvService,
OrgLookupService: resourceResolver,
WriteEventRecorder: infprom.NewEventRecorder("write"),
QueryEventRecorder: infprom.NewEventRecorder("query"),
@ -1252,7 +1261,7 @@ func (m *Launcher) run(ctx context.Context) (err error) {
pkger.WithBucketSVC(authorizer.NewBucketService(b.BucketService)),
pkger.WithCheckSVC(authorizer.NewCheckService(b.CheckService, authedUrmSVC, authedOrgSVC)),
pkger.WithDashboardSVC(authorizer.NewDashboardService(b.DashboardService)),
pkger.WithLabelSVC(authorizer.NewLabelServiceWithOrg(b.LabelService, b.OrgLookupService)),
pkger.WithLabelSVC(label.NewAuthedLabelService(labelSvc, b.OrgLookupService)),
pkger.WithNotificationEndpointSVC(authorizer.NewNotificationEndpointService(b.NotificationEndpointService, authedUrmSVC, authedOrgSVC)),
pkger.WithNotificationRuleSVC(authorizer.NewNotificationRuleStore(b.NotificationRuleStore, authedUrmSVC, authedOrgSVC)),
pkger.WithOrganizationService(authorizer.NewOrgService(b.OrganizationService)),
@ -1283,12 +1292,9 @@ func (m *Launcher) run(ctx context.Context) (err error) {
onboardHTTPServer := tenant.NewHTTPOnboardHandler(m.log, onboardSvc)
// feature flagging for new labels service
var oldLabelHandler nethttp.Handler
var labelHandler *label.LabelHandler
{
b := m.apibackend
labelSvcWithOrg := authorizer.NewLabelServiceWithOrg(labelSvc, b.OrgLookupService)
oldLabelHandler = http.NewLabelHandler(m.log.With(zap.String("handler", "labels")), labelSvcWithOrg, kithttp.ErrorHandler(0))
labelSvc = label.NewAuthedLabelService(labelSvc, b.OrgLookupService)
labelSvc = label.NewLabelLogger(m.log.With(zap.String("handler", "labels")), labelSvc)
@ -1363,7 +1369,7 @@ func (m *Launcher) run(ctx context.Context) (err error) {
http.WithResourceHandler(templatesHTTPServer),
http.WithResourceHandler(onboardHTTPServer),
http.WithResourceHandler(authHTTPServer),
http.WithResourceHandler(kithttp.NewFeatureHandler(feature.NewLabelPackage(), m.flagger, oldLabelHandler, labelHandler, labelHandler.Prefix())),
http.WithResourceHandler(labelHandler),
http.WithResourceHandler(sessionHTTPServer.SignInResourceHandler()),
http.WithResourceHandler(sessionHTTPServer.SignOutResourceHandler()),
http.WithResourceHandler(userHTTPServer.MeResourceHandler()),

View File

@ -22,10 +22,12 @@ import (
dashboardTransport "github.com/influxdata/influxdb/v2/dashboards/transport"
"github.com/influxdata/influxdb/v2/http"
"github.com/influxdata/influxdb/v2/kit/feature"
"github.com/influxdata/influxdb/v2/label"
"github.com/influxdata/influxdb/v2/mock"
"github.com/influxdata/influxdb/v2/pkg/httpc"
"github.com/influxdata/influxdb/v2/pkger"
"github.com/influxdata/influxdb/v2/query"
"github.com/influxdata/influxdb/v2/tenant"
dto "github.com/prometheus/client_model/go"
"github.com/prometheus/common/expfmt"
)
@ -363,9 +365,9 @@ func (tl *TestLauncher) FluxQueryService() *http.FluxQueryService {
return &http.FluxQueryService{Addr: tl.URL(), Token: tl.Auth.Token}
}
func (tl *TestLauncher) BucketService(tb testing.TB) *http.BucketService {
func (tl *TestLauncher) BucketService(tb testing.TB) *tenant.BucketClientService {
tb.Helper()
return &http.BucketService{Client: tl.HTTPClient(tb)}
return &tenant.BucketClientService{Client: tl.HTTPClient(tb)}
}
func (tl *TestLauncher) DashboardService(tb testing.TB) influxdb.DashboardService {
@ -373,9 +375,9 @@ func (tl *TestLauncher) DashboardService(tb testing.TB) influxdb.DashboardServic
return &dashboardTransport.DashboardService{Client: tl.HTTPClient(tb)}
}
func (tl *TestLauncher) LabelService(tb testing.TB) *http.LabelService {
func (tl *TestLauncher) LabelService(tb testing.TB) influxdb.LabelService {
tb.Helper()
return &http.LabelService{Client: tl.HTTPClient(tb)}
return &label.LabelClientService{Client: tl.HTTPClient(tb)}
}
func (tl *TestLauncher) NotificationEndpointService(tb testing.TB) *http.NotificationEndpointService {
@ -389,7 +391,8 @@ func (tl *TestLauncher) NotificationRuleService(tb testing.TB) influxdb.Notifica
}
func (tl *TestLauncher) OrgService(tb testing.TB) influxdb.OrganizationService {
return tl.kvService
tb.Helper()
return &tenant.OrgClientService{Client: tl.HTTPClient(tb)}
}
func (tl *TestLauncher) PkgerService(tb testing.TB) pkger.SVC {

View File

@ -9,8 +9,9 @@ import (
platform "github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/cmd/influxd/launcher"
"github.com/influxdata/influxdb/v2/http"
_ "github.com/influxdata/influxdb/v2/fluxinit/static"
"github.com/influxdata/influxdb/v2/http"
"github.com/influxdata/influxdb/v2/tenant"
)
// Default context.
@ -23,7 +24,12 @@ func TestLauncher_Setup(t *testing.T) {
}
defer l.Shutdown(ctx)
svc := &http.SetupService{Addr: l.URL()}
client, err := http.NewHTTPClient(l.URL(), "", false)
if err != nil {
t.Fatal(err)
}
svc := &tenant.OnboardClientService{Client: client}
if results, err := svc.OnboardInitialUser(ctx, &platform.OnboardingRequest{
User: "USER",
Password: "PASSWORD",

View File

@ -827,7 +827,7 @@ func TestLauncher_Pkger(t *testing.T) {
pkger.WithLabelSVC(&fakeLabelSVC{
// can't use the LabelService HTTP client b/c it doesn't cover the
// all the resources pkger supports... :sadpanda:
LabelService: l.kvService,
LabelService: l.LabelService(t),
createKillCount: -1,
deleteKillCount: 3,
}),
@ -866,6 +866,7 @@ func TestLauncher_Pkger(t *testing.T) {
ResourceType: res.resourceType,
})
require.NoError(t, err)
assert.Len(t, mappedLabels, 1, res.resourceType)
if len(mappedLabels) == 1 {
assert.Equal(t, sum.Labels[0].ID, pkger.SafeID(mappedLabels[0].ID))
@ -1561,7 +1562,7 @@ func TestLauncher_Pkger(t *testing.T) {
require.Len(t, updateSum.Labels, 1)
initial, updated := initialSum.Labels[0], updateSum.Labels[0]
assert.NotEqual(t, initial.ID, updated.ID)
assert.NotEqual(t, initial.ID, updated.ID, "label ID should be different")
initial.ID, updated.ID = 0, 0
assert.Equal(t, initial, updated)
})
@ -2246,11 +2247,7 @@ func TestLauncher_Pkger(t *testing.T) {
stack, initialSummary, cleanup := newLabelAssociationTestFn(t)
defer cleanup()
// TODO(jsteenb2): cannot figure out the issue with the http.LabelService returning
// the bad HTTP method error :-(. Revisit this and replace with the
// the HTTP client when possible. Goal is to remove the kvservice
// dep here.
err := l.kvService.DeleteLabelMapping(ctx, &influxdb.LabelMapping{
err := l.LabelService(t).DeleteLabelMapping(ctx, &influxdb.LabelMapping{
LabelID: influxdb.ID(initialSummary.Labels[0].ID),
ResourceID: influxdb.ID(initialSummary.Buckets[0].ID),
ResourceType: influxdb.BucketsResourceType,
@ -2274,14 +2271,10 @@ func TestLauncher_Pkger(t *testing.T) {
OrgID: l.Org.ID,
Name: "test-label-2",
}
require.NoError(t, l.kvService.CreateLabel(ctx, newLabel))
require.NoError(t, l.LabelService(t).CreateLabel(ctx, newLabel))
defer resourceCheck.mustDeleteLabel(t, newLabel.ID)
// TODO(jsteenb2): cannot figure out the issue with the http.LabelService returning
// the bad HTTP method error :-(. Revisit this and replace with the
// the HTTP client when possible. Goal is to remove the kvservice
// dep here.
err := l.kvService.CreateLabelMapping(ctx, &influxdb.LabelMapping{
err := l.LabelService(t).CreateLabelMapping(ctx, &influxdb.LabelMapping{
LabelID: newLabel.ID,
ResourceID: influxdb.ID(initialSummary.Buckets[0].ID),
ResourceType: influxdb.BucketsResourceType,

View File

@ -2,7 +2,6 @@ package upgrade
import (
"context"
"github.com/dustin/go-humanize"
"io/ioutil"
"net/http"
"net/url"
@ -11,6 +10,8 @@ import (
"strconv"
"testing"
"github.com/dustin/go-humanize"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/bolt"
"github.com/influxdata/influxdb/v2/cmd/influxd/launcher"
@ -161,7 +162,7 @@ func TestUpgradeRealDB(t *testing.T) {
}
}
auths, _, err := v2.kvService.FindAuthorizations(ctx, influxdb.AuthorizationFilter{})
auths, _, err := v2.authSvcV2.FindAuthorizations(ctx, influxdb.AuthorizationFilter{})
require.Nil(t, err)
require.Len(t, auths, 1)

View File

@ -10,8 +10,8 @@ import (
"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/kv"
"github.com/influxdata/influxdb/v2/kv/migration"
"github.com/influxdata/influxdb/v2/kv/migration/all"
"github.com/influxdata/influxdb/v2/tenant"
@ -162,13 +162,22 @@ func TestUpgradeSecurity(t *testing.T) {
require.NoError(t, err)
err = migrator.Up(ctx)
require.NoError(t, err)
authStore, err := authv1.NewStore(kvStore)
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(authStore, tenantSvc),
onboardSvc: tenant.NewOnboardService(tenantSvc, kv.NewService(zap.NewNop(), kvStore, kv.ServiceConfig{})),
authSvc: authv1.NewService(authStoreV1, tenantSvc),
onboardSvc: tenant.NewOnboardService(
tenantSvc,
authorization.NewService(authStoreV2, tenantSvc),
),
}
// onboard admin

View File

@ -12,6 +12,7 @@ import (
"strings"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/authorization"
"github.com/influxdata/influxdb/v2/bolt"
"github.com/influxdata/influxdb/v2/dbrp"
"github.com/influxdata/influxdb/v2/fluxinit"
@ -275,7 +276,7 @@ type influxDBv2 struct {
bucketSvc influxdb.BucketService
onboardSvc influxdb.OnboardingService
authSvc *authv1.Service
kvService *kv.Service
authSvcV2 influxdb.AuthorizationService
meta *meta.Client
}
@ -478,10 +479,6 @@ func newInfluxDBv2(ctx context.Context, opts *optionsV2, log *zap.Logger) (svc *
svc = &influxDBv2{}
svc.log = log
// *********************
// V2 specific services
serviceConfig := kv.ServiceConfig{}
// Create BoltDB store and K/V service
svc.boltClient = bolt.NewClient(log.With(zap.String("service", "bolt")))
svc.boltClient.Path = opts.boltPath
@ -493,7 +490,6 @@ func newInfluxDBv2(ctx context.Context, opts *optionsV2, log *zap.Logger) (svc *
svc.store = bolt.NewKVStore(log.With(zap.String("service", "kvstore-bolt")), opts.boltPath)
svc.store.WithDB(svc.boltClient.DB())
svc.kvStore = svc.store
svc.kvService = kv.NewService(log.With(zap.String("store", "kv")), svc.store, serviceConfig)
// ensure migrator is run
migrator, err := migration.NewMigrator(
@ -512,11 +508,6 @@ func newInfluxDBv2(ctx context.Context, opts *optionsV2, log *zap.Logger) (svc *
return nil, err
}
// other required services
var (
authSvc influxdb.AuthorizationService = svc.kvService
)
// Create Tenant service (orgs, buckets, )
svc.tenantStore = tenant.NewStore(svc.kvStore)
svc.ts = tenant.NewSystem(svc.tenantStore, log.With(zap.String("store", "new")), reg, metric.WithSuffix("new"))
@ -537,15 +528,24 @@ func newInfluxDBv2(ctx context.Context, opts *optionsV2, log *zap.Logger) (svc *
)
svc.ts.BucketService = storage.NewBucketService(log, svc.ts.BucketService, engine)
// on-boarding service (influx setup)
svc.onboardSvc = tenant.NewOnboardService(svc.ts, authSvc)
// v1 auth service
authStore, err := authv1.NewStore(svc.kvStore)
authStoreV2, err := authorization.NewStore(svc.store)
if err != nil {
return nil, err
}
svc.authSvc = authv1.NewService(authStore, svc.ts)
svc.authSvcV2 = authorization.NewService(authStoreV2, svc.ts)
// on-boarding service (influx setup)
svc.onboardSvc = tenant.NewOnboardService(svc.ts, svc.authSvcV2)
// v1 auth service
authStoreV1, err := authv1.NewStore(svc.kvStore)
if err != nil {
return nil, err
}
svc.authSvc = authv1.NewService(authStoreV1, svc.ts)
return svc, nil
}

View File

@ -12,6 +12,7 @@ import (
dashboardtesting "github.com/influxdata/influxdb/v2/dashboards/testing"
"github.com/influxdata/influxdb/v2/kv"
"github.com/influxdata/influxdb/v2/kv/migration/all"
"github.com/influxdata/influxdb/v2/mock"
"go.uber.org/zap/zaptest"
)
@ -38,7 +39,7 @@ func initDashboardService(s kv.SchemaStore, f dashboardtesting.DashboardFields,
}
ctx := context.Background()
kvSvc := kv.NewService(zaptest.NewLogger(t), s)
kvSvc := kv.NewService(zaptest.NewLogger(t), s, &mock.OrganizationService{})
kvSvc.IDGenerator = f.IDGenerator
kvSvc.TimeGenerator = f.TimeGenerator

View File

@ -4,8 +4,7 @@ import (
"context"
"testing"
platform "github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/kv"
"github.com/influxdata/influxdb/v2"
)
// TODO(goller): remove opPrefix argument
@ -29,14 +28,14 @@ func ErrorsEqual(t *testing.T, actual, expected error) {
t.Errorf("expected error %s but received nil", expected.Error())
}
if platform.ErrorCode(expected) != platform.ErrorCode(actual) {
if influxdb.ErrorCode(expected) != influxdb.ErrorCode(actual) {
t.Logf("\nexpected: %v\nactual: %v\n\n", expected, actual)
t.Errorf("expected error code %q but received %q", platform.ErrorCode(expected), platform.ErrorCode(actual))
t.Errorf("expected error code %q but received %q", influxdb.ErrorCode(expected), influxdb.ErrorCode(actual))
}
if platform.ErrorMessage(expected) != platform.ErrorMessage(actual) {
if influxdb.ErrorMessage(expected) != influxdb.ErrorMessage(actual) {
t.Logf("\nexpected: %v\nactual: %v\n\n", expected, actual)
t.Errorf("expected error message %q but received %q", platform.ErrorMessage(expected), platform.ErrorMessage(actual))
t.Errorf("expected error message %q but received %q", influxdb.ErrorMessage(expected), influxdb.ErrorMessage(actual))
}
}
@ -47,13 +46,13 @@ func FloatPtr(f float64) *float64 {
return p
}
func idPtr(id platform.ID) *platform.ID {
func idPtr(id influxdb.ID) *influxdb.ID {
return &id
}
// MustIDBase16 is an helper to ensure a correct ID is built during testing.
func MustIDBase16(s string) platform.ID {
id, err := platform.IDFromString(s)
func MustIDBase16(s string) influxdb.ID {
id, err := influxdb.IDFromString(s)
if err != nil {
panic(err)
}
@ -61,12 +60,12 @@ func MustIDBase16(s string) platform.ID {
}
// MustIDBase16Ptr is an helper to ensure a correct ID ptr ref is built during testing.
func MustIDBase16Ptr(s string) *platform.ID {
func MustIDBase16Ptr(s string) *influxdb.ID {
id := MustIDBase16(s)
return &id
}
func MustCreateOrgs(ctx context.Context, svc *kv.Service, os ...*platform.Organization) {
func MustCreateOrgs(ctx context.Context, svc influxdb.OrganizationService, os ...*influxdb.Organization) {
for _, o := range os {
if err := svc.CreateOrganization(ctx, o); err != nil {
panic(err)
@ -74,7 +73,7 @@ func MustCreateOrgs(ctx context.Context, svc *kv.Service, os ...*platform.Organi
}
}
func MustCreateLabels(ctx context.Context, svc *kv.Service, labels ...*platform.Label) {
func MustCreateLabels(ctx context.Context, svc influxdb.LabelService, labels ...*influxdb.Label) {
for _, l := range labels {
if err := svc.CreateLabel(ctx, l); err != nil {
panic(err)
@ -82,7 +81,7 @@ func MustCreateLabels(ctx context.Context, svc *kv.Service, labels ...*platform.
}
}
func MustCreateUsers(ctx context.Context, svc *kv.Service, us ...*platform.User) {
func MustCreateUsers(ctx context.Context, svc influxdb.UserService, us ...*influxdb.User) {
for _, u := range us {
if err := svc.CreateUser(ctx, u); err != nil {
panic(err)
@ -90,7 +89,7 @@ func MustCreateUsers(ctx context.Context, svc *kv.Service, us ...*platform.User)
}
}
func MustCreateMappings(ctx context.Context, svc *kv.Service, ms ...*platform.UserResourceMapping) {
func MustCreateMappings(ctx context.Context, svc influxdb.UserResourceMappingService, ms ...*influxdb.UserResourceMapping) {
for _, m := range ms {
if err := svc.CreateUserResourceMapping(ctx, m); err != nil {
panic(err)
@ -98,34 +97,34 @@ func MustCreateMappings(ctx context.Context, svc *kv.Service, ms ...*platform.Us
}
}
func MustMakeUsersOrgOwner(ctx context.Context, svc *kv.Service, oid platform.ID, uids ...platform.ID) {
ms := make([]*platform.UserResourceMapping, len(uids))
func MustMakeUsersOrgOwner(ctx context.Context, svc influxdb.UserResourceMappingService, oid influxdb.ID, uids ...influxdb.ID) {
ms := make([]*influxdb.UserResourceMapping, len(uids))
for i, uid := range uids {
ms[i] = &platform.UserResourceMapping{
ms[i] = &influxdb.UserResourceMapping{
UserID: uid,
UserType: platform.Owner,
ResourceType: platform.OrgsResourceType,
UserType: influxdb.Owner,
ResourceType: influxdb.OrgsResourceType,
ResourceID: oid,
}
}
MustCreateMappings(ctx, svc, ms...)
}
func MustMakeUsersOrgMember(ctx context.Context, svc *kv.Service, oid platform.ID, uids ...platform.ID) {
ms := make([]*platform.UserResourceMapping, len(uids))
func MustMakeUsersOrgMember(ctx context.Context, svc influxdb.UserResourceMappingService, oid influxdb.ID, uids ...influxdb.ID) {
ms := make([]*influxdb.UserResourceMapping, len(uids))
for i, uid := range uids {
ms[i] = &platform.UserResourceMapping{
ms[i] = &influxdb.UserResourceMapping{
UserID: uid,
UserType: platform.Member,
ResourceType: platform.OrgsResourceType,
UserType: influxdb.Member,
ResourceType: influxdb.OrgsResourceType,
ResourceID: oid,
}
}
MustCreateMappings(ctx, svc, ms...)
}
func MustNewPermissionAtID(id platform.ID, a platform.Action, rt platform.ResourceType, orgID platform.ID) *platform.Permission {
perm, err := platform.NewPermissionAtID(id, a, rt, orgID)
func MustNewPermissionAtID(id influxdb.ID, a influxdb.Action, rt influxdb.ResourceType, orgID influxdb.ID) *influxdb.Permission {
perm, err := influxdb.NewPermissionAtID(id, a, rt, orgID)
if err != nil {
panic(err)
}

View File

@ -1784,7 +1784,7 @@ func initDashboardService(f dashboardstesting.DashboardFields, t *testing.T) (in
log := zaptest.NewLogger(t)
store := newTestInmemStore(t)
kvsvc := kv.NewService(log, store)
kvsvc := kv.NewService(log, store, &mock.OrganizationService{})
kvsvc.IDGenerator = f.IDGenerator
svc := dashboards.NewService(

View File

@ -1,9 +1,11 @@
package http
import (
"context"
"net/http"
"github.com/go-chi/chi"
"github.com/influxdata/httprouter"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/authorizer"
"github.com/influxdata/influxdb/v2/chronograf/server"
@ -142,10 +144,6 @@ func NewAPIHandler(b *APIBackend, opts ...APIHandlerOptFn) *APIHandler {
deleteBackend := NewDeleteBackend(b.Logger.With(zap.String("handler", "delete")), b)
h.Mount(prefixDelete, NewDeleteHandler(b.Logger, deleteBackend))
documentBackend := NewDocumentBackend(b.Logger.With(zap.String("handler", "document")), b)
documentBackend.DocumentService = authorizer.NewDocumentService(b.DocumentService)
h.Mount(prefixDocuments, NewDocumentHandler(documentBackend))
fluxBackend := NewFluxBackend(b.Logger.With(zap.String("handler", "query")), b)
h.Mount(prefixQuery, NewFluxHandler(b.Logger, fluxBackend))
@ -268,3 +266,25 @@ func serveLinksHandler(errorHandler influxdb.HTTPErrorHandler) http.Handler {
}
return http.HandlerFunc(fn)
}
func decodeIDFromCtx(ctx context.Context, name string) (influxdb.ID, error) {
params := httprouter.ParamsFromContext(ctx)
idStr := params.ByName(name)
if idStr == "" {
return 0, &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "url missing " + name,
}
}
var i influxdb.ID
if err := i.DecodeFromString(idStr); err != nil {
return 0, &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
}
}
return i, nil
}

View File

@ -10,7 +10,7 @@ import (
"github.com/google/go-cmp/cmp"
kithttp "github.com/influxdata/influxdb/v2/kit/transport/http"
"github.com/influxdata/influxdb/v2/kv"
"github.com/influxdata/influxdb/v2/pkg/httpc"
"github.com/yudai/gojsondiff"
"github.com/yudai/gojsondiff/formatter"
"go.uber.org/zap/zaptest"
@ -120,8 +120,12 @@ func jsonEqual(s1, s2 string) (eq bool, diff string, err error) {
return cmp.Equal(o1, o2), diff, err
}
func newInMemKVSVC(t *testing.T) *kv.Service {
func mustNewHTTPClient(t *testing.T, addr, token string) *httpc.Client {
t.Helper()
return kv.NewService(zaptest.NewLogger(t), NewTestInmemStore(t))
httpClient, err := NewHTTPClient(addr, token, false)
if err != nil {
t.Fatal(err)
}
return httpClient
}

View File

@ -12,10 +12,12 @@ import (
"github.com/influxdata/httprouter"
platform "github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/authorization"
pcontext "github.com/influxdata/influxdb/v2/context"
kithttp "github.com/influxdata/influxdb/v2/kit/transport/http"
"github.com/influxdata/influxdb/v2/kv"
"github.com/influxdata/influxdb/v2/mock"
"github.com/influxdata/influxdb/v2/tenant"
platformtesting "github.com/influxdata/influxdb/v2/testing"
"go.uber.org/zap/zaptest"
)
@ -869,8 +871,17 @@ func initAuthorizationService(f platformtesting.AuthorizationFields, t *testing.
}
store := NewTestInmemStore(t)
svc := kv.NewService(zaptest.NewLogger(t), store)
svc.OrgIDs = f.OrgIDGenerator
tenantStore := tenant.NewStore(store)
tenantStore.OrgIDGen = f.OrgIDGenerator
tenantService := tenant.NewService(tenantStore)
authStore, err := authorization.NewStore(store)
if err != nil {
t.Fatal(err)
}
authService := authorization.NewService(authStore, tenantService)
svc := kv.NewService(zaptest.NewLogger(t), store, tenantService)
svc.IDGenerator = f.IDGenerator
svc.TokenGenerator = f.TokenGenerator
svc.TimeGenerator = f.TimeGenerator
@ -878,14 +889,13 @@ func initAuthorizationService(f platformtesting.AuthorizationFields, t *testing.
ctx := context.Background()
for _, u := range f.Users {
if err := svc.PutUser(ctx, u); err != nil {
if err := tenantService.CreateUser(ctx, u); err != nil {
t.Fatalf("failed to populate users")
}
}
for _, o := range f.Orgs {
o.ID = svc.OrgIDs.ID()
if err := svc.PutOrganization(ctx, o); err != nil {
if err := tenantService.CreateOrganization(ctx, o); err != nil {
t.Fatalf("failed to populate orgs")
}
}
@ -893,7 +903,7 @@ func initAuthorizationService(f platformtesting.AuthorizationFields, t *testing.
var token string
for _, a := range f.Authorizations {
if err := svc.PutAuthorization(ctx, a); err != nil {
if err := authService.CreateAuthorization(ctx, a); err != nil {
t.Fatalf("failed to populate authorizations")
}
@ -908,9 +918,9 @@ func initAuthorizationService(f platformtesting.AuthorizationFields, t *testing.
authorizationBackend := NewMockAuthorizationBackend(t)
authorizationBackend.HTTPErrorHandler = kithttp.ErrorHandler(0)
authorizationBackend.AuthorizationService = svc
authorizationBackend.AuthorizationService = authService
authorizationBackend.UserService = mus
authorizationBackend.OrganizationService = svc
authorizationBackend.OrganizationService = tenantService
authorizationBackend.LookupService = &mock.LookupService{
NameFn: func(ctx context.Context, resource platform.ResourceType, id platform.ID) (string, error) {
switch resource {
@ -925,7 +935,7 @@ func initAuthorizationService(f platformtesting.AuthorizationFields, t *testing.
authZ := NewAuthorizationHandler(zaptest.NewLogger(t), authorizationBackend)
authN := NewAuthenticationHandler(zaptest.NewLogger(t), kithttp.ErrorHandler(0))
authN.AuthorizationService = svc
authN.AuthorizationService = authService
authN.Handler = authZ
authN.UserService = mus

View File

@ -1,752 +0,0 @@
package http
import (
"context"
"fmt"
"net/http"
"path"
"strings"
"time"
"github.com/influxdata/httprouter"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/kit/tracing"
kithttp "github.com/influxdata/influxdb/v2/kit/transport/http"
"github.com/influxdata/influxdb/v2/pkg/httpc"
"go.uber.org/zap"
)
// BucketBackend is all services and associated parameters required to construct
// the BucketHandler.
type BucketBackend struct {
log *zap.Logger
influxdb.HTTPErrorHandler
BucketService influxdb.BucketService
BucketOperationLogService influxdb.BucketOperationLogService
UserResourceMappingService influxdb.UserResourceMappingService
LabelService influxdb.LabelService
UserService influxdb.UserService
OrganizationService influxdb.OrganizationService
}
// NewBucketBackend returns a new instance of BucketBackend.
func NewBucketBackend(log *zap.Logger, b *APIBackend) *BucketBackend {
return &BucketBackend{
HTTPErrorHandler: b.HTTPErrorHandler,
log: log,
BucketService: b.BucketService,
BucketOperationLogService: b.BucketOperationLogService,
UserResourceMappingService: b.UserResourceMappingService,
LabelService: b.LabelService,
UserService: b.UserService,
OrganizationService: b.OrganizationService,
}
}
// BucketHandler represents an HTTP API handler for buckets.
type BucketHandler struct {
*httprouter.Router
api *kithttp.API
log *zap.Logger
BucketService influxdb.BucketService
BucketOperationLogService influxdb.BucketOperationLogService
UserResourceMappingService influxdb.UserResourceMappingService
LabelService influxdb.LabelService
UserService influxdb.UserService
OrganizationService influxdb.OrganizationService
}
const (
prefixBuckets = "/api/v2/buckets"
bucketsIDPath = "/api/v2/buckets/:id"
bucketsIDMembersPath = "/api/v2/buckets/:id/members"
bucketsIDMembersIDPath = "/api/v2/buckets/:id/members/:userID"
bucketsIDOwnersPath = "/api/v2/buckets/:id/owners"
bucketsIDOwnersIDPath = "/api/v2/buckets/:id/owners/:userID"
bucketsIDLabelsPath = "/api/v2/buckets/:id/labels"
bucketsIDLabelsIDPath = "/api/v2/buckets/:id/labels/:lid"
)
// NewBucketHandler returns a new instance of BucketHandler.
func NewBucketHandler(log *zap.Logger, b *BucketBackend) *BucketHandler {
h := &BucketHandler{
Router: NewRouter(b.HTTPErrorHandler),
api: kithttp.NewAPI(kithttp.WithLog(log)),
log: log,
BucketService: b.BucketService,
BucketOperationLogService: b.BucketOperationLogService,
UserResourceMappingService: b.UserResourceMappingService,
LabelService: b.LabelService,
UserService: b.UserService,
OrganizationService: b.OrganizationService,
}
h.HandlerFunc("POST", prefixBuckets, h.handlePostBucket)
h.HandlerFunc("GET", prefixBuckets, h.handleGetBuckets)
h.HandlerFunc("GET", bucketsIDPath, h.handleGetBucket)
h.HandlerFunc("PATCH", bucketsIDPath, h.handlePatchBucket)
h.HandlerFunc("DELETE", bucketsIDPath, h.handleDeleteBucket)
memberBackend := MemberBackend{
HTTPErrorHandler: b.HTTPErrorHandler,
log: b.log.With(zap.String("handler", "member")),
ResourceType: influxdb.BucketsResourceType,
UserType: influxdb.Member,
UserResourceMappingService: b.UserResourceMappingService,
UserService: b.UserService,
}
h.HandlerFunc("POST", bucketsIDMembersPath, newPostMemberHandler(memberBackend))
h.HandlerFunc("GET", bucketsIDMembersPath, newGetMembersHandler(memberBackend))
h.HandlerFunc("DELETE", bucketsIDMembersIDPath, newDeleteMemberHandler(memberBackend))
ownerBackend := MemberBackend{
HTTPErrorHandler: b.HTTPErrorHandler,
log: b.log.With(zap.String("handler", "member")),
ResourceType: influxdb.BucketsResourceType,
UserType: influxdb.Owner,
UserResourceMappingService: b.UserResourceMappingService,
UserService: b.UserService,
}
h.HandlerFunc("POST", bucketsIDOwnersPath, newPostMemberHandler(ownerBackend))
h.HandlerFunc("GET", bucketsIDOwnersPath, newGetMembersHandler(ownerBackend))
h.HandlerFunc("DELETE", bucketsIDOwnersIDPath, newDeleteMemberHandler(ownerBackend))
labelBackend := &LabelBackend{
HTTPErrorHandler: b.HTTPErrorHandler,
log: b.log.With(zap.String("handler", "label")),
LabelService: b.LabelService,
ResourceType: influxdb.BucketsResourceType,
}
h.HandlerFunc("GET", bucketsIDLabelsPath, newGetLabelsHandler(labelBackend))
h.HandlerFunc("POST", bucketsIDLabelsPath, newPostLabelHandler(labelBackend))
h.HandlerFunc("DELETE", bucketsIDLabelsIDPath, newDeleteLabelHandler(labelBackend))
return h
}
// bucket is used for serialization/deserialization with duration string syntax.
type bucket struct {
ID influxdb.ID `json:"id,omitempty"`
OrgID influxdb.ID `json:"orgID,omitempty"`
Type string `json:"type"`
Description string `json:"description,omitempty"`
Name string `json:"name"`
RetentionPolicyName string `json:"rp,omitempty"` // This to support v1 sources
RetentionRules []retentionRule `json:"retentionRules"`
influxdb.CRUDLog
}
// retentionRule is the retention rule action for a bucket.
type retentionRule struct {
Type string `json:"type"`
EverySeconds int64 `json:"everySeconds"`
}
func (rr *retentionRule) RetentionPeriod() (time.Duration, error) {
t := time.Duration(rr.EverySeconds) * time.Second
if t < time.Second {
return t, &influxdb.Error{
Code: influxdb.EUnprocessableEntity,
Msg: "expiration seconds must be greater than or equal to one second",
}
}
return t, nil
}
func (b *bucket) toInfluxDB() (*influxdb.Bucket, error) {
if b == nil {
return nil, nil
}
var d time.Duration // zero value implies infinite retention policy
// Only support a single retention period for the moment
if len(b.RetentionRules) > 0 {
d = time.Duration(b.RetentionRules[0].EverySeconds) * time.Second
if d < time.Second {
return nil, &influxdb.Error{
Code: influxdb.EUnprocessableEntity,
Msg: "expiration seconds must be greater than or equal to one second",
}
}
}
return &influxdb.Bucket{
ID: b.ID,
OrgID: b.OrgID,
Type: influxdb.ParseBucketType(b.Type),
Description: b.Description,
Name: b.Name,
RetentionPolicyName: b.RetentionPolicyName,
RetentionPeriod: d,
CRUDLog: b.CRUDLog,
}, nil
}
func newBucket(pb *influxdb.Bucket) *bucket {
if pb == nil {
return nil
}
rules := []retentionRule{}
rp := int64(pb.RetentionPeriod.Round(time.Second) / time.Second)
if rp > 0 {
rules = append(rules, retentionRule{
Type: "expire",
EverySeconds: rp,
})
}
return &bucket{
ID: pb.ID,
OrgID: pb.OrgID,
Type: pb.Type.String(),
Name: pb.Name,
Description: pb.Description,
RetentionPolicyName: pb.RetentionPolicyName,
RetentionRules: rules,
CRUDLog: pb.CRUDLog,
}
}
// bucketUpdate is used for serialization/deserialization with retention rules.
type bucketUpdate struct {
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
RetentionRules []retentionRule `json:"retentionRules,omitempty"`
}
func (b *bucketUpdate) OK() error {
if len(b.RetentionRules) > 0 {
_, err := b.RetentionRules[0].RetentionPeriod()
if err != nil {
return err
}
}
return nil
}
func (b *bucketUpdate) toInfluxDB() *influxdb.BucketUpdate {
if b == nil {
return nil
}
// For now, only use a single retention rule.
var d time.Duration
if len(b.RetentionRules) > 0 {
d, _ = b.RetentionRules[0].RetentionPeriod()
}
return &influxdb.BucketUpdate{
Name: b.Name,
Description: b.Description,
RetentionPeriod: &d,
}
}
func newBucketUpdate(pb *influxdb.BucketUpdate) *bucketUpdate {
if pb == nil {
return nil
}
up := &bucketUpdate{
Name: pb.Name,
Description: pb.Description,
RetentionRules: []retentionRule{},
}
if pb.RetentionPeriod != nil {
d := int64((*pb.RetentionPeriod).Round(time.Second) / time.Second)
up.RetentionRules = append(up.RetentionRules, retentionRule{
Type: "expire",
EverySeconds: d,
})
}
return up
}
type bucketResponse struct {
bucket
Links map[string]string `json:"links"`
Labels []influxdb.Label `json:"labels"`
}
func NewBucketResponse(b *influxdb.Bucket, labels []*influxdb.Label) *bucketResponse {
res := &bucketResponse{
Links: map[string]string{
"labels": fmt.Sprintf("/api/v2/buckets/%s/labels", b.ID),
"logs": fmt.Sprintf("/api/v2/buckets/%s/logs", b.ID),
"members": fmt.Sprintf("/api/v2/buckets/%s/members", b.ID),
"org": fmt.Sprintf("/api/v2/orgs/%s", b.OrgID),
"owners": fmt.Sprintf("/api/v2/buckets/%s/owners", b.ID),
"self": fmt.Sprintf("/api/v2/buckets/%s", b.ID),
"write": fmt.Sprintf("/api/v2/write?org=%s&bucket=%s", b.OrgID, b.ID),
},
bucket: *newBucket(b),
Labels: []influxdb.Label{},
}
for _, l := range labels {
res.Labels = append(res.Labels, *l)
}
return res
}
type bucketsResponse struct {
Links *influxdb.PagingLinks `json:"links"`
Buckets []*bucketResponse `json:"buckets"`
}
func newBucketsResponse(ctx context.Context, opts influxdb.FindOptions, f influxdb.BucketFilter, bs []*influxdb.Bucket, labelService influxdb.LabelService) *bucketsResponse {
rs := make([]*bucketResponse, 0, len(bs))
for _, b := range bs {
labels, _ := labelService.FindResourceLabels(ctx, influxdb.LabelMappingFilter{ResourceID: b.ID, ResourceType: influxdb.BucketsResourceType})
rs = append(rs, NewBucketResponse(b, labels))
}
return &bucketsResponse{
Links: influxdb.NewPagingLinks(prefixBuckets, opts, f, len(bs)),
Buckets: rs,
}
}
// handlePostBucket is the HTTP handler for the POST /api/v2/buckets route.
func (h *BucketHandler) handlePostBucket(w http.ResponseWriter, r *http.Request) {
var b postBucketRequest
if err := h.api.DecodeJSON(r.Body, &b); err != nil {
h.api.Err(w, r, err)
return
}
bucket := b.toInfluxDB()
if err := h.BucketService.CreateBucket(r.Context(), bucket); err != nil {
h.api.Err(w, r, err)
return
}
h.log.Debug("Bucket created", zap.String("bucket", fmt.Sprint(bucket)))
h.api.Respond(w, r, http.StatusCreated, NewBucketResponse(bucket, []*influxdb.Label{}))
}
type postBucketRequest struct {
OrgID influxdb.ID `json:"orgID,omitempty"`
Name string `json:"name"`
Description string `json:"description"`
RetentionPolicyName string `json:"rp,omitempty"` // This to support v1 sources
RetentionRules []retentionRule `json:"retentionRules"`
}
func (b *postBucketRequest) OK() error {
if !b.OrgID.Valid() {
return &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "organization id must be provided",
}
}
// Only support a single retention period for the moment
if len(b.RetentionRules) > 0 {
if _, err := b.RetentionRules[0].RetentionPeriod(); err != nil {
return &influxdb.Error{
Code: influxdb.EUnprocessableEntity,
Msg: err.Error(),
}
}
}
// names starting with an underscore are reserved for system buckets
if err := validBucketName(b.toInfluxDB()); err != nil {
return err
}
return nil
}
func (b postBucketRequest) toInfluxDB() *influxdb.Bucket {
// Only support a single retention period for the moment
var dur time.Duration
if len(b.RetentionRules) > 0 {
dur, _ = b.RetentionRules[0].RetentionPeriod()
}
return &influxdb.Bucket{
OrgID: b.OrgID,
Description: b.Description,
Name: b.Name,
Type: influxdb.BucketTypeUser,
RetentionPolicyName: b.RetentionPolicyName,
RetentionPeriod: dur,
}
}
// handleGetBucket is the HTTP handler for the GET /api/v2/buckets/:id route.
func (h *BucketHandler) handleGetBucket(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
id, err := decodeIDFromCtx(r.Context(), "id")
if err != nil {
h.api.Err(w, r, err)
return
}
b, err := h.BucketService.FindBucketByID(ctx, id)
if err != nil {
h.api.Err(w, r, err)
return
}
labels, err := h.LabelService.FindResourceLabels(ctx, influxdb.LabelMappingFilter{ResourceID: b.ID, ResourceType: influxdb.BucketsResourceType})
if err != nil {
h.api.Err(w, r, err)
return
}
h.log.Debug("Bucket retrieved", zap.String("bucket", fmt.Sprint(b)))
h.api.Respond(w, r, http.StatusOK, NewBucketResponse(b, labels))
}
func bucketIDPath(id influxdb.ID) string {
return path.Join(prefixBuckets, id.String())
}
// handleDeleteBucket is the HTTP handler for the DELETE /api/v2/buckets/:id route.
func (h *BucketHandler) handleDeleteBucket(w http.ResponseWriter, r *http.Request) {
id, err := decodeIDFromCtx(r.Context(), "id")
if err != nil {
h.api.Err(w, r, err)
return
}
if err := h.BucketService.DeleteBucket(r.Context(), id); err != nil {
h.api.Err(w, r, err)
return
}
h.log.Debug("Bucket deleted", zap.String("bucketID", id.String()))
h.api.Respond(w, r, http.StatusNoContent, nil)
}
// handleGetBuckets is the HTTP handler for the GET /api/v2/buckets route.
func (h *BucketHandler) handleGetBuckets(w http.ResponseWriter, r *http.Request) {
var filter influxdb.BucketFilter
q := r.URL.Query()
if org := q.Get("org"); org != "" {
filter.Org = &org
}
if name := q.Get("name"); name != "" {
filter.Name = &name
}
bucketID, err := decodeIDFromQuery(q, "id")
if err != nil {
h.api.Err(w, r, err)
return
}
if bucketID > 0 {
filter.ID = &bucketID
}
orgID, err := decodeIDFromQuery(q, "orgID")
if err != nil {
h.api.Err(w, r, err)
return
}
if orgID > 0 {
filter.OrganizationID = &orgID
}
opts, err := influxdb.DecodeFindOptions(r)
if err != nil {
h.api.Err(w, r, err)
return
}
bs, _, err := h.BucketService.FindBuckets(r.Context(), filter, *opts)
if err != nil {
h.api.Err(w, r, err)
return
}
h.log.Debug("Buckets retrieved", zap.String("buckets", fmt.Sprint(bs)))
h.api.Respond(w, r, http.StatusOK, newBucketsResponse(r.Context(), *opts, filter, bs, h.LabelService))
}
type getBucketsRequest struct {
filter influxdb.BucketFilter
opts influxdb.FindOptions
}
func decodeGetBucketsRequest(r *http.Request) (*getBucketsRequest, error) {
qp := r.URL.Query()
req := &getBucketsRequest{}
opts, err := influxdb.DecodeFindOptions(r)
if err != nil {
return nil, err
}
req.opts = *opts
if orgID := qp.Get("orgID"); orgID != "" {
id, err := influxdb.IDFromString(orgID)
if err != nil {
return nil, err
}
req.filter.OrganizationID = id
}
if org := qp.Get("org"); org != "" {
req.filter.Org = &org
}
if name := qp.Get("name"); name != "" {
req.filter.Name = &name
}
if bucketID := qp.Get("id"); bucketID != "" {
id, err := influxdb.IDFromString(bucketID)
if err != nil {
return nil, err
}
req.filter.ID = id
}
return req, nil
}
// handlePatchBucket is the HTTP handler for the PATCH /api/v2/buckets route.
func (h *BucketHandler) handlePatchBucket(w http.ResponseWriter, r *http.Request) {
id, err := decodeIDFromCtx(r.Context(), "id")
if err != nil {
h.api.Err(w, r, err)
return
}
var reqBody bucketUpdate
if err := h.api.DecodeJSON(r.Body, &reqBody); err != nil {
h.api.Err(w, r, err)
return
}
if reqBody.Name != nil {
b, err := h.BucketService.FindBucketByID(r.Context(), id)
if err != nil {
h.api.Err(w, r, err)
return
}
b.Name = *reqBody.Name
if err := validBucketName(b); err != nil {
h.api.Err(w, r, err)
return
}
}
b, err := h.BucketService.UpdateBucket(r.Context(), id, *reqBody.toInfluxDB())
if err != nil {
h.api.Err(w, r, err)
return
}
// TODO: should move to service to encapsulate labels and what any other dependencies. Future
// work for service definition
labels, err := h.LabelService.FindResourceLabels(r.Context(), influxdb.LabelMappingFilter{
ResourceID: b.ID,
ResourceType: influxdb.BucketsResourceType,
})
if err != nil {
h.api.Err(w, r, err)
return
}
h.log.Debug("Bucket updated", zap.String("bucket", fmt.Sprint(b)))
h.api.Respond(w, r, http.StatusOK, NewBucketResponse(b, labels))
}
// BucketService connects to Influx via HTTP using tokens to manage buckets
type BucketService struct {
Client *httpc.Client
// OpPrefix is an additional property for error
// find bucket service, when finds nothing.
OpPrefix string
}
// FindBucketByName returns a single bucket by name
func (s *BucketService) FindBucketByName(ctx context.Context, orgID influxdb.ID, name string) (*influxdb.Bucket, error) {
span, _ := tracing.StartSpanFromContext(ctx)
defer span.Finish()
if name == "" {
return nil, &influxdb.Error{
Code: influxdb.EUnprocessableEntity,
Op: s.OpPrefix + influxdb.OpFindBuckets,
Msg: "bucket name is required",
}
}
bkts, n, err := s.FindBuckets(ctx, influxdb.BucketFilter{
Name: &name,
OrganizationID: &orgID,
})
if err != nil {
return nil, err
}
if n == 0 || len(bkts) == 0 {
return nil, &influxdb.Error{
Code: influxdb.ENotFound,
Op: s.OpPrefix + influxdb.OpFindBucket,
Msg: fmt.Sprintf("bucket %q not found", name),
}
}
return bkts[0], nil
}
// FindBucketByID returns a single bucket by ID.
func (s *BucketService) FindBucketByID(ctx context.Context, id influxdb.ID) (*influxdb.Bucket, error) {
// TODO(@jsteenb2): are tracing
span, _ := tracing.StartSpanFromContext(ctx)
defer span.Finish()
var br bucketResponse
err := s.Client.
Get(bucketIDPath(id)).
DecodeJSON(&br).
Do(ctx)
if err != nil {
return nil, err
}
return br.toInfluxDB()
}
// FindBucket returns the first bucket that matches filter.
func (s *BucketService) FindBucket(ctx context.Context, filter influxdb.BucketFilter) (*influxdb.Bucket, error) {
span, ctx := tracing.StartSpanFromContext(ctx)
defer span.Finish()
bs, n, err := s.FindBuckets(ctx, filter)
if err != nil {
return nil, err
}
if n == 0 && filter.Name != nil {
return nil, &influxdb.Error{
Code: influxdb.ENotFound,
Op: s.OpPrefix + influxdb.OpFindBucket,
Msg: fmt.Sprintf("bucket %q not found", *filter.Name),
}
} else if n == 0 {
return nil, &influxdb.Error{
Code: influxdb.ENotFound,
Op: s.OpPrefix + influxdb.OpFindBucket,
Msg: "bucket not found",
}
}
return bs[0], nil
}
// FindBuckets returns a list of buckets that match filter and the total count of matching buckets.
// Additional options provide pagination & sorting.
func (s *BucketService) FindBuckets(ctx context.Context, filter influxdb.BucketFilter, opt ...influxdb.FindOptions) ([]*influxdb.Bucket, int, error) {
span, _ := tracing.StartSpanFromContext(ctx)
defer span.Finish()
params := influxdb.FindOptionParams(opt...)
if filter.OrganizationID != nil {
params = append(params, [2]string{"orgID", filter.OrganizationID.String()})
}
if filter.Org != nil {
params = append(params, [2]string{"org", *filter.Org})
}
if filter.ID != nil {
params = append(params, [2]string{"id", filter.ID.String()})
}
if filter.Name != nil {
params = append(params, [2]string{"name", (*filter.Name)})
}
var bs bucketsResponse
err := s.Client.
Get(prefixBuckets).
QueryParams(params...).
DecodeJSON(&bs).
Do(ctx)
if err != nil {
return nil, 0, err
}
buckets := make([]*influxdb.Bucket, 0, len(bs.Buckets))
for _, b := range bs.Buckets {
pb, err := b.bucket.toInfluxDB()
if err != nil {
return nil, 0, err
}
buckets = append(buckets, pb)
}
return buckets, len(buckets), nil
}
// CreateBucket creates a new bucket and sets b.ID with the new identifier.
func (s *BucketService) CreateBucket(ctx context.Context, b *influxdb.Bucket) error {
span, _ := tracing.StartSpanFromContext(ctx)
defer span.Finish()
var br bucketResponse
err := s.Client.
PostJSON(newBucket(b), prefixBuckets).
DecodeJSON(&br).
Do(ctx)
if err != nil {
return err
}
pb, err := br.toInfluxDB()
if err != nil {
return err
}
*b = *pb
return nil
}
// UpdateBucket updates a single bucket with changeset.
// Returns the new bucket state after update.
func (s *BucketService) UpdateBucket(ctx context.Context, id influxdb.ID, upd influxdb.BucketUpdate) (*influxdb.Bucket, error) {
var br bucketResponse
err := s.Client.
PatchJSON(newBucketUpdate(&upd), bucketIDPath(id)).
DecodeJSON(&br).
Do(ctx)
if err != nil {
return nil, err
}
return br.toInfluxDB()
}
// DeleteBucket removes a bucket by ID.
func (s *BucketService) DeleteBucket(ctx context.Context, id influxdb.ID) error {
return s.Client.
Delete(bucketIDPath(id)).
Do(ctx)
}
// validBucketName reports any errors with bucket names
func validBucketName(bucket *influxdb.Bucket) error {
// names starting with an underscore are reserved for system buckets
if strings.HasPrefix(bucket.Name, "_") && bucket.Type != influxdb.BucketTypeSystem {
return &influxdb.Error{
Code: influxdb.EInvalid,
Op: "http/bucket",
Msg: fmt.Sprintf("bucket name %s is invalid. Buckets may not start with underscore", bucket.Name),
}
}
return nil
}

File diff suppressed because it is too large Load Diff

View File

@ -42,22 +42,15 @@ type Service struct {
Token string
InsecureSkipVerify bool
*AuthorizationService
*BackupService
*BucketService
*TaskService
*OrganizationService
*NotificationRuleService
*UserService
*VariableService
*WriteService
DocumentService
*CheckService
*NotificationEndpointService
*UserResourceMappingService
*TelegrafService
*LabelService
*SecretService
DBRPMappingServiceV2 *dbrp.Client
}
@ -75,30 +68,23 @@ type Service struct {
// in the behavior of the returned service.
func NewService(httpClient *httpc.Client, addr, token string) (*Service, error) {
return &Service{
Addr: addr,
Token: token,
AuthorizationService: &AuthorizationService{Client: httpClient},
Addr: addr,
Token: token,
BackupService: &BackupService{
Addr: addr,
Token: token,
},
BucketService: &BucketService{Client: httpClient},
TaskService: &TaskService{Client: httpClient},
OrganizationService: &OrganizationService{Client: httpClient},
NotificationRuleService: &NotificationRuleService{Client: httpClient},
UserService: &UserService{Client: httpClient},
VariableService: &VariableService{Client: httpClient},
WriteService: &WriteService{
Addr: addr,
Token: token,
},
DocumentService: NewDocumentService(httpClient),
CheckService: &CheckService{Client: httpClient},
NotificationEndpointService: &NotificationEndpointService{Client: httpClient},
UserResourceMappingService: &UserResourceMappingService{Client: httpClient},
TelegrafService: NewTelegrafService(httpClient),
LabelService: &LabelService{Client: httpClient},
SecretService: &SecretService{Client: httpClient},
DBRPMappingServiceV2: dbrp.NewClient(httpClient),
}, nil
}

View File

@ -16,6 +16,8 @@ import (
"go.uber.org/zap/zaptest"
)
var user1ID = influxtesting.MustIDBase16("020f755c3c082001")
// NewMockDeleteBackend returns a DeleteBackend with mock services.
func NewMockDeleteBackend(t *testing.T) *DeleteBackend {
return &DeleteBackend{

View File

@ -1,758 +0,0 @@
package http
import (
"context"
"encoding/json"
"fmt"
"net/http"
"path"
"github.com/influxdata/httprouter"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/kit/tracing"
"github.com/influxdata/influxdb/v2/pkg/httpc"
"go.uber.org/zap"
)
const prefixDocuments = "/api/v2/documents"
// DocumentService is an interface HTTP-exposed portion of the document service.
type DocumentService interface {
CreateDocument(ctx context.Context, namespace string, orgID influxdb.ID, d *influxdb.Document) error
GetDocuments(ctx context.Context, namespace string, orgID influxdb.ID) ([]*influxdb.Document, error)
GetDocument(ctx context.Context, namespace string, id influxdb.ID) (*influxdb.Document, error)
UpdateDocument(ctx context.Context, namespace string, d *influxdb.Document) error
DeleteDocument(ctx context.Context, namespace string, id influxdb.ID) error
GetDocumentLabels(ctx context.Context, namespace string, id influxdb.ID) ([]*influxdb.Label, error)
AddDocumentLabel(ctx context.Context, namespace string, did influxdb.ID, lid influxdb.ID) (*influxdb.Label, error)
DeleteDocumentLabel(ctx context.Context, namespace string, did influxdb.ID, lid influxdb.ID) error
}
// DocumentBackend is all services and associated parameters required to construct
// the DocumentHandler.
type DocumentBackend struct {
log *zap.Logger
influxdb.HTTPErrorHandler
DocumentService influxdb.DocumentService
LabelService influxdb.LabelService
OrganizationService influxdb.OrganizationService
}
// NewDocumentBackend returns a new instance of DocumentBackend.
func NewDocumentBackend(log *zap.Logger, b *APIBackend) *DocumentBackend {
return &DocumentBackend{
HTTPErrorHandler: b.HTTPErrorHandler,
log: log,
DocumentService: b.DocumentService,
LabelService: b.LabelService,
OrganizationService: b.OrganizationService,
}
}
// DocumentHandler represents an HTTP API handler for documents.
type DocumentHandler struct {
*httprouter.Router
log *zap.Logger
influxdb.HTTPErrorHandler
DocumentService influxdb.DocumentService
LabelService influxdb.LabelService
OrganizationService influxdb.OrganizationService
}
const (
documentsPath = "/api/v2/documents/:ns"
documentPath = "/api/v2/documents/:ns/:id"
documentLabelsPath = "/api/v2/documents/:ns/:id/labels"
documentLabelsIDPath = "/api/v2/documents/:ns/:id/labels/:lid"
)
// NewDocumentHandler returns a new instance of DocumentHandler.
// TODO(desa): this should probably take a namespace
func NewDocumentHandler(b *DocumentBackend) *DocumentHandler {
h := &DocumentHandler{
Router: NewRouter(b.HTTPErrorHandler),
HTTPErrorHandler: b.HTTPErrorHandler,
log: b.log,
DocumentService: b.DocumentService,
LabelService: b.LabelService,
OrganizationService: b.OrganizationService,
}
h.HandlerFunc("POST", documentsPath, h.handlePostDocument)
h.HandlerFunc("GET", documentsPath, h.handleGetDocuments)
h.HandlerFunc("GET", documentPath, h.handleGetDocument)
h.HandlerFunc("PUT", documentPath, h.handlePutDocument)
h.HandlerFunc("DELETE", documentPath, h.handleDeleteDocument)
h.HandlerFunc("GET", documentLabelsPath, h.handleGetDocumentLabel)
h.HandlerFunc("POST", documentLabelsPath, h.handlePostDocumentLabel)
h.HandlerFunc("DELETE", documentLabelsIDPath, h.handleDeleteDocumentLabel)
return h
}
func (h *DocumentHandler) getOrgID(ctx context.Context, org string, orgID influxdb.ID) (influxdb.ID, error) {
if org != "" && orgID.Valid() {
return 0, &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "Please provide either org or orgID, not both",
}
}
if orgID.Valid() {
return orgID, nil
}
if org != "" {
o, err := h.OrganizationService.FindOrganization(ctx, influxdb.OrganizationFilter{Name: &org})
if err != nil {
return 0, err
}
return o.ID, nil
}
return 0, &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "Please provide either org or orgID",
}
}
type documentResponse struct {
Links map[string]string `json:"links"`
*influxdb.Document
}
func newDocumentResponse(ns string, d *influxdb.Document) *documentResponse {
if d.Labels == nil {
d.Labels = []*influxdb.Label{}
}
return &documentResponse{
Links: map[string]string{
"self": fmt.Sprintf("/api/v2/documents/%s/%s", ns, d.ID),
},
Document: d,
}
}
type documentsResponse struct {
Documents []*documentResponse `json:"documents"`
}
func newDocumentsResponse(ns string, docs []*influxdb.Document) *documentsResponse {
ds := make([]*documentResponse, 0, len(docs))
for _, doc := range docs {
ds = append(ds, newDocumentResponse(ns, doc))
}
return &documentsResponse{
Documents: ds,
}
}
// handlePostDocument is the HTTP handler for the POST /api/v2/documents/:ns route.
func (h *DocumentHandler) handlePostDocument(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodePostDocumentRequest(ctx, r)
if err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
s, err := h.DocumentService.FindDocumentStore(ctx, req.Namespace)
if err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
orgID, err := h.getOrgID(ctx, req.Org, req.OrgID)
if err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
req.Document.Organizations = make(map[influxdb.ID]influxdb.UserType)
req.Document.Organizations[orgID] = influxdb.Owner
d := req.Document
for _, lid := range req.Labels {
d.Labels = append(d.Labels, &influxdb.Label{ID: lid})
}
if err := s.CreateDocument(ctx, d); err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
h.log.Debug("Document created", zap.String("document", fmt.Sprint(req.Document)))
if err := encodeResponse(ctx, w, http.StatusCreated, newDocumentResponse(req.Namespace, req.Document)); err != nil {
logEncodingError(h.log, r, err)
return
}
}
type postDocumentRequest struct {
*influxdb.Document
Namespace string `json:"-"`
Org string `json:"org"`
OrgID influxdb.ID `json:"orgID,omitempty"`
// TODO(affo): Why not accepting fully qualified labels?
// this forces us to convert them back and forth.
Labels []influxdb.ID `json:"labels"`
}
func decodePostDocumentRequest(ctx context.Context, r *http.Request) (*postDocumentRequest, error) {
req := &postDocumentRequest{}
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "document body error",
Err: err,
}
}
if req.Document == nil {
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "missing document body",
}
}
params := httprouter.ParamsFromContext(ctx)
req.Namespace = params.ByName("ns")
if req.Namespace == "" {
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "url missing namespace",
}
}
return req, nil
}
// handleGetDocuments is the HTTP handler for the GET /api/v2/documents/:ns route.
func (h *DocumentHandler) handleGetDocuments(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodeGetDocumentsRequest(ctx, r)
if err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
s, err := h.DocumentService.FindDocumentStore(ctx, req.Namespace)
if err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
orgID, err := h.getOrgID(ctx, req.Org, req.OrgID)
if err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
opts := []influxdb.DocumentFindOptions{influxdb.IncludeLabels, influxdb.WhereOrgID(orgID)}
ds, err := s.FindDocuments(ctx, opts...)
if err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
h.log.Debug("Documents retrieved", zap.String("documents", fmt.Sprint(ds)))
if err := encodeResponse(ctx, w, http.StatusOK, newDocumentsResponse(req.Namespace, ds)); err != nil {
logEncodingError(h.log, r, err)
return
}
}
type getDocumentsRequest struct {
Namespace string
Org string
OrgID influxdb.ID
}
func decodeGetDocumentsRequest(ctx context.Context, r *http.Request) (*getDocumentsRequest, error) {
params := httprouter.ParamsFromContext(ctx)
ns := params.ByName("ns")
if ns == "" {
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "url missing namespace",
}
}
qp := r.URL.Query()
req := &getDocumentsRequest{
Namespace: ns,
Org: qp.Get("org"),
}
if oidStr := qp.Get("orgID"); oidStr != "" {
oid, err := influxdb.IDFromString(oidStr)
if err != nil {
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "Invalid orgID",
}
}
req.OrgID = *oid
}
return req, nil
}
func (h *DocumentHandler) handlePostDocumentLabel(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
_, _, err := h.getDocument(w, r)
if err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
req, err := decodePostLabelMappingRequest(ctx, r, influxdb.DocumentsResourceType)
if err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
if err := req.Mapping.Validate(); err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
if err := h.LabelService.CreateLabelMapping(ctx, &req.Mapping); err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
label, err := h.LabelService.FindLabelByID(ctx, req.Mapping.LabelID)
if err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
h.log.Debug("Document label created", zap.String("label", fmt.Sprint(label)))
if err := encodeResponse(ctx, w, http.StatusCreated, newLabelResponse(label)); err != nil {
logEncodingError(h.log, r, err)
return
}
}
// handleDeleteDocumentLabel will first remove the label from the document,
// then remove that label.
func (h *DocumentHandler) handleDeleteDocumentLabel(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodeDeleteLabelMappingRequest(ctx, r)
if err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
_, _, err = h.getDocument(w, r)
if err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
_, err = h.LabelService.FindLabelByID(ctx, req.LabelID)
if err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
mapping := &influxdb.LabelMapping{
LabelID: req.LabelID,
ResourceID: req.ResourceID,
ResourceType: influxdb.DocumentsResourceType,
}
// remove the label
if err := h.LabelService.DeleteLabelMapping(ctx, mapping); err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
h.log.Debug("Document label deleted", zap.String("mapping", fmt.Sprint(mapping)))
w.WriteHeader(http.StatusNoContent)
}
func (h *DocumentHandler) handleGetDocumentLabel(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
d, _, err := h.getDocument(w, r)
if err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
h.log.Debug("Document label retrieved", zap.String("labels", fmt.Sprint(d.Labels)))
if err := encodeResponse(ctx, w, http.StatusOK, newLabelsResponse(d.Labels)); err != nil {
logEncodingError(h.log, r, err)
return
}
}
func (h *DocumentHandler) getDocument(w http.ResponseWriter, r *http.Request) (*influxdb.Document, string, error) {
ctx := r.Context()
req, err := decodeGetDocumentRequest(ctx, r)
if err != nil {
return nil, "", err
}
s, err := h.DocumentService.FindDocumentStore(ctx, req.Namespace)
if err != nil {
return nil, "", err
}
ds, err := s.FindDocument(ctx, req.ID)
if err != nil {
return nil, "", err
}
return ds, req.Namespace, nil
}
// handleGetDocument is the HTTP handler for the GET /api/v2/documents/:ns/:id route.
func (h *DocumentHandler) handleGetDocument(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
d, namespace, err := h.getDocument(w, r)
if err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
h.log.Debug("Document retrieved", zap.String("document", fmt.Sprint(d)))
if err := encodeResponse(ctx, w, http.StatusOK, newDocumentResponse(namespace, d)); err != nil {
logEncodingError(h.log, r, err)
return
}
}
type getDocumentRequest struct {
Namespace string
ID influxdb.ID
}
func decodeGetDocumentRequest(ctx context.Context, r *http.Request) (*getDocumentRequest, error) {
params := httprouter.ParamsFromContext(ctx)
ns := params.ByName("ns")
if ns == "" {
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "url missing namespace",
}
}
i := params.ByName("id")
if i == "" {
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "url missing id",
}
}
var id influxdb.ID
if err := id.DecodeFromString(i); err != nil {
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "bad id in url",
}
}
return &getDocumentRequest{
Namespace: ns,
ID: id,
}, nil
}
// handleDeleteDocument is the HTTP handler for the DELETE /api/v2/documents/:ns/:id route.
func (h *DocumentHandler) handleDeleteDocument(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodeDeleteDocumentRequest(ctx, r)
if err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
s, err := h.DocumentService.FindDocumentStore(ctx, req.Namespace)
if err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
if err := s.DeleteDocument(ctx, req.ID); err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
h.log.Debug("Document deleted", zap.String("documentID", fmt.Sprint(req.ID)))
w.WriteHeader(http.StatusNoContent)
}
type deleteDocumentRequest struct {
Namespace string
ID influxdb.ID
}
func decodeDeleteDocumentRequest(ctx context.Context, r *http.Request) (*deleteDocumentRequest, error) {
params := httprouter.ParamsFromContext(ctx)
ns := params.ByName("ns")
if ns == "" {
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "url missing namespace",
}
}
i := params.ByName("id")
if i == "" {
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "url missing id",
}
}
var id influxdb.ID
if err := id.DecodeFromString(i); err != nil {
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "bad id in url",
}
}
return &deleteDocumentRequest{
Namespace: ns,
ID: id,
}, nil
}
// handlePutDocument is the HTTP handler for the PUT /api/v2/documents/:ns/:id route.
func (h *DocumentHandler) handlePutDocument(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodePutDocumentRequest(ctx, r)
if err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
s, err := h.DocumentService.FindDocumentStore(ctx, req.Namespace)
if err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
if err := s.UpdateDocument(ctx, req.Document); err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
d, err := s.FindDocument(ctx, req.Document.ID)
if err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
h.log.Debug("Document updated", zap.String("document", fmt.Sprint(d)))
if err := encodeResponse(ctx, w, http.StatusOK, newDocumentResponse(req.Namespace, d)); err != nil {
logEncodingError(h.log, r, err)
return
}
}
type putDocumentRequest struct {
*influxdb.Document
Namespace string `json:"-"`
}
func decodePutDocumentRequest(ctx context.Context, r *http.Request) (*putDocumentRequest, error) {
req := &putDocumentRequest{}
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
return nil, err
}
params := httprouter.ParamsFromContext(ctx)
i := params.ByName("id")
if i == "" {
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "url missing id",
}
}
if err := req.ID.DecodeFromString(i); err != nil {
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
}
}
req.Namespace = params.ByName("ns")
if req.Namespace == "" {
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "url missing namespace",
}
}
return req, nil
}
type documentService struct {
Client *httpc.Client
}
// NewDocumentService creates a client to connect to Influx via HTTP to manage documents.
func NewDocumentService(client *httpc.Client) DocumentService {
return &documentService{
Client: client,
}
}
func buildDocumentsPath(namespace string) string {
return path.Join(prefixDocuments, namespace)
}
func buildDocumentPath(namespace string, id influxdb.ID) string {
return path.Join(prefixDocuments, namespace, id.String())
}
func buildDocumentLabelsPath(namespace string, id influxdb.ID) string {
return path.Join(prefixDocuments, namespace, id.String(), "labels")
}
func buildDocumentLabelPath(namespace string, did influxdb.ID, lid influxdb.ID) string {
return path.Join(prefixDocuments, namespace, did.String(), "labels", lid.String())
}
// CreateDocument creates a document in the specified namespace.
// Only the ids of the given labels will be used.
// After the call, if successful, the input document will contain the newly assigned ID.
func (s *documentService) CreateDocument(ctx context.Context, namespace string, orgID influxdb.ID, d *influxdb.Document) error {
span, _ := tracing.StartSpanFromContext(ctx)
defer span.Finish()
lids := make([]influxdb.ID, len(d.Labels))
for i := 0; i < len(lids); i++ {
lids[i] = d.Labels[i].ID
}
// Set a valid ID for proper marshaling.
// It will be assigned by the backend in any case.
d.ID = influxdb.ID(1)
req := &postDocumentRequest{
Document: d,
OrgID: orgID,
Labels: lids,
}
var resp documentResponse
if err := s.Client.
PostJSON(req, buildDocumentsPath(namespace)).
DecodeJSON(&resp).
Do(ctx); err != nil {
return err
}
d.ID = resp.ID
return nil
}
// GetDocuments returns the documents for a `namespace` and an `orgID`.
// Returned documents do not contain their content.
func (s *documentService) GetDocuments(ctx context.Context, namespace string, orgID influxdb.ID) ([]*influxdb.Document, error) {
span, _ := tracing.StartSpanFromContext(ctx)
defer span.Finish()
var resp documentsResponse
r := s.Client.
Get(buildDocumentsPath(namespace)).
DecodeJSON(&resp)
r = r.QueryParams([2]string{"orgID", orgID.String()})
if err := r.Do(ctx); err != nil {
return nil, err
}
docs := make([]*influxdb.Document, len(resp.Documents))
for i := 0; i < len(docs); i++ {
docs[i] = resp.Documents[i].Document
}
return docs, nil
}
// GetDocument returns the document with the specified id.
func (s *documentService) GetDocument(ctx context.Context, namespace string, id influxdb.ID) (*influxdb.Document, error) {
span, _ := tracing.StartSpanFromContext(ctx)
defer span.Finish()
var resp documentResponse
if err := s.Client.
Get(buildDocumentPath(namespace, id)).
DecodeJSON(&resp).
Do(ctx); err != nil {
return nil, err
}
return resp.Document, nil
}
// UpdateDocument updates the document with id `d.ID` and replaces the content of `d` with the patched value.
func (s *documentService) UpdateDocument(ctx context.Context, namespace string, d *influxdb.Document) error {
span, _ := tracing.StartSpanFromContext(ctx)
defer span.Finish()
var resp documentResponse
if err := s.Client.
PutJSON(d, buildDocumentPath(namespace, d.ID)).
DecodeJSON(&resp).
Do(ctx); err != nil {
return err
}
*d = *resp.Document
return nil
}
// DeleteDocument deletes the document with the given id.
func (s *documentService) DeleteDocument(ctx context.Context, namespace string, id influxdb.ID) error {
span, _ := tracing.StartSpanFromContext(ctx)
defer span.Finish()
if err := s.Client.
Delete(buildDocumentPath(namespace, id)).
Do(ctx); err != nil {
return err
}
return nil
}
// GetDocumentLabels returns the labels associated to the document with the given id.
func (s *documentService) GetDocumentLabels(ctx context.Context, namespace string, id influxdb.ID) ([]*influxdb.Label, error) {
span, _ := tracing.StartSpanFromContext(ctx)
defer span.Finish()
var resp labelsResponse
if err := s.Client.
Get(buildDocumentLabelsPath(namespace, id)).
DecodeJSON(&resp).
Do(ctx); err != nil {
return nil, err
}
return resp.Labels, nil
}
// AddDocumentLabel adds the label with id `lid` to the document with id `did`.
func (s *documentService) AddDocumentLabel(ctx context.Context, namespace string, did influxdb.ID, lid influxdb.ID) (*influxdb.Label, error) {
span, _ := tracing.StartSpanFromContext(ctx)
defer span.Finish()
mapping := &influxdb.LabelMapping{
LabelID: lid,
}
var resp labelResponse
if err := s.Client.
PostJSON(mapping, buildDocumentLabelsPath(namespace, did)).
DecodeJSON(&resp).
Do(ctx); err != nil {
return nil, err
}
return &resp.Label, nil
}
// DeleteDocumentLabel deletes the label with id `lid` from the document with id `did`.
func (s *documentService) DeleteDocumentLabel(ctx context.Context, namespace string, did influxdb.ID, lid influxdb.ID) error {
span, _ := tracing.StartSpanFromContext(ctx)
defer span.Finish()
if err := s.Client.
Delete(buildDocumentLabelPath(namespace, did, lid)).
Do(ctx); err != nil {
return err
}
return nil
}

View File

@ -1,760 +0,0 @@
package http
import (
"context"
"net/http/httptest"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/authorizer"
icontext "github.com/influxdata/influxdb/v2/context"
httpmock "github.com/influxdata/influxdb/v2/http/mock"
"github.com/influxdata/influxdb/v2/kit/transport/http"
"github.com/influxdata/influxdb/v2/kv"
"github.com/influxdata/influxdb/v2/mock"
itesting "github.com/influxdata/influxdb/v2/testing"
"go.uber.org/zap/zaptest"
)
const namespace = "templates"
type fixture struct {
Org *influxdb.Organization
Labels []*influxdb.Label
Document *influxdb.Document
AnotherDocument *influxdb.Document
}
func setup(t *testing.T) (func(auth influxdb.Authorizer) *httptest.Server, func(serverUrl string) DocumentService, fixture) {
ctx := context.Background()
// Need this to make resource creation work.
// We are not testing authorization in the setup.
ctx = icontext.SetAuthorizer(ctx, mock.NewMockAuthorizer(true, nil))
store := NewTestInmemStore(t)
svc := kv.NewService(zaptest.NewLogger(t), store)
ds, err := svc.CreateDocumentStore(ctx, namespace)
if err != nil {
t.Fatalf("failed to create document store: %v", err)
}
org := &influxdb.Organization{Name: "org"}
itesting.MustCreateOrgs(ctx, svc, org)
l1 := &influxdb.Label{Name: "l1", OrgID: org.ID}
l2 := &influxdb.Label{Name: "l2", OrgID: org.ID}
l3 := &influxdb.Label{Name: "l3", OrgID: org.ID}
itesting.MustCreateLabels(ctx, svc, l1, l2, l3)
doc := &influxdb.Document{
Content: "I am a free document",
Labels: []*influxdb.Label{l1, l3},
Organizations: map[influxdb.ID]influxdb.UserType{org.ID: influxdb.Owner},
}
if err := ds.CreateDocument(ctx, doc); err != nil {
panic(err)
}
// Organizations are needed only for creation.
// Need to cleanup for comparison later.
doc.Organizations = nil
adoc := &influxdb.Document{
Content: "I am another document",
Labels: []*influxdb.Label{l1, l2},
Organizations: map[influxdb.ID]influxdb.UserType{org.ID: influxdb.Owner},
}
if err := ds.CreateDocument(ctx, adoc); err != nil {
panic(err)
}
// Organizations are needed only for creation.
// Need to cleanup for comparison later.
adoc.Organizations = nil
backend := NewMockDocumentBackend(t)
backend.HTTPErrorHandler = http.ErrorHandler(0)
backend.DocumentService = authorizer.NewDocumentService(svc)
backend.LabelService = authorizer.NewLabelServiceWithOrg(svc, staticOrgIDResolver(org.ID))
serverFn := func(auth influxdb.Authorizer) *httptest.Server {
handler := httpmock.NewAuthMiddlewareHandler(NewDocumentHandler(backend), auth)
return httptest.NewServer(handler)
}
clientFn := func(serverUrl string) DocumentService {
return NewDocumentService(mustNewHTTPClient(t, serverUrl, ""))
}
f := fixture{
Org: org,
Labels: []*influxdb.Label{l1, l2, l3},
Document: doc,
AnotherDocument: adoc,
}
return serverFn, clientFn, f
}
func (f fixture) auth(action influxdb.Action) *influxdb.Authorization {
a := &influxdb.Authorization{
Permissions: []influxdb.Permission{
{
Action: action,
Resource: influxdb.Resource{
Type: influxdb.DocumentsResourceType,
OrgID: &f.Org.ID,
},
},
},
Status: influxdb.Active,
}
if action == influxdb.WriteAction {
a.Permissions = append(a.Permissions, influxdb.Permission{
Action: influxdb.ReadAction,
Resource: influxdb.Resource{
Type: influxdb.DocumentsResourceType,
OrgID: &f.Org.ID,
},
})
}
return a
}
func (f fixture) authKO() *influxdb.Authorization {
return &influxdb.Authorization{
Status: influxdb.Active,
}
}
func (f fixture) addLabelPermission(auth *influxdb.Authorization, action influxdb.Action, lid influxdb.ID) {
ps := []influxdb.Permission{
{
Action: action,
Resource: influxdb.Resource{
Type: influxdb.LabelsResourceType,
ID: &lid,
},
},
}
if action == influxdb.WriteAction {
ps = append(ps, influxdb.Permission{
Action: influxdb.ReadAction,
Resource: influxdb.Resource{
Type: influxdb.LabelsResourceType,
ID: &lid,
},
})
}
auth.Permissions = append(auth.Permissions, ps...)
}
// TestDocumentService tests all the service functions using the document HTTP client.
func TestDocumentService(t *testing.T) {
tests := []struct {
name string
fn func(t *testing.T)
}{
{
name: "CreateDocument",
fn: CreateDocument,
},
{
name: "GetDocument",
fn: GetDocument,
},
{
name: "GetDocuments",
fn: GetDocuments,
},
{
name: "UpdateDocument",
fn: UpdateDocument,
},
{
name: "DeleteDocument",
fn: DeleteDocument,
},
{
name: "GetLabels",
fn: GetLabels,
},
{
name: "AddLabels",
fn: AddLabels,
},
{
name: "DeleteLabel",
fn: DeleteLabel,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.fn(t)
})
}
}
func CreateDocument(t *testing.T) {
t.Run("with content", func(t *testing.T) {
serverFn, clientFn, fx := setup(t)
server := serverFn(fx.auth(influxdb.WriteAction))
defer server.Close()
client := clientFn(server.URL)
orgID := fx.Org.ID
d := &influxdb.Document{
Content: "I am the content",
}
if err := client.CreateDocument(context.Background(), namespace, orgID, d); err != nil {
t.Fatal(err)
}
if d.ID <= 1 {
t.Errorf("invalid document id: %v", d.ID)
}
if diff := cmp.Diff(d.Content, "I am the content"); diff != "" {
t.Errorf("got unexpected content:\n\t%s", diff)
}
if len(d.Labels) > 0 {
t.Errorf("got unexpected labels: %v", d.Labels)
}
})
t.Run("with content and labels", func(t *testing.T) {
serverFn, clientFn, fx := setup(t)
server := serverFn(fx.auth(influxdb.WriteAction))
defer server.Close()
client := clientFn(server.URL)
orgID := fx.Org.ID
d := &influxdb.Document{
Content: "I am the content",
Labels: fx.Labels,
}
if err := client.CreateDocument(context.Background(), namespace, orgID, d); err != nil {
t.Fatal(err)
}
if d.ID <= 1 {
t.Errorf("invalid document id: %v", d.ID)
}
if diff := cmp.Diff(d.Content, "I am the content"); diff != "" {
t.Errorf("got unexpected content:\n\t%s", diff)
}
if diff := cmp.Diff(d.Labels, fx.Labels); diff != "" {
t.Errorf("got unexpected labels:\n\t%v", diff)
}
})
t.Run("bad label", func(t *testing.T) {
serverFn, clientFn, fx := setup(t)
server := serverFn(fx.auth(influxdb.WriteAction))
defer server.Close()
client := clientFn(server.URL)
orgID := fx.Org.ID
d := &influxdb.Document{
Content: "I am the content",
Labels: []*influxdb.Label{
{
ID: influxdb.ID(1),
Name: "bad",
},
},
}
if err := client.CreateDocument(context.Background(), namespace, orgID, d); err != nil {
if !strings.Contains(err.Error(), "label not found") {
t.Errorf("unexpected error: %v", err)
}
} else {
t.Error("expected error got none")
}
})
t.Run("unauthorized", func(t *testing.T) {
serverFn, clientFn, fx := setup(t)
server := serverFn(fx.authKO())
defer server.Close()
client := clientFn(server.URL)
d := &influxdb.Document{
Content: "I am the content",
}
if err := client.CreateDocument(context.Background(), namespace, fx.Org.ID, d); err != nil {
if influxdb.ErrorCode(err) != influxdb.EUnauthorized {
t.Errorf("unexpected error: %v", err)
}
} else {
t.Error("expected error got none")
}
})
t.Run("unauthorized - insufficient", func(t *testing.T) {
serverFn, clientFn, fx := setup(t)
server := serverFn(fx.auth(influxdb.ReadAction))
defer server.Close()
client := clientFn(server.URL)
d := &influxdb.Document{
Content: "I am the content",
}
if err := client.CreateDocument(context.Background(), namespace, fx.Org.ID, d); err != nil {
if influxdb.ErrorCode(err) != influxdb.EUnauthorized {
t.Errorf("unexpected error: %v", err)
}
} else {
t.Error("expected error got none")
}
})
}
func GetDocument(t *testing.T) {
t.Run("existing", func(t *testing.T) {
serverFn, clientFn, fx := setup(t)
want := fx.Document
server := serverFn(fx.auth(influxdb.ReadAction))
defer server.Close()
client := clientFn(server.URL)
got, err := client.GetDocument(context.Background(), namespace, want.ID)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("got unexpected document:\n\t%s", diff)
}
})
t.Run("non existing", func(t *testing.T) {
serverFn, clientFn, fx := setup(t)
id := fx.Document.ID + 42
server := serverFn(fx.auth(influxdb.ReadAction))
defer server.Close()
client := clientFn(server.URL)
if _, err := client.GetDocument(context.Background(), namespace, id); err != nil {
if !strings.Contains(err.Error(), "document not found") {
t.Errorf("unexpected error: %v", err)
}
} else {
t.Error("expected error got none")
}
})
t.Run("unauthorized", func(t *testing.T) {
serverFn, clientFn, fx := setup(t)
server := serverFn(fx.authKO())
defer server.Close()
client := clientFn(server.URL)
if _, err := client.GetDocument(context.Background(), namespace, fx.Document.ID); err != nil {
if influxdb.ErrorCode(err) != influxdb.EUnauthorized {
t.Errorf("unexpected error: %v", err)
}
} else {
t.Error("expected error got none")
}
})
}
func GetDocuments(t *testing.T) {
t.Run("get existing documents", func(t *testing.T) {
serverFn, clientFn, fx := setup(t)
server := serverFn(fx.auth(influxdb.ReadAction))
defer server.Close()
client := clientFn(server.URL)
got, err := client.GetDocuments(context.Background(), namespace, fx.Org.ID)
if err != nil {
t.Fatal(err)
}
want := []*influxdb.Document{fx.Document, fx.AnotherDocument}
want[0].Content = nil // response will not contain the content of documents
want[1].Content = nil // response will not contain the content of documents
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("got unexpected document:\n\t%s", diff)
}
})
t.Run("unauthorized", func(t *testing.T) {
serverFn, clientFn, fx := setup(t)
server := serverFn(fx.authKO())
defer server.Close()
client := clientFn(server.URL)
docs, err := client.GetDocuments(context.Background(), namespace, fx.Org.ID)
if err != nil {
t.Fatal(err)
}
if len(docs) > 0 {
t.Fatalf("no document expected, returned %v instead", docs)
}
})
}
func UpdateDocument(t *testing.T) {
t.Run("update content", func(t *testing.T) {
serverFn, clientFn, fx := setup(t)
server := serverFn(fx.auth(influxdb.WriteAction))
defer server.Close()
client := clientFn(server.URL)
want := fx.Document
want.Content = "new content"
if err := client.UpdateDocument(context.Background(), namespace, want); err != nil {
t.Fatal(err)
}
got, err := client.GetDocument(context.Background(), namespace, want.ID)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("got unexpected document:\n\t%s", diff)
}
})
t.Run("update labels", func(t *testing.T) {
serverFn, clientFn, fx := setup(t)
server := serverFn(fx.auth(influxdb.WriteAction))
defer server.Close()
client := clientFn(server.URL)
want := fx.Document
want.Labels = want.Labels[:0]
if err := client.UpdateDocument(context.Background(), namespace, want); err != nil {
t.Fatal(err)
}
got, err := client.GetDocument(context.Background(), namespace, want.ID)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("got unexpected document:\n\t%s", diff)
}
})
t.Run("update meta", func(t *testing.T) {
serverFn, clientFn, fx := setup(t)
server := serverFn(fx.auth(influxdb.WriteAction))
defer server.Close()
client := clientFn(server.URL)
want := fx.Document
want.Meta.Name = "new name"
if err := client.UpdateDocument(context.Background(), namespace, want); err != nil {
t.Fatal(err)
}
got, err := client.GetDocument(context.Background(), namespace, want.ID)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("got unexpected document:\n\t%s", diff)
}
})
t.Run("unauthorized - wrong org", func(t *testing.T) {
serverFn, clientFn, fx := setup(t)
server := serverFn(func() influxdb.Authorizer {
a := fx.auth(influxdb.WriteAction)
for _, p := range a.Permissions {
*p.Resource.OrgID++
}
return a
}())
defer server.Close()
client := clientFn(server.URL)
want := fx.AnotherDocument
want.Content = "new content"
if err := client.UpdateDocument(context.Background(), namespace, want); err != nil {
if influxdb.ErrorCode(err) != influxdb.EUnauthorized {
t.Errorf("unexpected error: %v", err)
}
} else {
t.Error("expected error got none")
}
})
t.Run("unauthorized - insufficient permissions", func(t *testing.T) {
serverFn, clientFn, fx := setup(t)
server := serverFn(fx.authKO())
defer server.Close()
client := clientFn(server.URL)
want := fx.Document
want.Content = "new content"
if err := client.UpdateDocument(context.Background(), namespace, want); err != nil {
if influxdb.ErrorCode(err) != influxdb.EUnauthorized {
t.Errorf("unexpected error: %v", err)
}
} else {
t.Error("expected error got none")
}
})
}
func DeleteDocument(t *testing.T) {
t.Run("existing", func(t *testing.T) {
serverFn, clientFn, fx := setup(t)
want := fx.Document
server := serverFn(fx.auth(influxdb.WriteAction))
defer server.Close()
client := clientFn(server.URL)
pre, err := client.GetDocuments(context.Background(), namespace, fx.Org.ID)
if err != nil {
t.Fatal(err)
}
l := len(pre)
if err := client.DeleteDocument(context.Background(), namespace, want.ID); err != nil {
t.Fatal(err)
}
got, err := client.GetDocuments(context.Background(), namespace, fx.Org.ID)
if err != nil {
t.Fatal(err)
}
lgot := len(got)
lwant := l - 1
if diff := cmp.Diff(lgot, lwant); diff != "" {
t.Errorf("got unexpected length of docs:\n\t%v", diff)
}
})
t.Run("non existing", func(t *testing.T) {
serverFn, clientFn, fx := setup(t)
id := fx.Document.ID + 42
server := serverFn(fx.auth(influxdb.WriteAction))
defer server.Close()
client := clientFn(server.URL)
if err := client.DeleteDocument(context.Background(), namespace, id); err != nil {
if !strings.Contains(err.Error(), "document not found") {
t.Errorf("unexpected error: %v", err)
}
} else {
t.Error("expected error got none")
}
got, err := client.GetDocuments(context.Background(), namespace, fx.Org.ID)
if err != nil {
t.Fatal(err)
}
lgot := len(got)
lwant := 2
if diff := cmp.Diff(lgot, lwant); diff != "" {
t.Errorf("got unexpected length of docs:\n\t%v", diff)
}
})
t.Run("unauthorized", func(t *testing.T) {
serverFn, clientFn, fx := setup(t)
server := serverFn(fx.authKO())
defer server.Close()
client := clientFn(server.URL)
if err := client.DeleteDocument(context.Background(), namespace, fx.Document.ID); err != nil {
if influxdb.ErrorCode(err) != influxdb.EUnauthorized {
t.Errorf("unexpected error: %v", err)
}
} else {
t.Error("expected error got none")
}
})
t.Run("unauthorized - insufficient", func(t *testing.T) {
serverFn, clientFn, fx := setup(t)
server := serverFn(fx.auth(influxdb.ReadAction))
defer server.Close()
client := clientFn(server.URL)
if err := client.DeleteDocument(context.Background(), namespace, fx.Document.ID); err != nil {
if influxdb.ErrorCode(err) != influxdb.EUnauthorized {
t.Errorf("unexpected error: %v", err)
}
} else {
t.Error("expected error got none")
}
})
}
func GetLabels(t *testing.T) {
t.Run("get labels", func(t *testing.T) {
serverFn, clientFn, fx := setup(t)
// GetLabels does not pass through the LabelService. Org permissions are enough.
server := serverFn(fx.auth(influxdb.ReadAction))
defer server.Close()
client := clientFn(server.URL)
got, err := client.GetDocumentLabels(context.Background(), namespace, fx.Document.ID)
if err != nil {
t.Fatal(err)
}
want := fx.Document.Labels
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("got unexpected labels:\n\t%s", diff)
}
})
t.Run("unauthorized", func(t *testing.T) {
serverFn, clientFn, fx := setup(t)
server := serverFn(fx.authKO())
defer server.Close()
client := clientFn(server.URL)
if _, err := client.GetDocumentLabels(context.Background(), namespace, fx.AnotherDocument.ID); err != nil {
if influxdb.ErrorCode(err) != influxdb.EUnauthorized {
t.Errorf("unexpected error: %v", err)
}
} else {
t.Error("expected error got none")
}
})
}
func AddLabels(t *testing.T) {
t.Run("add one", func(t *testing.T) {
serverFn, clientFn, fx := setup(t)
server := serverFn(func() influxdb.Authorizer {
a := fx.auth(influxdb.WriteAction)
// The LabelService uses a "standard auth" mode.
// That's why we need to add further permissions and the org ones are not enough.
fx.addLabelPermission(a, influxdb.WriteAction, fx.Labels[1].ID)
return a
}())
defer server.Close()
client := clientFn(server.URL)
got, err := client.AddDocumentLabel(context.Background(), namespace, fx.Document.ID, fx.Labels[1].ID)
if err != nil {
t.Fatal(err)
}
want := fx.Labels[1]
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("got unexpected labels:\n\t%s", diff)
}
gotLs, err := client.GetDocumentLabels(context.Background(), namespace, fx.Document.ID)
if err != nil {
t.Fatal(err)
}
wantLs := fx.Labels
if diff := cmp.Diff(gotLs, wantLs); diff != "" {
t.Errorf("got unexpected labels:\n\t%s", diff)
}
})
t.Run("unauthorized", func(t *testing.T) {
serverFn, clientFn, fx := setup(t)
server := serverFn(fx.authKO())
defer server.Close()
client := clientFn(server.URL)
if _, err := client.AddDocumentLabel(context.Background(), namespace, fx.Document.ID, fx.Labels[1].ID); err != nil {
if influxdb.ErrorCode(err) != influxdb.EUnauthorized {
t.Errorf("unexpected error: %v", err)
}
} else {
t.Error("expected error got none")
}
})
t.Run("unauthorized - insufficient", func(t *testing.T) {
serverFn, clientFn, fx := setup(t)
server := serverFn(func() influxdb.Authorizer {
a := fx.auth(influxdb.WriteAction)
fx.addLabelPermission(a, influxdb.ReadAction, fx.Labels[1].ID)
return a
}())
defer server.Close()
client := clientFn(server.URL)
if _, err := client.AddDocumentLabel(context.Background(), namespace, fx.Document.ID, fx.Labels[1].ID); err != nil {
if influxdb.ErrorCode(err) != influxdb.EUnauthorized {
t.Errorf("unexpected error: %v", err)
}
} else {
t.Error("expected error got none")
}
})
t.Run("add same twice", func(t *testing.T) {
serverFn, clientFn, fx := setup(t)
server := serverFn(func() influxdb.Authorizer {
a := fx.auth(influxdb.WriteAction)
// The LabelService uses a "standard auth" mode.
// That's why we need to add further permissions and the org ones are not enough.
fx.addLabelPermission(a, influxdb.WriteAction, fx.Labels[0].ID)
return a
}())
defer server.Close()
client := clientFn(server.URL)
if _, err := client.AddDocumentLabel(context.Background(), namespace, fx.Document.ID, fx.Labels[0].ID); err != nil {
if !strings.Contains(err.Error(), influxdb.ErrLabelExistsOnResource.Msg) {
t.Errorf("unexpected error: %v", err.Error())
}
} else {
t.Error("expected error got none")
}
})
}
func DeleteLabel(t *testing.T) {
t.Run("existing", func(t *testing.T) {
serverFn, clientFn, fx := setup(t)
server := serverFn(func() influxdb.Authorizer {
a := fx.auth(influxdb.WriteAction)
// The LabelService uses a "standard auth" mode.
// That's why we need to add further permissions and the org ones are not enough.
fx.addLabelPermission(a, influxdb.WriteAction, fx.Document.Labels[0].ID)
return a
}())
defer server.Close()
client := clientFn(server.URL)
pre, err := client.GetDocumentLabels(context.Background(), namespace, fx.Document.ID)
if err != nil {
t.Fatal(err)
}
l := len(pre)
if err := client.DeleteDocumentLabel(context.Background(), namespace, fx.Document.ID, fx.Document.Labels[0].ID); err != nil {
t.Fatal(err)
}
got, err := client.GetDocumentLabels(context.Background(), namespace, fx.Document.ID)
if err != nil {
t.Fatal(err)
}
lgot := len(got)
lwant := l - 1
if diff := cmp.Diff(lgot, lwant); diff != "" {
t.Errorf("got unexpected length of docs:\n\t%v", diff)
}
})
t.Run("non existing", func(t *testing.T) {
serverFn, clientFn, fx := setup(t)
badLId := fx.Labels[2].ID + 42
server := serverFn(func() influxdb.Authorizer {
a := fx.auth(influxdb.WriteAction)
fx.addLabelPermission(a, influxdb.WriteAction, badLId)
return a
}())
defer server.Close()
client := clientFn(server.URL)
if err := client.DeleteDocumentLabel(context.Background(), namespace, fx.Document.ID, badLId); err != nil {
if !strings.Contains(err.Error(), "label not found") {
t.Errorf("unexpected error: %v", err)
}
} else {
t.Error("expected error got none")
}
got, err := client.GetDocumentLabels(context.Background(), namespace, fx.Document.ID)
if err != nil {
t.Fatal(err)
}
lgot := len(got)
lwant := 2
if diff := cmp.Diff(lgot, lwant); diff != "" {
t.Errorf("got unexpected length of labels:\n\t%v", diff)
}
})
t.Run("unauthorized - insufficient", func(t *testing.T) {
serverFn, clientFn, fx := setup(t)
server := serverFn(func() influxdb.Authorizer {
a := fx.auth(influxdb.WriteAction)
fx.addLabelPermission(a, influxdb.ReadAction, fx.Document.Labels[0].ID)
return a
}())
defer server.Close()
client := clientFn(server.URL)
if err := client.DeleteDocumentLabel(context.Background(), namespace, fx.Document.ID, fx.Document.Labels[0].ID); err != nil {
if influxdb.ErrorCode(err) != influxdb.EUnauthorized {
t.Errorf("unexpected error: %v", err)
}
} else {
t.Error("expected error got none")
}
got, err := client.GetDocumentLabels(context.Background(), namespace, fx.Document.ID)
if err != nil {
t.Fatal(err)
}
lgot := len(got)
lwant := 2
if diff := cmp.Diff(lgot, lwant); diff != "" {
t.Errorf("got unexpected length of labels:\n\t%v", diff)
}
})
}
type staticOrgIDResolver influxdb.ID
func (s staticOrgIDResolver) FindResourceOrganizationID(ctx context.Context, rt influxdb.ResourceType, id influxdb.ID) (influxdb.ID, error) {
return (influxdb.ID)(s), nil
}

View File

@ -1,960 +0,0 @@
package http
import (
"bytes"
"context"
"encoding/json"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/influxdata/httprouter"
"github.com/influxdata/influxdb/v2"
pcontext "github.com/influxdata/influxdb/v2/context"
kithttp "github.com/influxdata/influxdb/v2/kit/transport/http"
"github.com/influxdata/influxdb/v2/mock"
influxtesting "github.com/influxdata/influxdb/v2/testing"
"go.uber.org/zap/zaptest"
)
var (
doc1ID = influxtesting.MustIDBase16("020f755c3c082010")
doc2ID = influxtesting.MustIDBase16("020f755c3c082011")
doc3ID = influxtesting.MustIDBase16("020f755c3c082012")
doc4ID = influxtesting.MustIDBase16("020f755c3c082013")
doc5ID = influxtesting.MustIDBase16("020f755c3c082014")
doc6ID = influxtesting.MustIDBase16("020f755c3c082015")
user1ID = influxtesting.MustIDBase16("020f755c3c082001")
label1ID = influxtesting.MustIDBase16("020f755c3c082300")
label2ID = influxtesting.MustIDBase16("020f755c3c082301")
label3ID = influxtesting.MustIDBase16("020f755c3c082302")
label1 = influxdb.Label{
ID: label1ID,
Name: "l1",
}
label2 = influxdb.Label{
ID: label2ID,
Name: "l2",
}
label3 = influxdb.Label{
ID: label3ID,
Name: "l3",
}
label3MappingJSON, _ = json.Marshal(influxdb.LabelMapping{
LabelID: label3ID,
})
mockGen = mock.TimeGenerator{
FakeValue: time.Date(2006, 5, 24, 1, 2, 3, 4, time.UTC),
}
doc1 = influxdb.Document{
ID: doc1ID,
Meta: influxdb.DocumentMeta{
Name: "doc1",
Type: "typ1",
Description: "desc1",
},
Content: "content1",
Labels: []*influxdb.Label{
&label1,
},
}
doc2 = influxdb.Document{
ID: doc2ID,
Meta: influxdb.DocumentMeta{
Name: "doc2",
},
Content: "content2",
Labels: []*influxdb.Label{},
}
doc3 = influxdb.Document{
ID: doc3ID,
Meta: influxdb.DocumentMeta{
Name: "doc3",
},
Content: "content3",
Labels: []*influxdb.Label{
&label2,
},
}
doc4 = influxdb.Document{
ID: doc4ID,
Meta: influxdb.DocumentMeta{
Name: "doc4",
Type: "typ4",
},
Content: "content4",
}
doc5 = influxdb.Document{
ID: doc5ID,
Meta: influxdb.DocumentMeta{
Name: "doc5",
},
Content: "content5",
}
doc6 = influxdb.Document{
ID: doc6ID,
Meta: influxdb.DocumentMeta{
Name: "doc6",
},
Content: "content6",
}
docs = []*influxdb.Document{
&doc1,
&doc2,
}
docsResp = `{
"documents":[
{
"id": "020f755c3c082010",
"links": {
"self": "/api/v2/documents/template/020f755c3c082010"
},
"content": "content1",
"labels": [
{
"id": "020f755c3c082300",
"name": "l1"
}
],
"meta": {
"name": "doc1",
"type": "typ1",
"createdAt": "0001-01-01T00:00:00Z",
"updatedAt": "0001-01-01T00:00:00Z",
"description": "desc1"
}
},
{
"id": "020f755c3c082011",
"links": {
"self": "/api/v2/documents/template/020f755c3c082011"
},
"content": "content2",
"meta": {
"name": "doc2",
"createdAt": "0001-01-01T00:00:00Z",
"updatedAt": "0001-01-01T00:00:00Z"
}
}
]
}`
findDocsServiceMock = &mock.DocumentService{
FindDocumentStoreFn: func(context.Context, string) (influxdb.DocumentStore, error) {
return &mock.DocumentStore{
FindDocumentsFn: func(ctx context.Context, opts ...influxdb.DocumentFindOptions) ([]*influxdb.Document, error) {
return docs, nil
},
}, nil
},
}
findDoc1ServiceMock = &mock.DocumentService{
FindDocumentStoreFn: func(context.Context, string) (influxdb.DocumentStore, error) {
return &mock.DocumentStore{
FindDocumentFn: func(ctx context.Context, id influxdb.ID) (*influxdb.Document, error) {
return &doc1, nil
},
}, nil
},
}
findDoc2ServiceMock = &mock.DocumentService{
FindDocumentStoreFn: func(context.Context, string) (influxdb.DocumentStore, error) {
return &mock.DocumentStore{
FindDocumentFn: func(ctx context.Context, id influxdb.ID) (*influxdb.Document, error) {
return &doc2, nil
},
}, nil
},
}
getOrgIDServiceMock = &mock.OrganizationService{
FindOrganizationF: func(ctx context.Context, filter influxdb.OrganizationFilter) (*influxdb.Organization, error) {
return &influxdb.Organization{
ID: 1,
}, nil
},
}
)
// NewMockDocumentBackend returns a DocumentBackend with mock services.
func NewMockDocumentBackend(t *testing.T) *DocumentBackend {
return &DocumentBackend{
log: zaptest.NewLogger(t),
DocumentService: mock.NewDocumentService(),
LabelService: mock.NewLabelService(),
}
}
func TestService_handleDeleteDocumentLabel(t *testing.T) {
type fields struct {
DocumentService influxdb.DocumentService
LabelService influxdb.LabelService
}
type args struct {
authorizer influxdb.Authorizer
documentID influxdb.ID
labelID influxdb.ID
}
type wants struct {
statusCode int
contentType string
body string
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "bad doc id",
wants: wants{
statusCode: http.StatusBadRequest,
contentType: "application/json; charset=utf-8",
body: `{"code":"invalid", "message":"url missing resource id"}`,
},
},
{
name: "bad label id",
args: args{
authorizer: &influxdb.Session{UserID: user1ID},
documentID: doc1ID,
},
wants: wants{
statusCode: http.StatusBadRequest,
contentType: "application/json; charset=utf-8",
body: `{"code":"invalid", "message":"label id is missing"}`,
},
},
{
name: "label not found",
fields: fields{
DocumentService: findDoc2ServiceMock,
LabelService: &mock.LabelService{
FindLabelByIDFn: func(context.Context, influxdb.ID) (*influxdb.Label, error) {
return nil, &influxdb.Error{
Code: influxdb.ENotFound,
Msg: "label not found",
}
},
},
},
args: args{
authorizer: &influxdb.Session{UserID: user1ID},
documentID: doc2ID,
labelID: label1ID,
},
wants: wants{
statusCode: http.StatusNotFound,
contentType: "application/json; charset=utf-8",
body: `{"code":"not found", "message":"label not found"}`,
},
},
{
name: "regular get labels",
fields: fields{
DocumentService: &mock.DocumentService{
FindDocumentStoreFn: func(context.Context, string) (influxdb.DocumentStore, error) {
return &mock.DocumentStore{
FindDocumentFn: func(ctx context.Context, id influxdb.ID) (*influxdb.Document, error) {
return &doc3, nil
},
UpdateDocumentFn: func(ctx context.Context, d *influxdb.Document) error {
return nil
},
}, nil
},
},
LabelService: &mock.LabelService{
FindLabelByIDFn: func(context.Context, influxdb.ID) (*influxdb.Label, error) {
return &label2, nil
},
DeleteLabelMappingFn: func(context.Context, *influxdb.LabelMapping) error {
return nil
},
},
},
args: args{
authorizer: &influxdb.Session{UserID: user1ID},
documentID: doc3ID,
labelID: label2ID,
},
wants: wants{
statusCode: http.StatusNoContent,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
documentBackend := NewMockDocumentBackend(t)
documentBackend.HTTPErrorHandler = kithttp.ErrorHandler(0)
documentBackend.DocumentService = tt.fields.DocumentService
documentBackend.LabelService = tt.fields.LabelService
h := NewDocumentHandler(documentBackend)
r := httptest.NewRequest("DELETE", "http://any.url", nil)
r = r.WithContext(pcontext.SetAuthorizer(r.Context(), tt.args.authorizer))
r = r.WithContext(context.WithValue(r.Context(),
httprouter.ParamsKey,
httprouter.Params{
{
Key: "ns",
Value: "template",
},
{
Key: "id",
Value: tt.args.documentID.String(),
},
{
Key: "lid",
Value: tt.args.labelID.String(),
},
}))
w := httptest.NewRecorder()
h.handleDeleteDocumentLabel(w, r)
res := w.Result()
content := res.Header.Get("Content-Type")
body, _ := ioutil.ReadAll(res.Body)
if res.StatusCode != tt.wants.statusCode {
t.Errorf("%q. handleDeleteDocumentLabel() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode)
}
if tt.wants.contentType != "" && content != tt.wants.contentType {
t.Errorf("%q. handleDeleteDocumentLabel() = %v, want %v", tt.name, content, tt.wants.contentType)
}
if tt.wants.body != "" {
if eq, diff, err := jsonEqual(string(body), tt.wants.body); err != nil {
t.Errorf("%q, handleDeleteDocumentLabel(). error unmarshalling json %v", tt.name, err)
} else if !eq {
t.Errorf("%q. handleDeleteDocumentLabel() = ***%s***", tt.name, diff)
}
}
})
}
}
func TestService_handlePostDocumentLabel(t *testing.T) {
type fields struct {
DocumentService influxdb.DocumentService
LabelService influxdb.LabelService
}
type args struct {
body *bytes.Buffer
authorizer influxdb.Authorizer
documentID influxdb.ID
}
type wants struct {
statusCode int
contentType string
body string
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "bad doc id",
args: args{
body: new(bytes.Buffer),
},
wants: wants{
statusCode: http.StatusBadRequest,
contentType: "application/json; charset=utf-8",
body: `{"code":"invalid", "message":"url missing id"}`,
},
},
{
name: "doc not found",
fields: fields{
DocumentService: &mock.DocumentService{
FindDocumentStoreFn: func(context.Context, string) (influxdb.DocumentStore, error) {
return &mock.DocumentStore{
FindDocumentFn: func(ctx context.Context, id influxdb.ID) (*influxdb.Document, error) {
return nil, &influxdb.Error{
Code: influxdb.ENotFound,
Msg: "doc not found",
}
},
}, nil
},
},
},
args: args{
authorizer: &influxdb.Session{UserID: user1ID},
documentID: doc2ID,
body: new(bytes.Buffer),
},
wants: wants{
statusCode: http.StatusNotFound,
contentType: "application/json; charset=utf-8",
body: `{"code":"not found", "message":"doc not found"}`,
},
},
{
name: "empty post a label",
fields: fields{
DocumentService: &mock.DocumentService{
FindDocumentStoreFn: func(context.Context, string) (influxdb.DocumentStore, error) {
return &mock.DocumentStore{
FindDocumentFn: func(ctx context.Context, id influxdb.ID) (*influxdb.Document, error) {
return &doc3, nil
},
UpdateDocumentFn: func(ctx context.Context, d *influxdb.Document) error {
return nil
},
}, nil
},
},
LabelService: &mock.LabelService{
FindLabelByIDFn: func(context.Context, influxdb.ID) (*influxdb.Label, error) {
return &label2, nil
},
DeleteLabelMappingFn: func(context.Context, *influxdb.LabelMapping) error {
return nil
},
},
},
args: args{
authorizer: &influxdb.Session{UserID: user1ID},
documentID: doc3ID,
body: new(bytes.Buffer),
},
wants: wants{
statusCode: http.StatusBadRequest,
contentType: "application/json; charset=utf-8",
body: `{"code":"invalid", "message":"Invalid post label map request"}`,
},
},
{
name: "regular post a label",
fields: fields{
DocumentService: &mock.DocumentService{
FindDocumentStoreFn: func(context.Context, string) (influxdb.DocumentStore, error) {
return &mock.DocumentStore{
TimeGenerator: mockGen,
FindDocumentFn: func(ctx context.Context, id influxdb.ID) (*influxdb.Document, error) {
return &doc4, nil
},
UpdateDocumentFn: func(ctx context.Context, d *influxdb.Document) error {
return nil
},
}, nil
},
},
LabelService: &mock.LabelService{
CreateLabelMappingFn: func(context.Context, *influxdb.LabelMapping) error {
return nil
},
FindLabelByIDFn: func(context.Context, influxdb.ID) (*influxdb.Label, error) {
return &label3, nil
},
DeleteLabelMappingFn: func(context.Context, *influxdb.LabelMapping) error {
return nil
},
},
},
args: args{
authorizer: &influxdb.Session{UserID: user1ID},
documentID: doc3ID,
body: bytes.NewBuffer(label3MappingJSON),
},
wants: wants{
statusCode: http.StatusCreated,
contentType: "application/json; charset=utf-8",
body: `{"label": {
"id": "020f755c3c082302",
"name": "l3"
},
"links": {"self": "/api/v2/labels/020f755c3c082302"}}`,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
documentBackend := NewMockDocumentBackend(t)
documentBackend.HTTPErrorHandler = kithttp.ErrorHandler(0)
documentBackend.DocumentService = tt.fields.DocumentService
documentBackend.LabelService = tt.fields.LabelService
h := NewDocumentHandler(documentBackend)
r := httptest.NewRequest("POST", "http://any.url", tt.args.body)
r = r.WithContext(pcontext.SetAuthorizer(r.Context(), tt.args.authorizer))
r = r.WithContext(context.WithValue(r.Context(),
httprouter.ParamsKey,
httprouter.Params{
{
Key: "ns",
Value: "template",
},
{
Key: "id",
Value: tt.args.documentID.String(),
},
}))
w := httptest.NewRecorder()
h.handlePostDocumentLabel(w, r)
res := w.Result()
content := res.Header.Get("Content-Type")
body, _ := ioutil.ReadAll(res.Body)
if res.StatusCode != tt.wants.statusCode {
t.Errorf("%q. handlePostDocumentLabel() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode)
}
if tt.wants.contentType != "" && content != tt.wants.contentType {
t.Errorf("%q. handlePostDocumentLabel() = %v, want %v", tt.name, content, tt.wants.contentType)
}
if tt.wants.body != "" {
if eq, diff, err := jsonEqual(string(body), tt.wants.body); err != nil {
t.Errorf("%q, handlePostDocumentLabel(). error unmarshalling json %v", tt.name, err)
} else if !eq {
t.Errorf("%q. handlePostDocumentLabel() = ***%s***", tt.name, diff)
}
}
})
}
}
func TestService_handleGetDocumentLabels(t *testing.T) {
type fields struct {
DocumentService influxdb.DocumentService
LabelService influxdb.LabelService
}
type args struct {
queryParams map[string][]string
authorizer influxdb.Authorizer
documentID influxdb.ID
}
type wants struct {
statusCode int
contentType string
body string
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "invalid document id",
wants: wants{
statusCode: http.StatusBadRequest,
contentType: "application/json; charset=utf-8",
body: `{"code":"invalid", "message":"url missing id"}`,
},
},
{
name: "regular get labels",
fields: fields{
DocumentService: findDoc1ServiceMock,
},
args: args{
authorizer: &influxdb.Session{UserID: user1ID},
documentID: doc1ID,
},
wants: wants{
statusCode: http.StatusOK,
contentType: "application/json; charset=utf-8",
body: `{"labels": [{
"id": "020f755c3c082300",
"name": "l1"
}],"links":{"self":"/api/v2/labels"}}`},
},
{
name: "find no labels",
fields: fields{
DocumentService: findDoc2ServiceMock,
},
args: args{
authorizer: &influxdb.Session{UserID: user1ID},
documentID: doc1ID,
},
wants: wants{
statusCode: http.StatusOK,
contentType: "application/json; charset=utf-8",
body: `{"labels": [],"links":{"self":"/api/v2/labels"}}`},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
documentBackend := NewMockDocumentBackend(t)
documentBackend.HTTPErrorHandler = kithttp.ErrorHandler(0)
documentBackend.DocumentService = tt.fields.DocumentService
documentBackend.LabelService = tt.fields.LabelService
h := NewDocumentHandler(documentBackend)
r := httptest.NewRequest("GET", "http://any.url", nil)
qp := r.URL.Query()
for k, vs := range tt.args.queryParams {
for _, v := range vs {
qp.Add(k, v)
}
}
r.URL.RawQuery = qp.Encode()
r = r.WithContext(pcontext.SetAuthorizer(r.Context(), tt.args.authorizer))
r = r.WithContext(context.WithValue(r.Context(),
httprouter.ParamsKey,
httprouter.Params{
{
Key: "ns",
Value: "template",
},
{
Key: "id",
Value: tt.args.documentID.String(),
},
}))
w := httptest.NewRecorder()
h.handleGetDocumentLabel(w, r)
res := w.Result()
content := res.Header.Get("Content-Type")
body, _ := ioutil.ReadAll(res.Body)
if res.StatusCode != tt.wants.statusCode {
t.Errorf("%q. handleGetDocumentLabel() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode)
}
if tt.wants.contentType != "" && content != tt.wants.contentType {
t.Errorf("%q. handleGetDocumentLabel() = %v, want %v", tt.name, content, tt.wants.contentType)
}
if tt.wants.body != "" {
if eq, diff, err := jsonEqual(string(body), tt.wants.body); err != nil {
t.Errorf("%q, handleGetDocumentLabel(). error unmarshalling json %v", tt.name, err)
} else if !eq {
t.Errorf("%q. handleGetDocumentLabel() = ***%s***", tt.name, diff)
}
}
})
}
}
func TestService_handleGetDocuments(t *testing.T) {
type fields struct {
DocumentService influxdb.DocumentService
OrganizationService influxdb.OrganizationService
}
type args struct {
queryParams map[string][]string
authorizer influxdb.Authorizer
}
type wants struct {
statusCode int
contentType string
body string
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "get all documents with both org and orgID",
fields: fields{
DocumentService: findDocsServiceMock,
},
args: args{
queryParams: map[string][]string{
"orgID": []string{"020f755c3c082002"},
"org": []string{"org1"},
},
authorizer: &influxdb.Session{UserID: user1ID},
},
wants: wants{
statusCode: http.StatusBadRequest,
contentType: "application/json; charset=utf-8",
body: `{"code":"invalid", "message":"Please provide either org or orgID, not both"}`,
},
},
{
name: "get all documents with orgID",
fields: fields{
DocumentService: findDocsServiceMock,
},
args: args{
queryParams: map[string][]string{
"orgID": []string{"020f755c3c082002"},
},
authorizer: &influxdb.Session{UserID: user1ID},
},
wants: wants{
statusCode: http.StatusOK,
contentType: "application/json; charset=utf-8",
body: docsResp,
},
},
{
name: "get all documents with org name",
fields: fields{
DocumentService: findDocsServiceMock,
OrganizationService: getOrgIDServiceMock,
},
args: args{
queryParams: map[string][]string{
"org": []string{"org1"},
},
authorizer: &influxdb.Session{UserID: user1ID},
},
wants: wants{
statusCode: http.StatusOK,
contentType: "application/json; charset=utf-8",
body: docsResp,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
documentBackend := NewMockDocumentBackend(t)
documentBackend.HTTPErrorHandler = kithttp.ErrorHandler(0)
documentBackend.DocumentService = tt.fields.DocumentService
documentBackend.OrganizationService = tt.fields.OrganizationService
h := NewDocumentHandler(documentBackend)
r := httptest.NewRequest("GET", "http://any.url", nil)
qp := r.URL.Query()
for k, vs := range tt.args.queryParams {
for _, v := range vs {
qp.Add(k, v)
}
}
r.URL.RawQuery = qp.Encode()
r = r.WithContext(pcontext.SetAuthorizer(r.Context(), tt.args.authorizer))
r = r.WithContext(context.WithValue(r.Context(),
httprouter.ParamsKey,
httprouter.Params{
{
Key: "ns",
Value: "template",
}}))
w := httptest.NewRecorder()
h.handleGetDocuments(w, r)
res := w.Result()
content := res.Header.Get("Content-Type")
body, _ := ioutil.ReadAll(res.Body)
if res.StatusCode != tt.wants.statusCode {
t.Errorf("%q. handleGetDocuments() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode)
}
if tt.wants.contentType != "" && content != tt.wants.contentType {
t.Errorf("%q. handleGetDocuments() = %v, want %v", tt.name, content, tt.wants.contentType)
}
if tt.wants.body != "" {
if eq, diff, err := jsonEqual(string(body), tt.wants.body); err != nil {
t.Errorf("%q, handleGetDocuments(). error unmarshalling json %v", tt.name, err)
} else if !eq {
t.Errorf("%q. handleGetDocuments() = ***%s***", tt.name, diff)
}
}
})
}
}
func TestService_handlePostDocuments(t *testing.T) {
type fields struct {
DocumentService influxdb.DocumentService
LabelService influxdb.LabelService
OrganizationService influxdb.OrganizationService
}
type args struct {
body *bytes.Buffer
queryParams map[string][]string
authorizer influxdb.Authorizer
}
type wants struct {
statusCode int
contentType string
body string
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "blank body",
fields: fields{
DocumentService: &mock.DocumentService{},
LabelService: &mock.LabelService{},
},
args: args{
body: bytes.NewBuffer([]byte{}),
queryParams: map[string][]string{
"org": []string{"org1"},
},
authorizer: &influxdb.Session{UserID: user1ID},
},
wants: wants{
statusCode: http.StatusBadRequest,
contentType: "application/json; charset=utf-8",
body: `{"code":"invalid","message": "document body error: EOF"}`,
},
},
{
name: "empty json",
fields: fields{
DocumentService: &mock.DocumentService{},
LabelService: &mock.LabelService{},
},
args: args{
body: bytes.NewBuffer([]byte(`{}`)),
queryParams: map[string][]string{
"org": []string{"org1"},
},
authorizer: &influxdb.Session{UserID: user1ID},
},
wants: wants{
statusCode: http.StatusBadRequest,
contentType: "application/json; charset=utf-8",
body: `{"code":"invalid","message": "missing document body"}`,
},
},
{
name: "without label",
fields: fields{
DocumentService: &mock.DocumentService{
FindDocumentStoreFn: func(context.Context, string) (influxdb.DocumentStore, error) {
return &mock.DocumentStore{
TimeGenerator: mockGen,
CreateDocumentFn: func(ctx context.Context, d *influxdb.Document) error {
d.Meta.CreatedAt = mockGen.Now()
return nil
},
}, nil
},
},
LabelService: &mock.LabelService{},
OrganizationService: getOrgIDServiceMock,
},
args: args{
body: func() *bytes.Buffer {
req := postDocumentRequest{
Document: &doc5,
Org: "org1",
}
m, _ := json.Marshal(req)
return bytes.NewBuffer(m)
}(),
queryParams: map[string][]string{
"org": []string{"org1"},
},
authorizer: &influxdb.Session{UserID: user1ID},
},
wants: wants{
statusCode: http.StatusCreated,
contentType: "application/json; charset=utf-8",
body: `{
"content": "content5",
"id": "020f755c3c082014",
"links": {
"self": "/api/v2/documents/template/020f755c3c082014"
},
"meta": {
"name": "doc5",
"createdAt": "2006-05-24T01:02:03.000000004Z",
"updatedAt": "0001-01-01T00:00:00Z"
}}`,
},
},
{
name: "with label",
fields: fields{
DocumentService: &mock.DocumentService{
FindDocumentStoreFn: func(context.Context, string) (influxdb.DocumentStore, error) {
return &mock.DocumentStore{
TimeGenerator: mockGen,
CreateDocumentFn: func(ctx context.Context, d *influxdb.Document) error {
d.Labels = []*influxdb.Label{&label1, &label2}
d.Meta.CreatedAt = mockGen.Now()
return nil
},
}, nil
},
},
LabelService: &mock.LabelService{
FindLabelByIDFn: func(ctx context.Context, id influxdb.ID) (*influxdb.Label, error) {
if id == label1ID {
return &label1, nil
}
return &label2, nil
},
},
OrganizationService: getOrgIDServiceMock,
},
args: args{
body: func() *bytes.Buffer {
req := postDocumentRequest{
Document: &doc6,
Org: "org1",
}
m, _ := json.Marshal(req)
return bytes.NewBuffer(m)
}(),
authorizer: &influxdb.Session{UserID: user1ID},
},
wants: wants{
statusCode: http.StatusCreated,
contentType: "application/json; charset=utf-8",
body: `{
"content": "content6",
"id": "020f755c3c082015",
"links": {
"self": "/api/v2/documents/template/020f755c3c082015"
},
"labels": [{
"id": "020f755c3c082300",
"name": "l1"
},
{
"id": "020f755c3c082301",
"name": "l2"
}],
"meta": {
"name": "doc6",
"createdAt": "2006-05-24T01:02:03.000000004Z",
"updatedAt": "0001-01-01T00:00:00Z"
}}`,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
documentBackend := NewMockDocumentBackend(t)
documentBackend.HTTPErrorHandler = kithttp.ErrorHandler(0)
documentBackend.DocumentService = tt.fields.DocumentService
documentBackend.LabelService = tt.fields.LabelService
documentBackend.OrganizationService = tt.fields.OrganizationService
h := NewDocumentHandler(documentBackend)
r := httptest.NewRequest("POST", "http://any.url", tt.args.body)
r = r.WithContext(pcontext.SetAuthorizer(r.Context(), tt.args.authorizer))
r = r.WithContext(context.WithValue(r.Context(),
httprouter.ParamsKey,
httprouter.Params{
{
Key: "ns",
Value: "template",
},
}))
w := httptest.NewRecorder()
h.handlePostDocument(w, r)
res := w.Result()
content := res.Header.Get("Content-Type")
body, _ := ioutil.ReadAll(res.Body)
if res.StatusCode != tt.wants.statusCode {
t.Errorf("%q. handlePostDocument() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode)
}
if tt.wants.contentType != "" && content != tt.wants.contentType {
t.Errorf("%q. handlePostDocument() = %v, want %v", tt.name, content, tt.wants.contentType)
}
if tt.wants.body != "" {
if eq, diff, err := jsonEqual(tt.wants.body, string(body)); err != nil {
t.Errorf("%q, handlePostDocument(). error unmarshalling json %v", tt.name, err)
} else if !eq {
t.Errorf("%q. handlePostDocument() = ***%s***", tt.name, diff)
}
}
})
}
}

View File

@ -1,49 +0,0 @@
package http
import (
"context"
"net/url"
"github.com/influxdata/httprouter"
"github.com/influxdata/influxdb/v2"
)
// TODO remove this file once bucket and org service are moved to the tenant service
func decodeIDFromCtx(ctx context.Context, name string) (influxdb.ID, error) {
params := httprouter.ParamsFromContext(ctx)
idStr := params.ByName(name)
if idStr == "" {
return 0, &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "url missing " + name,
}
}
var i influxdb.ID
if err := i.DecodeFromString(idStr); err != nil {
return 0, &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
}
}
return i, nil
}
func decodeIDFromQuery(v url.Values, key string) (influxdb.ID, error) {
idStr := v.Get(key)
if idStr == "" {
return 0, nil
}
id, err := influxdb.IDFromString(idStr)
if err != nil {
return 0, &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
}
}
return *id, nil
}

View File

@ -637,6 +637,10 @@ func (s *LabelService) DeleteLabelMapping(ctx context.Context, m *influxdb.Label
}
return s.Client.
Delete(resourceIDPath(m.ResourceType, m.ResourceID, "labels")).
Delete(resourceIDMappingPath(m.ResourceType, m.ResourceID, "labels", m.LabelID)).
Do(ctx)
}
func resourceIDMappingPath(resourceType influxdb.ResourceType, resourceID influxdb.ID, p string, labelID influxdb.ID) string {
return path.Join("/api/v2/", string(resourceType), resourceID.String(), p, labelID.String())
}

View File

@ -13,7 +13,7 @@ import (
"github.com/influxdata/httprouter"
platform "github.com/influxdata/influxdb/v2"
kithttp "github.com/influxdata/influxdb/v2/kit/transport/http"
"github.com/influxdata/influxdb/v2/kv"
"github.com/influxdata/influxdb/v2/label"
"github.com/influxdata/influxdb/v2/mock"
platformtesting "github.com/influxdata/influxdb/v2/testing"
"go.uber.org/zap/zaptest"
@ -595,23 +595,34 @@ func TestService_handlePatchLabel(t *testing.T) {
func initLabelService(f platformtesting.LabelFields, t *testing.T) (platform.LabelService, string, func()) {
store := NewTestInmemStore(t)
svc := kv.NewService(zaptest.NewLogger(t), store)
svc.IDGenerator = f.IDGenerator
labelStore, err := label.NewStore(store)
if err != nil {
t.Fatal(err)
}
if f.IDGenerator != nil {
labelStore.IDGenerator = f.IDGenerator
}
labelService := label.NewService(labelStore)
ctx := context.Background()
for _, l := range f.Labels {
if err := svc.PutLabel(ctx, l); err != nil {
t.Fatalf("failed to populate labels: %v", err)
}
mock.SetIDForFunc(&labelStore.IDGenerator, l.ID, func() {
if err := labelService.CreateLabel(ctx, l); err != nil {
t.Fatalf("failed to populate labels: %v", err)
}
})
}
for _, m := range f.Mappings {
if err := svc.PutLabelMapping(ctx, m); err != nil {
if err := labelService.CreateLabelMapping(ctx, m); err != nil {
t.Fatalf("failed to populate label mappings: %v", err)
}
}
handler := NewLabelHandler(zaptest.NewLogger(t), svc, kithttp.ErrorHandler(0))
handler := NewLabelHandler(zaptest.NewLogger(t), labelService, kithttp.ErrorHandler(0))
server := httptest.NewServer(handler)
client := LabelService{
Client: mustNewHTTPClient(t, server.URL, ""),

View File

@ -125,6 +125,12 @@ func ignoreMethod(ignoredMethods ...string) isValidMethodFn {
}
}
const (
prefixSetup = "/api/v2/setup"
organizationsIDSecretsPath = "/api/v2/orgs/:id/secrets"
organizationsIDSecretsDeletePath = "/api/v2/orgs/:id/secrets/delete"
)
// TODO(@jsteenb2): make this a stronger type that handlers can register routes that should not be logged.
var blacklistEndpoints = map[string]isValidMethodFn{
prefixSignIn: ignoreMethod(),
@ -153,15 +159,3 @@ func (b *bodyEchoer) Read(p []byte) (int, error) {
func (b *bodyEchoer) Close() error {
return b.rc.Close()
}
func applyMW(h http.Handler, m ...kithttp.Middleware) http.Handler {
if len(m) < 1 {
return h
}
wrapped := h
for i := len(m) - 1; i >= 0; i-- {
wrapped = m[i](wrapped)
}
return wrapped
}

View File

@ -21,8 +21,10 @@ import (
"github.com/influxdata/influxdb/v2/notification/endpoint/service"
endpointTesting "github.com/influxdata/influxdb/v2/notification/endpoint/service/testing"
"github.com/influxdata/influxdb/v2/pkg/testttp"
"github.com/influxdata/influxdb/v2/secret"
"github.com/influxdata/influxdb/v2/tenant"
influxTesting "github.com/influxdata/influxdb/v2/testing"
"github.com/stretchr/testify/require"
"go.uber.org/zap/zaptest"
)
@ -1065,17 +1067,22 @@ func initNotificationEndpointService(f endpointTesting.NotificationEndpointField
ctx := context.Background()
store := NewTestInmemStore(t)
logger := zaptest.NewLogger(t)
kvSvc := kv.NewService(logger, store)
tenantStore := tenant.NewStore(store)
tenantService := tenant.NewService(tenantStore)
kvSvc := kv.NewService(logger, store, tenantService)
kvSvc.IDGenerator = f.IDGenerator
kvSvc.TimeGenerator = f.TimeGenerator
secretStore, err := secret.NewStore(store)
require.NoError(t, err)
secretSvc := secret.NewService(secretStore)
endpointStore := service.NewStore(store)
endpointStore.IDGenerator = f.IDGenerator
endpointStore.TimeGenerator = f.TimeGenerator
endpointService := service.New(endpointStore, kvSvc)
tenantStore := tenant.NewStore(store)
tenantService := tenant.NewService(tenantStore)
endpointService := service.New(endpointStore, secretSvc)
for _, o := range f.Orgs {
withOrgID(tenantStore, o.ID, func() {
@ -1107,7 +1114,8 @@ func initNotificationEndpointService(f endpointTesting.NotificationEndpointField
done := server.Close
client := mustNewHTTPClient(t, server.URL, "")
return NewNotificationEndpointService(client), kvSvc, done
return NewNotificationEndpointService(client), secretSvc, done
}
func TestNotificationEndpointService(t *testing.T) {

View File

@ -731,20 +731,12 @@ func (h *NotificationRuleHandler) handleDeleteNotificationRule(w http.ResponseWr
// NotificationRuleService is an http client that implements the NotificationRuleStore interface
type NotificationRuleService struct {
Client *httpc.Client
*UserResourceMappingService
*OrganizationService
}
// NewNotificationRuleService wraps an httpc.Client in a NotificationRuleService
func NewNotificationRuleService(client *httpc.Client) *NotificationRuleService {
return &NotificationRuleService{
Client: client,
UserResourceMappingService: &UserResourceMappingService{
Client: client,
},
OrganizationService: &OrganizationService{
Client: client,
},
}
}

View File

@ -1,217 +0,0 @@
package http
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"github.com/influxdata/httprouter"
platform "github.com/influxdata/influxdb/v2"
"go.uber.org/zap"
)
// SetupBackend is all services and associated parameters required to construct
// the SetupHandler.
type SetupBackend struct {
platform.HTTPErrorHandler
log *zap.Logger
OnboardingService platform.OnboardingService
}
// NewSetupBackend returns a new instance of SetupBackend.
func NewSetupBackend(log *zap.Logger, b *APIBackend) *SetupBackend {
return &SetupBackend{
HTTPErrorHandler: b.HTTPErrorHandler,
log: log,
// OnboardingService: b.OnboardingService,
}
}
// SetupHandler represents an HTTP API handler for onboarding setup.
type SetupHandler struct {
*httprouter.Router
platform.HTTPErrorHandler
log *zap.Logger
OnboardingService platform.OnboardingService
}
const (
prefixSetup = "/api/v2/setup"
)
// NewSetupHandler returns a new instance of SetupHandler.
func NewSetupHandler(log *zap.Logger, b *SetupBackend) *SetupHandler {
h := &SetupHandler{
Router: NewRouter(b.HTTPErrorHandler),
HTTPErrorHandler: b.HTTPErrorHandler,
log: log,
OnboardingService: b.OnboardingService,
}
h.HandlerFunc("POST", prefixSetup, h.handlePostSetup)
h.HandlerFunc("GET", prefixSetup, h.isOnboarding)
return h
}
type isOnboardingResponse struct {
Allowed bool `json:"allowed"`
}
// isOnboarding is the HTTP handler for the GET /api/v2/setup route.
func (h *SetupHandler) isOnboarding(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
result, err := h.OnboardingService.IsOnboarding(ctx)
if err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
h.log.Debug("Onboarding eligibility check finished", zap.String("result", fmt.Sprint(result)))
if err := encodeResponse(ctx, w, http.StatusOK, isOnboardingResponse{result}); err != nil {
logEncodingError(h.log, r, err)
return
}
}
// isOnboarding is the HTTP handler for the POST /api/v2/setup route.
func (h *SetupHandler) handlePostSetup(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodePostSetupRequest(ctx, r)
if err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
results, err := h.OnboardingService.OnboardInitialUser(ctx, req)
if err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
h.log.Debug("Onboarding setup completed", zap.String("results", fmt.Sprint(results)))
if err := encodeResponse(ctx, w, http.StatusCreated, newOnboardingResponse(results)); err != nil {
logEncodingError(h.log, r, err)
return
}
}
type onboardingResponse struct {
User *UserResponse `json:"user"`
Bucket *bucketResponse `json:"bucket"`
Organization orgResponse `json:"org"`
Auth *authResponse `json:"auth"`
}
func newOnboardingResponse(results *platform.OnboardingResults) *onboardingResponse {
// when onboarding the permissions are for all resources and no
// specifically named resources. Therefore, there is no need to
// lookup the name.
ps := make([]permissionResponse, len(results.Auth.Permissions))
for i, p := range results.Auth.Permissions {
ps[i] = permissionResponse{
Action: p.Action,
Resource: resourceResponse{
Resource: p.Resource,
},
}
}
return &onboardingResponse{
User: newUserResponse(results.User),
Bucket: NewBucketResponse(results.Bucket, []*platform.Label{}),
Organization: newOrgResponse(*results.Org),
Auth: newAuthResponse(results.Auth, results.Org, results.User, ps),
}
}
func decodePostSetupRequest(ctx context.Context, r *http.Request) (*platform.OnboardingRequest, error) {
req := &platform.OnboardingRequest{}
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
return nil, err
}
return req, nil
}
// SetupService connects to Influx via HTTP to perform onboarding operations
type SetupService struct {
Addr string
InsecureSkipVerify bool
}
// IsOnboarding determine if onboarding request is allowed.
func (s *SetupService) IsOnboarding(ctx context.Context) (bool, error) {
u, err := NewURL(s.Addr, prefixSetup)
if err != nil {
return false, err
}
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return false, err
}
hc := NewClient(u.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return false, err
}
defer resp.Body.Close()
if err := CheckError(resp); err != nil {
return false, err
}
var ir isOnboardingResponse
if err := json.NewDecoder(resp.Body).Decode(&ir); err != nil {
return false, err
}
return ir.Allowed, nil
}
// OnboardInitialUser OnboardingResults.
func (s *SetupService) OnboardInitialUser(ctx context.Context, or *platform.OnboardingRequest) (*platform.OnboardingResults, error) {
u, err := NewURL(s.Addr, prefixSetup)
if err != nil {
return nil, err
}
octets, err := json.Marshal(or)
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", u.String(), bytes.NewReader(octets))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
hc := NewClient(u.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// TODO(jsternberg): Should this check for a 201 explicitly?
if err := CheckError(resp); err != nil {
return nil, err
}
var oResp onboardingResponse
if err := json.NewDecoder(resp.Body).Decode(&oResp); err != nil {
return nil, err
}
bkt, err := oResp.Bucket.toInfluxDB()
if err != nil {
return nil, err
}
return &platform.OnboardingResults{
Org: &oResp.Organization.Organization,
User: &oResp.User.User,
Auth: oResp.Auth.toPlatform(),
Bucket: bkt,
}, nil
}
func (s *SetupService) OnboardUser(ctx context.Context, or *platform.OnboardingRequest) (*platform.OnboardingResults, error) {
return nil, errors.New("not yet implemented")
}

View File

@ -1,67 +0,0 @@
package http
import (
"context"
"net/http/httptest"
"testing"
platform "github.com/influxdata/influxdb/v2"
kithttp "github.com/influxdata/influxdb/v2/kit/transport/http"
"github.com/influxdata/influxdb/v2/kv"
"github.com/influxdata/influxdb/v2/mock"
platformtesting "github.com/influxdata/influxdb/v2/testing"
"go.uber.org/zap/zaptest"
)
// NewMockSetupBackend returns a SetupBackend with mock services.
func NewMockSetupBackend(t *testing.T) *SetupBackend {
return &SetupBackend{
log: zaptest.NewLogger(t),
OnboardingService: mock.NewOnboardingService(),
}
}
func initOnboardingService(f platformtesting.OnboardingFields, t *testing.T) (platform.OnboardingService, func()) {
t.Helper()
store := NewTestInmemStore(t)
svc := kv.NewService(zaptest.NewLogger(t), store)
svc.IDGenerator = f.IDGenerator
svc.OrgIDs = f.IDGenerator
svc.BucketIDs = f.IDGenerator
svc.TokenGenerator = f.TokenGenerator
if f.TimeGenerator == nil {
f.TimeGenerator = platform.RealTimeGenerator{}
}
svc.TimeGenerator = f.TimeGenerator
ctx := context.Background()
if err := svc.PutOnboardingStatus(ctx, !f.IsOnboarding); err != nil {
t.Fatalf("failed to set new onboarding finished: %v", err)
}
setupBackend := NewMockSetupBackend(t)
setupBackend.HTTPErrorHandler = kithttp.ErrorHandler(0)
setupBackend.OnboardingService = svc
handler := NewSetupHandler(zaptest.NewLogger(t), setupBackend)
server := httptest.NewServer(handler)
client := struct {
*SetupService
*Service
platform.PasswordsService
}{
SetupService: &SetupService{
Addr: server.URL,
},
Service: &Service{
Addr: server.URL,
},
PasswordsService: svc,
}
done := server.Close
return client, done
}
func TestOnboardingService(t *testing.T) {
platformtesting.OnboardInitialUser(initOnboardingService, t)
}

View File

@ -1,624 +0,0 @@
package http
import (
"context"
"fmt"
"net/http"
"path"
"strings"
"github.com/influxdata/httprouter"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/kit/tracing"
kithttp "github.com/influxdata/influxdb/v2/kit/transport/http"
"github.com/influxdata/influxdb/v2/pkg/httpc"
"go.uber.org/zap"
)
// OrgBackend is all services and associated parameters required to construct
// the OrgHandler.
type OrgBackend struct {
influxdb.HTTPErrorHandler
log *zap.Logger
OrganizationService influxdb.OrganizationService
OrganizationOperationLogService influxdb.OrganizationOperationLogService
UserResourceMappingService influxdb.UserResourceMappingService
SecretService influxdb.SecretService
LabelService influxdb.LabelService
UserService influxdb.UserService
}
// NewOrgBackend is a datasource used by the org handler.
func NewOrgBackend(log *zap.Logger, b *APIBackend) *OrgBackend {
return &OrgBackend{
HTTPErrorHandler: b.HTTPErrorHandler,
log: log,
OrganizationService: b.OrganizationService,
OrganizationOperationLogService: b.OrganizationOperationLogService,
UserResourceMappingService: b.UserResourceMappingService,
SecretService: b.SecretService,
LabelService: b.LabelService,
UserService: b.UserService,
}
}
// OrgHandler represents an HTTP API handler for orgs.
type OrgHandler struct {
*httprouter.Router
*kithttp.API
log *zap.Logger
OrgSVC influxdb.OrganizationService
OrganizationOperationLogService influxdb.OrganizationOperationLogService
UserResourceMappingService influxdb.UserResourceMappingService
SecretService influxdb.SecretService
LabelService influxdb.LabelService
UserService influxdb.UserService
}
const (
prefixOrganizations = "/api/v2/orgs"
organizationsIDPath = "/api/v2/orgs/:id"
organizationsIDMembersPath = "/api/v2/orgs/:id/members"
organizationsIDMembersIDPath = "/api/v2/orgs/:id/members/:userID"
organizationsIDOwnersPath = "/api/v2/orgs/:id/owners"
organizationsIDOwnersIDPath = "/api/v2/orgs/:id/owners/:userID"
organizationsIDSecretsPath = "/api/v2/orgs/:id/secrets"
// TODO(desa): need a way to specify which secrets to delete. this should work for now
organizationsIDSecretsDeletePath = "/api/v2/orgs/:id/secrets/delete"
organizationsIDLabelsPath = "/api/v2/orgs/:id/labels"
organizationsIDLabelsIDPath = "/api/v2/orgs/:id/labels/:lid"
)
func checkOrganizationExists(orgHandler *OrgHandler) kithttp.Middleware {
fn := func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
orgID, err := decodeIDFromCtx(ctx, "id")
if err != nil {
orgHandler.API.Err(w, r, err)
return
}
if _, err := orgHandler.OrgSVC.FindOrganizationByID(ctx, orgID); err != nil {
orgHandler.API.Err(w, r, err)
return
}
next.ServeHTTP(w, r)
})
}
return fn
}
// NewOrgHandler returns a new instance of OrgHandler.
func NewOrgHandler(log *zap.Logger, b *OrgBackend) *OrgHandler {
h := &OrgHandler{
Router: NewRouter(b.HTTPErrorHandler),
API: kithttp.NewAPI(kithttp.WithLog(log)),
log: log,
OrgSVC: b.OrganizationService,
OrganizationOperationLogService: b.OrganizationOperationLogService,
UserResourceMappingService: b.UserResourceMappingService,
SecretService: b.SecretService,
LabelService: b.LabelService,
UserService: b.UserService,
}
h.HandlerFunc("POST", prefixOrganizations, h.handlePostOrg)
h.HandlerFunc("GET", prefixOrganizations, h.handleGetOrgs)
h.HandlerFunc("GET", organizationsIDPath, h.handleGetOrg)
h.HandlerFunc("PATCH", organizationsIDPath, h.handlePatchOrg)
h.HandlerFunc("DELETE", organizationsIDPath, h.handleDeleteOrg)
memberBackend := MemberBackend{
HTTPErrorHandler: b.HTTPErrorHandler,
log: b.log.With(zap.String("handler", "member")),
ResourceType: influxdb.OrgsResourceType,
UserType: influxdb.Member,
UserResourceMappingService: b.UserResourceMappingService,
UserService: b.UserService,
}
h.HandlerFunc("POST", organizationsIDMembersPath, newPostMemberHandler(memberBackend))
h.Handler("GET", organizationsIDMembersPath, applyMW(newGetMembersHandler(memberBackend), checkOrganizationExists(h)))
h.HandlerFunc("DELETE", organizationsIDMembersIDPath, newDeleteMemberHandler(memberBackend))
ownerBackend := MemberBackend{
HTTPErrorHandler: b.HTTPErrorHandler,
log: b.log.With(zap.String("handler", "member")),
ResourceType: influxdb.OrgsResourceType,
UserType: influxdb.Owner,
UserResourceMappingService: b.UserResourceMappingService,
UserService: b.UserService,
}
h.HandlerFunc("POST", organizationsIDOwnersPath, newPostMemberHandler(ownerBackend))
h.Handler("GET", organizationsIDOwnersPath, applyMW(newGetMembersHandler(ownerBackend), checkOrganizationExists(h)))
h.HandlerFunc("DELETE", organizationsIDOwnersIDPath, newDeleteMemberHandler(ownerBackend))
h.HandlerFunc("GET", organizationsIDSecretsPath, h.handleGetSecrets)
h.HandlerFunc("PATCH", organizationsIDSecretsPath, h.handlePatchSecrets)
// TODO(desa): need a way to specify which secrets to delete. this should work for now
h.HandlerFunc("POST", organizationsIDSecretsDeletePath, h.handleDeleteSecrets)
labelBackend := &LabelBackend{
HTTPErrorHandler: b.HTTPErrorHandler,
log: b.log.With(zap.String("handler", "label")),
LabelService: b.LabelService,
ResourceType: influxdb.OrgsResourceType,
}
h.HandlerFunc("GET", organizationsIDLabelsPath, newGetLabelsHandler(labelBackend))
h.HandlerFunc("POST", organizationsIDLabelsPath, newPostLabelHandler(labelBackend))
h.HandlerFunc("DELETE", organizationsIDLabelsIDPath, newDeleteLabelHandler(labelBackend))
return h
}
type orgsResponse struct {
Links map[string]string `json:"links"`
Organizations []orgResponse `json:"orgs"`
}
func (o orgsResponse) toInfluxdb() []*influxdb.Organization {
orgs := make([]*influxdb.Organization, len(o.Organizations))
for i := range o.Organizations {
orgs[i] = &o.Organizations[i].Organization
}
return orgs
}
func newOrgsResponse(orgs []*influxdb.Organization) *orgsResponse {
res := orgsResponse{
Links: map[string]string{
"self": "/api/v2/orgs",
},
Organizations: []orgResponse{},
}
for _, org := range orgs {
res.Organizations = append(res.Organizations, newOrgResponse(*org))
}
return &res
}
type orgResponse struct {
Links map[string]string `json:"links"`
influxdb.Organization
}
func newOrgResponse(o influxdb.Organization) orgResponse {
return orgResponse{
Links: map[string]string{
"self": fmt.Sprintf("/api/v2/orgs/%s", o.ID),
"logs": fmt.Sprintf("/api/v2/orgs/%s/logs", o.ID),
"members": fmt.Sprintf("/api/v2/orgs/%s/members", o.ID),
"owners": fmt.Sprintf("/api/v2/orgs/%s/owners", o.ID),
"secrets": fmt.Sprintf("/api/v2/orgs/%s/secrets", o.ID),
"labels": fmt.Sprintf("/api/v2/orgs/%s/labels", o.ID),
"buckets": fmt.Sprintf("/api/v2/buckets?org=%s", o.Name),
"tasks": fmt.Sprintf("/api/v2/tasks?org=%s", o.Name),
"dashboards": fmt.Sprintf("/api/v2/dashboards?org=%s", o.Name),
},
Organization: o,
}
}
// handlePostOrg is the HTTP handler for the POST /api/v2/orgs route.
func (h *OrgHandler) handlePostOrg(w http.ResponseWriter, r *http.Request) {
var org influxdb.Organization
if err := h.API.DecodeJSON(r.Body, &org); err != nil {
h.API.Err(w, r, err)
return
}
if err := h.OrgSVC.CreateOrganization(r.Context(), &org); err != nil {
h.API.Err(w, r, err)
return
}
h.log.Debug("Org created", zap.String("org", fmt.Sprint(org)))
h.API.Respond(w, r, http.StatusCreated, newOrgResponse(org))
}
// handleGetOrg is the HTTP handler for the GET /api/v2/orgs/:id route.
func (h *OrgHandler) handleGetOrg(w http.ResponseWriter, r *http.Request) {
id, err := decodeIDFromCtx(r.Context(), "id")
if err != nil {
h.API.Err(w, r, err)
return
}
org, err := h.OrgSVC.FindOrganizationByID(r.Context(), id)
if err != nil {
h.API.Err(w, r, err)
return
}
h.log.Debug("Org retrieved", zap.String("org", fmt.Sprint(org)))
h.API.Respond(w, r, http.StatusOK, newOrgResponse(*org))
}
// handleGetOrgs is the HTTP handler for the GET /api/v2/orgs route.
func (h *OrgHandler) handleGetOrgs(w http.ResponseWriter, r *http.Request) {
qp := r.URL.Query()
opts, err := influxdb.DecodeFindOptions(r)
if err != nil {
h.API.Err(w, r, err)
return
}
var filter influxdb.OrganizationFilter
if name := qp.Get(Org); name != "" {
filter.Name = &name
}
if orgID := qp.Get("orgID"); orgID != "" {
id, err := influxdb.IDFromString(orgID)
if err != nil {
h.API.Err(w, r, err)
return
}
filter.ID = id
}
if userID := qp.Get("userID"); userID != "" {
id, err := influxdb.IDFromString(userID)
if err != nil {
h.API.Err(w, r, err)
return
}
filter.UserID = id
}
orgs, _, err := h.OrgSVC.FindOrganizations(r.Context(), filter, *opts)
if err != nil {
h.API.Err(w, r, err)
return
}
h.log.Debug("Orgs retrieved", zap.String("org", fmt.Sprint(orgs)))
h.API.Respond(w, r, http.StatusOK, newOrgsResponse(orgs))
}
// handleDeleteOrganization is the HTTP handler for the DELETE /api/v2/orgs/:id route.
func (h *OrgHandler) handleDeleteOrg(w http.ResponseWriter, r *http.Request) {
id, err := decodeIDFromCtx(r.Context(), "id")
if err != nil {
h.API.Err(w, r, err)
return
}
ctx := r.Context()
if err := h.OrgSVC.DeleteOrganization(ctx, id); err != nil {
h.API.Err(w, r, err)
return
}
h.log.Debug("Org deleted", zap.String("orgID", fmt.Sprint(id)))
h.API.Respond(w, r, http.StatusNoContent, nil)
}
// handlePatchOrg is the HTTP handler for the PATH /api/v2/orgs route.
func (h *OrgHandler) handlePatchOrg(w http.ResponseWriter, r *http.Request) {
id, err := decodeIDFromCtx(r.Context(), "id")
if err != nil {
h.API.Err(w, r, err)
return
}
var upd influxdb.OrganizationUpdate
if err := h.API.DecodeJSON(r.Body, &upd); err != nil {
h.API.Err(w, r, err)
return
}
org, err := h.OrgSVC.UpdateOrganization(r.Context(), id, upd)
if err != nil {
h.API.Err(w, r, err)
return
}
h.log.Debug("Org updated", zap.String("org", fmt.Sprint(org)))
h.API.Respond(w, r, http.StatusOK, newOrgResponse(*org))
}
type secretsResponse struct {
Links map[string]string `json:"links"`
Secrets []string `json:"secrets"`
}
func newSecretsResponse(orgID influxdb.ID, ks []string) *secretsResponse {
if ks == nil {
ks = []string{}
}
return &secretsResponse{
Links: map[string]string{
"org": fmt.Sprintf("/api/v2/orgs/%s", orgID),
"self": fmt.Sprintf("/api/v2/orgs/%s/secrets", orgID),
},
Secrets: ks,
}
}
// handleGetSecrets is the HTTP handler for the GET /api/v2/orgs/:id/secrets route.
func (h *OrgHandler) handleGetSecrets(w http.ResponseWriter, r *http.Request) {
orgID, err := decodeIDFromCtx(r.Context(), "id")
if err != nil {
h.API.Err(w, r, err)
return
}
ks, err := h.SecretService.GetSecretKeys(r.Context(), orgID)
if err != nil && influxdb.ErrorCode(err) != influxdb.ENotFound {
h.API.Err(w, r, err)
return
}
h.API.Respond(w, r, http.StatusOK, newSecretsResponse(orgID, ks))
}
// handleGetPatchSecrets is the HTTP handler for the PATCH /api/v2/orgs/:id/secrets route.
func (h *OrgHandler) handlePatchSecrets(w http.ResponseWriter, r *http.Request) {
orgID, err := decodeIDFromCtx(r.Context(), "id")
if err != nil {
h.API.Err(w, r, err)
return
}
var secrets map[string]string
if err := h.API.DecodeJSON(r.Body, &secrets); err != nil {
h.API.Err(w, r, err)
return
}
if err := h.SecretService.PatchSecrets(r.Context(), orgID, secrets); err != nil {
h.API.Err(w, r, err)
return
}
h.API.Respond(w, r, http.StatusNoContent, nil)
}
type secretsDeleteBody struct {
Secrets []string `json:"secrets"`
}
// handleDeleteSecrets is the HTTP handler for the DELETE /api/v2/orgs/:id/secrets route.
func (h *OrgHandler) handleDeleteSecrets(w http.ResponseWriter, r *http.Request) {
orgID, err := decodeIDFromCtx(r.Context(), "id")
if err != nil {
h.API.Err(w, r, err)
return
}
var reqBody secretsDeleteBody
if err := h.API.DecodeJSON(r.Body, &reqBody); err != nil {
h.API.Err(w, r, err)
return
}
if err := h.SecretService.DeleteSecret(r.Context(), orgID, reqBody.Secrets...); err != nil {
h.API.Err(w, r, err)
return
}
h.API.Respond(w, r, http.StatusNoContent, nil)
}
// SecretService connects to Influx via HTTP using tokens to manage secrets.
type SecretService struct {
Client *httpc.Client
}
// LoadSecret is not implemented for http
func (s *SecretService) LoadSecret(ctx context.Context, orgID influxdb.ID, k string) (string, error) {
return "", &influxdb.Error{
Code: influxdb.EMethodNotAllowed,
Msg: "load secret is not implemented for http",
}
}
// PutSecret is not implemented for http.
func (s *SecretService) PutSecret(ctx context.Context, orgID influxdb.ID, k string, v string) error {
return &influxdb.Error{
Code: influxdb.EMethodNotAllowed,
Msg: "put secret is not implemented for http",
}
}
// GetSecretKeys get all secret keys mathing an org ID via HTTP.
func (s *SecretService) GetSecretKeys(ctx context.Context, orgID influxdb.ID) ([]string, error) {
span, _ := tracing.StartSpanFromContext(ctx)
defer span.Finish()
span.LogKV("org-id", orgID)
path := strings.Replace(organizationsIDSecretsPath, ":id", orgID.String(), 1)
var ss secretsResponse
err := s.Client.
Get(path).
DecodeJSON(&ss).
Do(ctx)
if err != nil {
return nil, err
}
return ss.Secrets, nil
}
// PutSecrets is not implemented for http.
func (s *SecretService) PutSecrets(ctx context.Context, orgID influxdb.ID, m map[string]string) error {
return &influxdb.Error{
Code: influxdb.EMethodNotAllowed,
Msg: "put secrets is not implemented for http",
}
}
// PatchSecrets will update the existing secret with new via http.
func (s *SecretService) PatchSecrets(ctx context.Context, orgID influxdb.ID, m map[string]string) error {
span, _ := tracing.StartSpanFromContext(ctx)
defer span.Finish()
if orgID != 0 {
span.LogKV("org-id", orgID)
}
path := strings.Replace(organizationsIDSecretsPath, ":id", orgID.String(), 1)
return s.Client.
PatchJSON(m, path).
Do(ctx)
}
// DeleteSecret removes a single secret via HTTP.
func (s *SecretService) DeleteSecret(ctx context.Context, orgID influxdb.ID, ks ...string) error {
span, _ := tracing.StartSpanFromContext(ctx)
defer span.Finish()
path := strings.Replace(organizationsIDSecretsDeletePath, ":id", orgID.String(), 1)
return s.Client.
PostJSON(secretsDeleteBody{
Secrets: ks,
}, path).
Do(ctx)
}
// OrganizationService connects to Influx via HTTP using tokens to manage organizations.
type OrganizationService struct {
Client *httpc.Client
// OpPrefix is for not found errors.
OpPrefix string
}
// FindOrganizationByID gets a single organization with a given id using HTTP.
func (s *OrganizationService) FindOrganizationByID(ctx context.Context, id influxdb.ID) (*influxdb.Organization, error) {
filter := influxdb.OrganizationFilter{ID: &id}
o, err := s.FindOrganization(ctx, filter)
if err != nil {
return nil, &influxdb.Error{
Err: err,
Op: s.OpPrefix + influxdb.OpFindOrganizationByID,
}
}
return o, nil
}
// FindOrganization gets a single organization matching the filter using HTTP.
func (s *OrganizationService) FindOrganization(ctx context.Context, filter influxdb.OrganizationFilter) (*influxdb.Organization, error) {
if filter.ID == nil && filter.Name == nil {
return nil, influxdb.ErrInvalidOrgFilter
}
os, n, err := s.FindOrganizations(ctx, filter)
if err != nil {
return nil, &influxdb.Error{
Err: err,
Op: s.OpPrefix + influxdb.OpFindOrganization,
}
}
if n == 0 {
return nil, &influxdb.Error{
Code: influxdb.ENotFound,
Op: s.OpPrefix + influxdb.OpFindOrganization,
Msg: "organization not found",
}
}
return os[0], nil
}
// FindOrganizations returns all organizations that match the filter via HTTP.
func (s *OrganizationService) FindOrganizations(ctx context.Context, filter influxdb.OrganizationFilter, opt ...influxdb.FindOptions) ([]*influxdb.Organization, int, error) {
span, _ := tracing.StartSpanFromContext(ctx)
defer span.Finish()
params := influxdb.FindOptionParams(opt...)
if filter.Name != nil {
span.LogKV("org", *filter.Name)
params = append(params, [2]string{"org", *filter.Name})
}
if filter.ID != nil {
span.LogKV("org-id", *filter.ID)
params = append(params, [2]string{"orgID", filter.ID.String()})
}
for _, o := range opt {
if o.Offset != 0 {
span.LogKV("offset", o.Offset)
}
span.LogKV("descending", o.Descending)
if o.Limit > 0 {
span.LogKV("limit", o.Limit)
}
if o.SortBy != "" {
span.LogKV("sortBy", o.SortBy)
}
}
var os orgsResponse
err := s.Client.
Get(prefixOrganizations).
QueryParams(params...).
DecodeJSON(&os).
Do(ctx)
if err != nil {
return nil, 0, err
}
orgs := os.toInfluxdb()
return orgs, len(orgs), nil
}
// CreateOrganization creates an organization.
func (s *OrganizationService) CreateOrganization(ctx context.Context, o *influxdb.Organization) error {
span, _ := tracing.StartSpanFromContext(ctx)
defer span.Finish()
if o.Name != "" {
span.LogKV("org", o.Name)
}
if o.ID != 0 {
span.LogKV("org-id", o.ID)
}
return s.Client.
PostJSON(o, prefixOrganizations).
DecodeJSON(o).
Do(ctx)
}
// UpdateOrganization updates the organization over HTTP.
func (s *OrganizationService) UpdateOrganization(ctx context.Context, id influxdb.ID, upd influxdb.OrganizationUpdate) (*influxdb.Organization, error) {
span, _ := tracing.StartSpanFromContext(ctx)
defer span.Finish()
span.LogKV("org-id", id)
span.LogKV("name", upd.Name)
var o influxdb.Organization
err := s.Client.
PatchJSON(upd, prefixOrganizations, id.String()).
DecodeJSON(&o).
Do(ctx)
if err != nil {
return nil, tracing.LogError(span, err)
}
return &o, nil
}
// DeleteOrganization removes organization id over HTTP.
func (s *OrganizationService) DeleteOrganization(ctx context.Context, id influxdb.ID) error {
span, _ := tracing.StartSpanFromContext(ctx)
defer span.Finish()
return s.Client.
Delete(prefixOrganizations, id.String()).
Do(ctx)
}
func organizationIDPath(id influxdb.ID) string {
return path.Join(prefixOrganizations, id.String())
}

View File

@ -1,426 +0,0 @@
package http
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/inmem"
kithttp "github.com/influxdata/influxdb/v2/kit/transport/http"
"github.com/influxdata/influxdb/v2/kv"
"github.com/influxdata/influxdb/v2/kv/migration/all"
"github.com/influxdata/influxdb/v2/mock"
influxdbtesting "github.com/influxdata/influxdb/v2/testing"
"go.uber.org/zap/zaptest"
)
// NewMockOrgBackend returns a OrgBackend with mock services.
func NewMockOrgBackend(t *testing.T) *OrgBackend {
return &OrgBackend{
log: zaptest.NewLogger(t),
OrganizationService: mock.NewOrganizationService(),
OrganizationOperationLogService: mock.NewOrganizationOperationLogService(),
UserResourceMappingService: mock.NewUserResourceMappingService(),
SecretService: mock.NewSecretService(),
LabelService: mock.NewLabelService(),
UserService: mock.NewUserService(),
}
}
func initOrganizationService(f influxdbtesting.OrganizationFields, t *testing.T) (influxdb.OrganizationService, string, func()) {
t.Helper()
ctx := context.Background()
logger := zaptest.NewLogger(t)
store := NewTestInmemStore(t)
svc := kv.NewService(logger, store)
svc.IDGenerator = f.IDGenerator
svc.OrgIDs = f.OrgBucketIDs
svc.BucketIDs = f.OrgBucketIDs
svc.TimeGenerator = f.TimeGenerator
if f.TimeGenerator == nil {
svc.TimeGenerator = influxdb.RealTimeGenerator{}
}
for _, o := range f.Organizations {
// PutOrgs no longer creates an ID
// that is what CreateOrganization does
// so we have to generate one
o.ID = svc.OrgIDs.ID()
if err := svc.PutOrganization(ctx, o); err != nil {
t.Fatalf("failed to populate organizations")
}
}
orgBackend := NewMockOrgBackend(t)
orgBackend.HTTPErrorHandler = kithttp.ErrorHandler(0)
orgBackend.OrganizationService = svc
orgBackend.SecretService = svc
handler := NewOrgHandler(zaptest.NewLogger(t), orgBackend)
server := httptest.NewServer(handler)
client := OrganizationService{
Client: mustNewHTTPClient(t, server.URL, ""),
}
done := server.Close
return &client, "", done
}
func initSecretService(f influxdbtesting.SecretServiceFields, t *testing.T) (influxdb.SecretService, func()) {
t.Helper()
ctx := context.Background()
store := inmem.NewKVStore()
logger := zaptest.NewLogger(t)
if err := all.Up(ctx, logger, store); err != nil {
t.Fatal(err)
}
svc := kv.NewService(logger, store)
for _, ss := range f.Secrets {
if err := svc.PutSecrets(ctx, ss.OrganizationID, ss.Env); err != nil {
t.Fatalf("failed to populate secrets")
}
}
scrBackend := NewMockOrgBackend(t)
scrBackend.HTTPErrorHandler = kithttp.ErrorHandler(0)
scrBackend.SecretService = svc
handler := NewOrgHandler(zaptest.NewLogger(t), scrBackend)
server := httptest.NewServer(handler)
client := SecretService{
Client: mustNewHTTPClient(t, server.URL, ""),
}
done := server.Close
return &client, done
}
func TestOrganizationService(t *testing.T) {
t.Parallel()
influxdbtesting.OrganizationService(initOrganizationService, t)
}
func TestSecretService(t *testing.T) {
t.Parallel()
influxdbtesting.DeleteSecrets(initSecretService, t)
influxdbtesting.GetSecretKeys(initSecretService, t)
influxdbtesting.PatchSecrets(initSecretService, t)
}
func TestSecretService_handleGetSecrets(t *testing.T) {
type fields struct {
SecretService influxdb.SecretService
}
type args struct {
orgID influxdb.ID
}
type wants struct {
statusCode int
contentType string
body string
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "get basic secrets",
fields: fields{
&mock.SecretService{
GetSecretKeysFn: func(ctx context.Context, orgID influxdb.ID) ([]string, error) {
return []string{"hello", "world"}, nil
},
},
},
args: args{
orgID: 1,
},
wants: wants{
statusCode: http.StatusOK,
contentType: "application/json; charset=utf-8",
body: `
{
"links": {
"org": "/api/v2/orgs/0000000000000001",
"self": "/api/v2/orgs/0000000000000001/secrets"
},
"secrets": [
"hello",
"world"
]
}
`,
},
},
{
name: "get secrets when there are none",
fields: fields{
&mock.SecretService{
GetSecretKeysFn: func(ctx context.Context, orgID influxdb.ID) ([]string, error) {
return []string{}, nil
},
},
},
args: args{
orgID: 1,
},
wants: wants{
statusCode: http.StatusOK,
contentType: "application/json; charset=utf-8",
body: `
{
"links": {
"org": "/api/v2/orgs/0000000000000001",
"self": "/api/v2/orgs/0000000000000001/secrets"
},
"secrets": []
}
`,
},
},
{
name: "get secrets when organization has no secret keys",
fields: fields{
&mock.SecretService{
GetSecretKeysFn: func(ctx context.Context, orgID influxdb.ID) ([]string, error) {
return []string{}, &influxdb.Error{
Code: influxdb.ENotFound,
Msg: "organization has no secret keys",
}
},
},
},
args: args{
orgID: 1,
},
wants: wants{
statusCode: http.StatusOK,
contentType: "application/json; charset=utf-8",
body: `
{
"links": {
"org": "/api/v2/orgs/0000000000000001",
"self": "/api/v2/orgs/0000000000000001/secrets"
},
"secrets": []
}
`,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
orgBackend := NewMockOrgBackend(t)
orgBackend.HTTPErrorHandler = kithttp.ErrorHandler(0)
orgBackend.SecretService = tt.fields.SecretService
h := NewOrgHandler(zaptest.NewLogger(t), orgBackend)
u := fmt.Sprintf("http://any.url/api/v2/orgs/%s/secrets", tt.args.orgID)
r := httptest.NewRequest("GET", u, nil)
w := httptest.NewRecorder()
h.ServeHTTP(w, r)
res := w.Result()
content := res.Header.Get("Content-Type")
body, _ := ioutil.ReadAll(res.Body)
if res.StatusCode != tt.wants.statusCode {
t.Errorf("handleGetSecrets() = %v, want %v", res.StatusCode, tt.wants.statusCode)
}
if tt.wants.contentType != "" && content != tt.wants.contentType {
t.Errorf("handleGetSecrets() = %v, want %v", content, tt.wants.contentType)
}
if tt.wants.body != "" {
if eq, diff, err := jsonEqual(string(body), tt.wants.body); err != nil {
t.Errorf("%q, handleGetSecrets(). error unmarshalling json %v", tt.name, err)
} else if !eq {
t.Errorf("%q. handleGetSecrets() = ***%s***", tt.name, diff)
}
}
})
}
}
func TestSecretService_handlePatchSecrets(t *testing.T) {
type fields struct {
SecretService influxdb.SecretService
}
type args struct {
orgID influxdb.ID
secrets map[string]string
}
type wants struct {
statusCode int
contentType string
body string
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "get basic secrets",
fields: fields{
&mock.SecretService{
PatchSecretsFn: func(ctx context.Context, orgID influxdb.ID, s map[string]string) error {
return nil
},
},
},
args: args{
orgID: 1,
secrets: map[string]string{
"abc": "123",
},
},
wants: wants{
statusCode: http.StatusNoContent,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
orgBackend := NewMockOrgBackend(t)
orgBackend.HTTPErrorHandler = kithttp.ErrorHandler(0)
orgBackend.SecretService = tt.fields.SecretService
h := NewOrgHandler(zaptest.NewLogger(t), orgBackend)
b, err := json.Marshal(tt.args.secrets)
if err != nil {
t.Fatalf("failed to marshal secrets: %v", err)
}
buf := bytes.NewReader(b)
u := fmt.Sprintf("http://any.url/api/v2/orgs/%s/secrets", tt.args.orgID)
r := httptest.NewRequest("PATCH", u, buf)
w := httptest.NewRecorder()
h.ServeHTTP(w, r)
res := w.Result()
content := res.Header.Get("Content-Type")
body, _ := ioutil.ReadAll(res.Body)
if res.StatusCode != tt.wants.statusCode {
t.Errorf("handlePatchSecrets() = %v, want %v", res.StatusCode, tt.wants.statusCode)
}
if tt.wants.contentType != "" && content != tt.wants.contentType {
t.Errorf("handlePatchSecrets() = %v, want %v", content, tt.wants.contentType)
}
if tt.wants.body != "" {
if eq, diff, err := jsonEqual(string(body), tt.wants.body); err != nil {
t.Errorf("%q, handlePatchSecrets(). error unmarshalling json %v", tt.name, err)
} else if !eq {
t.Errorf("%q. handlePatchSecrets() = ***%s***", tt.name, diff)
}
}
})
}
}
func TestSecretService_handleDeleteSecrets(t *testing.T) {
type fields struct {
SecretService influxdb.SecretService
}
type args struct {
orgID influxdb.ID
secrets []string
}
type wants struct {
statusCode int
contentType string
body string
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "get basic secrets",
fields: fields{
&mock.SecretService{
DeleteSecretFn: func(ctx context.Context, orgID influxdb.ID, s ...string) error {
return nil
},
},
},
args: args{
orgID: 1,
secrets: []string{
"abc",
},
},
wants: wants{
statusCode: http.StatusNoContent,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
orgBackend := NewMockOrgBackend(t)
orgBackend.HTTPErrorHandler = kithttp.ErrorHandler(0)
orgBackend.SecretService = tt.fields.SecretService
h := NewOrgHandler(zaptest.NewLogger(t), orgBackend)
b, err := json.Marshal(struct {
Secrets []string `json:"secrets"`
}{
Secrets: tt.args.secrets,
})
if err != nil {
t.Fatalf("failed to marshal secrets: %v", err)
}
buf := bytes.NewReader(b)
u := fmt.Sprintf("http://any.url/api/v2/orgs/%s/secrets/delete", tt.args.orgID)
r := httptest.NewRequest("POST", u, buf)
w := httptest.NewRecorder()
h.ServeHTTP(w, r)
res := w.Result()
content := res.Header.Get("Content-Type")
body, _ := ioutil.ReadAll(res.Body)
if res.StatusCode != tt.wants.statusCode {
t.Errorf("handleDeleteSecrets() = %v, want %v", res.StatusCode, tt.wants.statusCode)
}
if tt.wants.contentType != "" && content != tt.wants.contentType {
t.Errorf("handleDeleteSecrets() = %v, want %v", content, tt.wants.contentType)
}
if tt.wants.body != "" {
if eq, diff, err := jsonEqual(string(body), tt.wants.body); err != nil {
t.Errorf("%q, handleDeleteSecrets(). error unmarshalling json %v", tt.name, err)
} else if !eq {
t.Errorf("%q. handleDeleteSecrets() = ***%s***", tt.name, diff)
}
}
})
}
}

View File

@ -30,6 +30,7 @@ import (
"github.com/influxdata/influxdb/v2/query"
"github.com/influxdata/influxdb/v2/query/fluxlang"
"github.com/influxdata/influxdb/v2/query/mock"
"github.com/influxdata/influxdb/v2/tenant"
"go.uber.org/zap/zaptest"
)
@ -326,7 +327,8 @@ var _ metric.EventRecorder = noopEventRecorder{}
func TestFluxHandler_PostQuery_Errors(t *testing.T) {
defer tracetesting.SetupInMemoryTracing(t.Name())()
orgSVC := newInMemKVSVC(t)
store := NewTestInmemStore(t)
orgSVC := tenant.NewService(tenant.NewStore(store))
b := &FluxBackend{
HTTPErrorHandler: kithttp.ErrorHandler(0),
log: zaptest.NewLogger(t),

View File

@ -14,6 +14,11 @@ import (
"go.uber.org/zap"
)
const (
prefixOrganizations = "/api/v2/orgs"
prefixBuckets = "/api/v2/buckets"
)
// ScraperBackend is all services and associated parameters required to construct
// the ScraperHandler.
type ScraperBackend struct {
@ -628,3 +633,11 @@ func (h *ScraperHandler) newTargetResponse(ctx context.Context, target influxdb.
return res, nil
}
func organizationIDPath(id influxdb.ID) string {
return path.Join(prefixOrganizations, id.String())
}
func bucketIDPath(id influxdb.ID) string {
return path.Join(prefixBuckets, id.String())
}

View File

@ -15,7 +15,9 @@ import (
platcontext "github.com/influxdata/influxdb/v2/context"
httpMock "github.com/influxdata/influxdb/v2/http/mock"
kithttp "github.com/influxdata/influxdb/v2/kit/transport/http"
"github.com/influxdata/influxdb/v2/kv"
"github.com/influxdata/influxdb/v2/mock"
"github.com/influxdata/influxdb/v2/tenant"
platformtesting "github.com/influxdata/influxdb/v2/testing"
"go.uber.org/zap/zaptest"
)
@ -814,7 +816,12 @@ func TestService_handlePatchScraperTarget(t *testing.T) {
func initScraperService(f platformtesting.TargetFields, t *testing.T) (influxdb.ScraperTargetStoreService, string, func()) {
t.Helper()
svc := newInMemKVSVC(t)
store := NewTestInmemStore(t)
tenantStore := tenant.NewStore(store)
tenantService := tenant.NewService(tenantStore)
svc := kv.NewService(zaptest.NewLogger(t), store, tenantService)
svc.IDGenerator = f.IDGenerator
ctx := context.Background()
@ -825,15 +832,17 @@ func initScraperService(f platformtesting.TargetFields, t *testing.T) (influxdb.
}
for _, o := range f.Organizations {
if err := svc.PutOrganization(ctx, o); err != nil {
t.Fatalf("failed to populate orgs")
}
mock.SetIDForFunc(&tenantStore.OrgIDGen, o.ID, func() {
if err := tenantService.CreateOrganization(ctx, o); err != nil {
t.Fatalf("failed to populate orgs")
}
})
}
scraperBackend := NewMockScraperBackend(t)
scraperBackend.HTTPErrorHandler = kithttp.ErrorHandler(0)
scraperBackend.ScraperStorageService = svc
scraperBackend.OrganizationService = svc
scraperBackend.OrganizationService = tenantService
scraperBackend.BucketService = &mock.BucketService{
FindBucketByIDFn: func(ctx context.Context, id influxdb.ID) (*influxdb.Bucket, error) {
return &influxdb.Bucket{
@ -856,8 +865,8 @@ func initScraperService(f platformtesting.TargetFields, t *testing.T) (influxdb.
influxdb.OrganizationService
ScraperService
}{
UserResourceMappingService: svc,
OrganizationService: svc,
UserResourceMappingService: tenantService,
OrganizationService: tenantService,
ScraperService: ScraperService{
Token: "tok",
Addr: server.URL,

View File

@ -5,12 +5,13 @@ import (
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/influxdata/flux"
"github.com/influxdata/flux/csv"
"github.com/influxdata/flux/lang"
"github.com/influxdata/httprouter"
platform "github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/pkg/httpc"
"github.com/influxdata/influxdb/v2/query"
"github.com/influxdata/influxdb/v2/query/influxql"
@ -22,15 +23,15 @@ const (
)
type sourceResponse struct {
*platform.Source
*influxdb.Source
Links map[string]interface{} `json:"links"`
}
func newSourceResponse(s *platform.Source) *sourceResponse {
func newSourceResponse(s *influxdb.Source) *sourceResponse {
s.Password = ""
s.SharedSecret = ""
if s.Type == platform.SelfSourceType {
if s.Type == influxdb.SelfSourceType {
return &sourceResponse{
Source: s,
Links: map[string]interface{}{
@ -58,7 +59,7 @@ type sourcesResponse struct {
Links map[string]interface{} `json:"links"`
}
func newSourcesResponse(srcs []*platform.Source) *sourcesResponse {
func newSourcesResponse(srcs []*influxdb.Source) *sourcesResponse {
res := &sourcesResponse{
Links: map[string]interface{}{
"self": prefixSources,
@ -76,13 +77,13 @@ func newSourcesResponse(srcs []*platform.Source) *sourcesResponse {
// SourceBackend is all services and associated parameters required to construct
// the SourceHandler.
type SourceBackend struct {
platform.HTTPErrorHandler
influxdb.HTTPErrorHandler
log *zap.Logger
SourceService platform.SourceService
LabelService platform.LabelService
BucketService platform.BucketService
NewQueryService func(s *platform.Source) (query.ProxyQueryService, error)
SourceService influxdb.SourceService
LabelService influxdb.LabelService
BucketService influxdb.BucketService
NewQueryService func(s *influxdb.Source) (query.ProxyQueryService, error)
}
// NewSourceBackend returns a new instance of SourceBackend.
@ -101,15 +102,15 @@ func NewSourceBackend(log *zap.Logger, b *APIBackend) *SourceBackend {
// SourceHandler is a handler for sources
type SourceHandler struct {
*httprouter.Router
platform.HTTPErrorHandler
influxdb.HTTPErrorHandler
log *zap.Logger
SourceService platform.SourceService
LabelService platform.LabelService
BucketService platform.BucketService
SourceService influxdb.SourceService
LabelService influxdb.LabelService
BucketService influxdb.BucketService
// TODO(desa): this was done so in order to remove an import cycle and to allow
// for http mocking.
NewQueryService func(s *platform.Source) (query.ProxyQueryService, error)
NewQueryService func(s *influxdb.Source) (query.ProxyQueryService, error)
}
// NewSourceHandler returns a new instance of SourceHandler.
@ -147,7 +148,7 @@ func decodeSourceQueryRequest(r *http.Request) (*query.ProxyRequest, error) {
DB string `json:"db"`
RP string `json:"rp"`
Cluster string `json:"cluster"`
OrganizationID platform.ID `json:"organizationID"`
OrganizationID influxdb.ID `json:"organizationID"`
// TODO(desa): support influxql dialect
Dialect csv.Dialect `json:"dialect"`
}{}
@ -263,6 +264,149 @@ func decodeGetSourceBucketsRequest(ctx context.Context, r *http.Request) (*getSo
}, nil
}
type getBucketsRequest struct {
filter influxdb.BucketFilter
opts influxdb.FindOptions
}
func decodeGetBucketsRequest(r *http.Request) (*getBucketsRequest, error) {
qp := r.URL.Query()
req := &getBucketsRequest{}
opts, err := influxdb.DecodeFindOptions(r)
if err != nil {
return nil, err
}
req.opts = *opts
if orgID := qp.Get("orgID"); orgID != "" {
id, err := influxdb.IDFromString(orgID)
if err != nil {
return nil, err
}
req.filter.OrganizationID = id
}
if org := qp.Get("org"); org != "" {
req.filter.Org = &org
}
if name := qp.Get("name"); name != "" {
req.filter.Name = &name
}
if bucketID := qp.Get("id"); bucketID != "" {
id, err := influxdb.IDFromString(bucketID)
if err != nil {
return nil, err
}
req.filter.ID = id
}
return req, nil
}
type bucketResponse struct {
bucket
Links map[string]string `json:"links"`
Labels []influxdb.Label `json:"labels"`
}
type bucket struct {
ID influxdb.ID `json:"id,omitempty"`
OrgID influxdb.ID `json:"orgID,omitempty"`
Type string `json:"type"`
Description string `json:"description,omitempty"`
Name string `json:"name"`
RetentionPolicyName string `json:"rp,omitempty"` // This to support v1 sources
RetentionRules []retentionRule `json:"retentionRules"`
influxdb.CRUDLog
}
func newBucket(pb *influxdb.Bucket) *bucket {
if pb == nil {
return nil
}
rules := []retentionRule{}
rp := int64(pb.RetentionPeriod.Round(time.Second) / time.Second)
if rp > 0 {
rules = append(rules, retentionRule{
Type: "expire",
EverySeconds: rp,
})
}
return &bucket{
ID: pb.ID,
OrgID: pb.OrgID,
Type: pb.Type.String(),
Name: pb.Name,
Description: pb.Description,
RetentionPolicyName: pb.RetentionPolicyName,
RetentionRules: rules,
CRUDLog: pb.CRUDLog,
}
}
// retentionRule is the retention rule action for a bucket.
type retentionRule struct {
Type string `json:"type"`
EverySeconds int64 `json:"everySeconds"`
}
func (rr *retentionRule) RetentionPeriod() (time.Duration, error) {
t := time.Duration(rr.EverySeconds) * time.Second
if t < time.Second {
return t, &influxdb.Error{
Code: influxdb.EUnprocessableEntity,
Msg: "expiration seconds must be greater than or equal to one second",
}
}
return t, nil
}
func NewBucketResponse(b *influxdb.Bucket, labels []*influxdb.Label) *bucketResponse {
res := &bucketResponse{
Links: map[string]string{
"labels": fmt.Sprintf("/api/v2/buckets/%s/labels", b.ID),
"logs": fmt.Sprintf("/api/v2/buckets/%s/logs", b.ID),
"members": fmt.Sprintf("/api/v2/buckets/%s/members", b.ID),
"org": fmt.Sprintf("/api/v2/orgs/%s", b.OrgID),
"owners": fmt.Sprintf("/api/v2/buckets/%s/owners", b.ID),
"self": fmt.Sprintf("/api/v2/buckets/%s", b.ID),
"write": fmt.Sprintf("/api/v2/write?org=%s&bucket=%s", b.OrgID, b.ID),
},
bucket: *newBucket(b),
Labels: []influxdb.Label{},
}
for _, l := range labels {
res.Labels = append(res.Labels, *l)
}
return res
}
type bucketsResponse struct {
Links *influxdb.PagingLinks `json:"links"`
Buckets []*bucketResponse `json:"buckets"`
}
func newBucketsResponse(ctx context.Context, opts influxdb.FindOptions, f influxdb.BucketFilter, bs []*influxdb.Bucket, labelService influxdb.LabelService) *bucketsResponse {
rs := make([]*bucketResponse, 0, len(bs))
for _, b := range bs {
labels, _ := labelService.FindResourceLabels(ctx, influxdb.LabelMappingFilter{ResourceID: b.ID, ResourceType: influxdb.BucketsResourceType})
rs = append(rs, NewBucketResponse(b, labels))
}
return &bucketsResponse{
Links: influxdb.NewPagingLinks(prefixBuckets, opts, f, len(bs)),
Buckets: rs,
}
}
// handlePostSource is the HTTP handler for the POST /api/v2/sources route.
func (h *SourceHandler) handlePostSource(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
@ -286,11 +430,11 @@ func (h *SourceHandler) handlePostSource(w http.ResponseWriter, r *http.Request)
}
type postSourceRequest struct {
Source *platform.Source
Source *influxdb.Source
}
func decodePostSourceRequest(ctx context.Context, r *http.Request) (*postSourceRequest, error) {
b := &platform.Source{}
b := &influxdb.Source{}
if err := json.NewDecoder(r.Body).Decode(b); err != nil {
return nil, err
}
@ -349,20 +493,20 @@ func (h *SourceHandler) handleGetSourceHealth(w http.ResponseWriter, r *http.Req
}
type getSourceRequest struct {
SourceID platform.ID
SourceID influxdb.ID
}
func decodeGetSourceRequest(ctx context.Context, r *http.Request) (*getSourceRequest, error) {
params := httprouter.ParamsFromContext(ctx)
id := params.ByName("id")
if id == "" {
return nil, &platform.Error{
Code: platform.EInvalid,
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "url missing id",
}
}
var i platform.ID
var i influxdb.ID
if err := i.DecodeFromString(id); err != nil {
return nil, err
}
@ -392,20 +536,20 @@ func (h *SourceHandler) handleDeleteSource(w http.ResponseWriter, r *http.Reques
}
type deleteSourceRequest struct {
SourceID platform.ID
SourceID influxdb.ID
}
func decodeDeleteSourceRequest(ctx context.Context, r *http.Request) (*deleteSourceRequest, error) {
params := httprouter.ParamsFromContext(ctx)
id := params.ByName("id")
if id == "" {
return nil, &platform.Error{
Code: platform.EInvalid,
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "url missing id",
}
}
var i platform.ID
var i influxdb.ID
if err := i.DecodeFromString(id); err != nil {
return nil, err
}
@ -441,7 +585,7 @@ func (h *SourceHandler) handleGetSources(w http.ResponseWriter, r *http.Request)
}
type getSourcesRequest struct {
findOptions platform.FindOptions
findOptions influxdb.FindOptions
}
func decodeGetSourcesRequest(ctx context.Context, r *http.Request) (*getSourcesRequest, error) {
@ -472,26 +616,26 @@ func (h *SourceHandler) handlePatchSource(w http.ResponseWriter, r *http.Request
}
type patchSourceRequest struct {
Update platform.SourceUpdate
SourceID platform.ID
Update influxdb.SourceUpdate
SourceID influxdb.ID
}
func decodePatchSourceRequest(ctx context.Context, r *http.Request) (*patchSourceRequest, error) {
params := httprouter.ParamsFromContext(ctx)
id := params.ByName("id")
if id == "" {
return nil, &platform.Error{
Code: platform.EInvalid,
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "url missing id",
}
}
var i platform.ID
var i influxdb.ID
if err := i.DecodeFromString(id); err != nil {
return nil, err
}
var upd platform.SourceUpdate
var upd influxdb.SourceUpdate
if err := json.NewDecoder(r.Body).Decode(&upd); err != nil {
return nil, err
}
@ -508,8 +652,8 @@ type SourceService struct {
}
// FindSourceByID returns a single source by ID.
func (s *SourceService) FindSourceByID(ctx context.Context, id platform.ID) (*platform.Source, error) {
var b platform.Source
func (s *SourceService) FindSourceByID(ctx context.Context, id influxdb.ID) (*influxdb.Source, error) {
var b influxdb.Source
err := s.Client.
Get(prefixSources, id.String()).
DecodeJSON(&b).
@ -522,8 +666,8 @@ func (s *SourceService) FindSourceByID(ctx context.Context, id platform.ID) (*pl
// FindSources returns a list of sources that match filter and the total count of matching sources.
// Additional options provide pagination & sorting.
func (s *SourceService) FindSources(ctx context.Context, opt platform.FindOptions) ([]*platform.Source, int, error) {
var bs []*platform.Source
func (s *SourceService) FindSources(ctx context.Context, opt influxdb.FindOptions) ([]*influxdb.Source, int, error) {
var bs []*influxdb.Source
err := s.Client.
Get(prefixSources).
DecodeJSON(&bs).
@ -536,7 +680,7 @@ func (s *SourceService) FindSources(ctx context.Context, opt platform.FindOption
}
// CreateSource creates a new source and sets b.ID with the new identifier.
func (s *SourceService) CreateSource(ctx context.Context, b *platform.Source) error {
func (s *SourceService) CreateSource(ctx context.Context, b *influxdb.Source) error {
return s.Client.
PostJSON(b, prefixSources).
DecodeJSON(b).
@ -545,8 +689,8 @@ func (s *SourceService) CreateSource(ctx context.Context, b *platform.Source) er
// UpdateSource updates a single source with changeset.
// Returns the new source state after update.
func (s *SourceService) UpdateSource(ctx context.Context, id platform.ID, upd platform.SourceUpdate) (*platform.Source, error) {
var b platform.Source
func (s *SourceService) UpdateSource(ctx context.Context, id influxdb.ID, upd influxdb.SourceUpdate) (*influxdb.Source, error) {
var b influxdb.Source
err := s.Client.
PatchJSON(upd, prefixSources, id.String()).
DecodeJSON(&b).
@ -558,7 +702,7 @@ func (s *SourceService) UpdateSource(ctx context.Context, id platform.ID, upd pl
}
// DeleteSource removes a source by ID.
func (s *SourceService) DeleteSource(ctx context.Context, id platform.ID) error {
func (s *SourceService) DeleteSource(ctx context.Context, id influxdb.ID) error {
return s.Client.
Delete(prefixSources, id.String()).
StatusFn(func(resp *http.Response) error {

View File

@ -18,7 +18,6 @@ import (
"github.com/influxdata/influxdb/v2/authorization"
pcontext "github.com/influxdata/influxdb/v2/context"
kithttp "github.com/influxdata/influxdb/v2/kit/transport/http"
"github.com/influxdata/influxdb/v2/kv"
"github.com/influxdata/influxdb/v2/label"
"github.com/influxdata/influxdb/v2/mock"
_ "github.com/influxdata/influxdb/v2/fluxinit/static"
@ -31,6 +30,9 @@ import (
// NewMockTaskBackend returns a TaskBackend with mock services.
func NewMockTaskBackend(t *testing.T) *TaskBackend {
t.Helper()
store := NewTestInmemStore(t)
tenantService := tenant.NewService(tenant.NewStore(store))
return &TaskBackend{
log: zaptest.NewLogger(t).With(zap.String("handler", "task")),
@ -60,7 +62,7 @@ func NewMockTaskBackend(t *testing.T) *TaskBackend {
return org, nil
},
},
UserResourceMappingService: newInMemKVSVC(t),
UserResourceMappingService: tenantService,
LabelService: mock.NewLabelService(),
UserService: mock.NewUserService(),
}
@ -819,14 +821,19 @@ func TestTaskHandler_handleGetRuns(t *testing.T) {
func TestTaskHandler_NotFoundStatus(t *testing.T) {
// Ensure that the HTTP handlers return 404s for missing resources, and OKs for matching.
im := newInMemKVSVC(t)
store := NewTestInmemStore(t)
tenantService := tenant.NewService(tenant.NewStore(store))
labelStore, _ := label.NewStore(store)
labelService := label.NewService(labelStore)
taskBackend := NewMockTaskBackend(t)
taskBackend.HTTPErrorHandler = kithttp.ErrorHandler(0)
h := NewTaskHandler(zaptest.NewLogger(t), taskBackend)
h.UserResourceMappingService = im
h.LabelService = im
h.UserService = im
h.OrganizationService = im
h.UserResourceMappingService = tenantService
h.LabelService = labelService
h.UserService = tenantService
h.OrganizationService = tenantService
o := influxdb.Organization{Name: "o"}
ctx := context.Background()
@ -1308,11 +1315,22 @@ func TestTaskHandler_Sessions(t *testing.T) {
t.Skip("rework these")
// Common setup to get a working base for using tasks.
st := NewTestInmemStore(t)
i := kv.NewService(zaptest.NewLogger(t), st)
tStore := tenant.NewStore(st)
tSvc := tenant.NewService(tStore)
labelStore, err := label.NewStore(st)
if err != nil {
t.Fatal(err)
}
labelService := label.NewService(labelStore)
authStore, err := authorization.NewStore(st)
if err != nil {
t.Fatal(err)
}
authService := authorization.NewService(authStore, tSvc)
ctx := context.Background()
// Set up user and org.
@ -1357,10 +1375,10 @@ func TestTaskHandler_Sessions(t *testing.T) {
log: zaptest.NewLogger(t),
TaskService: ts,
AuthorizationService: i,
AuthorizationService: authService,
OrganizationService: tSvc,
UserResourceMappingService: tSvc,
LabelService: i,
LabelService: labelService,
UserService: tSvc,
BucketService: tSvc,
})
@ -1369,7 +1387,7 @@ func TestTaskHandler_Sessions(t *testing.T) {
t.Run("get runs for a task", func(t *testing.T) {
// Unique authorization to associate with our fake task.
taskAuth := &influxdb.Authorization{OrgID: o.ID, UserID: u.ID}
if err := i.CreateAuthorization(ctx, taskAuth); err != nil {
if err := authService.CreateAuthorization(ctx, taskAuth); err != nil {
t.Fatal(err)
}
@ -1434,7 +1452,7 @@ func TestTaskHandler_Sessions(t *testing.T) {
// Other user without permissions on the task or authorization should be disallowed.
otherUser := &influxdb.User{Name: "other-" + t.Name()}
if err := i.CreateUser(ctx, otherUser); err != nil {
if err := tSvc.CreateUser(ctx, otherUser); err != nil {
t.Fatal(err)
}
@ -1461,7 +1479,7 @@ func TestTaskHandler_Sessions(t *testing.T) {
t.Run("get single run for a task", func(t *testing.T) {
// Unique authorization to associate with our fake task.
taskAuth := &influxdb.Authorization{OrgID: o.ID, UserID: u.ID}
if err := i.CreateAuthorization(ctx, taskAuth); err != nil {
if err := authService.CreateAuthorization(ctx, taskAuth); err != nil {
t.Fatal(err)
}
@ -1528,7 +1546,7 @@ func TestTaskHandler_Sessions(t *testing.T) {
// Other user without permissions on the task or authorization should be disallowed.
otherUser := &influxdb.User{Name: "other-" + t.Name()}
if err := i.CreateUser(ctx, otherUser); err != nil {
if err := tSvc.CreateUser(ctx, otherUser); err != nil {
t.Fatal(err)
}
@ -1555,7 +1573,7 @@ func TestTaskHandler_Sessions(t *testing.T) {
t.Run("get logs for a run", func(t *testing.T) {
// Unique authorization to associate with our fake task.
taskAuth := &influxdb.Authorization{OrgID: o.ID, UserID: u.ID}
if err := i.CreateAuthorization(ctx, taskAuth); err != nil {
if err := authService.CreateAuthorization(ctx, taskAuth); err != nil {
t.Fatal(err)
}
@ -1623,7 +1641,7 @@ func TestTaskHandler_Sessions(t *testing.T) {
// Other user without permissions on the task or authorization should be disallowed.
otherUser := &influxdb.User{Name: "other-" + t.Name()}
if err := i.CreateUser(ctx, otherUser); err != nil {
if err := tSvc.CreateUser(ctx, otherUser); err != nil {
t.Fatal(err)
}
@ -1650,7 +1668,7 @@ func TestTaskHandler_Sessions(t *testing.T) {
t.Run("retry a run", func(t *testing.T) {
// Unique authorization to associate with our fake task.
taskAuth := &influxdb.Authorization{OrgID: o.ID, UserID: u.ID}
if err := i.CreateAuthorization(ctx, taskAuth); err != nil {
if err := authService.CreateAuthorization(ctx, taskAuth); err != nil {
t.Fatal(err)
}
@ -1717,7 +1735,7 @@ func TestTaskHandler_Sessions(t *testing.T) {
// Other user without permissions on the task or authorization should be disallowed.
otherUser := &influxdb.User{Name: "other-" + t.Name()}
if err := i.CreateUser(ctx, otherUser); err != nil {
if err := tSvc.CreateUser(ctx, otherUser); err != nil {
t.Fatal(err)
}

View File

@ -1,129 +0,0 @@
package http
import (
"context"
"errors"
"net/http"
"time"
"github.com/influxdata/httprouter"
platform "github.com/influxdata/influxdb/v2"
"go.uber.org/zap"
)
// UsageHandler represents an HTTP API handler for usages.
type UsageHandler struct {
*httprouter.Router
platform.HTTPErrorHandler
log *zap.Logger
UsageService platform.UsageService
}
// NewUsageHandler returns a new instance of UsageHandler.
func NewUsageHandler(log *zap.Logger, he platform.HTTPErrorHandler) *UsageHandler {
h := &UsageHandler{
Router: NewRouter(he),
log: log,
}
h.HandlerFunc("GET", "/api/v2/usage", h.handleGetUsage)
return h
}
// handleGetUsage is the HTTP handler for the GET /api/v2/usage route.
func (h *UsageHandler) handleGetUsage(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodeGetUsageRequest(ctx, r)
if err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
b, err := h.UsageService.GetUsage(ctx, req.filter)
if err != nil {
h.HandleHTTPError(ctx, err, w)
return
}
if err := encodeResponse(ctx, w, http.StatusOK, b); err != nil {
logEncodingError(h.log, r, err)
return
}
}
type getUsageRequest struct {
filter platform.UsageFilter
}
func decodeGetUsageRequest(ctx context.Context, r *http.Request) (*getUsageRequest, error) {
req := &getUsageRequest{}
qp := r.URL.Query()
orgID := qp.Get("orgID")
if orgID != "" {
var id platform.ID
if err := (&id).DecodeFromString(orgID); err != nil {
return nil, err
}
req.filter.OrgID = &id
}
bucketID := qp.Get("bucketID")
if bucketID != "" {
var id platform.ID
if err := (&id).DecodeFromString(bucketID); err != nil {
return nil, err
}
req.filter.BucketID = &id
}
start := qp.Get("start")
stop := qp.Get("stop")
if start == "" && stop != "" {
return nil, errors.New("start query param required")
}
if stop == "" && start != "" {
return nil, errors.New("stop query param required")
}
if start == "" && stop == "" {
now := time.Now()
month := roundToMonth(now)
req.filter.Range = &platform.Timespan{
Start: month,
Stop: now,
}
}
if start != "" && stop != "" {
startTime, err := time.Parse(time.RFC3339, start)
if err != nil {
return nil, err
}
stopTime, err := time.Parse(time.RFC3339, stop)
if err != nil {
return nil, err
}
req.filter.Range = &platform.Timespan{
Start: startTime,
Stop: stopTime,
}
}
return req, nil
}
func roundToMonth(t time.Time) time.Time {
h, m, s := t.Clock()
d := t.Day()
delta := (time.Duration(d) * 24 * time.Hour) + time.Duration(h)*time.Hour + time.Duration(m)*time.Minute + time.Duration(s)*time.Second
return t.Add(-1 * delta).Round(time.Minute)
}

View File

@ -12,6 +12,7 @@ import (
kithttp "github.com/influxdata/influxdb/v2/kit/transport/http"
"github.com/influxdata/influxdb/v2/mock"
"github.com/influxdata/influxdb/v2/pkg/testttp"
"github.com/influxdata/influxdb/v2/tenant"
platformtesting "github.com/influxdata/influxdb/v2/testing"
"go.uber.org/zap/zaptest"
)
@ -29,19 +30,22 @@ func NewMockUserBackend(t *testing.T) *UserBackend {
func initUserService(f platformtesting.UserFields, t *testing.T) (platform.UserService, string, func()) {
t.Helper()
svc := newInMemKVSVC(t)
svc.IDGenerator = f.IDGenerator
store := NewTestInmemStore(t)
tenantStore := tenant.NewStore(store)
tenantStore.IDGen = f.IDGenerator
tenantService := tenant.NewService(tenantStore)
ctx := context.Background()
for _, u := range f.Users {
if err := svc.PutUser(ctx, u); err != nil {
if err := tenantService.CreateUser(ctx, u); err != nil {
t.Fatalf("failed to populate users")
}
}
userBackend := NewMockUserBackend(t)
userBackend.HTTPErrorHandler = kithttp.ErrorHandler(0)
userBackend.UserService = svc
userBackend.UserService = tenantService
handler := NewUserHandler(zaptest.NewLogger(t), userBackend)
server := httptest.NewServer(handler)

View File

@ -14,7 +14,9 @@ import (
"github.com/influxdata/httprouter"
platform "github.com/influxdata/influxdb/v2"
kithttp "github.com/influxdata/influxdb/v2/kit/transport/http"
"github.com/influxdata/influxdb/v2/kv"
"github.com/influxdata/influxdb/v2/mock"
"github.com/influxdata/influxdb/v2/tenant"
itesting "github.com/influxdata/influxdb/v2/testing"
"go.uber.org/zap/zaptest"
)
@ -992,7 +994,10 @@ func TestService_handlePostVariableLabel(t *testing.T) {
}
func initVariableService(f itesting.VariableFields, t *testing.T) (platform.VariableService, string, func()) {
svc := newInMemKVSVC(t)
store := NewTestInmemStore(t)
tenantService := tenant.NewService(tenant.NewStore(store))
svc := kv.NewService(zaptest.NewLogger(t), store, tenantService)
svc.IDGenerator = f.IDGenerator
svc.TimeGenerator = f.TimeGenerator

View File

@ -1,561 +0,0 @@
package kv
import (
"context"
"encoding/json"
"fmt"
"github.com/buger/jsonparser"
influxdb "github.com/influxdata/influxdb/v2"
jsonp "github.com/influxdata/influxdb/v2/pkg/jsonparser"
)
var (
authBucket = []byte("authorizationsv1")
authIndex = []byte("authorizationindexv1")
)
var _ influxdb.AuthorizationService = (*Service)(nil)
// FindAuthorizationByID retrieves a authorization by id.
func (s *Service) FindAuthorizationByID(ctx context.Context, id influxdb.ID) (*influxdb.Authorization, error) {
var a *influxdb.Authorization
err := s.kv.View(ctx, func(tx Tx) error {
auth, err := s.findAuthorizationByID(ctx, tx, id)
if err != nil {
return err
}
a = auth
return nil
})
if err != nil {
return nil, err
}
return a, nil
}
func (s *Service) findAuthorizationByID(ctx context.Context, tx Tx, id influxdb.ID) (*influxdb.Authorization, error) {
encodedID, err := id.Encode()
if err != nil {
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
}
}
b, err := tx.Bucket(authBucket)
if err != nil {
return nil, err
}
v, err := b.Get(encodedID)
if IsNotFound(err) {
return nil, &influxdb.Error{
Code: influxdb.ENotFound,
Msg: "authorization not found",
}
}
if err != nil {
return nil, err
}
a := &influxdb.Authorization{}
if err := decodeAuthorization(v, a); err != nil {
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
}
}
return a, nil
}
// FindAuthorizationByToken returns a authorization by token for a particular authorization.
func (s *Service) FindAuthorizationByToken(ctx context.Context, n string) (*influxdb.Authorization, error) {
var a *influxdb.Authorization
err := s.kv.View(ctx, func(tx Tx) error {
auth, err := s.findAuthorizationByToken(ctx, tx, n)
if err != nil {
return err
}
a = auth
return nil
})
if err != nil {
return nil, err
}
return a, nil
}
func (s *Service) findAuthorizationByToken(ctx context.Context, tx Tx, n string) (*influxdb.Authorization, error) {
idx, err := authIndexBucket(tx)
if err != nil {
return nil, err
}
a, err := idx.Get(authIndexKey(n))
if IsNotFound(err) {
return nil, &influxdb.Error{
Code: influxdb.ENotFound,
Msg: "authorization not found",
}
}
var id influxdb.ID
if err := id.Decode(a); err != nil {
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
}
}
return s.findAuthorizationByID(ctx, tx, id)
}
func authorizationsPredicateFn(f influxdb.AuthorizationFilter) CursorPredicateFunc {
// if any errors occur reading the JSON data, the predicate will always return true
// to ensure the value is included and handled higher up.
if f.ID != nil {
exp := *f.ID
return func(_, value []byte) bool {
got, err := jsonp.GetID(value, "id")
if err != nil {
return true
}
return got == exp
}
}
if f.Token != nil {
exp := *f.Token
return func(_, value []byte) bool {
// it is assumed that token never has escaped string data
got, _, _, err := jsonparser.Get(value, "token")
if err != nil {
return true
}
return string(got) == exp
}
}
var pred CursorPredicateFunc
if f.OrgID != nil {
exp := *f.OrgID
pred = func(_, value []byte) bool {
got, err := jsonp.GetID(value, "orgID")
if err != nil {
return true
}
return got == exp
}
}
if f.UserID != nil {
exp := *f.UserID
prevFn := pred
pred = func(key, value []byte) bool {
prev := prevFn == nil || prevFn(key, value)
got, exists, err := jsonp.GetOptionalID(value, "userID")
return prev && ((exp == got && exists) || err != nil)
}
}
return pred
}
func filterAuthorizationsFn(filter influxdb.AuthorizationFilter) func(a *influxdb.Authorization) bool {
if filter.ID != nil {
return func(a *influxdb.Authorization) bool {
return a.ID == *filter.ID
}
}
if filter.Token != nil {
return func(a *influxdb.Authorization) bool {
return a.Token == *filter.Token
}
}
// Filter by org and user
if filter.OrgID != nil && filter.UserID != nil {
return func(a *influxdb.Authorization) bool {
return a.OrgID == *filter.OrgID && a.UserID == *filter.UserID
}
}
if filter.OrgID != nil {
return func(a *influxdb.Authorization) bool {
return a.OrgID == *filter.OrgID
}
}
if filter.UserID != nil {
return func(a *influxdb.Authorization) bool {
return a.UserID == *filter.UserID
}
}
return func(a *influxdb.Authorization) bool { return true }
}
// FindAuthorizations retrieves all authorizations that match an arbitrary authorization filter.
// Filters using ID, or Token should be efficient.
// Other filters will do a linear scan across all authorizations searching for a match.
func (s *Service) FindAuthorizations(ctx context.Context, filter influxdb.AuthorizationFilter, opt ...influxdb.FindOptions) ([]*influxdb.Authorization, int, error) {
if filter.ID != nil {
a, err := s.FindAuthorizationByID(ctx, *filter.ID)
if err != nil {
return nil, 0, &influxdb.Error{
Err: err,
}
}
return []*influxdb.Authorization{a}, 1, nil
}
if filter.Token != nil {
a, err := s.FindAuthorizationByToken(ctx, *filter.Token)
if err != nil {
return nil, 0, &influxdb.Error{
Err: err,
}
}
return []*influxdb.Authorization{a}, 1, nil
}
as := []*influxdb.Authorization{}
err := s.kv.View(ctx, func(tx Tx) error {
auths, err := s.findAuthorizations(ctx, tx, filter)
if err != nil {
return err
}
as = auths
return nil
})
if err != nil {
return nil, 0, &influxdb.Error{
Err: err,
}
}
return as, len(as), nil
}
func (s *Service) findAuthorizations(ctx context.Context, tx Tx, f influxdb.AuthorizationFilter) ([]*influxdb.Authorization, error) {
// If the users name was provided, look up user by ID first
if f.User != nil {
u, err := s.findUserByName(ctx, tx, *f.User)
if err != nil {
return nil, err
}
f.UserID = &u.ID
}
if f.Org != nil {
o, err := s.findOrganizationByName(ctx, tx, *f.Org)
if err != nil {
return nil, err
}
f.OrgID = &o.ID
}
var as []*influxdb.Authorization
pred := authorizationsPredicateFn(f)
filterFn := filterAuthorizationsFn(f)
err := s.forEachAuthorization(ctx, tx, pred, func(a *influxdb.Authorization) bool {
if filterFn(a) {
as = append(as, a)
}
return true
})
if err != nil {
return nil, err
}
return as, nil
}
// CreateAuthorization creates a influxdb authorization and sets b.ID, and b.UserID if not provided.
func (s *Service) CreateAuthorization(ctx context.Context, a *influxdb.Authorization) error {
return s.kv.Update(ctx, func(tx Tx) error {
return s.createAuthorization(ctx, tx, a)
})
}
// CreateAuthorizationTx is used when importing kv as a library
func (s *Service) CreateAuthorizationTx(ctx context.Context, tx Tx, a *influxdb.Authorization) error {
return s.createAuthorization(ctx, tx, a)
}
func (s *Service) createAuthorization(ctx context.Context, tx Tx, a *influxdb.Authorization) error {
if err := a.Valid(); err != nil {
return &influxdb.Error{
Err: err,
}
}
if _, err := s.findUserByID(ctx, tx, a.UserID); err != nil {
return influxdb.ErrUnableToCreateToken
}
if _, err := s.findOrganizationByID(ctx, tx, a.OrgID); err != nil {
return influxdb.ErrUnableToCreateToken
}
if err := s.uniqueAuthToken(ctx, tx, a); err != nil {
return err
}
if a.Token == "" {
token, err := s.TokenGenerator.Token()
if err != nil {
return &influxdb.Error{
Err: err,
}
}
a.Token = token
}
a.ID = s.IDGenerator.ID()
now := s.TimeGenerator.Now()
a.SetCreatedAt(now)
a.SetUpdatedAt(now)
if err := s.putAuthorization(ctx, tx, a); err != nil {
return err
}
return nil
}
// PutAuthorization will put a authorization without setting an ID.
func (s *Service) PutAuthorization(ctx context.Context, a *influxdb.Authorization) error {
return s.kv.Update(ctx, func(tx Tx) error {
return s.putAuthorization(ctx, tx, a)
})
}
func encodeAuthorization(a *influxdb.Authorization) ([]byte, error) {
switch a.Status {
case influxdb.Active, influxdb.Inactive:
case "":
a.Status = influxdb.Active
default:
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "unknown authorization status",
}
}
return json.Marshal(a)
}
func (s *Service) putAuthorization(ctx context.Context, tx Tx, a *influxdb.Authorization) error {
v, err := encodeAuthorization(a)
if err != nil {
return &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
}
}
encodedID, err := a.ID.Encode()
if err != nil {
return &influxdb.Error{
Code: influxdb.ENotFound,
Err: err,
}
}
idx, err := authIndexBucket(tx)
if err != nil {
return err
}
if err := idx.Put(authIndexKey(a.Token), encodedID); err != nil {
return &influxdb.Error{
Code: influxdb.EInternal,
Err: err,
}
}
b, err := tx.Bucket(authBucket)
if err != nil {
return err
}
if err := b.Put(encodedID, v); err != nil {
return &influxdb.Error{
Err: err,
}
}
return nil
}
func authIndexKey(n string) []byte {
return []byte(n)
}
func decodeAuthorization(b []byte, a *influxdb.Authorization) error {
if err := json.Unmarshal(b, a); err != nil {
return err
}
if a.Status == "" {
a.Status = influxdb.Active
}
return nil
}
// forEachAuthorization will iterate through all authorizations while fn returns true.
func (s *Service) forEachAuthorization(ctx context.Context, tx Tx, pred CursorPredicateFunc, fn func(*influxdb.Authorization) bool) error {
b, err := tx.Bucket(authBucket)
if err != nil {
return err
}
var cur Cursor
if pred != nil {
cur, err = b.Cursor(WithCursorHintPredicate(pred))
} else {
cur, err = b.Cursor()
}
if err != nil {
return err
}
for k, v := cur.First(); k != nil; k, v = cur.Next() {
// preallocate Permissions to reduce multiple slice re-allocations
a := &influxdb.Authorization{
Permissions: make([]influxdb.Permission, 64),
}
if err := decodeAuthorization(v, a); err != nil {
return err
}
if !fn(a) {
break
}
}
return nil
}
// DeleteAuthorization deletes a authorization and prunes it from the index.
func (s *Service) DeleteAuthorization(ctx context.Context, id influxdb.ID) error {
return s.kv.Update(ctx, func(tx Tx) (err error) {
return s.deleteAuthorization(ctx, tx, id)
})
}
func (s *Service) deleteAuthorization(ctx context.Context, tx Tx, id influxdb.ID) error {
a, err := s.findAuthorizationByID(ctx, tx, id)
if err != nil {
return err
}
idx, err := authIndexBucket(tx)
if err != nil {
return err
}
if err := idx.Delete(authIndexKey(a.Token)); err != nil {
return &influxdb.Error{
Err: err,
}
}
encodedID, err := id.Encode()
if err != nil {
return &influxdb.Error{
Err: err,
}
}
b, err := tx.Bucket(authBucket)
if err != nil {
return err
}
if err := b.Delete(encodedID); err != nil {
return &influxdb.Error{
Err: err,
}
}
return nil
}
// UpdateAuthorization updates the status and description if available.
func (s *Service) UpdateAuthorization(ctx context.Context, id influxdb.ID, upd *influxdb.AuthorizationUpdate) (*influxdb.Authorization, error) {
var a *influxdb.Authorization
var err error
err = s.kv.Update(ctx, func(tx Tx) error {
a, err = s.updateAuthorization(ctx, tx, id, upd)
return err
})
return a, err
}
func (s *Service) updateAuthorization(ctx context.Context, tx Tx, id influxdb.ID, upd *influxdb.AuthorizationUpdate) (*influxdb.Authorization, error) {
a, err := s.findAuthorizationByID(ctx, tx, id)
if err != nil {
return nil, err
}
if upd.Status != nil {
a.Status = *upd.Status
}
if upd.Description != nil {
a.Description = *upd.Description
}
now := s.TimeGenerator.Now()
a.SetUpdatedAt(now)
if err := s.putAuthorization(ctx, tx, a); err != nil {
return nil, err
}
return a, nil
}
func authIndexBucket(tx Tx) (Bucket, error) {
b, err := tx.Bucket([]byte(authIndex))
if err != nil {
return nil, UnexpectedAuthIndexError(err)
}
return b, nil
}
// UnexpectedAuthIndexError is used when the error comes from an internal system.
func UnexpectedAuthIndexError(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EInternal,
Msg: fmt.Sprintf("unexpected error retrieving auth index; Err: %v", err),
Op: "kv/authIndex",
}
}
func (s *Service) uniqueAuthToken(ctx context.Context, tx Tx, a *influxdb.Authorization) error {
err := s.unique(ctx, tx, authIndex, authIndexKey(a.Token))
if err == NotUniqueError {
// by returning a generic error we are trying to hide when
// a token is non-unique.
return influxdb.ErrUnableToCreateToken
}
// otherwise, this is some sort of internal server error and we
// should provide some debugging information.
return err
}

View File

@ -1,149 +0,0 @@
package kv
import (
"encoding/json"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/influxdata/influxdb/v2"
)
func mustMarshal(t testing.TB, v interface{}) []byte {
t.Helper()
d, err := json.Marshal(v)
if err != nil {
t.Fatal(err)
}
return d
}
func Test_authorizationsPredicateFn(t *testing.T) {
t.Run("ID", func(t *testing.T) {
val := influxdb.ID(1)
f := influxdb.AuthorizationFilter{ID: &val}
fn := authorizationsPredicateFn(f)
t.Run("does match", func(t *testing.T) {
a := &influxdb.Authorization{ID: val, OrgID: 2}
if got, exp := fn(nil, mustMarshal(t, a)), true; got != exp {
t.Errorf("unexpected result -got/+exp\n%s", cmp.Diff(got, exp))
}
})
t.Run("does not match", func(t *testing.T) {
a := &influxdb.Authorization{ID: 10, OrgID: 2}
if got, exp := fn(nil, mustMarshal(t, a)), false; got != exp {
t.Errorf("unexpected result -got/+exp\n%s", cmp.Diff(got, exp))
}
})
})
t.Run("token", func(t *testing.T) {
val := "token_token"
f := influxdb.AuthorizationFilter{Token: &val}
fn := authorizationsPredicateFn(f)
t.Run("does match", func(t *testing.T) {
a := &influxdb.Authorization{ID: 10, OrgID: 2, Token: val}
if got, exp := fn(nil, mustMarshal(t, a)), true; got != exp {
t.Errorf("unexpected result -got/+exp\n%s", cmp.Diff(got, exp))
}
})
t.Run("does not match", func(t *testing.T) {
a := &influxdb.Authorization{ID: 10, OrgID: 2, Token: "no_no_no"}
if got, exp := fn(nil, mustMarshal(t, a)), false; got != exp {
t.Errorf("unexpected result -got/+exp\n%s", cmp.Diff(got, exp))
}
})
})
t.Run("orgID", func(t *testing.T) {
val := influxdb.ID(1)
f := influxdb.AuthorizationFilter{OrgID: &val}
fn := authorizationsPredicateFn(f)
t.Run("does match", func(t *testing.T) {
a := &influxdb.Authorization{ID: 10, OrgID: val}
if got, exp := fn(nil, mustMarshal(t, a)), true; got != exp {
t.Errorf("unexpected result -got/+exp\n%s", cmp.Diff(got, exp))
}
})
t.Run("does not match", func(t *testing.T) {
a := &influxdb.Authorization{ID: 10, OrgID: 2}
if got, exp := fn(nil, mustMarshal(t, a)), false; got != exp {
t.Errorf("unexpected result -got/+exp\n%s", cmp.Diff(got, exp))
}
})
})
t.Run("userID", func(t *testing.T) {
val := influxdb.ID(1)
f := influxdb.AuthorizationFilter{UserID: &val}
fn := authorizationsPredicateFn(f)
t.Run("does match", func(t *testing.T) {
a := &influxdb.Authorization{ID: 10, OrgID: 5, UserID: val}
if got, exp := fn(nil, mustMarshal(t, a)), true; got != exp {
t.Errorf("unexpected result -got/+exp\n%s", cmp.Diff(got, exp))
}
})
t.Run("does not match", func(t *testing.T) {
a := &influxdb.Authorization{ID: 10, OrgID: 5, UserID: 2}
if got, exp := fn(nil, mustMarshal(t, a)), false; got != exp {
t.Errorf("unexpected result -got/+exp\n%s", cmp.Diff(got, exp))
}
})
t.Run("missing userID", func(t *testing.T) {
a := &influxdb.Authorization{ID: 10, OrgID: 5}
if got, exp := fn(nil, mustMarshal(t, a)), false; got != exp {
t.Errorf("unexpected result -got/+exp\n%s", cmp.Diff(got, exp))
}
})
})
t.Run("orgID and userID", func(t *testing.T) {
orgID := influxdb.ID(1)
userID := influxdb.ID(10)
f := influxdb.AuthorizationFilter{OrgID: &orgID, UserID: &userID}
fn := authorizationsPredicateFn(f)
t.Run("does match", func(t *testing.T) {
a := &influxdb.Authorization{ID: 10, OrgID: orgID, UserID: userID}
if got, exp := fn(nil, mustMarshal(t, a)), true; got != exp {
t.Errorf("unexpected result -got/+exp\n%s", cmp.Diff(got, exp))
}
})
t.Run("org match user not match", func(t *testing.T) {
a := &influxdb.Authorization{ID: 10, OrgID: orgID, UserID: 11}
if got, exp := fn(nil, mustMarshal(t, a)), false; got != exp {
t.Errorf("unexpected result -got/+exp\n%s", cmp.Diff(got, exp))
}
})
t.Run("org not match user match", func(t *testing.T) {
a := &influxdb.Authorization{ID: 10, OrgID: 2, UserID: userID}
if got, exp := fn(nil, mustMarshal(t, a)), false; got != exp {
t.Errorf("unexpected result -got/+exp\n%s", cmp.Diff(got, exp))
}
})
t.Run("org and user not match", func(t *testing.T) {
a := &influxdb.Authorization{ID: 10, OrgID: 2, UserID: 11}
if got, exp := fn(nil, mustMarshal(t, a)), false; got != exp {
t.Errorf("unexpected result -got/+exp\n%s", cmp.Diff(got, exp))
}
})
t.Run("org match user missing", func(t *testing.T) {
a := &influxdb.Authorization{ID: 10, OrgID: orgID}
if got, exp := fn(nil, mustMarshal(t, a)), false; got != exp {
t.Errorf("unexpected result -got/+exp\n%s", cmp.Diff(got, exp))
}
})
})
}

View File

@ -1,76 +0,0 @@
package kv_test
import (
"context"
"testing"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/kv"
influxdbtesting "github.com/influxdata/influxdb/v2/testing"
"go.uber.org/zap/zaptest"
)
func TestBoltAuthorizationService(t *testing.T) {
influxdbtesting.AuthorizationService(initBoltAuthorizationService, t)
}
func initBoltAuthorizationService(f influxdbtesting.AuthorizationFields, t *testing.T) (influxdb.AuthorizationService, string, func()) {
s, closeBolt, err := NewTestBoltStore(t)
if err != nil {
t.Fatalf("failed to create new kv store: %v", err)
}
svc, op, closeSvc := initAuthorizationService(s, f, t)
return svc, op, func() {
closeSvc()
closeBolt()
}
}
func initAuthorizationService(s kv.SchemaStore, f influxdbtesting.AuthorizationFields, t *testing.T) (influxdb.AuthorizationService, string, func()) {
ctx := context.Background()
svc := kv.NewService(zaptest.NewLogger(t), s)
svc.IDGenerator = f.IDGenerator
svc.OrgIDs = f.OrgIDGenerator
svc.TokenGenerator = f.TokenGenerator
svc.TimeGenerator = f.TimeGenerator
for _, u := range f.Users {
if err := svc.PutUser(ctx, u); err != nil {
t.Fatalf("failed to populate users")
}
}
for _, o := range f.Orgs {
o.ID = svc.OrgIDs.ID()
if err := svc.PutOrganization(ctx, o); err != nil {
t.Fatalf("failed to populate orgs")
}
}
for _, a := range f.Authorizations {
if err := svc.PutAuthorization(ctx, a); err != nil {
t.Fatalf("failed to populate authorizations %s", err)
}
}
return svc, kv.OpPrefix, func() {
for _, u := range f.Users {
if err := svc.DeleteUser(ctx, u.ID); err != nil {
t.Logf("failed to remove user: %v", err)
}
}
for _, o := range f.Orgs {
if err := svc.DeleteOrganization(ctx, o.ID); err != nil {
t.Logf("failed to remove org: %v", err)
}
}
for _, a := range f.Authorizations {
if err := svc.DeleteAuthorization(ctx, a.ID); err != nil {
t.Logf("failed to remove authorizations: %v", err)
}
}
}
}

View File

@ -1,914 +0,0 @@
package kv
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/influxdata/influxdb/v2"
icontext "github.com/influxdata/influxdb/v2/context"
"github.com/influxdata/influxdb/v2/kit/tracing"
"github.com/influxdata/influxdb/v2/resource"
)
var (
bucketBucket = []byte("bucketsv1")
bucketIndex = []byte("bucketindexv1")
)
var _ influxdb.BucketService = (*Service)(nil)
var _ influxdb.BucketOperationLogService = (*Service)(nil)
func (s *Service) bucketsBucket(tx Tx) (Bucket, error) {
b, err := tx.Bucket(bucketBucket)
if err != nil {
return nil, UnexpectedBucketError(err)
}
return b, nil
}
func (s *Service) bucketsIndexBucket(tx Tx) (Bucket, error) {
b, err := tx.Bucket(bucketIndex)
if err != nil {
return nil, UnexpectedBucketIndexError(err)
}
return b, nil
}
// FindBucketByID retrieves a bucket by id.
func (s *Service) FindBucketByID(ctx context.Context, id influxdb.ID) (*influxdb.Bucket, error) {
span, ctx := tracing.StartSpanFromContext(ctx)
defer span.Finish()
var b *influxdb.Bucket
var err error
err = s.kv.View(ctx, func(tx Tx) error {
bkt, pe := s.findBucketByID(ctx, tx, id)
if pe != nil {
err = pe
return err
}
b = bkt
return nil
})
if err != nil {
return nil, err
}
return b, nil
}
func (s *Service) findBucketByID(ctx context.Context, tx Tx, id influxdb.ID) (*influxdb.Bucket, error) {
span, _ := tracing.StartSpanFromContext(ctx)
defer span.Finish()
var b influxdb.Bucket
encodedID, err := id.Encode()
if err != nil {
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
}
}
bkt, err := s.bucketsBucket(tx)
if err != nil {
return nil, err
}
v, err := bkt.Get(encodedID)
if IsNotFound(err) {
return nil, &influxdb.Error{
Code: influxdb.ENotFound,
Msg: "bucket not found",
}
}
if err != nil {
return nil, err
}
if err := json.Unmarshal(v, &b); err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
return &b, nil
}
// FindBucketByName returns a bucket by name for a particular organization.
// TODO: have method for finding bucket using organization name and bucket name.
func (s *Service) FindBucketByName(ctx context.Context, orgID influxdb.ID, n string) (*influxdb.Bucket, error) {
span, ctx := tracing.StartSpanFromContext(ctx)
defer span.Finish()
var b *influxdb.Bucket
err := s.kv.View(ctx, func(tx Tx) error {
bkt, pe := s.findBucketByName(ctx, tx, orgID, n)
if pe != nil {
return pe
}
b = bkt
return nil
})
return b, err
}
// CreateSystemBuckets for an organization
func (s *Service) CreateSystemBuckets(ctx context.Context, o *influxdb.Organization) error {
return s.kv.Update(ctx, func(tx Tx) error {
return s.createSystemBuckets(ctx, tx, o)
})
}
// createSystemBuckets creates the task and monitoring system buckets for an organization
func (s *Service) createSystemBuckets(ctx context.Context, tx Tx, o *influxdb.Organization) error {
tb := &influxdb.Bucket{
OrgID: o.ID,
Type: influxdb.BucketTypeSystem,
Name: influxdb.TasksSystemBucketName,
RetentionPeriod: influxdb.TasksSystemBucketRetention,
Description: "System bucket for task logs",
}
if err := s.createBucket(ctx, tx, tb); err != nil {
return err
}
mb := &influxdb.Bucket{
OrgID: o.ID,
Type: influxdb.BucketTypeSystem,
Name: influxdb.MonitoringSystemBucketName,
RetentionPeriod: influxdb.MonitoringSystemBucketRetention,
Description: "System bucket for monitoring logs",
}
return s.createBucket(ctx, tx, mb)
}
func (s *Service) findBucketByName(ctx context.Context, tx Tx, orgID influxdb.ID, n string) (*influxdb.Bucket, error) {
span, ctx := tracing.StartSpanFromContext(ctx)
defer span.Finish()
b := &influxdb.Bucket{
OrgID: orgID,
Name: n,
}
key, err := bucketIndexKey(b)
if err != nil {
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
}
}
idx, err := s.bucketsIndexBucket(tx)
if err != nil {
return nil, err
}
buf, err := idx.Get(key)
if IsNotFound(err) {
return nil, &influxdb.Error{
Code: influxdb.ENotFound,
Msg: fmt.Sprintf("bucket %q not found", n),
}
}
if err != nil {
return nil, err
}
var id influxdb.ID
if err := id.Decode(buf); err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
return s.findBucketByID(ctx, tx, id)
}
// FindBucket retrieves a bucket using an arbitrary bucket filter.
// Filters using ID, or OrganizationID and bucket Name should be efficient.
// Other filters will do a linear scan across buckets until it finds a match.
func (s *Service) FindBucket(ctx context.Context, filter influxdb.BucketFilter) (*influxdb.Bucket, error) {
span, ctx := tracing.StartSpanFromContext(ctx)
defer span.Finish()
var b *influxdb.Bucket
var err error
if filter.ID != nil {
b, err = s.FindBucketByID(ctx, *filter.ID)
if err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
return b, nil
}
if filter.Name != nil && filter.OrganizationID != nil {
return s.FindBucketByName(ctx, *filter.OrganizationID, *filter.Name)
}
err = s.kv.View(ctx, func(tx Tx) error {
if filter.Org != nil {
o, err := s.findOrganizationByName(ctx, tx, *filter.Org)
if err != nil {
return err
}
filter.OrganizationID = &o.ID
}
filterFn := filterBucketsFn(filter)
return s.forEachBucket(ctx, tx, false, func(bkt *influxdb.Bucket) bool {
if filterFn(bkt) {
b = bkt
return false
}
return true
})
})
if err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
if b == nil {
return nil, &influxdb.Error{
Code: influxdb.ENotFound,
Msg: "bucket not found",
}
}
return b, nil
}
func filterBucketsFn(filter influxdb.BucketFilter) func(b *influxdb.Bucket) bool {
if filter.ID != nil {
return func(b *influxdb.Bucket) bool {
return b.ID == *filter.ID
}
}
if filter.Name != nil && filter.OrganizationID != nil {
return func(b *influxdb.Bucket) bool {
return b.Name == *filter.Name && b.OrgID == *filter.OrganizationID
}
}
if filter.Name != nil {
return func(b *influxdb.Bucket) bool {
return b.Name == *filter.Name
}
}
if filter.OrganizationID != nil {
return func(b *influxdb.Bucket) bool {
return b.OrgID == *filter.OrganizationID
}
}
return func(b *influxdb.Bucket) bool { return true }
}
// FindBuckets retrieves all buckets that match an arbitrary bucket filter.
// Filters using ID, or OrganizationID and bucket Name should be efficient.
// Other filters will do a linear scan across all buckets searching for a match.
func (s *Service) FindBuckets(ctx context.Context, filter influxdb.BucketFilter, opts ...influxdb.FindOptions) ([]*influxdb.Bucket, int, error) {
span, ctx := tracing.StartSpanFromContext(ctx)
defer span.Finish()
if filter.ID != nil {
b, err := s.FindBucketByID(ctx, *filter.ID)
if err != nil {
return nil, 0, err
}
return []*influxdb.Bucket{b}, 1, nil
}
if filter.Name != nil && filter.OrganizationID != nil {
b, err := s.FindBucketByName(ctx, *filter.OrganizationID, *filter.Name)
if err != nil {
return nil, 0, err
}
return []*influxdb.Bucket{b}, 1, nil
}
bs := []*influxdb.Bucket{}
err := s.kv.View(ctx, func(tx Tx) error {
bkts, err := s.findBuckets(ctx, tx, filter, opts...)
if err != nil {
return err
}
bs = bkts
return nil
})
if err != nil {
return nil, 0, err
}
// Don't append system buckets if Name is set. Users who don't have real
// system buckets won't get mocked buckets if they query for a bucket by name
// without the orgID, but this is a vanishing small number of users and has
// limited utility anyways. Can be removed once mock system code is ripped out.
if filter.Name != nil {
return bs, len(bs), nil
}
return bs, len(bs), nil
}
func (s *Service) findBuckets(ctx context.Context, tx Tx, filter influxdb.BucketFilter, opts ...influxdb.FindOptions) ([]*influxdb.Bucket, error) {
span, ctx := tracing.StartSpanFromContext(ctx)
defer span.Finish()
bs := []*influxdb.Bucket{}
if filter.Org != nil {
o, err := s.findOrganizationByName(ctx, tx, *filter.Org)
if err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
filter.OrganizationID = &o.ID
}
var (
offset, limit, count int
descending bool
)
after := func(*influxdb.Bucket) bool {
return true
}
if len(opts) > 0 {
offset = opts[0].Offset
limit = opts[0].Limit
descending = opts[0].Descending
if opts[0].After != nil {
after = func(b *influxdb.Bucket) bool {
if descending {
return b.ID < *opts[0].After
}
return b.ID > *opts[0].After
}
}
}
filterFn := filterBucketsFn(filter)
err := s.forEachBucket(ctx, tx, descending, func(b *influxdb.Bucket) bool {
if filterFn(b) {
if count >= offset && after(b) {
bs = append(bs, b)
}
count++
}
if limit > 0 && len(bs) >= limit {
return false
}
return true
})
if err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
return bs, nil
}
// CreateBucket creates a influxdb bucket and sets b.ID.
func (s *Service) CreateBucket(ctx context.Context, b *influxdb.Bucket) error {
span, ctx := tracing.StartSpanFromContext(ctx)
defer span.Finish()
return s.kv.Update(ctx, func(tx Tx) error {
return s.createBucket(ctx, tx, b)
})
}
// CreateBucketTx is used when importing kv as a library
func (s *Service) CreateBucketTx(ctx context.Context, tx Tx, b *influxdb.Bucket) (err error) {
return s.createBucket(ctx, tx, b)
}
func (s *Service) createBucket(ctx context.Context, tx Tx, b *influxdb.Bucket) (err error) {
if b.OrgID.Valid() {
span, ctx := tracing.StartSpanFromContext(ctx)
defer span.Finish()
_, pe := s.findOrganizationByID(ctx, tx, b.OrgID)
if pe != nil {
return &influxdb.Error{
Err: pe,
}
}
}
if err := s.uniqueBucketName(ctx, tx, b); err != nil {
return err
}
if err := validBucketName(b.Name, b.Type); err != nil {
return err
}
if b.ID, err = s.generateBucketID(ctx, tx); err != nil {
return err
}
b.CreatedAt = s.Now()
b.UpdatedAt = s.Now()
if err := s.appendBucketEventToLog(ctx, tx, b.ID, bucketCreatedEvent); err != nil {
return &influxdb.Error{
Err: err,
}
}
v, err := json.Marshal(b)
if err != nil {
return influxdb.ErrInternalBucketServiceError(influxdb.OpCreateBucket, err)
}
if err := s.putBucket(ctx, tx, b, v); err != nil {
return err
}
if err := s.createUserResourceMappingForOrg(ctx, tx, b.OrgID, b.ID, influxdb.BucketsResourceType); err != nil {
return err
}
uid, _ := icontext.GetUserID(ctx)
return s.audit.Log(resource.Change{
Type: resource.Create,
ResourceID: b.ID,
ResourceType: influxdb.BucketsResourceType,
OrganizationID: b.OrgID,
UserID: uid,
ResourceBody: v,
Time: time.Now(),
})
}
func (s *Service) generateBucketID(ctx context.Context, tx Tx) (influxdb.ID, error) {
return s.generateSafeID(ctx, tx, bucketBucket, s.BucketIDs)
}
// PutBucket will put a bucket without setting an ID.
func (s *Service) PutBucket(ctx context.Context, b *influxdb.Bucket) error {
return s.kv.Update(ctx, func(tx Tx) error {
v, err := json.Marshal(b)
if err != nil {
return influxdb.ErrInternalBucketServiceError(influxdb.OpPutBucket, err)
}
if err := s.putBucket(ctx, tx, b, v); err != nil {
return err
}
uid, _ := icontext.GetUserID(ctx)
return s.audit.Log(resource.Change{
Type: resource.Put,
ResourceID: b.ID,
ResourceType: influxdb.BucketsResourceType,
OrganizationID: b.OrgID,
UserID: uid,
ResourceBody: v,
Time: time.Now(),
})
})
}
func (s *Service) putBucket(ctx context.Context, tx Tx, b *influxdb.Bucket, v []byte) error {
span, _ := tracing.StartSpanFromContext(ctx)
defer span.Finish()
encodedID, err := b.ID.Encode()
if err != nil {
return &influxdb.Error{
Err: err,
}
}
key, err := bucketIndexKey(b)
if err != nil {
return err
}
idx, err := s.bucketsIndexBucket(tx)
if err != nil {
return err
}
if err := idx.Put(key, encodedID); err != nil {
return &influxdb.Error{
Err: err,
}
}
bkt, err := s.bucketsBucket(tx)
if bkt.Put(encodedID, v); err != nil {
return &influxdb.Error{
Err: err,
}
}
return nil
}
// bucketIndexKey is a combination of the orgID and the bucket name.
func bucketIndexKey(b *influxdb.Bucket) ([]byte, error) {
orgID, err := b.OrgID.Encode()
if err != nil {
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
}
}
k := make([]byte, influxdb.IDLength+len(b.Name))
copy(k, orgID)
copy(k[influxdb.IDLength:], []byte(b.Name))
return k, nil
}
// forEachBucket will iterate through all buckets while fn returns true.
func (s *Service) forEachBucket(ctx context.Context, tx Tx, descending bool, fn func(*influxdb.Bucket) bool) error {
span, _ := tracing.StartSpanFromContext(ctx)
defer span.Finish()
bkt, err := s.bucketsBucket(tx)
if err != nil {
return err
}
direction := CursorAscending
if descending {
direction = CursorDescending
}
cur, err := bkt.ForwardCursor(nil, WithCursorDirection(direction))
if err != nil {
return err
}
for k, v := cur.Next(); k != nil; k, v = cur.Next() {
b := &influxdb.Bucket{}
if err := json.Unmarshal(v, b); err != nil {
return err
}
if !fn(b) {
break
}
}
return nil
}
func (s *Service) uniqueBucketName(ctx context.Context, tx Tx, b *influxdb.Bucket) error {
key, err := bucketIndexKey(b)
if err != nil {
return err
}
// if the bucket name is not unique for this organization, then, do not
// allow creation.
err = s.unique(ctx, tx, bucketIndex, key)
if err == NotUniqueError {
return BucketAlreadyExistsError(b)
}
return err
}
// validBucketName reports any errors with bucket names
func validBucketName(name string, typ influxdb.BucketType) error {
// names starting with an underscore are reserved for system buckets
if strings.HasPrefix(name, "_") && typ != influxdb.BucketTypeSystem {
return &influxdb.Error{
Code: influxdb.EInvalid,
Msg: fmt.Sprintf("bucket name %s is invalid. Buckets may not start with underscore", name),
Op: influxdb.OpCreateBucket,
}
}
// quotation marks will cause queries to fail
if strings.Contains(name, "\"") {
return &influxdb.Error{
Code: influxdb.EInvalid,
Msg: fmt.Sprintf("bucket name %s is invalid. Bucket names may not include quotation marks", name),
Op: influxdb.OpCreateBucket,
}
}
return nil
}
// UpdateBucket updates a bucket according the parameters set on upd.
func (s *Service) UpdateBucket(ctx context.Context, id influxdb.ID, upd influxdb.BucketUpdate) (*influxdb.Bucket, error) {
span, ctx := tracing.StartSpanFromContext(ctx)
defer span.Finish()
var b *influxdb.Bucket
err := s.kv.Update(ctx, func(tx Tx) error {
bkt, err := s.updateBucket(ctx, tx, id, upd)
if err != nil {
return err
}
b = bkt
return nil
})
return b, err
}
func (s *Service) updateBucket(ctx context.Context, tx Tx, id influxdb.ID, upd influxdb.BucketUpdate) (*influxdb.Bucket, error) {
span, ctx := tracing.StartSpanFromContext(ctx)
defer span.Finish()
b, err := s.findBucketByID(ctx, tx, id)
if err != nil {
return nil, err
}
if upd.Name != nil && b.Type == influxdb.BucketTypeSystem {
err = &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "system buckets cannot be renamed",
}
return nil, err
}
if upd.RetentionPeriod != nil {
b.RetentionPeriod = *upd.RetentionPeriod
}
if upd.Description != nil {
b.Description = *upd.Description
}
if upd.Name != nil {
b0, err := s.findBucketByName(ctx, tx, b.OrgID, *upd.Name)
if err == nil && b0.ID != id {
return nil, &influxdb.Error{
Code: influxdb.EConflict,
Msg: "bucket name is not unique",
}
}
if err := validBucketName(*upd.Name, b.Type); err != nil {
return nil, err
}
key, err := bucketIndexKey(b)
if err != nil {
return nil, err
}
idx, err := s.bucketsIndexBucket(tx)
if err != nil {
return nil, err
}
// Buckets are indexed by name and so the bucket index must be pruned when name is modified.
if err := idx.Delete(key); err != nil {
return nil, err
}
b.Name = *upd.Name
}
b.UpdatedAt = s.Now()
if err := s.appendBucketEventToLog(ctx, tx, b.ID, bucketUpdatedEvent); err != nil {
return nil, err
}
v, err := json.Marshal(b)
if err != nil {
return nil, influxdb.ErrInternalBucketServiceError(influxdb.OpUpdateBucket, err)
}
if err := s.putBucket(ctx, tx, b, v); err != nil {
return nil, err
}
uid, _ := icontext.GetUserID(ctx)
if err := s.audit.Log(resource.Change{
Type: resource.Update,
ResourceID: b.ID,
ResourceType: influxdb.BucketsResourceType,
OrganizationID: b.OrgID,
UserID: uid,
ResourceBody: v,
Time: time.Now(),
}); err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
return b, nil
}
// DeleteBucket deletes a bucket and prunes it from the index.
func (s *Service) DeleteBucket(ctx context.Context, id influxdb.ID) error {
return s.kv.Update(ctx, func(tx Tx) error {
bucket, err := s.findBucketByID(ctx, tx, id)
if err != nil && !IsNotFound(err) {
return err
}
if !IsNotFound(err) && bucket.Type == influxdb.BucketTypeSystem {
return &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "system buckets cannot be deleted",
}
}
if err := s.deleteBucket(ctx, tx, id); err != nil {
return err
}
uid, _ := icontext.GetUserID(ctx)
return s.audit.Log(resource.Change{
Type: resource.Delete,
ResourceID: id,
ResourceType: influxdb.BucketsResourceType,
OrganizationID: bucket.OrgID,
UserID: uid,
Time: time.Now(),
})
})
}
func (s *Service) deleteBucket(ctx context.Context, tx Tx, id influxdb.ID) error {
b, pe := s.findBucketByID(ctx, tx, id)
if pe != nil {
return pe
}
key, pe := bucketIndexKey(b)
if pe != nil {
return pe
}
idx, err := s.bucketsIndexBucket(tx)
if err != nil {
return err
}
if err := idx.Delete(key); err != nil {
return &influxdb.Error{
Err: err,
}
}
encodedID, err := id.Encode()
if err != nil {
return &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
}
}
bkt, err := s.bucketsBucket(tx)
if err != nil {
return err
}
if err := bkt.Delete(encodedID); err != nil {
return &influxdb.Error{
Err: err,
}
}
if err := s.deleteUserResourceMappings(ctx, tx, influxdb.UserResourceMappingFilter{
ResourceID: id,
ResourceType: influxdb.BucketsResourceType,
}); err != nil {
return err
}
return nil
}
const bucketOperationLogKeyPrefix = "bucket"
func encodeBucketOperationLogKey(id influxdb.ID) ([]byte, error) {
buf, err := id.Encode()
if err != nil {
return nil, err
}
return append([]byte(bucketOperationLogKeyPrefix), buf...), nil
}
// GetBucketOperationLog retrieves a buckets operation log.
func (s *Service) GetBucketOperationLog(ctx context.Context, id influxdb.ID, opts influxdb.FindOptions) ([]*influxdb.OperationLogEntry, int, error) {
// TODO(desa): might be worthwhile to allocate a slice of size opts.Limit
log := []*influxdb.OperationLogEntry{}
err := s.kv.View(ctx, func(tx Tx) error {
key, err := encodeBucketOperationLogKey(id)
if err != nil {
return err
}
return s.ForEachLogEntryTx(ctx, tx, key, opts, func(v []byte, t time.Time) error {
e := &influxdb.OperationLogEntry{}
if err := json.Unmarshal(v, e); err != nil {
return err
}
e.Time = t
log = append(log, e)
return nil
})
})
if err != nil && err != ErrKeyValueLogBoundsNotFound {
return nil, 0, err
}
return log, len(log), nil
}
// TODO(desa): what do we want these to be?
const (
bucketCreatedEvent = "Bucket Created"
bucketUpdatedEvent = "Bucket Updated"
)
func (s *Service) appendBucketEventToLog(ctx context.Context, tx Tx, id influxdb.ID, st string) error {
e := &influxdb.OperationLogEntry{
Description: st,
}
// TODO(desa): this is fragile and non explicit since it requires an authorizer to be on context. It should be
// replaced with a higher level transaction so that adding to the log can take place in the http handler
// where the userID will exist explicitly.
a, err := icontext.GetAuthorizer(ctx)
if err == nil {
// Add the user to the log if you can, but don't error if its not there.
e.UserID = a.GetUserID()
}
v, err := json.Marshal(e)
if err != nil {
return err
}
k, err := encodeBucketOperationLogKey(id)
if err != nil {
return err
}
return s.AddLogEntryTx(ctx, tx, k, v, s.Now())
}
// UnexpectedBucketError is used when the error comes from an internal system.
func UnexpectedBucketError(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EInternal,
Msg: fmt.Sprintf("unexpected error retrieving bucket's bucket; Err %v", err),
Op: "kv/bucketBucket",
}
}
// UnexpectedBucketIndexError is used when the error comes from an internal system.
func UnexpectedBucketIndexError(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EInternal,
Msg: fmt.Sprintf("unexpected error retrieving bucket index; Err: %v", err),
Op: "kv/bucketIndex",
}
}
// BucketAlreadyExistsError is used when creating a bucket with a name
// that already exists within an organization.
func BucketAlreadyExistsError(b *influxdb.Bucket) error {
return &influxdb.Error{
Code: influxdb.EConflict,
Op: "kv/bucket",
Msg: fmt.Sprintf("bucket with name %s already exists", b.Name),
}
}

View File

@ -1,69 +0,0 @@
package kv_test
import (
"context"
"testing"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/kv"
influxdbtesting "github.com/influxdata/influxdb/v2/testing"
"go.uber.org/zap/zaptest"
)
func TestBoltBucketService(t *testing.T) {
influxdbtesting.BucketService(initBoltBucketService, t)
}
func initBoltBucketService(f influxdbtesting.BucketFields, t *testing.T) (influxdb.BucketService, string, func()) {
s, closeBolt, err := NewTestBoltStore(t)
if err != nil {
t.Fatalf("failed to create new kv store: %v", err)
}
svc, op, closeSvc := initBucketService(s, f, t)
return svc, op, func() {
closeSvc()
closeBolt()
}
}
func initBucketService(s kv.SchemaStore, f influxdbtesting.BucketFields, t *testing.T) (influxdb.BucketService, string, func()) {
ctx := context.Background()
svc := kv.NewService(zaptest.NewLogger(t), s)
svc.OrgIDs = f.OrgIDs
svc.BucketIDs = f.BucketIDs
svc.IDGenerator = f.IDGenerator
svc.TimeGenerator = f.TimeGenerator
if f.TimeGenerator == nil {
svc.TimeGenerator = influxdb.RealTimeGenerator{}
}
for _, o := range f.Organizations {
// new tenant services do this properly
o.ID = svc.OrgIDs.ID()
if err := svc.PutOrganization(ctx, o); err != nil {
t.Fatalf("failed to populate organizations: %s", err)
}
}
for _, b := range f.Buckets {
// new tenant services do this properly
b.ID = svc.BucketIDs.ID()
if err := svc.PutBucket(ctx, b); err != nil {
t.Fatalf("failed to populate buckets: %s", err)
}
}
return svc, kv.OpPrefix, func() {
for _, o := range f.Organizations {
if err := svc.DeleteOrganization(ctx, o.ID); err != nil {
t.Logf("failed to remove organization: %v", err)
}
}
for _, b := range f.Buckets {
if err := svc.DeleteBucket(ctx, b.ID); err != nil {
t.Logf("failed to remove bucket: %v", err)
}
}
}
}

View File

@ -1,586 +0,0 @@
package kv
import (
"context"
"encoding/json"
"path"
"github.com/influxdata/influxdb/v2"
)
const (
documentContentBucket = "/documents/content"
documentMetaBucket = "/documents/meta"
)
// DocumentStore implements influxdb.DocumentStore.
type DocumentStore struct {
service *Service
namespace string
}
// CreateDocumentStore creates an instance of a document store by instantiating the buckets for the store.
func (s *Service) CreateDocumentStore(ctx context.Context, ns string) (influxdb.DocumentStore, error) {
// TODO(desa): keep track of which namespaces exist.
return s.createDocumentStore(ctx, ns)
}
func (s *Service) createDocumentStore(ctx context.Context, ns string) (influxdb.DocumentStore, error) {
return &DocumentStore{
namespace: ns,
service: s,
}, nil
}
// FindDocumentStore finds the buckets associated with the namespace provided.
func (s *Service) FindDocumentStore(ctx context.Context, ns string) (influxdb.DocumentStore, error) {
var ds influxdb.DocumentStore
err := s.kv.View(ctx, func(tx Tx) error {
if _, err := tx.Bucket([]byte(path.Join(ns, documentContentBucket))); err != nil {
return err
}
if _, err := tx.Bucket([]byte(path.Join(ns, documentMetaBucket))); err != nil {
return err
}
ds = &DocumentStore{
namespace: ns,
service: s,
}
return nil
})
if err != nil {
return nil, err
}
return ds, nil
}
// CreateDocument creates an instance of a document and sets the ID. After which it applies each of the options provided.
func (s *DocumentStore) CreateDocument(ctx context.Context, d *influxdb.Document) error {
return s.service.kv.Update(ctx, func(tx Tx) error {
// Check that labels exist before creating the document.
// Mapping creation would check for that, but cannot anticipate that until we
// have a valid document ID.
for _, l := range d.Labels {
if _, err := s.service.findLabelByID(ctx, tx, l.ID); err != nil {
return err
}
}
err := s.service.createDocument(ctx, tx, s.namespace, d)
if err != nil {
return err
}
for orgID := range d.Organizations {
if err := s.service.addDocumentOwner(ctx, tx, orgID, d.ID); err != nil {
return err
}
}
for _, l := range d.Labels {
if err := s.addDocumentLabelMapping(ctx, tx, d.ID, l.ID); err != nil {
return err
}
}
return nil
})
}
func (s *Service) createDocument(ctx context.Context, tx Tx, ns string, d *influxdb.Document) error {
d.ID = s.IDGenerator.ID()
d.Meta.CreatedAt = s.Now()
d.Meta.UpdatedAt = s.Now()
return s.putDocument(ctx, tx, ns, d)
}
// Affo: the only resource in which a org owns something and not a precise user.
func (s *Service) addDocumentOwner(ctx context.Context, tx Tx, orgID influxdb.ID, docID influxdb.ID) error {
// In this case UserID refers to an organization rather than a user.
m := &influxdb.UserResourceMapping{
UserID: orgID,
UserType: influxdb.Owner,
MappingType: influxdb.OrgMappingType,
ResourceType: influxdb.DocumentsResourceType,
ResourceID: docID,
}
return s.createUserResourceMapping(ctx, tx, m)
}
func (s *DocumentStore) addDocumentLabelMapping(ctx context.Context, tx Tx, docID, labelID influxdb.ID) error {
m := &influxdb.LabelMapping{
LabelID: labelID,
ResourceType: influxdb.DocumentsResourceType,
ResourceID: docID,
}
if err := s.service.createLabelMapping(ctx, tx, m); err != nil {
return err
}
return nil
}
func (s *Service) putDocument(ctx context.Context, tx Tx, ns string, d *influxdb.Document) error {
if err := s.putDocumentMeta(ctx, tx, ns, d.ID, d.Meta); err != nil {
return err
}
if err := s.putDocumentContent(ctx, tx, ns, d.ID, d.Content); err != nil {
return err
}
// TODO(desa): index document meta
return nil
}
func (s *Service) putAtID(ctx context.Context, tx Tx, bucket string, id influxdb.ID, i interface{}) error {
v, err := json.Marshal(i)
if err != nil {
return err
}
k, err := id.Encode()
if err != nil {
return err
}
b, err := tx.Bucket([]byte(bucket))
if err != nil {
return err
}
if err := b.Put(k, v); err != nil {
return err
}
return nil
}
func (s *Service) putDocumentContent(ctx context.Context, tx Tx, ns string, id influxdb.ID, data interface{}) error {
return s.putAtID(ctx, tx, path.Join(ns, documentContentBucket), id, data)
}
func (s *Service) putDocumentMeta(ctx context.Context, tx Tx, ns string, id influxdb.ID, m influxdb.DocumentMeta) error {
return s.putAtID(ctx, tx, path.Join(ns, documentMetaBucket), id, m)
}
func (s *DocumentStore) PutDocument(ctx context.Context, d *influxdb.Document) error {
return s.service.kv.Update(ctx, func(tx Tx) error {
return s.service.putDocument(ctx, tx, s.namespace, d)
})
}
func (s *Service) findDocumentsByID(ctx context.Context, tx Tx, ns string, ids ...influxdb.ID) ([]*influxdb.Document, error) {
ds := make([]*influxdb.Document, 0, len(ids))
for _, id := range ids {
d, err := s.findDocumentByID(ctx, tx, ns, id)
if err != nil {
return nil, err
}
ds = append(ds, d)
}
return ds, nil
}
func (s *Service) findDocumentByID(ctx context.Context, tx Tx, ns string, id influxdb.ID) (*influxdb.Document, error) {
m, err := s.findDocumentMetaByID(ctx, tx, ns, id)
if err != nil {
return nil, err
}
return &influxdb.Document{
ID: id,
Meta: *m,
Labels: []*influxdb.Label{},
}, nil
}
func (s *Service) findByID(ctx context.Context, tx Tx, bucket string, id influxdb.ID, i interface{}) error {
b, err := tx.Bucket([]byte(bucket))
if err != nil {
return err
}
k, err := id.Encode()
if err != nil {
return err
}
v, err := b.Get(k)
if err != nil {
return err
}
if err := json.Unmarshal(v, i); err != nil {
return err
}
return nil
}
func (s *Service) findDocumentMetaByID(ctx context.Context, tx Tx, ns string, id influxdb.ID) (*influxdb.DocumentMeta, error) {
m := &influxdb.DocumentMeta{}
if err := s.findByID(ctx, tx, path.Join(ns, documentMetaBucket), id, m); err != nil {
return nil, err
}
return m, nil
}
func (s *Service) findDocumentContentByID(ctx context.Context, tx Tx, ns string, id influxdb.ID) (interface{}, error) {
var data interface{}
if err := s.findByID(ctx, tx, path.Join(ns, documentContentBucket), id, &data); err != nil {
return nil, err
}
return data, nil
}
// FindDocument retrieves the specified document with all its content and labels.
func (s *DocumentStore) FindDocument(ctx context.Context, id influxdb.ID) (*influxdb.Document, error) {
var d *influxdb.Document
err := s.service.kv.View(ctx, func(tx Tx) error {
m, err := s.service.findDocumentMetaByID(ctx, tx, s.namespace, id)
if err != nil {
return err
}
c, err := s.service.findDocumentContentByID(ctx, tx, s.namespace, id)
if err != nil {
return err
}
d = &influxdb.Document{
ID: id,
Meta: *m,
Content: c,
}
if err := s.decorateDocumentWithLabels(ctx, tx, d); err != nil {
return err
}
if err := s.decorateDocumentWithOrgs(ctx, tx, d); err != nil {
return err
}
return nil
})
if IsNotFound(err) {
return nil, &influxdb.Error{
Code: influxdb.ENotFound,
Msg: influxdb.ErrDocumentNotFound,
}
}
if err != nil {
return nil, err
}
return d, nil
}
// FindDocuments retrieves all documents returned by the document find options.
func (s *DocumentStore) FindDocuments(ctx context.Context, opts ...influxdb.DocumentFindOptions) ([]*influxdb.Document, error) {
var ds []*influxdb.Document
err := s.service.kv.View(ctx, func(tx Tx) error {
if len(opts) == 0 {
// TODO(desa): might be a better way to do get all.
if err := s.service.findDocuments(ctx, tx, s.namespace, &ds); err != nil {
return err
}
return nil
}
idx := &DocumentIndex{
service: s.service,
namespace: s.namespace,
tx: tx,
ctx: ctx,
}
dd := &DocumentDecorator{}
var ids []influxdb.ID
for _, opt := range opts {
is, err := opt(idx, dd)
if err != nil {
return err
}
ids = append(ids, is...)
}
docs, err := s.service.findDocumentsByID(ctx, tx, s.namespace, ids...)
if err != nil {
return err
}
if dd.data {
for _, doc := range docs {
d, err := s.service.findDocumentContentByID(ctx, tx, s.namespace, doc.ID)
if err != nil {
return err
}
doc.Content = d
}
}
if dd.labels {
for _, doc := range docs {
if err := s.decorateDocumentWithLabels(ctx, tx, doc); err != nil {
return err
}
}
}
if dd.orgs {
for _, doc := range docs {
if err := s.decorateDocumentWithOrgs(ctx, tx, doc); err != nil {
return err
}
}
}
ds = append(ds, docs...)
return nil
})
if IsNotFound(err) {
return nil, &influxdb.Error{
Code: influxdb.ENotFound,
Msg: influxdb.ErrDocumentNotFound,
}
}
if err != nil {
return nil, err
}
return ds, nil
}
func (s *Service) findDocuments(ctx context.Context, tx Tx, ns string, ds *[]*influxdb.Document) error {
metab, err := tx.Bucket([]byte(path.Join(ns, documentMetaBucket)))
if err != nil {
return err
}
cur, err := metab.ForwardCursor(nil)
if err != nil {
return err
}
for k, v := cur.Next(); len(k) != 0; k, v = cur.Next() {
d := &influxdb.Document{}
if err := d.ID.Decode(k); err != nil {
return err
}
if err := json.Unmarshal(v, &d.Meta); err != nil {
return err
}
*ds = append(*ds, d)
}
return nil
}
func (s *Service) getDocumentsAccessors(ctx context.Context, tx Tx, docID influxdb.ID) (map[influxdb.ID]influxdb.UserType, error) {
f := influxdb.UserResourceMappingFilter{
ResourceType: influxdb.DocumentsResourceType,
ResourceID: docID,
}
ms, err := s.findUserResourceMappings(ctx, tx, f)
if err != nil {
return nil, err
}
// The only URM created when creating a document is
// from an org to the document (not a user).
orgs := make(map[influxdb.ID]influxdb.UserType, len(ms))
for _, m := range ms {
if m.MappingType == influxdb.OrgMappingType {
orgs[m.UserID] = m.UserType
}
}
return orgs, nil
}
// DeleteDocument removes the specified document.
func (s *DocumentStore) DeleteDocument(ctx context.Context, id influxdb.ID) error {
return s.service.kv.Update(ctx, func(tx Tx) error {
if err := s.service.deleteDocument(ctx, tx, s.namespace, id); err != nil {
if IsNotFound(err) {
return &influxdb.Error{
Code: influxdb.ENotFound,
Msg: influxdb.ErrDocumentNotFound,
}
}
return err
}
return nil
})
}
// DeleteDocuments removes all documents returned by the options.
func (s *DocumentStore) DeleteDocuments(ctx context.Context, opts ...influxdb.DocumentFindOptions) error {
return s.service.kv.Update(ctx, func(tx Tx) error {
idx := &DocumentIndex{
service: s.service,
namespace: s.namespace,
tx: tx,
ctx: ctx,
writable: true,
}
dd := &DocumentDecorator{writable: true}
var ids []influxdb.ID
for _, opt := range opts {
dids, err := opt(idx, dd)
if err != nil {
return err
}
ids = append(ids, dids...)
}
for _, id := range ids {
if err := s.service.deleteDocument(ctx, tx, s.namespace, id); err != nil {
if IsNotFound(err) {
return &influxdb.Error{
Code: influxdb.ENotFound,
Msg: influxdb.ErrDocumentNotFound,
}
}
return err
}
}
return nil
})
}
func (s *Service) removeDocumentAccess(ctx context.Context, tx Tx, orgID, docID influxdb.ID) error {
filter := influxdb.UserResourceMappingFilter{
ResourceID: docID,
UserID: orgID,
}
if err := s.deleteUserResourceMapping(ctx, tx, filter); err != nil {
return err
}
return nil
}
func (s *Service) deleteDocument(ctx context.Context, tx Tx, ns string, id influxdb.ID) error {
// Delete mappings.
orgs, err := s.getDocumentsAccessors(ctx, tx, id)
if err != nil {
return err
}
for orgID := range orgs {
if err := s.removeDocumentAccess(ctx, tx, orgID, id); err != nil {
return err
}
}
// Delete document.
if _, err := s.findDocumentMetaByID(ctx, tx, ns, id); err != nil {
return err
}
if err := s.deleteDocumentMeta(ctx, tx, ns, id); err != nil {
return err
}
if err := s.deleteDocumentContent(ctx, tx, ns, id); err != nil {
return err
}
// TODO(desa): deindex document meta
return nil
}
func (s *Service) deleteAtID(ctx context.Context, tx Tx, bucket string, id influxdb.ID) error {
k, err := id.Encode()
if err != nil {
return err
}
b, err := tx.Bucket([]byte(bucket))
if err != nil {
return err
}
if err := b.Delete(k); err != nil {
return err
}
return nil
}
func (s *Service) deleteDocumentContent(ctx context.Context, tx Tx, ns string, id influxdb.ID) error {
return s.deleteAtID(ctx, tx, path.Join(ns, documentContentBucket), id)
}
func (s *Service) deleteDocumentMeta(ctx context.Context, tx Tx, ns string, id influxdb.ID) error {
return s.deleteAtID(ctx, tx, path.Join(ns, documentMetaBucket), id)
}
// UpdateDocument updates the document.
func (s *DocumentStore) UpdateDocument(ctx context.Context, d *influxdb.Document) error {
return s.service.kv.Update(ctx, func(tx Tx) error {
if err := s.service.updateDocument(ctx, tx, s.namespace, d); err != nil {
return err
}
if err := s.decorateDocumentWithLabels(ctx, tx, d); err != nil {
return err
}
return nil
})
}
func (s *Service) updateDocument(ctx context.Context, tx Tx, ns string, d *influxdb.Document) error {
// TODO(desa): deindex meta
d.Meta.UpdatedAt = s.Now()
if err := s.putDocument(ctx, tx, ns, d); err != nil {
return err
}
return nil
}
func (s *DocumentStore) decorateDocumentWithLabels(ctx context.Context, tx Tx, d *influxdb.Document) error {
var ls []*influxdb.Label
f := influxdb.LabelMappingFilter{
ResourceID: d.ID,
ResourceType: influxdb.DocumentsResourceType,
}
if err := s.service.findResourceLabels(ctx, tx, f, &ls); err != nil {
return err
}
d.Labels = append(d.Labels, ls...)
return nil
}
func (s *DocumentStore) decorateDocumentWithOrgs(ctx context.Context, tx Tx, d *influxdb.Document) error {
// If the orgs are already there, then this is a nop.
if len(d.Organizations) > 0 {
return nil
}
orgs, err := s.service.getDocumentsAccessors(ctx, tx, d.ID)
if err != nil {
return err
}
d.Organizations = orgs
return nil
}

View File

@ -1,216 +0,0 @@
package kv
import (
"context"
"fmt"
"github.com/influxdata/influxdb/v2"
)
// DocumentIndex implements influxdb.DocumentIndex. It is used to access labels/owners of documents.
type DocumentIndex struct {
service *Service
namespace string
ctx context.Context
tx Tx
writable bool
}
// FindLabelByID retrieves a label by id.
func (i *DocumentIndex) FindLabelByID(id influxdb.ID) error {
_, err := i.service.findLabelByID(i.ctx, i.tx, id)
return err
}
// UsersOrgs retrieves a list of all orgs that a user is an accessor of.
func (i *DocumentIndex) UsersOrgs(userID influxdb.ID) ([]influxdb.ID, error) {
f := influxdb.UserResourceMappingFilter{
UserID: userID,
ResourceType: influxdb.OrgsResourceType,
}
ms, err := i.service.findUserResourceMappings(i.ctx, i.tx, f)
if err != nil {
return nil, err
}
ids := make([]influxdb.ID, 0, len(ms))
for _, m := range ms {
ids = append(ids, m.ResourceID)
}
return ids, nil
}
// IsOrgAccessor checks to see if the user is an accessor of the org provided. If the operation
// is writable it ensures that the user is owner.
func (i *DocumentIndex) IsOrgAccessor(userID influxdb.ID, orgID influxdb.ID) error {
f := influxdb.UserResourceMappingFilter{
UserID: userID,
ResourceType: influxdb.OrgsResourceType,
ResourceID: orgID,
}
if i.writable {
f.UserType = influxdb.Owner
}
ms, err := i.service.findUserResourceMappings(i.ctx, i.tx, f)
if err != nil {
return err
}
for _, m := range ms {
switch m.UserType {
case influxdb.Owner, influxdb.Member:
return nil
default:
continue
}
}
return &influxdb.Error{
Code: influxdb.EUnauthorized,
Msg: "user is not org member",
}
}
func (i *DocumentIndex) ownerExists(ownerType string, ownerID influxdb.ID) error {
switch ownerType {
case "org":
if _, err := i.service.findOrganizationByID(i.ctx, i.tx, ownerID); err != nil {
return err
}
case "user":
if _, err := i.service.findUserByID(i.ctx, i.tx, ownerID); err != nil {
return err
}
default:
return &influxdb.Error{
Code: influxdb.EInternal,
Msg: fmt.Sprintf("unknown owner type %q", ownerType),
}
}
return nil
}
// FindOrganizationByName retrieves the organization ID of the org provided.
func (i *DocumentIndex) FindOrganizationByName(org string) (influxdb.ID, error) {
o, err := i.service.findOrganizationByName(i.ctx, i.tx, org)
if err != nil {
return influxdb.InvalidID(), err
}
return o.ID, nil
}
// FindOrganizationByID checks if the org existence by the org id provided.
func (i *DocumentIndex) FindOrganizationByID(id influxdb.ID) error {
_, err := i.service.findOrganizationByID(i.ctx, i.tx, id)
if err != nil {
return err
}
return nil
}
// GetDocumentsAccessors retrieves the list of accessors of a document.
func (i *DocumentIndex) GetDocumentsAccessors(docID influxdb.ID) ([]influxdb.ID, error) {
f := influxdb.UserResourceMappingFilter{
ResourceType: influxdb.DocumentsResourceType,
ResourceID: docID,
}
if i.writable {
f.UserType = influxdb.Owner
}
ms, err := i.service.findUserResourceMappings(i.ctx, i.tx, f)
if err != nil {
return nil, err
}
ids := make([]influxdb.ID, 0, len(ms))
for _, m := range ms {
if m.MappingType == influxdb.UserMappingType {
continue
}
// TODO(desa): this is really an orgID, eventually we should support users and org as owners of documents
ids = append(ids, m.UserID)
}
return ids, nil
}
// GetAccessorsDocuments retrieves the list of documents a user is allowed to access.
func (i *DocumentIndex) GetAccessorsDocuments(ownerType string, ownerID influxdb.ID) ([]influxdb.ID, error) {
if err := i.ownerExists(ownerType, ownerID); err != nil {
return nil, err
}
f := influxdb.UserResourceMappingFilter{
UserID: ownerID,
ResourceType: influxdb.DocumentsResourceType,
}
if i.writable {
f.UserType = influxdb.Owner
}
ms, err := i.service.findUserResourceMappings(i.ctx, i.tx, f)
if err != nil {
return nil, err
}
ids := make([]influxdb.ID, 0, len(ms))
for _, m := range ms {
ids = append(ids, m.ResourceID)
}
return ids, nil
}
// DocumentDecorator is used to communication the decoration of documents to the
// document store.
type DocumentDecorator struct {
data bool
labels bool
orgs bool
writable bool
}
// IncludeContent signals that the document should include its content when returned.
func (d *DocumentDecorator) IncludeContent() error {
if d.writable {
return &influxdb.Error{
Code: influxdb.EInternal,
Msg: "cannot include data in document",
}
}
d.data = true
return nil
}
// IncludeLabels signals that the document should include its labels when returned.
func (d *DocumentDecorator) IncludeLabels() error {
if d.writable {
return &influxdb.Error{
Code: influxdb.EInternal,
Msg: "cannot include labels in document",
}
}
d.labels = true
return nil
}
// IncludeOwner signals that the document should include its owner.
func (d *DocumentDecorator) IncludeOrganizations() error {
if d.writable {
return &influxdb.Error{
Code: influxdb.EInternal,
Msg: "cannot include labels in document",
}
}
d.orgs = true
return nil
}

View File

@ -1,17 +0,0 @@
package kv_test
import (
"testing"
influxdbtesting "github.com/influxdata/influxdb/v2/testing"
)
func TestBoltDocumentStore(t *testing.T) {
boltStore, closeBolt, err := NewTestBoltStore(t)
if err != nil {
t.Fatalf("failed to create new bolt kv store: %v", err)
}
defer closeBolt()
t.Run("bolt", influxdbtesting.NewDocumentIntegrationTest(boltStore))
}

23
kv/errors.go Normal file
View File

@ -0,0 +1,23 @@
package kv
import (
"fmt"
"github.com/influxdata/influxdb/v2"
)
// UnexpectedIndexError is used when the error comes from an internal system.
func UnexpectedIndexError(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EInternal,
Msg: fmt.Sprintf("unexpected error retrieving index; Err: %v", err),
Op: "kv/index",
}
}
// NotUniqueError is used when attempting to create a resource that already
// exists.
var NotUniqueError = &influxdb.Error{
Code: influxdb.EConflict,
Msg: "name already exists",
}

View File

@ -22,33 +22,33 @@ func (m InitialMigration) Up(ctx context.Context, store SchemaStore) error {
// defined in NewInitialMigration.
for _, bucket := range [][]byte{
authBucket,
authIndex,
bucketBucket,
bucketIndex,
[]byte("authorizationsv1"),
[]byte("authorizationindexv1"),
[]byte("bucketsv1"),
[]byte("bucketindexv1"),
[]byte("dashboardsv2"),
[]byte("orgsdashboardsv1"),
[]byte("dashboardcellviewsv1"),
kvlogBucket,
kvlogIndex,
labelBucket,
labelMappingBucket,
labelIndex,
onboardingBucket,
organizationBucket,
organizationIndex,
[]byte("labelsv1"),
[]byte("labelmappingsv1"),
[]byte("labelindexv1"),
[]byte("onboardingv1"),
[]byte("organizationsv1"),
[]byte("organizationindexv1"),
taskBucket,
taskRunBucket,
taskIndexBucket,
userpasswordBucket,
[]byte("userspasswordv1"),
scrapersBucket,
secretBucket,
[]byte("secretsv1"),
[]byte("telegrafv1"),
[]byte("telegrafPluginsv1"),
urmBucket,
[]byte("userresourcemappingsv1"),
[]byte("notificationRulev1"),
userBucket,
userIndex,
[]byte("usersv1"),
[]byte("userindexv1"),
sourceBucket,
// these are the "document" (aka templates) key prefixes
[]byte("templates/documents/content"),

View File

@ -6,6 +6,7 @@ import (
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/kv"
"github.com/influxdata/influxdb/v2/tenant"
influxdbtesting "github.com/influxdata/influxdb/v2/testing"
"go.uber.org/zap/zaptest"
)
@ -29,7 +30,7 @@ func initBoltKeyValueLog(f influxdbtesting.KeyValueLogFields, t *testing.T) (inf
func initKeyValueLog(s kv.SchemaStore, f influxdbtesting.KeyValueLogFields, t *testing.T) (influxdb.KeyValueLog, func()) {
ctx := context.Background()
svc := kv.NewService(zaptest.NewLogger(t), s)
svc := kv.NewService(zaptest.NewLogger(t), s, tenant.NewService(tenant.NewStore(s)))
for _, e := range f.LogEntries {
if err := svc.AddLogEntry(ctx, e.Key, e.Value, e.Time); err != nil {

View File

@ -1,659 +0,0 @@
package kv
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/kit/tracing"
)
var (
labelBucket = []byte("labelsv1")
labelMappingBucket = []byte("labelmappingsv1")
labelIndex = []byte("labelindexv1")
)
// FindLabelByID finds a label by its ID
func (s *Service) FindLabelByID(ctx context.Context, id influxdb.ID) (*influxdb.Label, error) {
var l *influxdb.Label
err := s.kv.View(ctx, func(tx Tx) error {
label, pe := s.findLabelByID(ctx, tx, id)
if pe != nil {
return pe
}
l = label
return nil
})
if err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
return l, nil
}
func (s *Service) findLabelByID(ctx context.Context, tx Tx, id influxdb.ID) (*influxdb.Label, error) {
encodedID, err := id.Encode()
if err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
b, err := tx.Bucket(labelBucket)
if err != nil {
return nil, err
}
v, err := b.Get(encodedID)
if IsNotFound(err) {
return nil, &influxdb.Error{
Code: influxdb.ENotFound,
Msg: influxdb.ErrLabelNotFound,
}
}
if err != nil {
return nil, err
}
var l influxdb.Label
if err := json.Unmarshal(v, &l); err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
return &l, nil
}
func filterLabelsFn(filter influxdb.LabelFilter) func(l *influxdb.Label) bool {
return func(label *influxdb.Label) bool {
return (filter.Name == "" || (strings.EqualFold(filter.Name, label.Name))) &&
((filter.OrgID == nil) || (filter.OrgID != nil && *filter.OrgID == label.OrgID))
}
}
// FindLabels returns a list of labels that match a filter.
func (s *Service) FindLabels(ctx context.Context, filter influxdb.LabelFilter, opt ...influxdb.FindOptions) ([]*influxdb.Label, error) {
ls := []*influxdb.Label{}
err := s.kv.View(ctx, func(tx Tx) error {
labels, err := s.findLabels(ctx, tx, filter)
if err != nil {
return err
}
ls = labels
return nil
})
if err != nil {
return nil, err
}
return ls, nil
}
func (s *Service) findLabels(ctx context.Context, tx Tx, filter influxdb.LabelFilter) ([]*influxdb.Label, error) {
ls := []*influxdb.Label{}
filterFn := filterLabelsFn(filter)
err := s.forEachLabel(ctx, tx, func(l *influxdb.Label) bool {
if filterFn(l) {
ls = append(ls, l)
}
return true
})
if err != nil {
return nil, err
}
return ls, nil
}
func decodeLabelMappingKey(key []byte) (resourceID influxdb.ID, labelID influxdb.ID, err error) {
if len(key) != 2*influxdb.IDLength {
return 0, 0, &influxdb.Error{Code: influxdb.EInvalid, Msg: "malformed label mapping key (please report this error)"}
}
if err := (&resourceID).Decode(key[:influxdb.IDLength]); err != nil {
return 0, 0, &influxdb.Error{Code: influxdb.EInvalid, Msg: "bad resource id", Err: influxdb.ErrInvalidID}
}
if err := (&labelID).Decode(key[influxdb.IDLength:]); err != nil {
return 0, 0, &influxdb.Error{Code: influxdb.EInvalid, Msg: "bad label id", Err: influxdb.ErrInvalidID}
}
return resourceID, labelID, nil
}
func (s *Service) findResourceLabels(ctx context.Context, tx Tx, filter influxdb.LabelMappingFilter, ls *[]*influxdb.Label) error {
if !filter.ResourceID.Valid() {
return &influxdb.Error{Code: influxdb.EInvalid, Msg: "filter requires a valid resource id", Err: influxdb.ErrInvalidID}
}
idx, err := tx.Bucket(labelMappingBucket)
if err != nil {
return err
}
prefix, err := filter.ResourceID.Encode()
if err != nil {
return err
}
cur, err := idx.ForwardCursor(prefix, WithCursorPrefix(prefix))
if err != nil {
return err
}
for k, _ := cur.Next(); k != nil; k, _ = cur.Next() {
_, id, err := decodeLabelMappingKey(k)
if err != nil {
return err
}
l, err := s.findLabelByID(ctx, tx, id)
if l == nil && err != nil {
// TODO(jm): return error instead of continuing once orphaned mappings are fixed
// (see https://github.com/influxdata/influxdb/issues/11278)
continue
}
*ls = append(*ls, l)
}
return nil
}
func (s *Service) FindResourceLabels(ctx context.Context, filter influxdb.LabelMappingFilter) ([]*influxdb.Label, error) {
ls := []*influxdb.Label{}
if err := s.kv.View(ctx, func(tx Tx) error {
return s.findResourceLabels(ctx, tx, filter, &ls)
}); err != nil {
return nil, err
}
return ls, nil
}
// CreateLabelMapping creates a new mapping between a resource and a label.
func (s *Service) CreateLabelMapping(ctx context.Context, m *influxdb.LabelMapping) error {
return s.kv.Update(ctx, func(tx Tx) error {
return s.createLabelMapping(ctx, tx, m)
})
}
// createLabelMapping creates a new mapping between a resource and a label.
func (s *Service) createLabelMapping(ctx context.Context, tx Tx, m *influxdb.LabelMapping) error {
if _, err := s.findLabelByID(ctx, tx, m.LabelID); err != nil {
return err
}
ls := []*influxdb.Label{}
err := s.findResourceLabels(ctx, tx, influxdb.LabelMappingFilter{ResourceID: m.ResourceID, ResourceType: m.ResourceType}, &ls)
if err != nil {
return err
}
for i := 0; i < len(ls); i++ {
if ls[i].ID == m.LabelID {
return influxdb.ErrLabelExistsOnResource
}
}
if err := s.putLabelMapping(ctx, tx, m); err != nil {
return err
}
return nil
}
// DeleteLabelMapping deletes a label mapping.
func (s *Service) DeleteLabelMapping(ctx context.Context, m *influxdb.LabelMapping) error {
err := s.kv.Update(ctx, func(tx Tx) error {
return s.deleteLabelMapping(ctx, tx, m)
})
if err != nil {
return &influxdb.Error{
Err: err,
}
}
return nil
}
func (s *Service) deleteLabelMapping(ctx context.Context, tx Tx, m *influxdb.LabelMapping) error {
key, err := labelMappingKey(m)
if err != nil {
return &influxdb.Error{
Err: err,
}
}
idx, err := tx.Bucket(labelMappingBucket)
if err != nil {
return err
}
if err := idx.Delete(key); err != nil {
return &influxdb.Error{
Err: err,
}
}
return nil
}
// CreateLabel creates a new label.
func (s *Service) CreateLabel(ctx context.Context, l *influxdb.Label) error {
err := s.kv.Update(ctx, func(tx Tx) error {
if err := l.Validate(); err != nil {
return &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
}
}
l.Name = strings.TrimSpace(l.Name)
if err := s.uniqueLabelName(ctx, tx, l); err != nil {
return err
}
l.ID = s.IDGenerator.ID()
if err := s.putLabel(ctx, tx, l); err != nil {
return err
}
if err := s.createUserResourceMappingForOrg(ctx, tx, l.OrgID, l.ID, influxdb.LabelsResourceType); err != nil {
return err
}
return nil
})
if err != nil {
return &influxdb.Error{
Err: err,
}
}
return nil
}
// PutLabel creates a label from the provided struct, without generating a new ID.
func (s *Service) PutLabel(ctx context.Context, l *influxdb.Label) error {
return s.kv.Update(ctx, func(tx Tx) error {
var err error
pe := s.putLabel(ctx, tx, l)
if pe != nil {
err = pe
}
return err
})
}
// CreateUserResourceMappingForOrg is a public function that calls createUserResourceMappingForOrg used only for the label service
// it can be removed when URMs are removed from the label service
func (s *Service) CreateUserResourceMappingForOrg(ctx context.Context, tx Tx, orgID influxdb.ID, resID influxdb.ID, resType influxdb.ResourceType) error {
err := s.createUserResourceMappingForOrg(ctx, tx, orgID, resID, resType)
return err
}
func (s *Service) createUserResourceMappingForOrg(ctx context.Context, tx Tx, orgID influxdb.ID, resID influxdb.ID, resType influxdb.ResourceType) error {
span, ctx := tracing.StartSpanFromContext(ctx)
defer span.Finish()
ms, err := s.findUserResourceMappings(ctx, tx, influxdb.UserResourceMappingFilter{
ResourceType: influxdb.OrgsResourceType,
ResourceID: orgID,
})
if err != nil {
return &influxdb.Error{
Err: err,
}
}
for _, m := range ms {
if err := s.createUserResourceMapping(ctx, tx, &influxdb.UserResourceMapping{
ResourceType: resType,
ResourceID: resID,
UserID: m.UserID,
UserType: m.UserType,
}); err != nil {
return &influxdb.Error{
Err: err,
}
}
}
return nil
}
func labelMappingKey(m *influxdb.LabelMapping) ([]byte, error) {
lid, err := m.LabelID.Encode()
if err != nil {
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
}
}
rid, err := m.ResourceID.Encode()
if err != nil {
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
}
}
key := make([]byte, influxdb.IDLength+influxdb.IDLength) // len(rid) + len(lid)
copy(key, rid)
copy(key[len(rid):], lid)
return key, nil
}
func (s *Service) forEachLabel(ctx context.Context, tx Tx, fn func(*influxdb.Label) bool) error {
b, err := tx.Bucket(labelBucket)
if err != nil {
return err
}
cur, err := b.ForwardCursor(nil)
if err != nil {
return err
}
for k, v := cur.Next(); k != nil; k, v = cur.Next() {
l := &influxdb.Label{}
if err := json.Unmarshal(v, l); err != nil {
return err
}
if !fn(l) {
break
}
}
return nil
}
// UpdateLabel updates a label.
func (s *Service) UpdateLabel(ctx context.Context, id influxdb.ID, upd influxdb.LabelUpdate) (*influxdb.Label, error) {
var label *influxdb.Label
err := s.kv.Update(ctx, func(tx Tx) error {
labelResponse, pe := s.updateLabel(ctx, tx, id, upd)
if pe != nil {
return &influxdb.Error{
Err: pe,
}
}
label = labelResponse
return nil
})
return label, err
}
func (s *Service) updateLabel(ctx context.Context, tx Tx, id influxdb.ID, upd influxdb.LabelUpdate) (*influxdb.Label, error) {
label, err := s.findLabelByID(ctx, tx, id)
if err != nil {
return nil, err
}
if len(upd.Properties) > 0 && label.Properties == nil {
label.Properties = make(map[string]string)
}
for k, v := range upd.Properties {
if v == "" {
delete(label.Properties, k)
} else {
label.Properties[k] = v
}
}
if upd.Name != "" {
upd.Name = strings.TrimSpace(upd.Name)
idx, err := tx.Bucket(labelIndex)
if err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
key, err := labelIndexKey(label)
if err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
if err := idx.Delete(key); err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
label.Name = upd.Name
if err := s.uniqueLabelName(ctx, tx, label); err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
}
if err := label.Validate(); err != nil {
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
}
}
if err := s.putLabel(ctx, tx, label); err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
return label, nil
}
// set a label and overwrite any existing label
func (s *Service) putLabel(ctx context.Context, tx Tx, l *influxdb.Label) error {
v, err := json.Marshal(l)
if err != nil {
return &influxdb.Error{
Err: err,
}
}
encodedID, err := l.ID.Encode()
if err != nil {
return &influxdb.Error{
Err: err,
}
}
idx, err := tx.Bucket(labelIndex)
if err != nil {
return &influxdb.Error{
Err: err,
}
}
key, err := labelIndexKey(l)
if err != nil {
return &influxdb.Error{
Err: err,
}
}
if err := idx.Put([]byte(key), encodedID); err != nil {
return &influxdb.Error{
Err: err,
}
}
b, err := tx.Bucket(labelBucket)
if err != nil {
return err
}
if err := b.Put(encodedID, v); err != nil {
return &influxdb.Error{
Err: err,
}
}
return nil
}
// PutLabelMapping writes a label mapping to boltdb
func (s *Service) PutLabelMapping(ctx context.Context, m *influxdb.LabelMapping) error {
return s.kv.Update(ctx, func(tx Tx) error {
var err error
pe := s.putLabelMapping(ctx, tx, m)
if pe != nil {
err = pe
}
return err
})
}
func (s *Service) putLabelMapping(ctx context.Context, tx Tx, m *influxdb.LabelMapping) error {
v, err := json.Marshal(m)
if err != nil {
return &influxdb.Error{
Err: err,
}
}
key, err := labelMappingKey(m)
if err != nil {
return &influxdb.Error{
Err: err,
}
}
idx, err := tx.Bucket(labelMappingBucket)
if err != nil {
return err
}
if err := idx.Put(key, v); err != nil {
return &influxdb.Error{
Err: err,
}
}
return nil
}
// DeleteLabel deletes a label.
func (s *Service) DeleteLabel(ctx context.Context, id influxdb.ID) error {
err := s.kv.Update(ctx, func(tx Tx) error {
return s.deleteLabel(ctx, tx, id)
})
if err != nil {
return &influxdb.Error{
Err: err,
}
}
return nil
}
func (s *Service) deleteLabel(ctx context.Context, tx Tx, id influxdb.ID) error {
label, err := s.findLabelByID(ctx, tx, id)
if err != nil {
return err
}
encodedID, idErr := id.Encode()
if idErr != nil {
return &influxdb.Error{
Err: idErr,
}
}
b, err := tx.Bucket(labelBucket)
if err != nil {
return err
}
if err := b.Delete(encodedID); err != nil {
return &influxdb.Error{
Err: err,
}
}
idx, err := tx.Bucket(labelIndex)
if err != nil {
return &influxdb.Error{
Err: err,
}
}
key, err := labelIndexKey(label)
if err != nil {
return &influxdb.Error{
Err: err,
}
}
if err := idx.Delete(key); err != nil {
return &influxdb.Error{
Err: err,
}
}
if err := s.deleteUserResourceMappings(ctx, tx, influxdb.UserResourceMappingFilter{
ResourceID: id,
ResourceType: influxdb.LabelsResourceType,
}); err != nil {
return err
}
return nil
}
// labelAlreadyExistsError is used when creating a new label with
// a name that has already been used. Label names must be unique.
func labelAlreadyExistsError(lbl *influxdb.Label) error {
return &influxdb.Error{
Code: influxdb.EConflict,
Msg: fmt.Sprintf("label with name %s already exists", lbl.Name),
}
}
func labelIndexKey(l *influxdb.Label) ([]byte, error) {
orgID, err := l.OrgID.Encode()
if err != nil {
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
}
}
k := make([]byte, influxdb.IDLength+len(l.Name))
copy(k, orgID)
copy(k[influxdb.IDLength:], []byte(strings.ToLower((l.Name))))
return k, nil
}
func (s *Service) uniqueLabelName(ctx context.Context, tx Tx, lbl *influxdb.Label) error {
key, err := labelIndexKey(lbl)
if err != nil {
return err
}
// labels are unique by `organization:label_name`
err = s.unique(ctx, tx, labelIndex, key)
if err == NotUniqueError {
return labelAlreadyExistsError(lbl)
}
return err
}

View File

@ -1,54 +0,0 @@
package kv_test
import (
"context"
"testing"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/kv"
influxdbtesting "github.com/influxdata/influxdb/v2/testing"
"go.uber.org/zap/zaptest"
)
func TestBoltLabelService(t *testing.T) {
influxdbtesting.LabelService(initBoltLabelService, t)
}
func initBoltLabelService(f influxdbtesting.LabelFields, t *testing.T) (influxdb.LabelService, string, func()) {
s, closeBolt, err := NewTestBoltStore(t)
if err != nil {
t.Fatalf("failed to create new kv store: %v", err)
}
svc, op, closeSvc := initLabelService(s, f, t)
return svc, op, func() {
closeSvc()
closeBolt()
}
}
func initLabelService(s kv.SchemaStore, f influxdbtesting.LabelFields, t *testing.T) (influxdb.LabelService, string, func()) {
ctx := context.Background()
svc := kv.NewService(zaptest.NewLogger(t), s)
svc.IDGenerator = f.IDGenerator
for _, l := range f.Labels {
if err := svc.PutLabel(ctx, l); err != nil {
t.Fatalf("failed to populate labels: %v", err)
}
}
for _, m := range f.Mappings {
if err := svc.PutLabelMapping(ctx, m); err != nil {
t.Fatalf("failed to populate label mappings: %v", err)
}
}
return svc, kv.OpPrefix, func() {
for _, l := range f.Labels {
if err := svc.DeleteLabel(ctx, l.ID); err != nil {
t.Logf("failed to remove label: %v", err)
}
}
}
}

View File

@ -1,6 +1,9 @@
package all
import "github.com/influxdata/influxdb/v2/kv"
import (
"github.com/influxdata/influxdb/v2/kv"
"github.com/influxdata/influxdb/v2/tenant/index"
)
// Migration0002_AddURMByUserIndex creates the URM by user index and populates missing entries based on the source.
var Migration0002_AddURMByUserIndex = kv.NewIndexMigration(kv.URMByUserIndexMapping, kv.WithIndexMigrationCleanup)
var Migration0002_AddURMByUserIndex = kv.NewIndexMigration(index.URMByUserIndexMapping, kv.WithIndexMigrationCleanup)

View File

@ -7,9 +7,11 @@ import (
"github.com/benbjohnson/clock"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/authorization"
"github.com/influxdata/influxdb/v2/inmem"
"github.com/influxdata/influxdb/v2/kv"
"github.com/influxdata/influxdb/v2/kv/migration"
"github.com/influxdata/influxdb/v2/tenant"
"go.uber.org/zap/zaptest"
)
@ -114,18 +116,27 @@ func newService(t *testing.T, ctx context.Context) *testService {
t.Fatal(err)
}
ts.Service = kv.NewService(logger, ts.Store)
store := tenant.NewStore(ts.Store)
tenantSvc := tenant.NewService(store)
authStore, err := authorization.NewStore(ts.Store)
if err != nil {
t.Fatal(err)
}
authSvc := authorization.NewService(authStore, tenantSvc)
ts.Service = kv.NewService(logger, ts.Store, tenantSvc)
ts.User = influxdb.User{Name: t.Name() + "-user"}
if err := ts.Service.CreateUser(ctx, &ts.User); err != nil {
if err := tenantSvc.CreateUser(ctx, &ts.User); err != nil {
t.Fatal(err)
}
ts.Org = influxdb.Organization{Name: t.Name() + "-org"}
if err := ts.Service.CreateOrganization(ctx, &ts.Org); err != nil {
if err := tenantSvc.CreateOrganization(ctx, &ts.Org); err != nil {
t.Fatal(err)
}
if err := ts.Service.CreateUserResourceMapping(ctx, &influxdb.UserResourceMapping{
if err := tenantSvc.CreateUserResourceMapping(ctx, &influxdb.UserResourceMapping{
ResourceType: influxdb.OrgsResourceType,
ResourceID: ts.Org.ID,
UserID: ts.User.ID,
@ -139,7 +150,7 @@ func newService(t *testing.T, ctx context.Context) *testService {
UserID: ts.User.ID,
Permissions: influxdb.OperPermissions(),
}
if err := ts.Service.CreateAuthorization(context.Background(), &ts.Auth); err != nil {
if err := authSvc.CreateAuthorization(context.Background(), &ts.Auth); err != nil {
t.Fatal(err)
}

View File

@ -1,177 +0,0 @@
package kv
import (
"context"
"errors"
"fmt"
"github.com/influxdata/influxdb/v2"
)
var (
onboardingBucket = []byte("onboardingv1")
onboardingKey = []byte("onboarding_key")
)
var _ influxdb.OnboardingService = (*Service)(nil)
// IsOnboarding means if the initial setup of influxdb has happened.
// true means that the onboarding setup has not yet happened.
// false means that the onboarding has been completed.
func (s *Service) IsOnboarding(ctx context.Context) (bool, error) {
notSetup := true
err := s.kv.View(ctx, func(tx Tx) error {
bucket, err := tx.Bucket(onboardingBucket)
if err != nil {
return err
}
v, err := bucket.Get(onboardingKey)
// If the sentinel onboarding key is not found, then, setup
// has not been performed.
if IsNotFound(err) {
notSetup = true
return nil
}
if err != nil {
return err
}
// If the sentinel key has any bytes whatsoever, then,
if len(v) > 0 {
notSetup = false // this means that it is setup. I hate bools.
}
return nil
})
return notSetup, err
}
// PutOnboardingStatus will update the flag,
// so future onboarding request will be denied.
// true means that onboarding is NOT needed.
// false means that onboarding is needed.
func (s *Service) PutOnboardingStatus(ctx context.Context, hasBeenOnboarded bool) error {
return s.kv.Update(ctx, func(tx Tx) error {
return s.putOnboardingStatus(ctx, tx, hasBeenOnboarded)
})
}
func (s *Service) putOnboardingStatus(ctx context.Context, tx Tx, hasBeenOnboarded bool) error {
if hasBeenOnboarded {
return s.setOnboarded(ctx, tx)
}
return s.setOffboarded(ctx, tx)
}
func (s *Service) setOffboarded(ctx context.Context, tx Tx) error {
bucket, err := tx.Bucket(onboardingBucket)
if err != nil {
// TODO(goller): check err
return err
}
err = bucket.Delete(onboardingKey)
if err != nil {
// TODO(goller): check err
return err
}
return nil
}
func (s *Service) setOnboarded(ctx context.Context, tx Tx) error {
bucket, err := tx.Bucket(onboardingBucket)
if err != nil {
// TODO(goller): check err
return err
}
err = bucket.Put(onboardingKey, []byte{0x1})
if err != nil {
// TODO(goller): check err
return err
}
return nil
}
// OnboardInitialUser OnboardingResults from onboarding request,
// update db so this request will be disabled for the second run.
func (s *Service) OnboardInitialUser(ctx context.Context, req *influxdb.OnboardingRequest) (*influxdb.OnboardingResults, error) {
isOnboarding, err := s.IsOnboarding(ctx)
if err != nil {
return nil, err
}
if !isOnboarding {
return nil, &influxdb.Error{
Code: influxdb.EConflict,
Msg: "onboarding has already been completed",
}
}
if err := req.Valid(); err != nil {
return nil, err
}
u := &influxdb.User{Name: req.User}
o := &influxdb.Organization{Name: req.Org}
bucket := &influxdb.Bucket{
Name: req.Bucket,
RetentionPeriod: req.RetentionPeriod,
}
mapping := &influxdb.UserResourceMapping{
ResourceType: influxdb.OrgsResourceType,
UserType: influxdb.Owner,
}
auth := &influxdb.Authorization{
Description: fmt.Sprintf("%s's Token", u.Name),
Permissions: influxdb.OperPermissions(),
Token: req.Token,
}
var passwordHash []byte
passwordHash, err = s.generatePasswordHash(req.Password)
if err != nil {
return nil, err
}
err = s.kv.Update(ctx, func(tx Tx) error {
if err := s.createUser(ctx, tx, u); err != nil {
return err
}
if err := s.setPasswordHashTx(ctx, tx, u.ID, passwordHash); err != nil {
return err
}
if err := s.createOrganization(ctx, tx, o); err != nil {
return err
}
bucket.OrgID = o.ID
if err := s.createBucket(ctx, tx, bucket); err != nil {
return err
}
mapping.ResourceID = o.ID
mapping.UserID = u.ID
if err := s.createUserResourceMapping(ctx, tx, mapping); err != nil {
return err
}
auth.UserID = u.ID
auth.OrgID = o.ID
if err := s.createAuthorization(ctx, tx, auth); err != nil {
return err
}
return s.putOnboardingStatus(ctx, tx, true)
})
if err != nil {
return nil, err
}
return &influxdb.OnboardingResults{
User: u,
Org: o,
Bucket: bucket,
Auth: auth,
}, nil
}
func (s *Service) OnboardUser(ctx context.Context, req *influxdb.OnboardingRequest) (*influxdb.OnboardingResults, error) {
return nil, errors.New("not yet implemented")
}

View File

@ -1,52 +0,0 @@
package kv_test
import (
"context"
"testing"
influxdb "github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/kv"
influxdbtesting "github.com/influxdata/influxdb/v2/testing"
"go.uber.org/zap/zaptest"
)
func TestBoltOnboardingService(t *testing.T) {
influxdbtesting.OnboardInitialUser(initBoltOnboardingService, t)
}
func initBoltOnboardingService(f influxdbtesting.OnboardingFields, t *testing.T) (influxdb.OnboardingService, func()) {
s, closeStore, err := NewTestBoltStore(t)
if err != nil {
t.Fatalf("failed to create new bolt kv store: %v", err)
}
svc, closeSvc := initOnboardingService(s, f, t)
return svc, func() {
closeSvc()
closeStore()
}
}
func initOnboardingService(s kv.SchemaStore, f influxdbtesting.OnboardingFields, t *testing.T) (influxdb.OnboardingService, func()) {
ctx := context.Background()
svc := kv.NewService(zaptest.NewLogger(t), s)
svc.IDGenerator = f.IDGenerator
svc.OrgIDs = f.IDGenerator
svc.BucketIDs = f.IDGenerator
svc.TokenGenerator = f.TokenGenerator
svc.TimeGenerator = f.TimeGenerator
if f.TimeGenerator == nil {
svc.TimeGenerator = influxdb.RealTimeGenerator{}
}
t.Logf("Onboarding: %v", f.IsOnboarding)
if err := svc.PutOnboardingStatus(ctx, !f.IsOnboarding); err != nil {
t.Fatalf("failed to set new onboarding finished: %v", err)
}
return svc, func() {
if err := svc.PutOnboardingStatus(ctx, false); err != nil {
t.Logf("failed to remove onboarding finished: %v", err)
}
}
}

727
kv/org.go
View File

@ -1,727 +0,0 @@
package kv
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/influxdata/influxdb/v2/resource"
"go.uber.org/zap"
"github.com/influxdata/influxdb/v2"
icontext "github.com/influxdata/influxdb/v2/context"
"github.com/influxdata/influxdb/v2/kit/tracing"
)
const (
// MaxIDGenerationN is the maximum number of times an ID generation is done before failing.
MaxIDGenerationN = 100
)
var (
organizationBucket = []byte("organizationsv1")
organizationIndex = []byte("organizationindexv1")
)
// ErrFailureGeneratingID occurs ony when the random number generator
// cannot generate an ID in MaxIDGenerationN times.
var ErrFailureGeneratingID = &influxdb.Error{
Code: influxdb.EInternal,
Msg: "unable to generate valid id",
}
var _ influxdb.OrganizationService = (*Service)(nil)
var _ influxdb.OrganizationOperationLogService = (*Service)(nil)
// FindOrganizationByID retrieves a organization by id.
func (s *Service) FindOrganizationByID(ctx context.Context, id influxdb.ID) (*influxdb.Organization, error) {
var o *influxdb.Organization
err := s.kv.View(ctx, func(tx Tx) error {
org, pe := s.findOrganizationByID(ctx, tx, id)
if pe != nil {
return &influxdb.Error{
Err: pe,
}
}
o = org
return nil
})
if err != nil {
return nil, err
}
return o, nil
}
func (s *Service) findOrganizationByID(ctx context.Context, tx Tx, id influxdb.ID) (*influxdb.Organization, error) {
span, _ := tracing.StartSpanFromContext(ctx)
defer span.Finish()
encodedID, err := id.Encode()
if err != nil {
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
}
}
b, err := tx.Bucket(organizationBucket)
if err != nil {
return nil, err
}
v, err := b.Get(encodedID)
if IsNotFound(err) {
return nil, &influxdb.Error{
Code: influxdb.ENotFound,
Msg: "organization not found",
}
}
if err != nil {
return nil, err
}
var o influxdb.Organization
if err := json.Unmarshal(v, &o); err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
return &o, nil
}
// FindOrganizationByName returns a organization by name for a particular organization.
func (s *Service) FindOrganizationByName(ctx context.Context, n string) (*influxdb.Organization, error) {
span, ctx := tracing.StartSpanFromContext(ctx)
defer span.Finish()
var o *influxdb.Organization
err := s.kv.View(ctx, func(tx Tx) error {
org, err := s.findOrganizationByName(ctx, tx, n)
if err != nil {
return err
}
o = org
return nil
})
return o, err
}
func (s *Service) findOrganizationByName(ctx context.Context, tx Tx, n string) (*influxdb.Organization, error) {
span, ctx := tracing.StartSpanFromContext(ctx)
defer span.Finish()
b, err := tx.Bucket(organizationIndex)
if err != nil {
return nil, err
}
o, err := b.Get(organizationIndexKey(n))
if IsNotFound(err) {
return nil, &influxdb.Error{
Code: influxdb.ENotFound,
Msg: fmt.Sprintf("organization name \"%s\" not found", n),
}
}
if err != nil {
return nil, err
}
var id influxdb.ID
if err := id.Decode(o); err != nil {
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
}
}
return s.findOrganizationByID(ctx, tx, id)
}
// FindOrganization retrieves a organization using an arbitrary organization filter.
// Filters using ID, or Name should be efficient.
// Other filters will do a linear scan across organizations until it finds a match.
func (s *Service) FindOrganization(ctx context.Context, filter influxdb.OrganizationFilter) (*influxdb.Organization, error) {
span, ctx := tracing.StartSpanFromContext(ctx)
defer span.Finish()
var o *influxdb.Organization
err := s.kv.View(ctx, func(tx Tx) error {
org, err := s.findOrganization(ctx, tx, filter)
if err != nil {
return err
}
o = org
return nil
})
return o, err
}
func (s *Service) findOrganization(ctx context.Context, tx Tx, filter influxdb.OrganizationFilter) (*influxdb.Organization, error) {
if filter.ID != nil {
return s.findOrganizationByID(ctx, tx, *filter.ID)
}
if filter.Name != nil {
return s.findOrganizationByName(ctx, tx, *filter.Name)
}
// If name and ID are not set, then, this is an invalid usage of the API.
return nil, influxdb.ErrInvalidOrgFilter
}
func filterOrganizationsFn(filter influxdb.OrganizationFilter) func(o *influxdb.Organization) bool {
if filter.ID != nil {
return func(o *influxdb.Organization) bool {
return o.ID == *filter.ID
}
}
if filter.Name != nil {
return func(o *influxdb.Organization) bool {
return o.Name == *filter.Name
}
}
return func(o *influxdb.Organization) bool { return true }
}
// FindOrganizations retrieves all organizations that match an arbitrary organization filter.
// Filters using ID, or Name should be efficient.
// Other filters will do a linear scan across all organizations searching for a match.
func (s *Service) FindOrganizations(ctx context.Context, filter influxdb.OrganizationFilter, opt ...influxdb.FindOptions) ([]*influxdb.Organization, int, error) {
if filter.ID != nil {
o, err := s.FindOrganizationByID(ctx, *filter.ID)
if err != nil {
return nil, 0, &influxdb.Error{
Err: err,
}
}
return []*influxdb.Organization{o}, 1, nil
}
if filter.Name != nil {
o, err := s.FindOrganizationByName(ctx, *filter.Name)
if err != nil {
return nil, 0, &influxdb.Error{
Err: err,
}
}
return []*influxdb.Organization{o}, 1, nil
}
var offset, limit, count int
var descending bool
if len(opt) > 0 {
offset = opt[0].Offset
limit = opt[0].Limit
descending = opt[0].Descending
}
os := []*influxdb.Organization{}
if filter.UserID != nil {
// find urms for orgs with this user
urms, _, err := s.FindUserResourceMappings(ctx, influxdb.UserResourceMappingFilter{
UserID: *filter.UserID,
ResourceType: influxdb.OrgsResourceType,
})
if err != nil {
return nil, 0, err
}
// find orgs by the urm's resource ids.
for _, urm := range urms {
o, err := s.FindOrganizationByID(ctx, urm.ResourceID)
if err == nil {
// if there is an error then this is a crufty urm and we should just move on
if count >= offset {
os = append(os, o)
}
count++
}
if limit > 0 && len(os) >= limit {
break
}
}
if descending {
for i, j := 0, len(os)-1; i < j; i, j = i+1, j-1 {
os[i], os[j] = os[j], os[i]
}
}
return os, len(os), nil
}
filterFn := filterOrganizationsFn(filter)
err := s.kv.View(ctx, func(tx Tx) error {
return forEachOrganization(ctx, tx, descending, func(o *influxdb.Organization) bool {
if filterFn(o) { // TODO: Currently filterFn is useless here as we have finished all filtering jobs before. Keep it for future changes.
if count >= offset {
os = append(os, o)
}
count++
}
if limit > 0 && len(os) >= limit {
return false
}
return true
})
})
if err != nil {
return nil, 0, &influxdb.Error{
Err: err,
}
}
return os, len(os), nil
}
// CreateOrganization creates a influxdb organization and sets b.ID.
func (s *Service) CreateOrganization(ctx context.Context, o *influxdb.Organization) error {
return s.kv.Update(ctx, func(tx Tx) error {
if err := s.createOrganization(ctx, tx, o); err != nil {
return err
}
// Attempt to add user as owner of organization, if that is not possible allow the
// organization to be created anyways.
if err := s.addOrgOwner(ctx, tx, o.ID); err != nil {
s.log.Info("Failed to make user owner of organization", zap.Error(err))
}
return s.createSystemBuckets(ctx, tx, o)
})
}
// addOrgOwner attempts to create a user resource mapping for the user on the
// authorizer found on context. If no authorizer is found on context if returns an error.
func (s *Service) addOrgOwner(ctx context.Context, tx Tx, orgID influxdb.ID) error {
return s.addResourceOwner(ctx, tx, influxdb.OrgsResourceType, orgID)
}
// CreateOrganizationTx is used when importing kv as a library
func (s *Service) CreateOrganizationTx(ctx context.Context, tx Tx, o *influxdb.Organization) (err error) {
return s.createOrganization(ctx, tx, o)
}
func (s *Service) createOrganization(ctx context.Context, tx Tx, o *influxdb.Organization) (err error) {
if err := s.validOrganizationName(ctx, tx, o); err != nil {
return err
}
if o.ID, err = s.generateOrgID(ctx, tx); err != nil {
return err
}
o.CreatedAt = s.Now()
o.UpdatedAt = s.Now()
if err := s.appendOrganizationEventToLog(ctx, tx, o.ID, organizationCreatedEvent); err != nil {
return &influxdb.Error{
Err: err,
}
}
v, err := json.Marshal(o)
if err != nil {
return influxdb.ErrInternalOrgServiceError(influxdb.OpCreateOrganization, err)
}
if err := s.putOrganization(ctx, tx, o, v); err != nil {
return &influxdb.Error{
Err: err,
}
}
uid, _ := icontext.GetUserID(ctx)
return s.audit.Log(resource.Change{
Type: resource.Create,
ResourceID: o.ID,
ResourceType: influxdb.OrgsResourceType,
OrganizationID: o.ID,
UserID: uid,
ResourceBody: v,
Time: time.Now(),
})
}
func (s *Service) generateOrgID(ctx context.Context, tx Tx) (influxdb.ID, error) {
return s.generateSafeID(ctx, tx, organizationBucket, s.OrgIDs)
}
// PutOrganization will put a organization without setting an ID.
func (s *Service) PutOrganization(ctx context.Context, o *influxdb.Organization) error {
return s.kv.Update(ctx, func(tx Tx) error {
v, err := json.Marshal(o)
if err != nil {
return influxdb.ErrInternalOrgServiceError(influxdb.OpPutOrganization, err)
}
if err := s.putOrganization(ctx, tx, o, v); err != nil {
return err
}
uid, _ := icontext.GetUserID(ctx)
return s.audit.Log(resource.Change{
Type: resource.Put,
ResourceID: o.ID,
ResourceType: influxdb.OrgsResourceType,
OrganizationID: o.ID,
UserID: uid,
ResourceBody: v,
Time: time.Now(),
})
})
}
func (s *Service) putOrganization(ctx context.Context, tx Tx, o *influxdb.Organization, v []byte) error {
encodedID, err := o.ID.Encode()
if err != nil {
return &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
}
}
idx, err := tx.Bucket(organizationIndex)
if err != nil {
return err
}
if err := idx.Put(organizationIndexKey(o.Name), encodedID); err != nil {
return &influxdb.Error{
Err: err,
}
}
b, err := tx.Bucket(organizationBucket)
if err != nil {
return err
}
if err = b.Put(encodedID, v); err != nil {
return &influxdb.Error{
Err: err,
}
}
return nil
}
func organizationIndexKey(n string) []byte {
return []byte(n)
}
// forEachOrganization will iterate through all organizations while fn returns true.
func forEachOrganization(ctx context.Context, tx Tx, descending bool, fn func(*influxdb.Organization) bool) error {
b, err := tx.Bucket(organizationBucket)
if err != nil {
return err
}
direction := CursorAscending
if descending {
direction = CursorDescending
}
cur, err := b.ForwardCursor(nil, WithCursorDirection(direction))
if err != nil {
return err
}
for k, v := cur.Next(); k != nil; k, v = cur.Next() {
o := &influxdb.Organization{}
if err := json.Unmarshal(v, o); err != nil {
return err
}
if !fn(o) {
break
}
}
return nil
}
func (s *Service) validOrganizationName(ctx context.Context, tx Tx, o *influxdb.Organization) error {
if o.Name = strings.TrimSpace(o.Name); o.Name == "" {
return influxdb.ErrOrgNameisEmpty
}
key := organizationIndexKey(o.Name)
// if the name is not unique across all organizations, then, do not
// allow creation.
err := s.unique(ctx, tx, organizationIndex, key)
if err == NotUniqueError {
return OrgAlreadyExistsError(o)
}
return err
}
// UpdateOrganization updates a organization according the parameters set on upd.
func (s *Service) UpdateOrganization(ctx context.Context, id influxdb.ID, upd influxdb.OrganizationUpdate) (*influxdb.Organization, error) {
var o *influxdb.Organization
err := s.kv.Update(ctx, func(tx Tx) error {
org, pe := s.updateOrganization(ctx, tx, id, upd)
if pe != nil {
return &influxdb.Error{
Err: pe,
}
}
o = org
return nil
})
return o, err
}
func (s *Service) updateOrganization(ctx context.Context, tx Tx, id influxdb.ID, upd influxdb.OrganizationUpdate) (*influxdb.Organization, error) {
o, pe := s.findOrganizationByID(ctx, tx, id)
if pe != nil {
return nil, pe
}
if upd.Name != nil {
// Organizations are indexed by name and so the organization index must be pruned
// when name is modified.
idx, err := tx.Bucket(organizationIndex)
if err != nil {
return nil, err
}
if err := idx.Delete(organizationIndexKey(o.Name)); err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
o.Name = *upd.Name
if err := s.validOrganizationName(ctx, tx, o); err != nil {
return nil, err
}
}
if upd.Description != nil {
o.Description = *upd.Description
}
o.UpdatedAt = s.Now()
if err := s.appendOrganizationEventToLog(ctx, tx, o.ID, organizationUpdatedEvent); err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
v, err := json.Marshal(o)
if err != nil {
return nil, influxdb.ErrInternalOrgServiceError(influxdb.OpUpdateOrganization, err)
}
if pe := s.putOrganization(ctx, tx, o, v); pe != nil {
return nil, pe
}
uid, _ := icontext.GetUserID(ctx)
if err := s.audit.Log(resource.Change{
Type: resource.Update,
ResourceID: o.ID,
ResourceType: influxdb.OrgsResourceType,
OrganizationID: o.ID,
UserID: uid,
ResourceBody: v,
Time: time.Now(),
}); err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
return o, nil
}
func (s *Service) deleteOrganizationsBuckets(ctx context.Context, tx Tx, id influxdb.ID) error {
filter := influxdb.BucketFilter{
OrganizationID: &id,
}
bs, err := s.findBuckets(ctx, tx, filter)
if err != nil {
return err
}
for _, b := range bs {
if err := s.deleteBucket(ctx, tx, b.ID); err != nil {
s.log.Warn("Bucket was not deleted", zap.Stringer("bucketID", b.ID), zap.Stringer("orgID", b.OrgID))
}
}
return nil
}
// DeleteOrganization deletes a organization and prunes it from the index.
func (s *Service) DeleteOrganization(ctx context.Context, id influxdb.ID) error {
err := s.kv.Update(ctx, func(tx Tx) error {
if err := s.deleteOrganizationsBuckets(ctx, tx, id); err != nil {
return err
}
if pe := s.deleteOrganization(ctx, tx, id); pe != nil {
return pe
}
uid, _ := icontext.GetUserID(ctx)
return s.audit.Log(resource.Change{
Type: resource.Delete,
ResourceID: id,
ResourceType: influxdb.OrgsResourceType,
OrganizationID: id,
UserID: uid,
Time: time.Now(),
})
})
if err != nil {
return &influxdb.Error{
Err: err,
}
}
return nil
}
func (s *Service) deleteOrganization(ctx context.Context, tx Tx, id influxdb.ID) error {
o, pe := s.findOrganizationByID(ctx, tx, id)
if pe != nil {
return pe
}
idx, err := tx.Bucket(organizationIndex)
if err != nil {
return err
}
if err := idx.Delete(organizationIndexKey(o.Name)); err != nil {
return &influxdb.Error{
Err: err,
}
}
encodedID, err := id.Encode()
if err != nil {
return &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
}
}
b, err := tx.Bucket(organizationBucket)
if err != nil {
return err
}
if err = b.Delete(encodedID); err != nil {
return &influxdb.Error{
Err: err,
}
}
return nil
}
//func (s *Service) deleteOrganizationsBuckets(ctx context.Context, tx Tx, id influxdb.ID) error {
// filter := influxdb.BucketFilter{
// OrganizationID: &id,
// }
// bs, pe := s.findBuckets(ctx, tx, filter)
// if pe != nil {
// return pe
// }
// for _, b := range bs {
// if pe := s.deleteBucket(ctx, tx, b.ID); pe != nil {
// return pe
// }
// }
// return nil
//}
// GetOrganizationOperationLog retrieves a organization operation log.
func (s *Service) GetOrganizationOperationLog(ctx context.Context, id influxdb.ID, opts influxdb.FindOptions) ([]*influxdb.OperationLogEntry, int, error) {
// TODO(desa): might be worthwhile to allocate a slice of size opts.Limit
log := []*influxdb.OperationLogEntry{}
err := s.kv.View(ctx, func(tx Tx) error {
key, err := encodeOrganizationOperationLogKey(id)
if err != nil {
return err
}
return s.ForEachLogEntryTx(ctx, tx, key, opts, func(v []byte, t time.Time) error {
e := &influxdb.OperationLogEntry{}
if err := json.Unmarshal(v, e); err != nil {
return err
}
e.Time = t
log = append(log, e)
return nil
})
})
if err != nil && err != ErrKeyValueLogBoundsNotFound {
return nil, 0, err
}
return log, len(log), nil
}
// TODO(desa): what do we want these to be?
const (
organizationCreatedEvent = "Organization Created"
organizationUpdatedEvent = "Organization Updated"
)
const orgOperationLogKeyPrefix = "org"
func encodeOrganizationOperationLogKey(id influxdb.ID) ([]byte, error) {
buf, err := id.Encode()
if err != nil {
return nil, err
}
return append([]byte(orgOperationLogKeyPrefix), buf...), nil
}
func (s *Service) appendOrganizationEventToLog(ctx context.Context, tx Tx, id influxdb.ID, st string) error {
e := &influxdb.OperationLogEntry{
Description: st,
}
// TODO(desa): this is fragile and non explicit since it requires an authorizer to be on context. It should be
// replaced with a higher level transaction so that adding to the log can take place in the http handler
// where the organizationID will exist explicitly.
a, err := icontext.GetAuthorizer(ctx)
if err == nil {
// Add the organization to the log if you can, but don't error if its not there.
e.UserID = a.GetUserID()
}
v, err := json.Marshal(e)
if err != nil {
return err
}
k, err := encodeOrganizationOperationLogKey(id)
if err != nil {
return err
}
return s.AddLogEntryTx(ctx, tx, k, v, s.Now())
}
// OrgAlreadyExistsError is used when creating a new organization with
// a name that has already been used. Organization names must be unique.
func OrgAlreadyExistsError(o *influxdb.Organization) error {
return &influxdb.Error{
Code: influxdb.EConflict,
Msg: fmt.Sprintf("organization with name %s already exists", o.Name),
}
}

View File

@ -1,55 +0,0 @@
package kv_test
import (
"context"
"testing"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/kv"
influxdbtesting "github.com/influxdata/influxdb/v2/testing"
"go.uber.org/zap/zaptest"
)
func TestBoltOrganizationService(t *testing.T) {
influxdbtesting.OrganizationService(initBoltOrganizationService, t)
}
func initBoltOrganizationService(f influxdbtesting.OrganizationFields, t *testing.T) (influxdb.OrganizationService, string, func()) {
s, closeBolt, err := NewTestBoltStore(t)
if err != nil {
t.Fatalf("failed to create new kv store: %v", err)
}
svc, op, closeSvc := initOrganizationService(s, f, t)
return svc, op, func() {
closeSvc()
closeBolt()
}
}
func initOrganizationService(s kv.SchemaStore, f influxdbtesting.OrganizationFields, t *testing.T) (influxdb.OrganizationService, string, func()) {
ctx := context.Background()
svc := kv.NewService(zaptest.NewLogger(t), s)
svc.OrgIDs = f.OrgBucketIDs
svc.BucketIDs = f.OrgBucketIDs
svc.IDGenerator = f.IDGenerator
svc.TimeGenerator = f.TimeGenerator
if f.TimeGenerator == nil {
svc.TimeGenerator = influxdb.RealTimeGenerator{}
}
for _, o := range f.Organizations {
o.ID = svc.OrgIDs.ID()
if err := svc.PutOrganization(ctx, o); err != nil {
t.Fatalf("failed to populate organizations: %s", err)
}
}
return svc, kv.OpPrefix, func() {
for _, u := range f.Organizations {
if err := svc.DeleteOrganization(ctx, u.ID); err != nil {
t.Logf("failed to remove organizations: %v", err)
}
}
}
}

View File

@ -1,229 +0,0 @@
package kv
import (
"context"
"fmt"
"golang.org/x/crypto/bcrypt"
"github.com/influxdata/influxdb/v2"
)
// MinPasswordLength is the shortest password we allow into the system.
const MinPasswordLength = 8
var (
// EIncorrectPassword is returned when any password operation fails in which
// we do not want to leak information.
EIncorrectPassword = &influxdb.Error{
Code: influxdb.EForbidden,
Msg: "your username or password is incorrect",
}
// EIncorrectUser is returned when any user is failed to be found which indicates
// the userID provided is for a user that does not exist.
EIncorrectUser = &influxdb.Error{
Code: influxdb.EForbidden,
Msg: "your userID is incorrect",
}
// EShortPassword is used when a password is less than the minimum
// acceptable password length.
EShortPassword = &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "passwords must be at least 8 characters long",
}
)
// UnavailablePasswordServiceError is used if we aren't able to add the
// password to the store, it means the store is not available at the moment
// (e.g. network).
func UnavailablePasswordServiceError(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EUnavailable,
Msg: fmt.Sprintf("Unable to connect to password service. Please try again; Err: %v", err),
Op: "kv/setPassword",
}
}
// CorruptUserIDError is used when the ID was encoded incorrectly previously.
// This is some sort of internal server error.
func CorruptUserIDError(userID string, err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EInternal,
Msg: fmt.Sprintf("User ID %s has been corrupted; Err: %v", userID, err),
Op: "kv/setPassword",
}
}
// InternalPasswordHashError is used if the hasher is unable to generate
// a hash of the password. This is some sort of internal server error.
func InternalPasswordHashError(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EInternal,
Msg: fmt.Sprintf("Unable to generate password; Err: %v", err),
Op: "kv/setPassword",
}
}
var (
userpasswordBucket = []byte("userspasswordv1")
)
var _ influxdb.PasswordsService = (*Service)(nil)
// CompareAndSetPassword checks the password and if they match
// updates to the new password.
func (s *Service) CompareAndSetPassword(ctx context.Context, userID influxdb.ID, old string, new string) error {
newHash, err := s.generatePasswordHash(new)
if err != nil {
return err
}
if err := s.compareUserPassword(ctx, userID, old); err != nil {
return err
}
return s.setPasswordHash(ctx, userID, newHash)
}
// SetPassword overrides the password of a known user.
func (s *Service) SetPassword(ctx context.Context, userID influxdb.ID, password string) error {
hash, err := s.generatePasswordHash(password)
if err != nil {
return err
}
return s.setPasswordHash(ctx, userID, hash)
}
// ComparePassword checks if the password matches the password recorded.
// Passwords that do not match return errors.
func (s *Service) ComparePassword(ctx context.Context, userID influxdb.ID, password string) error {
return s.compareUserPassword(ctx, userID, password)
}
func (s *Service) getPasswordHash(ctx context.Context, userID influxdb.ID) ([]byte, error) {
var passwordHash []byte
err := s.kv.View(ctx, func(tx Tx) error {
var err error
encodedID, err := userID.Encode()
if err != nil {
return CorruptUserIDError(userID.String(), err)
}
if _, err := s.findUserByID(ctx, tx, userID); err != nil {
return EIncorrectUser
}
b, err := tx.Bucket(userpasswordBucket)
if err != nil {
return UnavailablePasswordServiceError(err)
}
passwordHash, err = b.Get(encodedID)
if err != nil {
return EIncorrectPassword
}
return nil
})
return passwordHash, err
}
func (s *Service) compareUserPassword(ctx context.Context, userID influxdb.ID, password string) error {
passwordHash, err := s.getPasswordHash(ctx, userID)
if err != nil {
return err
}
hasher := s.Hash
if hasher == nil {
hasher = &Bcrypt{}
}
if err := hasher.CompareHashAndPassword(passwordHash, []byte(password)); err != nil {
return EIncorrectPassword
}
return nil
}
func (s *Service) setPasswordHash(ctx context.Context, userID influxdb.ID, hash []byte) error {
return s.kv.Update(ctx, func(tx Tx) error {
return s.setPasswordHashTx(ctx, tx, userID, hash)
})
}
func (s *Service) setPasswordHashTx(ctx context.Context, tx Tx, userID influxdb.ID, hash []byte) error {
encodedID, err := userID.Encode()
if err != nil {
return CorruptUserIDError(userID.String(), err)
}
if _, err := s.findUserByID(ctx, tx, userID); err != nil {
return EIncorrectUser
}
b, err := tx.Bucket(userpasswordBucket)
if err != nil {
return UnavailablePasswordServiceError(err)
}
if err := b.Put(encodedID, hash); err != nil {
return UnavailablePasswordServiceError(err)
}
return nil
}
func (s *Service) generatePasswordHash(password string) ([]byte, error) {
if len(password) < MinPasswordLength {
return nil, EShortPassword
}
hasher := s.Hash
if hasher == nil {
hasher = &Bcrypt{}
}
hash, err := hasher.GenerateFromPassword([]byte(password), DefaultCost)
if err != nil {
return nil, InternalPasswordHashError(err)
}
return hash, nil
}
// DefaultCost is the cost that will actually be set if a cost below MinCost
// is passed into GenerateFromPassword
var DefaultCost = bcrypt.DefaultCost
// Crypt represents a cryptographic hashing function.
type Crypt interface {
// CompareHashAndPassword compares a hashed password with its possible plaintext equivalent.
// Returns nil on success, or an error on failure.
CompareHashAndPassword(hashedPassword, password []byte) error
// GenerateFromPassword returns the hash of the password at the given cost.
// If the cost given is less than MinCost, the cost will be set to DefaultCost, instead.
GenerateFromPassword(password []byte, cost int) ([]byte, error)
}
var _ Crypt = (*Bcrypt)(nil)
// Bcrypt implements Crypt using golang.org/x/crypto/bcrypt
type Bcrypt struct{}
// CompareHashAndPassword compares a hashed password with its possible plaintext equivalent.
// Returns nil on success, or an error on failure.
func (b *Bcrypt) CompareHashAndPassword(hashedPassword, password []byte) error {
return bcrypt.CompareHashAndPassword(hashedPassword, password)
}
// GenerateFromPassword returns the hash of the password at the given cost.
// If the cost given is less than MinCost, the cost will be set to DefaultCost, instead.
func (b *Bcrypt) GenerateFromPassword(password []byte, cost int) ([]byte, error) {
if cost < bcrypt.MinCost {
cost = DefaultCost
}
return bcrypt.GenerateFromPassword(password, cost)
}

View File

@ -1,479 +0,0 @@
package kv_test
import (
"context"
"errors"
"fmt"
"testing"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/kv"
"github.com/influxdata/influxdb/v2/mock"
influxdbtesting "github.com/influxdata/influxdb/v2/testing"
"go.uber.org/zap/zaptest"
)
func TestBoltPasswordService(t *testing.T) {
influxdbtesting.PasswordsService(initBoltPasswordsService, t)
}
func initBoltPasswordsService(f influxdbtesting.PasswordFields, t *testing.T) (influxdb.PasswordsService, func()) {
s, closeStore, err := NewTestBoltStore(t)
if err != nil {
t.Fatalf("failed to create new bolt kv store: %v", err)
}
svc, closeSvc := initPasswordsService(s, f, t)
return svc, func() {
closeSvc()
closeStore()
}
}
func initPasswordsService(s kv.SchemaStore, f influxdbtesting.PasswordFields, t *testing.T) (influxdb.PasswordsService, func()) {
ctx := context.Background()
svc := kv.NewService(zaptest.NewLogger(t), s)
svc.IDGenerator = f.IDGenerator
for _, u := range f.Users {
if err := svc.PutUser(ctx, u); err != nil {
t.Fatalf("error populating users: %v", err)
}
}
for i := range f.Passwords {
if err := svc.SetPassword(ctx, f.Users[i].ID, f.Passwords[i]); err != nil {
t.Fatalf("error setting passsword user, %s %s: %v", f.Users[i].Name, f.Passwords[i], err)
}
}
return svc, func() {
for _, u := range f.Users {
if err := svc.DeleteUser(ctx, u.ID); err != nil {
t.Logf("error removing users: %v", err)
}
}
}
}
type MockHasher struct {
GenerateError error
CompareError error
}
func (m *MockHasher) CompareHashAndPassword(hashedPassword, password []byte) error {
return m.CompareError
}
func (m *MockHasher) GenerateFromPassword(password []byte, cost int) ([]byte, error) {
return nil, m.GenerateError
}
func TestService_SetPassword(t *testing.T) {
type fields struct {
kv kv.Store
Hash kv.Crypt
}
type args struct {
id influxdb.ID
password string
}
type wants struct {
err error
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "if store somehow has a corrupted user index, then, we get back an internal error",
fields: fields{
kv: &mock.Store{
UpdateFn: func(fn func(kv.Tx) error) error {
tx := &mock.Tx{
BucketFn: func(b []byte) (kv.Bucket, error) {
return &mock.Bucket{
GetFn: func(key []byte) ([]byte, error) {
return nil, errors.New("its broked")
},
}, nil
},
}
return fn(tx)
},
},
},
args: args{
id: 1,
password: "howdydoody",
},
wants: wants{
err: fmt.Errorf("your userID is incorrect"),
},
},
{
name: "if user id is not found return a generic sounding error",
fields: fields{
kv: &mock.Store{
UpdateFn: func(fn func(kv.Tx) error) error {
tx := &mock.Tx{
BucketFn: func(b []byte) (kv.Bucket, error) {
return &mock.Bucket{
GetFn: func(key []byte) ([]byte, error) {
return nil, kv.ErrKeyNotFound
},
PutFn: func(key, val []byte) error {
return nil
},
}, nil
},
}
return fn(tx)
},
},
},
args: args{
id: 1,
password: "howdydoody",
},
wants: wants{
err: fmt.Errorf("your userID is incorrect"),
},
},
{
name: "if store somehow has a corrupted user id, then, we get back an internal error",
fields: fields{
kv: &mock.Store{
UpdateFn: func(fn func(kv.Tx) error) error {
tx := &mock.Tx{
BucketFn: func(b []byte) (kv.Bucket, error) {
return &mock.Bucket{
GetFn: func(key []byte) ([]byte, error) {
if string(key) == "0000000000000001" {
return []byte(`{"name": "user1"}`), nil
}
return nil, kv.ErrKeyNotFound
},
}, nil
},
}
return fn(tx)
},
},
},
args: args{
id: 0,
password: "howdydoody",
},
wants: wants{
err: fmt.Errorf("User ID has been corrupted; Err: invalid ID"),
},
},
{
name: "if password store is not available, then, we get back an internal error",
fields: fields{
kv: &mock.Store{
UpdateFn: func(fn func(kv.Tx) error) error {
tx := &mock.Tx{
BucketFn: func(b []byte) (kv.Bucket, error) {
if string(b) == "userspasswordv1" {
return nil, fmt.Errorf("internal bucket error")
}
return &mock.Bucket{
GetFn: func(key []byte) ([]byte, error) {
if string(key) == "0000000000000001" {
return []byte(`{"id": "0000000000000001", "name": "user1"}`), nil
}
return nil, kv.ErrKeyNotFound
},
}, nil
},
}
return fn(tx)
},
},
},
args: args{
id: 1,
password: "howdydoody",
},
wants: wants{
err: fmt.Errorf("Unable to connect to password service. Please try again; Err: internal bucket error"),
},
},
{
name: "if hashing algorithm has an error, then, we get back an internal error",
fields: fields{
Hash: &MockHasher{
GenerateError: fmt.Errorf("generate error"),
},
kv: &mock.Store{
UpdateFn: func(fn func(kv.Tx) error) error {
tx := &mock.Tx{
BucketFn: func(b []byte) (kv.Bucket, error) {
if string(b) == "userspasswordv1" {
return nil, nil
}
return &mock.Bucket{
GetFn: func(key []byte) ([]byte, error) {
if string(key) == "0000000000000001" {
return []byte(`{"id": "0000000000000001", "name": "user1"}`), nil
}
return nil, kv.ErrKeyNotFound
},
}, nil
},
}
return fn(tx)
},
},
},
args: args{
id: 1,
password: "howdydoody",
},
wants: wants{
fmt.Errorf("Unable to generate password; Err: generate error"),
},
},
{
name: "if not able to store the hashed password should have an internal error",
fields: fields{
kv: &mock.Store{
UpdateFn: func(fn func(kv.Tx) error) error {
tx := &mock.Tx{
BucketFn: func(b []byte) (kv.Bucket, error) {
if string(b) == "userspasswordv1" {
return &mock.Bucket{
PutFn: func(key, value []byte) error {
return fmt.Errorf("internal error")
},
}, nil
}
return &mock.Bucket{
GetFn: func(key []byte) ([]byte, error) {
if string(key) == "0000000000000001" {
return []byte(`{"id": "0000000000000001", "name": "user1"}`), nil
}
return nil, kv.ErrKeyNotFound
},
PutFn: func(key, val []byte) error {
return nil
},
}, nil
},
}
return fn(tx)
},
},
},
args: args{
id: 1,
password: "howdydoody",
},
wants: wants{
fmt.Errorf("Unable to connect to password service. Please try again; Err: internal error"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &kv.Service{
Hash: tt.fields.Hash,
}
s.WithStore(tt.fields.kv)
err := s.SetPassword(context.Background(), tt.args.id, tt.args.password)
if (err != nil && tt.wants.err == nil) || (err == nil && tt.wants.err != nil) {
t.Fatalf("Service.SetPassword() error = %v, want %v", err, tt.wants.err)
return
}
if err != nil {
if got, want := err.Error(), tt.wants.err.Error(); got != want {
t.Errorf("Service.SetPassword() error = %v, want %v", got, want)
}
}
})
}
}
func TestService_ComparePassword(t *testing.T) {
type fields struct {
kv kv.Store
Hash kv.Crypt
}
type args struct {
id influxdb.ID
password string
}
type wants struct {
err error
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "if store somehow has a corrupted user index, then, we get back an internal error",
fields: fields{
kv: &mock.Store{
ViewFn: func(fn func(kv.Tx) error) error {
tx := &mock.Tx{
BucketFn: func(b []byte) (kv.Bucket, error) {
return &mock.Bucket{
GetFn: func(key []byte) ([]byte, error) {
return nil, nil
},
}, nil
},
}
return fn(tx)
},
},
},
args: args{
id: 1,
password: "howdydoody",
},
wants: wants{
err: fmt.Errorf("your userID is incorrect"),
},
},
{
name: "if store somehow has a corrupted user id, then, we get back an internal error",
fields: fields{
kv: &mock.Store{
ViewFn: func(fn func(kv.Tx) error) error {
tx := &mock.Tx{
BucketFn: func(b []byte) (kv.Bucket, error) {
return &mock.Bucket{
GetFn: func(key []byte) ([]byte, error) {
if string(key) == "user1" {
return []byte("0000000000000001"), nil
}
if string(key) == "0000000000000001" {
return []byte(`{"name": "user1"}`), nil
}
return nil, kv.ErrKeyNotFound
},
}, nil
},
}
return fn(tx)
},
},
},
args: args{
id: 0,
password: "howdydoody",
},
wants: wants{
err: fmt.Errorf("User ID has been corrupted; Err: invalid ID"),
},
},
{
name: "if password store is not available, then, we get back an internal error",
fields: fields{
kv: &mock.Store{
ViewFn: func(fn func(kv.Tx) error) error {
tx := &mock.Tx{
BucketFn: func(b []byte) (kv.Bucket, error) {
if string(b) == "userspasswordv1" {
return nil, fmt.Errorf("internal bucket error")
}
return &mock.Bucket{
GetFn: func(key []byte) ([]byte, error) {
if string(key) == "user1" {
return []byte("0000000000000001"), nil
}
if string(key) == "0000000000000001" {
return []byte(`{"id": "0000000000000001", "name": "user1"}`), nil
}
return nil, kv.ErrKeyNotFound
},
}, nil
},
}
return fn(tx)
},
},
},
args: args{
id: 1,
password: "howdydoody",
},
wants: wants{
err: fmt.Errorf("Unable to connect to password service. Please try again; Err: internal bucket error"),
},
},
{
name: "if the password doesn't has correctly we get an invalid password error",
fields: fields{
Hash: &MockHasher{
CompareError: fmt.Errorf("generate error"),
},
kv: &mock.Store{
ViewFn: func(fn func(kv.Tx) error) error {
tx := &mock.Tx{
BucketFn: func(b []byte) (kv.Bucket, error) {
if string(b) == "userspasswordv1" {
return &mock.Bucket{
GetFn: func([]byte) ([]byte, error) {
return []byte("hash"), nil
},
}, nil
}
return &mock.Bucket{
GetFn: func(key []byte) ([]byte, error) {
if string(key) == "user1" {
return []byte("0000000000000001"), nil
}
if string(key) == "0000000000000001" {
return []byte(`{"id": "0000000000000001", "name": "user1"}`), nil
}
return nil, kv.ErrKeyNotFound
},
}, nil
},
}
return fn(tx)
},
},
},
args: args{
id: 1,
password: "howdydoody",
},
wants: wants{
fmt.Errorf("your username or password is incorrect"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &kv.Service{
Hash: tt.fields.Hash,
}
s.WithStore(tt.fields.kv)
err := s.ComparePassword(context.Background(), tt.args.id, tt.args.password)
if (err != nil && tt.wants.err == nil) || (err == nil && tt.wants.err != nil) {
t.Fatalf("Service.ComparePassword() error = %v, want %v", err, tt.wants.err)
return
}
if err != nil {
if got, want := err.Error(), tt.wants.err.Error(); got != want {
t.Errorf("Service.ComparePassword() error = %v, want %v", got, want)
}
}
})
}
}

View File

@ -92,6 +92,17 @@ func (s *Service) scrapersBucket(tx Tx) (Bucket, error) {
// ListTargets will list all scrape targets.
func (s *Service) ListTargets(ctx context.Context, filter influxdb.ScraperTargetFilter) ([]influxdb.ScraperTarget, error) {
if filter.Org != nil {
org, err := s.orgs.FindOrganization(ctx, influxdb.OrganizationFilter{
Name: filter.Org,
})
if err != nil {
return nil, err
}
filter.OrgID = &org.ID
}
targets := []influxdb.ScraperTarget{}
err := s.kv.View(ctx, func(tx Tx) error {
var err error
@ -113,21 +124,6 @@ func (s *Service) listTargets(ctx context.Context, tx Tx, filter influxdb.Scrape
return nil, UnexpectedScrapersBucketError(err)
}
var org *influxdb.Organization
if filter.Org != nil {
org, err = s.findOrganizationByName(ctx, tx, *filter.Org)
if err != nil {
return nil, err
}
}
if filter.OrgID != nil {
org, err = s.findOrganizationByID(ctx, tx, *filter.OrgID)
if err != nil {
return nil, err
}
}
for k, v := cur.Next(); k != nil; k, v = cur.Next() {
target, err := unmarshalScraper(v)
if err != nil {
@ -142,7 +138,7 @@ func (s *Service) listTargets(ctx context.Context, tx Tx, filter influxdb.Scrape
continue
}
if org != nil && target.OrgID != org.ID {
if filter.OrgID != nil && target.OrgID != *filter.OrgID {
continue
}

View File

@ -6,6 +6,8 @@ import (
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/kv"
"github.com/influxdata/influxdb/v2/mock"
"github.com/influxdata/influxdb/v2/tenant"
influxdbtesting "github.com/influxdata/influxdb/v2/testing"
"go.uber.org/zap/zaptest"
)
@ -29,8 +31,14 @@ func initBoltTargetService(f influxdbtesting.TargetFields, t *testing.T) (influx
func initScraperTargetStoreService(s kv.SchemaStore, f influxdbtesting.TargetFields, t *testing.T) (influxdb.ScraperTargetStoreService, string, func()) {
ctx := context.Background()
svc := kv.NewService(zaptest.NewLogger(t), s)
svc.IDGenerator = f.IDGenerator
tenantStore := tenant.NewStore(s)
tenantSvc := tenant.NewService(tenantStore)
svc := kv.NewService(zaptest.NewLogger(t), s, tenantSvc)
if f.IDGenerator != nil {
svc.IDGenerator = f.IDGenerator
}
for _, target := range f.Targets {
if err := svc.PutTarget(ctx, target); err != nil {
@ -39,9 +47,11 @@ func initScraperTargetStoreService(s kv.SchemaStore, f influxdbtesting.TargetFie
}
for _, o := range f.Organizations {
if err := svc.PutOrganization(ctx, o); err != nil {
t.Fatalf("failed to populate organization")
}
mock.SetIDForFunc(&tenantStore.OrgIDGen, o.ID, func() {
if err := tenantSvc.CreateOrganization(ctx, o); err != nil {
t.Fatalf("failed to populate organization")
}
})
}
return svc, kv.OpPrefix, func() {
@ -51,7 +61,7 @@ func initScraperTargetStoreService(s kv.SchemaStore, f influxdbtesting.TargetFie
}
}
for _, o := range f.Organizations {
if err := svc.DeleteOrganization(ctx, o.ID); err != nil {
if err := tenantSvc.DeleteOrganization(ctx, o.ID); err != nil {
t.Logf("failed to remove orgs: %v", err)
}
}

View File

@ -1,255 +0,0 @@
package kv
import (
"context"
"encoding/base64"
"errors"
"github.com/influxdata/influxdb/v2"
)
var (
secretBucket = []byte("secretsv1")
_ influxdb.SecretService = (*Service)(nil)
)
// LoadSecret retrieves the secret value v found at key k for organization orgID.
func (s *Service) LoadSecret(ctx context.Context, orgID influxdb.ID, k string) (string, error) {
var v string
err := s.kv.View(ctx, func(tx Tx) error {
val, err := s.loadSecret(ctx, tx, orgID, k)
if err != nil {
return err
}
v = val
return nil
})
if err != nil {
return "", err
}
return v, nil
}
func (s *Service) loadSecret(ctx context.Context, tx Tx, orgID influxdb.ID, k string) (string, error) {
key, err := encodeSecretKey(orgID, k)
if err != nil {
return "", err
}
b, err := tx.Bucket(secretBucket)
if err != nil {
return "", err
}
val, err := b.Get(key)
if IsNotFound(err) {
return "", &influxdb.Error{
Code: influxdb.ENotFound,
Msg: influxdb.ErrSecretNotFound,
}
}
if err != nil {
return "", err
}
v, err := decodeSecretValue(val)
if err != nil {
return "", err
}
return v, nil
}
// GetSecretKeys retrieves all secret keys that are stored for the organization orgID.
func (s *Service) GetSecretKeys(ctx context.Context, orgID influxdb.ID) ([]string, error) {
var vs []string
err := s.kv.View(ctx, func(tx Tx) error {
vals, err := s.getSecretKeys(ctx, tx, orgID)
if err != nil {
return err
}
vs = vals
return nil
})
if err != nil {
return nil, err
}
return vs, nil
}
func (s *Service) getSecretKeys(ctx context.Context, tx Tx, orgID influxdb.ID) ([]string, error) {
b, err := tx.Bucket(secretBucket)
if err != nil {
return nil, err
}
prefix, err := orgID.Encode()
if err != nil {
return nil, err
}
cur, err := b.ForwardCursor(prefix, WithCursorPrefix(prefix))
if err != nil {
return nil, err
}
keys := []string{}
for k, _ := cur.Next(); k != nil; k, _ = cur.Next() {
id, key, err := decodeSecretKey(k)
if err != nil {
return nil, err
}
if id != orgID {
// We've reached the end of the keyspace for the provided orgID
break
}
keys = append(keys, key)
}
return keys, nil
}
// PutSecret stores the secret pair (k,v) for the organization orgID.
func (s *Service) PutSecret(ctx context.Context, orgID influxdb.ID, k, v string) error {
return s.kv.Update(ctx, func(tx Tx) error {
return s.putSecret(ctx, tx, orgID, k, v)
})
}
func (s *Service) putSecret(ctx context.Context, tx Tx, orgID influxdb.ID, k, v string) error {
key, err := encodeSecretKey(orgID, k)
if err != nil {
return err
}
val := encodeSecretValue(v)
b, err := tx.Bucket(secretBucket)
if err != nil {
return err
}
if err := b.Put(key, val); err != nil {
return err
}
return nil
}
func encodeSecretKey(orgID influxdb.ID, k string) ([]byte, error) {
buf, err := orgID.Encode()
if err != nil {
return nil, err
}
key := make([]byte, 0, influxdb.IDLength+len(k))
key = append(key, buf...)
key = append(key, k...)
return key, nil
}
func decodeSecretKey(key []byte) (influxdb.ID, string, error) {
if len(key) < influxdb.IDLength {
// This should not happen.
return influxdb.InvalidID(), "", errors.New("provided key is too short to contain an ID (please report this error)")
}
var id influxdb.ID
if err := id.Decode(key[:influxdb.IDLength]); err != nil {
return influxdb.InvalidID(), "", err
}
k := string(key[influxdb.IDLength:])
return id, k, nil
}
func decodeSecretValue(val []byte) (string, error) {
// store the secret value base64 encoded so that it's marginally better than plaintext
v, err := base64.StdEncoding.DecodeString(string(val))
if err != nil {
return "", err
}
return string(v), nil
}
func encodeSecretValue(v string) []byte {
val := make([]byte, base64.StdEncoding.EncodedLen(len(v)))
base64.StdEncoding.Encode(val, []byte(v))
return val
}
// PutSecrets puts all provided secrets and overwrites any previous values.
func (s *Service) PutSecrets(ctx context.Context, orgID influxdb.ID, m map[string]string) error {
return s.kv.Update(ctx, func(tx Tx) error {
keys, err := s.getSecretKeys(ctx, tx, orgID)
if err != nil {
return err
}
for k, v := range m {
if err := s.putSecret(ctx, tx, orgID, k, v); err != nil {
return err
}
}
for _, k := range keys {
if _, ok := m[k]; !ok {
if err := s.deleteSecret(ctx, tx, orgID, k); err != nil {
return err
}
}
}
return nil
})
}
// PatchSecrets patches all provided secrets and updates any previous values.
func (s *Service) PatchSecrets(ctx context.Context, orgID influxdb.ID, m map[string]string) error {
return s.kv.Update(ctx, func(tx Tx) error {
for k, v := range m {
if err := s.putSecret(ctx, tx, orgID, k, v); err != nil {
return err
}
}
return nil
})
}
// DeleteSecret removes secrets from the secret store.
func (s *Service) DeleteSecret(ctx context.Context, orgID influxdb.ID, ks ...string) error {
return s.kv.Update(ctx, func(tx Tx) error {
for _, k := range ks {
if err := s.deleteSecret(ctx, tx, orgID, k); err != nil {
return err
}
}
return nil
})
}
func (s *Service) deleteSecret(ctx context.Context, tx Tx, orgID influxdb.ID, k string) error {
key, err := encodeSecretKey(orgID, k)
if err != nil {
return err
}
b, err := tx.Bucket(secretBucket)
if err != nil {
return err
}
return b.Delete(key)
}

View File

@ -1,43 +0,0 @@
package kv_test
import (
"context"
"testing"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/kv"
influxdbtesting "github.com/influxdata/influxdb/v2/testing"
"go.uber.org/zap/zaptest"
)
func TestBoltSecretService(t *testing.T) {
influxdbtesting.SecretService(initBoltSecretService, t)
}
func initBoltSecretService(f influxdbtesting.SecretServiceFields, t *testing.T) (influxdb.SecretService, func()) {
s, closeBolt, err := NewTestBoltStore(t)
if err != nil {
t.Fatalf("failed to create new kv store: %v", err)
}
svc, closeSvc := initSecretService(s, f, t)
return svc, func() {
closeSvc()
closeBolt()
}
}
func initSecretService(s kv.SchemaStore, f influxdbtesting.SecretServiceFields, t *testing.T) (influxdb.SecretService, func()) {
ctx := context.Background()
svc := kv.NewService(zaptest.NewLogger(t), s)
for _, s := range f.Secrets {
for k, v := range s.Env {
if err := svc.PutSecret(ctx, s.OrganizationID, k, v); err != nil {
t.Fatalf("failed to populate secrets")
}
}
}
return svc, func() {}
}

View File

@ -1,8 +1,6 @@
package kv
import (
"time"
"github.com/benbjohnson/clock"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/rand"
@ -12,10 +10,6 @@ import (
"go.uber.org/zap"
)
var (
_ influxdb.UserService = (*Service)(nil)
)
// OpPrefix is the prefix for kv errors.
const OpPrefix = "kv/"
@ -33,51 +27,37 @@ type Service struct {
// will fail.
FluxLanguageService influxdb.FluxLanguageService
// special ID generator that never returns bytes with backslash,
// comma, or space. Used to support very specific encoding of org &
// bucket into the old measurement in storage.
OrgIDs influxdb.IDGenerator
BucketIDs influxdb.IDGenerator
TokenGenerator influxdb.TokenGenerator
// TODO(desa:ariel): this should not be embedded
influxdb.TimeGenerator
Hash Crypt
orgs influxdb.OrganizationService
variableStore *IndexStore
urmByUserIndex *Index
}
// NewService returns an instance of a Service.
func NewService(log *zap.Logger, kv Store, configs ...ServiceConfig) *Service {
func NewService(log *zap.Logger, kv Store, orgs influxdb.OrganizationService, configs ...ServiceConfig) *Service {
s := &Service{
log: log,
IDGenerator: snowflake.NewIDGenerator(),
// Seed the random number generator with the current time
OrgIDs: rand.NewOrgBucketID(time.Now().UnixNano()),
BucketIDs: rand.NewOrgBucketID(time.Now().UnixNano()),
log: log,
IDGenerator: snowflake.NewIDGenerator(),
TokenGenerator: rand.NewTokenGenerator(64),
Hash: &Bcrypt{},
kv: kv,
orgs: orgs,
audit: noop.ResourceLogger{},
TimeGenerator: influxdb.RealTimeGenerator{},
variableStore: newVariableStore(),
urmByUserIndex: NewIndex(URMByUserIndexMapping, WithIndexReadPathEnabled),
}
if len(configs) > 0 {
s.Config = configs[0]
}
if s.Config.SessionLength == 0 {
s.Config.SessionLength = influxdb.DefaultSessionLength
}
s.clock = s.Config.Clock
if s.clock == nil {
s.clock = clock.New()
}
s.FluxLanguageService = s.Config.FluxLanguageService
return s
@ -85,7 +65,6 @@ func NewService(log *zap.Logger, kv Store, configs ...ServiceConfig) *Service {
// ServiceConfig allows us to configure Services
type ServiceConfig struct {
SessionLength time.Duration
Clock clock.Clock
FluxLanguageService influxdb.FluxLanguageService
}

View File

@ -1,49 +0,0 @@
package kv_test
import (
"context"
"io"
"testing"
"time"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/kv"
"go.uber.org/zap/zaptest"
)
type mockStore struct {
}
func (s mockStore) View(context.Context, func(kv.Tx) error) error {
return nil
}
func (s mockStore) Update(context.Context, func(kv.Tx) error) error {
return nil
}
func (s mockStore) Backup(ctx context.Context, w io.Writer) error {
return nil
}
func (s mockStore) Restore(ctx context.Context, r io.Reader) error {
return nil
}
func TestNewService(t *testing.T) {
s := kv.NewService(zaptest.NewLogger(t), mockStore{})
if s.Config.SessionLength != influxdb.DefaultSessionLength {
t.Errorf("Service session length should use default length when not set")
}
config := kv.ServiceConfig{
SessionLength: time.Duration(time.Hour * 4),
}
s = kv.NewService(zaptest.NewLogger(t), mockStore{}, config)
if s.Config != config {
t.Errorf("Service config not set by constructor")
}
}

View File

@ -6,6 +6,7 @@ import (
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/kv"
"github.com/influxdata/influxdb/v2/mock"
influxdbtesting "github.com/influxdata/influxdb/v2/testing"
"go.uber.org/zap/zaptest"
)
@ -32,7 +33,7 @@ func initBoltSourceService(f influxdbtesting.SourceFields, t *testing.T) (influx
func initSourceService(s kv.SchemaStore, f influxdbtesting.SourceFields, t *testing.T) (influxdb.SourceService, string, func()) {
ctx := context.Background()
svc := kv.NewService(zaptest.NewLogger(t), s)
svc := kv.NewService(zaptest.NewLogger(t), s, &mock.OrganizationService{})
svc.IDGenerator = f.IDGenerator
for _, b := range f.Sources {

View File

@ -3,6 +3,8 @@ package kv
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
@ -11,7 +13,6 @@ import (
"github.com/influxdata/influxdb/v2/kit/feature"
"github.com/influxdata/influxdb/v2/resource"
"github.com/influxdata/influxdb/v2/task/options"
"go.uber.org/zap"
)
// Task Storage Schema
@ -133,41 +134,23 @@ func (s *Service) findTaskByID(ctx context.Context, tx Tx, id influxdb.ID) (*inf
t.LatestCompleted = t.CreatedAt
}
// Attempt to fill in the owner ID based on the auth.
if !t.OwnerID.Valid() {
authType := struct {
AuthorizationID influxdb.ID `json:"authorizationID"`
}{}
if err := json.Unmarshal(v, &authType); err != nil {
return nil, influxdb.ErrInternalTaskServiceError(err)
}
auth, err := s.findAuthorizationByID(ctx, tx, authType.AuthorizationID)
if err == nil {
t.OwnerID = auth.GetUserID()
}
}
// Attempt to fill in the ownerID based on the organization owners.
// If they have multiple owners just use the first one because any org owner
// will have sufficient permissions to run a task.
if !t.OwnerID.Valid() {
owners, err := s.findUserResourceMappings(ctx, tx, influxdb.UserResourceMappingFilter{
ResourceID: t.OrganizationID,
ResourceType: influxdb.OrgsResourceType,
UserType: influxdb.Owner,
})
if err == nil && len(owners) > 0 {
t.OwnerID = owners[0].UserID
}
}
return t, nil
}
// FindTasks returns a list of tasks that match a filter (limit 100) and the total count
// of matching tasks.
func (s *Service) FindTasks(ctx context.Context, filter influxdb.TaskFilter) ([]*influxdb.Task, int, error) {
if filter.Organization != "" {
org, err := s.orgs.FindOrganization(ctx, influxdb.OrganizationFilter{
Name: &filter.Organization,
})
if err != nil {
return nil, 0, err
}
filter.OrganizationID = &org.ID
}
var ts []*influxdb.Task
err := s.kv.View(ctx, func(tx Tx) error {
tasks, _, err := s.findTasks(ctx, tx, filter)
@ -185,20 +168,6 @@ func (s *Service) FindTasks(ctx context.Context, filter influxdb.TaskFilter) ([]
}
func (s *Service) findTasks(ctx context.Context, tx Tx, filter influxdb.TaskFilter) ([]*influxdb.Task, int, error) {
var org *influxdb.Organization
var err error
if filter.OrganizationID != nil {
org, err = s.findOrganizationByID(ctx, tx, *filter.OrganizationID)
if err != nil {
return nil, 0, err
}
} else if filter.Organization != "" {
org, err = s.findOrganizationByName(ctx, tx, filter.Organization)
if err != nil {
return nil, 0, err
}
}
// complain about limits
if filter.Limit < 0 {
return nil, 0, influxdb.ErrPageSizeTooSmall
@ -212,7 +181,7 @@ func (s *Service) findTasks(ctx context.Context, tx Tx, filter influxdb.TaskFilt
// if no user or organization is passed, assume contexts auth is the user we are looking for.
// it is possible for a internal system to call this with no auth so we shouldnt fail if no auth is found.
if org == nil && filter.User == nil {
if filter.OrganizationID == nil && filter.User == nil {
userAuth, err := icontext.GetAuthorizer(ctx)
if err == nil {
userID := userAuth.GetUserID()
@ -225,8 +194,8 @@ func (s *Service) findTasks(ctx context.Context, tx Tx, filter influxdb.TaskFilt
// filter by user id.
if filter.User != nil {
return s.findTasksByUser(ctx, tx, filter)
} else if org != nil {
return s.findTasksByOrg(ctx, tx, filter)
} else if filter.OrganizationID != nil {
return s.findTasksByOrg(ctx, tx, *filter.OrganizationID, filter)
}
return s.findAllTasks(ctx, tx, filter)
@ -285,23 +254,10 @@ func (s *Service) findTasksByUser(ctx context.Context, tx Tx, filter influxdb.Ta
}
// findTasksByOrg is a subset of the find tasks function. Used for cleanliness
func (s *Service) findTasksByOrg(ctx context.Context, tx Tx, filter influxdb.TaskFilter) ([]*influxdb.Task, int, error) {
var org *influxdb.Organization
func (s *Service) findTasksByOrg(ctx context.Context, tx Tx, orgID influxdb.ID, filter influxdb.TaskFilter) ([]*influxdb.Task, int, error) {
var err error
if filter.OrganizationID != nil {
org, err = s.findOrganizationByID(ctx, tx, *filter.OrganizationID)
if err != nil {
return nil, 0, err
}
} else if filter.Organization != "" {
org, err = s.findOrganizationByName(ctx, tx, filter.Organization)
if err != nil {
return nil, 0, err
}
}
if org == nil {
return nil, 0, influxdb.ErrTaskNotFound
if !orgID.Valid() {
return nil, 0, fmt.Errorf("finding tasks by organization ID: %w", influxdb.ErrInvalidID)
}
var ts []*influxdb.Task
@ -311,7 +267,7 @@ func (s *Service) findTasksByOrg(ctx context.Context, tx Tx, filter influxdb.Tas
return nil, 0, influxdb.ErrUnexpectedTaskBucketErr(err)
}
prefix, err := org.ID.Encode()
prefix, err := orgID.Encode()
if err != nil {
return nil, 0, influxdb.ErrInvalidTaskID
}
@ -322,7 +278,7 @@ func (s *Service) findTasksByOrg(ctx context.Context, tx Tx, filter influxdb.Tas
)
// we can filter by orgID
if filter.After != nil {
key, err = taskOrgKey(org.ID, *filter.After)
key, err = taskOrgKey(orgID, *filter.After)
if err != nil {
return nil, 0, err
}
@ -360,7 +316,7 @@ func (s *Service) findTasksByOrg(ctx context.Context, tx Tx, filter influxdb.Tas
}
// If the new task doesn't belong to the org we have looped outside the org filter
if org != nil && t.OrganizationID != org.ID {
if t.OrganizationID != orgID {
break
}
@ -492,40 +448,36 @@ func (s *Service) findAllTasks(ctx context.Context, tx Tx, filter influxdb.TaskF
// CreateTask creates a new task.
// The owner of the task is inferred from the authorizer associated with ctx.
func (s *Service) CreateTask(ctx context.Context, tc influxdb.TaskCreate) (*influxdb.Task, error) {
var orgFilter influxdb.OrganizationFilter
if tc.Organization != "" {
orgFilter.Name = &tc.Organization
} else if tc.OrganizationID.Valid() {
orgFilter.ID = &tc.OrganizationID
} else {
return nil, errors.New("organization required")
}
org, err := s.orgs.FindOrganization(ctx, orgFilter)
if err != nil {
return nil, err
}
var t *influxdb.Task
err := s.kv.Update(ctx, func(tx Tx) error {
task, err := s.createTask(ctx, tx, tc)
err = s.kv.Update(ctx, func(tx Tx) error {
task, err := s.createTask(ctx, tx, org, tc)
if err != nil {
return err
}
t = task
return nil
})
if err != nil {
return nil, err
}
return t, nil
return t, err
}
func (s *Service) createTask(ctx context.Context, tx Tx, tc influxdb.TaskCreate) (*influxdb.Task, error) {
var err error
var org *influxdb.Organization
if tc.OrganizationID.Valid() {
org, err = s.findOrganizationByID(ctx, tx, tc.OrganizationID)
if err != nil {
return nil, err
}
} else if tc.Organization != "" {
org, err = s.findOrganizationByName(ctx, tx, tc.Organization)
if err != nil {
return nil, err
}
}
if org == nil {
return nil, influxdb.ErrOrgNotFound
}
func (s *Service) createTask(ctx context.Context, tx Tx, org *influxdb.Organization, tc influxdb.TaskCreate) (*influxdb.Task, error) {
// TODO: Uncomment this once the checks/notifications no longer create tasks in kv
// confirm the owner is a real user.
// if _, err = s.findUserByID(ctx, tx, tc.OwnerID); err != nil {
@ -863,12 +815,6 @@ func (s *Service) deleteTask(ctx context.Context, tx Tx, id influxdb.ID) error {
return influxdb.ErrUnexpectedTaskBucketErr(err)
}
if err := s.deleteUserResourceMapping(ctx, tx, influxdb.UserResourceMappingFilter{
ResourceID: task.ID,
}); err != nil {
s.log.Debug("Error deleting user resource mapping for task", zap.Stringer("taskID", task.ID), zap.Error(err))
}
uid, _ := icontext.GetUserID(ctx)
return s.audit.Log(resource.Change{
Type: resource.Delete,

View File

@ -34,11 +34,6 @@ func TestBoltTaskService(t *testing.T) {
t.Fatal(err)
}
ctx, cancelFunc := context.WithCancel(context.Background())
service := kv.NewService(zaptest.NewLogger(t), store, kv.ServiceConfig{
FluxLanguageService: fluxlang.DefaultService,
})
tenantStore := tenant.NewStore(store)
ts := tenant.NewService(tenantStore)
@ -46,6 +41,11 @@ func TestBoltTaskService(t *testing.T) {
require.NoError(t, err)
authSvc := authorization.NewService(authStore, ts)
ctx, cancelFunc := context.WithCancel(context.Background())
service := kv.NewService(zaptest.NewLogger(t), store, ts, kv.ServiceConfig{
FluxLanguageService: fluxlang.DefaultService,
})
go func() {
<-ctx.Done()
close()
@ -100,21 +100,28 @@ func newService(t *testing.T, ctx context.Context, c clock.Clock) *testService {
ts.Store = store
ts.Service = kv.NewService(zaptest.NewLogger(t), store, kv.ServiceConfig{
tenantStore := tenant.NewStore(store)
tenantSvc := tenant.NewService(tenantStore)
authStore, err := authorization.NewStore(store)
require.NoError(t, err)
authSvc := authorization.NewService(authStore, tenantSvc)
ts.Service = kv.NewService(zaptest.NewLogger(t), store, tenantSvc, kv.ServiceConfig{
Clock: c,
FluxLanguageService: fluxlang.DefaultService,
})
ts.User = influxdb.User{Name: t.Name() + "-user"}
if err := ts.Service.CreateUser(ctx, &ts.User); err != nil {
if err := tenantSvc.CreateUser(ctx, &ts.User); err != nil {
t.Fatal(err)
}
ts.Org = influxdb.Organization{Name: t.Name() + "-org"}
if err := ts.Service.CreateOrganization(ctx, &ts.Org); err != nil {
if err := tenantSvc.CreateOrganization(ctx, &ts.Org); err != nil {
t.Fatal(err)
}
if err := ts.Service.CreateUserResourceMapping(ctx, &influxdb.UserResourceMapping{
if err := tenantSvc.CreateUserResourceMapping(ctx, &influxdb.UserResourceMapping{
ResourceType: influxdb.OrgsResourceType,
ResourceID: ts.Org.ID,
UserID: ts.User.ID,
@ -128,7 +135,7 @@ func newService(t *testing.T, ctx context.Context, c clock.Clock) *testService {
UserID: ts.User.ID,
Permissions: influxdb.OperPermissions(),
}
if err := ts.Service.CreateAuthorization(context.Background(), &ts.Auth); err != nil {
if err := authSvc.CreateAuthorization(context.Background(), &ts.Auth); err != nil {
t.Fatal(err)
}
@ -269,20 +276,27 @@ func TestTaskRunCancellation(t *testing.T) {
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
service := kv.NewService(zaptest.NewLogger(t), store, kv.ServiceConfig{
tenantStore := tenant.NewStore(store)
tenantSvc := tenant.NewService(tenantStore)
authStore, err := authorization.NewStore(store)
require.NoError(t, err)
authSvc := authorization.NewService(authStore, tenantSvc)
service := kv.NewService(zaptest.NewLogger(t), store, tenantSvc, kv.ServiceConfig{
FluxLanguageService: fluxlang.DefaultService,
})
u := &influxdb.User{Name: t.Name() + "-user"}
if err := service.CreateUser(ctx, u); err != nil {
if err := tenantSvc.CreateUser(ctx, u); err != nil {
t.Fatal(err)
}
o := &influxdb.Organization{Name: t.Name() + "-org"}
if err := service.CreateOrganization(ctx, o); err != nil {
if err := tenantSvc.CreateOrganization(ctx, o); err != nil {
t.Fatal(err)
}
if err := service.CreateUserResourceMapping(ctx, &influxdb.UserResourceMapping{
if err := tenantSvc.CreateUserResourceMapping(ctx, &influxdb.UserResourceMapping{
ResourceType: influxdb.OrgsResourceType,
ResourceID: o.ID,
UserID: u.ID,
@ -296,7 +310,7 @@ func TestTaskRunCancellation(t *testing.T) {
UserID: u.ID,
Permissions: influxdb.OperPermissions(),
}
if err := service.CreateAuthorization(context.Background(), &authz); err != nil {
if err := authSvc.CreateAuthorization(context.Background(), &authz); err != nil {
t.Fatal(err)
}

View File

@ -1,97 +0,0 @@
package kv_test
import (
"context"
"fmt"
"testing"
"github.com/influxdata/influxdb/v2"
icontext "github.com/influxdata/influxdb/v2/context"
"github.com/influxdata/influxdb/v2/kv"
itesting "github.com/influxdata/influxdb/v2/testing"
"go.uber.org/zap/zaptest"
)
func TestBoltTenantService(t *testing.T) {
t.Skip("some tests fail using `*kv.Service` as `influxdb.TenantService`, see the NOTEs in `testing/tenant.go`.")
itesting.TenantService(t, initBoltTenantService)
}
//lint:ignore U1000 erroneously flagged by staticcheck since it is used in skipped tests
func initBoltTenantService(t *testing.T, f itesting.TenantFields) (influxdb.TenantService, func()) {
s, closeStore, err := NewTestBoltStore(t)
if err != nil {
t.Fatalf("failed to create new bolt kv store: %v", err)
}
ctx := context.Background()
svc := kv.NewService(zaptest.NewLogger(t), s)
// Create a mapping from user-specified IDs to kv generated ones.
// This way we can map the user intent to the actual IDs.
uIDs := make(map[influxdb.ID]influxdb.ID, len(f.Users))
oIDs := make(map[influxdb.ID]influxdb.ID, len(f.Organizations))
bIDs := make(map[influxdb.ID]influxdb.ID, len(f.Buckets))
for _, u := range f.Users {
id := u.ID
if err := svc.CreateUser(ctx, u); err != nil {
t.Fatalf("error populating users: %v", err)
}
uIDs[id] = u.ID
}
for i := range f.Passwords {
if err := svc.SetPassword(ctx, f.Users[i].ID, f.Passwords[i]); err != nil {
t.Fatalf("error setting passsword user, %s %s: %v", f.Users[i].Name, f.Passwords[i], err)
}
}
// Preserve only the urms that do not express ownership of an org.
// Those will be expressed on org creation through the authorizer.
urms := f.UserResourceMappings[:0]
orgOwners := make(map[influxdb.ID]influxdb.ID)
for _, urm := range f.UserResourceMappings {
if urm.ResourceType == influxdb.OrgsResourceType && urm.UserType == influxdb.Owner {
orgOwners[urm.ResourceID] = urm.UserID
} else {
urms = append(urms, urm)
}
}
for _, o := range f.Organizations {
id := o.ID
ctx := context.Background()
ctx = icontext.SetAuthorizer(ctx, &influxdb.Authorization{
UserID: uIDs[orgOwners[id]],
})
if err := svc.CreateOrganization(ctx, o); err != nil {
t.Fatalf("error populating orgs: %v", err)
}
oIDs[id] = o.ID
}
for _, b := range f.Buckets {
id := b.ID
b.OrgID = oIDs[b.OrgID]
if err := svc.CreateBucket(ctx, b); err != nil {
t.Fatalf("error populating buckets: %v", err)
}
bIDs[id] = b.ID
}
for _, urm := range urms {
urm.UserID = uIDs[urm.UserID]
switch urm.ResourceType {
case influxdb.BucketsResourceType:
urm.ResourceID = bIDs[urm.ResourceID]
case influxdb.OrgsResourceType:
urm.ResourceID = oIDs[urm.ResourceID]
default:
panic(fmt.Errorf("invalid resource type: %v", urm.ResourceType))
}
if err := svc.CreateUserResourceMapping(ctx, urm); err != nil {
t.Fatalf("error populating urms: %v", err)
}
}
return svc, func() {
closeStore()
}
}

View File

@ -1,98 +0,0 @@
package kv
import (
"context"
"fmt"
influxdb "github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/kit/tracing"
)
// UnexpectedIndexError is used when the error comes from an internal system.
func UnexpectedIndexError(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EInternal,
Msg: fmt.Sprintf("unexpected error retrieving index; Err: %v", err),
Op: "kv/index",
}
}
// NotUniqueError is used when attempting to create a resource that already
// exists.
var NotUniqueError = &influxdb.Error{
Code: influxdb.EConflict,
Msg: "name already exists",
}
// NotUniqueIDError is used when attempting to create an org or bucket that already
// exists.
var NotUniqueIDError = &influxdb.Error{
Code: influxdb.EConflict,
Msg: "ID already exists",
}
func (s *Service) unique(ctx context.Context, tx Tx, indexBucket, indexKey []byte) error {
bucket, err := tx.Bucket(indexBucket)
if err != nil {
return UnexpectedIndexError(err)
}
_, err = bucket.Get(indexKey)
// if not found then this is _unique_.
if IsNotFound(err) {
return nil
}
// no error means this is not unique
if err == nil {
return NotUniqueError
}
// any other error is some sort of internal server error
return UnexpectedIndexError(err)
}
func (s *Service) uniqueID(ctx context.Context, tx Tx, bucket []byte, id influxdb.ID) error {
span, _ := tracing.StartSpanFromContext(ctx)
defer span.Finish()
encodedID, err := id.Encode()
if err != nil {
return &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
}
}
b, err := tx.Bucket(bucket)
if err != nil {
return err
}
_, err = b.Get(encodedID)
if IsNotFound(err) {
return nil
}
return NotUniqueIDError
}
// generateSafeID attempts to create ids for buckets
// and orgs that are without backslash, commas, and spaces, BUT ALSO do not already exist.
func (s *Service) generateSafeID(ctx context.Context, tx Tx, bucket []byte, gen influxdb.IDGenerator) (influxdb.ID, error) {
for i := 0; i < MaxIDGenerationN; i++ {
id := gen.ID()
err := s.uniqueID(ctx, tx, bucket, id)
if err == nil {
return id, nil
}
if err == NotUniqueIDError {
continue
}
return influxdb.InvalidID(), err
}
return influxdb.InvalidID(), ErrFailureGeneratingID
}

497
kv/urm.go
View File

@ -1,497 +0,0 @@
package kv
import (
"context"
"encoding/json"
"fmt"
"github.com/influxdata/influxdb/v2"
icontext "github.com/influxdata/influxdb/v2/context"
"github.com/influxdata/influxdb/v2/kit/tracing"
"go.uber.org/zap"
)
var (
urmBucket = []byte("userresourcemappingsv1")
// ErrInvalidURMID is used when the service was provided
// an invalid ID format.
ErrInvalidURMID = &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "provided user resource mapping ID has invalid format",
}
// ErrURMNotFound is used when the user resource mapping is not found.
ErrURMNotFound = &influxdb.Error{
Msg: "user to resource mapping not found",
Code: influxdb.ENotFound,
}
// URMByUserIndeMappingx is the mapping description of an index
// between a user and a URM
URMByUserIndexMapping = NewIndexMapping(
urmBucket,
[]byte("userresourcemappingsbyuserindexv1"),
func(v []byte) ([]byte, error) {
var urm influxdb.UserResourceMapping
if err := json.Unmarshal(v, &urm); err != nil {
return nil, err
}
id, _ := urm.UserID.Encode()
return id, nil
},
)
)
// UnavailableURMServiceError is used if we aren't able to interact with the
// store, it means the store is not available at the moment (e.g. network).
func UnavailableURMServiceError(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EInternal,
Msg: fmt.Sprintf("Unable to connect to resource mapping service. Please try again; Err: %v", err),
Op: "kv/userResourceMapping",
}
}
// CorruptURMError is used when the config cannot be unmarshalled from the
// bytes stored in the kv.
func CorruptURMError(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EInternal,
Msg: fmt.Sprintf("Unknown internal user resource mapping data error; Err: %v", err),
Op: "kv/userResourceMapping",
}
}
// ErrUnprocessableMapping is used when a user resource mapping is not able to be converted to JSON.
func ErrUnprocessableMapping(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EUnprocessableEntity,
Msg: fmt.Sprintf("unable to convert mapping of user to resource into JSON; Err %v", err),
}
}
// NonUniqueMappingError is an internal error when a user already has
// been mapped to a resource
func NonUniqueMappingError(userID influxdb.ID) error {
return &influxdb.Error{
Code: influxdb.EInternal,
Msg: fmt.Sprintf("Unexpected error when assigning user to a resource: mapping for user %s already exists", userID.String()),
}
}
func filterMappingsFn(filter influxdb.UserResourceMappingFilter) func(m *influxdb.UserResourceMapping) bool {
return func(mapping *influxdb.UserResourceMapping) bool {
return (!filter.UserID.Valid() || (filter.UserID == mapping.UserID)) &&
(!filter.ResourceID.Valid() || (filter.ResourceID == mapping.ResourceID)) &&
(filter.UserType == "" || (filter.UserType == mapping.UserType)) &&
(filter.ResourceType == "" || (filter.ResourceType == mapping.ResourceType))
}
}
// FindUserResourceMappings returns a list of UserResourceMappings that match filter and the total count of matching mappings.
func (s *Service) FindUserResourceMappings(ctx context.Context, filter influxdb.UserResourceMappingFilter, opt ...influxdb.FindOptions) ([]*influxdb.UserResourceMapping, int, error) {
var ms []*influxdb.UserResourceMapping
err := s.kv.View(ctx, func(tx Tx) error {
var err error
ms, err = s.findUserResourceMappings(ctx, tx, filter)
return err
})
if err != nil {
return nil, 0, err
}
return ms, len(ms), nil
}
func userResourceMappingPredicate(filter influxdb.UserResourceMappingFilter) CursorPredicateFunc {
switch {
case filter.ResourceID.Valid() && filter.UserID.Valid():
keyPredicate := filter.ResourceID.String() + filter.UserID.String()
return func(key, _ []byte) bool {
return len(key) >= 32 && string(key[:32]) == keyPredicate
}
case !filter.ResourceID.Valid() && filter.UserID.Valid():
keyPredicate := filter.UserID.String()
return func(key, _ []byte) bool {
return len(key) >= 32 && string(key[16:32]) == keyPredicate
}
case filter.ResourceID.Valid() && !filter.UserID.Valid():
keyPredicate := filter.ResourceID.String()
return func(key, _ []byte) bool {
return len(key) >= 16 && string(key[:16]) == keyPredicate
}
default:
return nil
}
}
func (s *Service) findUserResourceMappings(ctx context.Context, tx Tx, filter influxdb.UserResourceMappingFilter) (ms []*influxdb.UserResourceMapping, _ error) {
filterFn := filterMappingsFn(filter)
if filter.UserID.Valid() {
// urm by user index lookup
userID, _ := filter.UserID.Encode()
if err := s.urmByUserIndex.Walk(ctx, tx, userID, func(k, v []byte) (bool, error) {
m := &influxdb.UserResourceMapping{}
if err := json.Unmarshal(v, m); err != nil {
return false, CorruptURMError(err)
}
if filterFn(m) {
ms = append(ms, m)
}
return true, nil
}); err != nil {
return nil, err
}
if len(ms) > 0 {
return
}
}
pred := userResourceMappingPredicate(filter)
err := s.forEachUserResourceMapping(ctx, tx, pred, func(m *influxdb.UserResourceMapping) bool {
if filterFn(m) {
ms = append(ms, m)
}
return true
})
return ms, err
}
func (s *Service) findUserResourceMapping(ctx context.Context, tx Tx, filter influxdb.UserResourceMappingFilter) (*influxdb.UserResourceMapping, error) {
ms, err := s.findUserResourceMappings(ctx, tx, filter)
if err != nil {
return nil, err
}
if len(ms) == 0 {
return nil, ErrURMNotFound
}
return ms[0], nil
}
// CreateUserResourceMapping associates a user to a resource either as a member
// or owner.
func (s *Service) CreateUserResourceMapping(ctx context.Context, m *influxdb.UserResourceMapping) error {
return s.kv.Update(ctx, func(tx Tx) error {
return s.createUserResourceMapping(ctx, tx, m)
})
}
// CreateUserResourceMappingTx is used when importing kv as a library
func (s *Service) CreateUserResourceMappingTx(ctx context.Context, tx Tx, m *influxdb.UserResourceMapping) error {
return s.createUserResourceMapping(ctx, tx, m)
}
func (s *Service) createUserResourceMapping(ctx context.Context, tx Tx, m *influxdb.UserResourceMapping) error {
span, ctx := tracing.StartSpanFromContext(ctx)
defer span.Finish()
if err := s.uniqueUserResourceMapping(ctx, tx, m); err != nil {
return err
}
v, err := json.Marshal(m)
if err != nil {
return ErrUnprocessableMapping(err)
}
key, err := userResourceKey(m)
if err != nil {
return err
}
b, err := tx.Bucket(urmBucket)
if err != nil {
return UnavailableURMServiceError(err)
}
if err := b.Put(key, v); err != nil {
return UnavailableURMServiceError(err)
}
userID, err := m.UserID.Encode()
if err != nil {
return err
}
// insert urm into by user index
if err := s.urmByUserIndex.Insert(tx, userID, key); err != nil {
return err
}
if m.ResourceType == influxdb.OrgsResourceType {
return s.createOrgDependentMappings(ctx, tx, m)
}
return nil
}
// This method creates the user/resource mappings for resources that belong to an organization.
func (s *Service) createOrgDependentMappings(ctx context.Context, tx Tx, m *influxdb.UserResourceMapping) error {
span, ctx := tracing.StartSpanFromContext(ctx)
defer span.Finish()
bf := influxdb.BucketFilter{OrganizationID: &m.ResourceID}
bs, err := s.findBuckets(ctx, tx, bf)
if err != nil {
return err
}
for _, b := range bs {
m := &influxdb.UserResourceMapping{
ResourceType: influxdb.BucketsResourceType,
ResourceID: b.ID,
UserType: m.UserType,
UserID: m.UserID,
}
if err := s.createUserResourceMapping(ctx, tx, m); err != nil {
return err
}
// TODO(desa): add support for all other resource types.
}
return nil
}
func userResourceKey(m *influxdb.UserResourceMapping) ([]byte, error) {
encodedResourceID, err := m.ResourceID.Encode()
if err != nil {
return nil, ErrInvalidURMID
}
encodedUserID, err := m.UserID.Encode()
if err != nil {
return nil, ErrInvalidURMID
}
key := make([]byte, len(encodedResourceID)+len(encodedUserID))
copy(key, encodedResourceID)
copy(key[len(encodedResourceID):], encodedUserID)
return key, nil
}
func (s *Service) forEachUserResourceMapping(ctx context.Context, tx Tx, pred CursorPredicateFunc, fn func(*influxdb.UserResourceMapping) bool) error {
b, err := tx.Bucket(urmBucket)
if err != nil {
return UnavailableURMServiceError(err)
}
var cur Cursor
if pred != nil {
cur, err = b.Cursor(WithCursorHintPredicate(pred))
} else {
cur, err = b.Cursor()
}
if err != nil {
return UnavailableURMServiceError(err)
}
for k, v := cur.First(); k != nil; k, v = cur.Next() {
m := &influxdb.UserResourceMapping{}
if err := json.Unmarshal(v, m); err != nil {
return CorruptURMError(err)
}
if !fn(m) {
break
}
}
return nil
}
func (s *Service) uniqueUserResourceMapping(ctx context.Context, tx Tx, m *influxdb.UserResourceMapping) error {
key, err := userResourceKey(m)
if err != nil {
return err
}
b, err := tx.Bucket(urmBucket)
if err != nil {
return UnavailableURMServiceError(err)
}
_, err = b.Get(key)
if !IsNotFound(err) {
return NonUniqueMappingError(m.UserID)
}
return nil
}
// DeleteUserResourceMapping deletes a user resource mapping.
func (s *Service) DeleteUserResourceMapping(ctx context.Context, resourceID influxdb.ID, userID influxdb.ID) error {
return s.kv.Update(ctx, func(tx Tx) error {
// TODO(goller): I don't think this find is needed as delete also finds.
m, err := s.findUserResourceMapping(ctx, tx, influxdb.UserResourceMappingFilter{
ResourceID: resourceID,
UserID: userID,
})
if err != nil {
return err
}
filter := influxdb.UserResourceMappingFilter{
ResourceID: resourceID,
UserID: userID,
}
if err := s.deleteUserResourceMapping(ctx, tx, filter); err != nil {
return err
}
if m.ResourceType == influxdb.OrgsResourceType {
return s.deleteOrgDependentMappings(ctx, tx, m)
}
return nil
})
}
func (s *Service) deleteUserResourceMapping(ctx context.Context, tx Tx, filter influxdb.UserResourceMappingFilter) error {
// TODO(goller): do we really need to find here? Seems like a Get is
// good enough.
ms, err := s.findUserResourceMappings(ctx, tx, filter)
if err != nil {
return err
}
if len(ms) == 0 {
return ErrURMNotFound
}
key, err := userResourceKey(ms[0])
if err != nil {
return err
}
b, err := tx.Bucket(urmBucket)
if err != nil {
return UnavailableURMServiceError(err)
}
_, err = b.Get(key)
if IsNotFound(err) {
return ErrURMNotFound
}
if err != nil {
return UnavailableURMServiceError(err)
}
if err := b.Delete(key); err != nil {
return UnavailableURMServiceError(err)
}
userID, err := ms[0].UserID.Encode()
if err != nil {
return err
}
// remove user resource mapping from by user index
if err := s.urmByUserIndex.Delete(tx, userID, key); err != nil {
return err
}
return nil
}
func (s *Service) deleteUserResourceMappings(ctx context.Context, tx Tx, filter influxdb.UserResourceMappingFilter) error {
ms, err := s.findUserResourceMappings(ctx, tx, filter)
if err != nil {
return err
}
for _, m := range ms {
key, err := userResourceKey(m)
if err != nil {
return err
}
b, err := tx.Bucket(urmBucket)
if err != nil {
return UnavailableURMServiceError(err)
}
_, err = b.Get(key)
if IsNotFound(err) {
return ErrURMNotFound
}
if err != nil {
return UnavailableURMServiceError(err)
}
if err := b.Delete(key); err != nil {
return UnavailableURMServiceError(err)
}
userID, err := m.UserID.Encode()
if err != nil {
return err
}
// remove user resource mapping from by user index
if err := s.urmByUserIndex.Delete(tx, userID, key); err != nil {
return err
}
}
return nil
}
// This method deletes the user/resource mappings for resources that belong to an organization.
func (s *Service) deleteOrgDependentMappings(ctx context.Context, tx Tx, m *influxdb.UserResourceMapping) error {
bf := influxdb.BucketFilter{OrganizationID: &m.ResourceID}
bs, err := s.findBuckets(ctx, tx, bf)
if err != nil {
return err
}
for _, b := range bs {
if err := s.deleteUserResourceMapping(ctx, tx, influxdb.UserResourceMappingFilter{
ResourceType: influxdb.BucketsResourceType,
ResourceID: b.ID,
UserID: m.UserID,
}); err != nil {
if influxdb.ErrorCode(err) == influxdb.ENotFound {
s.log.Info("URM bucket is missing", zap.Stringer("orgID", m.ResourceID))
continue
}
return err
}
// TODO(desa): add support for all other resource types.
}
return nil
}
func (s *Service) addResourceOwner(ctx context.Context, tx Tx, rt influxdb.ResourceType, id influxdb.ID) error {
a, err := icontext.GetAuthorizer(ctx)
if err != nil {
return &influxdb.Error{
Code: influxdb.EInternal,
Msg: fmt.Sprintf("could not find authorizer on context when adding user to resource type %s", rt),
}
}
urm := &influxdb.UserResourceMapping{
ResourceID: id,
ResourceType: rt,
UserID: a.GetUserID(),
UserType: influxdb.Owner,
}
if err := s.createUserResourceMapping(ctx, tx, urm); err != nil {
return &influxdb.Error{
Code: influxdb.EInternal,
Msg: "could not create user resource mapping",
Err: err,
}
}
return nil
}

View File

@ -1,70 +0,0 @@
package kv
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/influxdata/influxdb/v2"
)
func Test_userResourceMappingPredicate(t *testing.T) {
mk := func(rid, uid influxdb.ID) (urm *influxdb.UserResourceMapping, key []byte) {
t.Helper()
urm = &influxdb.UserResourceMapping{UserID: rid, ResourceID: uid}
key, err := userResourceKey(urm)
if err != nil {
t.Fatal(err)
}
return urm, key
}
t.Run("match only ResourceID", func(t *testing.T) {
u, k := mk(10, 20)
f := influxdb.UserResourceMappingFilter{ResourceID: u.ResourceID}
fn := userResourceMappingPredicate(f)
if got, exp := fn(k, nil), true; got != exp {
t.Errorf("unexpected result -got/+exp\n%s", cmp.Diff(got, exp))
}
_, k = mk(10, 21)
if got, exp := fn(k, nil), false; got != exp {
t.Errorf("unexpected result -got/+exp\n%s", cmp.Diff(got, exp))
}
})
t.Run("match only UserID", func(t *testing.T) {
u, k := mk(10, 20)
f := influxdb.UserResourceMappingFilter{UserID: u.UserID}
fn := userResourceMappingPredicate(f)
if got, exp := fn(k, nil), true; got != exp {
t.Errorf("unexpected result -got/+exp\n%s", cmp.Diff(got, exp))
}
_, k = mk(11, 20)
if got, exp := fn(k, nil), false; got != exp {
t.Errorf("unexpected result -got/+exp\n%s", cmp.Diff(got, exp))
}
})
t.Run("match ResourceID and UserID", func(t *testing.T) {
u, k := mk(10, 20)
f := influxdb.UserResourceMappingFilter{ResourceID: u.ResourceID, UserID: u.UserID}
fn := userResourceMappingPredicate(f)
if got, exp := fn(k, nil), true; got != exp {
t.Errorf("unexpected result -got/+exp\n%s", cmp.Diff(got, exp))
}
_, k = mk(11, 20)
if got, exp := fn(k, nil), false; got != exp {
t.Errorf("unexpected result -got/+exp\n%s", cmp.Diff(got, exp))
}
})
t.Run("no match function", func(t *testing.T) {
f := influxdb.UserResourceMappingFilter{}
fn := userResourceMappingPredicate(f)
if fn != nil {
t.Errorf("expected nil")
}
})
}

View File

@ -1,136 +0,0 @@
package kv_test
import (
"context"
"testing"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/inmem"
"github.com/influxdata/influxdb/v2/kv"
"github.com/influxdata/influxdb/v2/snowflake"
influxdbtesting "github.com/influxdata/influxdb/v2/testing"
"go.uber.org/zap/zaptest"
)
type testable interface {
Helper()
Logf(string, ...interface{})
Error(args ...interface{})
Errorf(string, ...interface{})
Fail()
Failed() bool
Name() string
FailNow()
Fatal(args ...interface{})
Fatalf(format string, args ...interface{})
}
func TestBoltUserResourceMappingService(t *testing.T) {
influxdbtesting.UserResourceMappingService(initURMServiceFunc(NewTestBoltStore), t)
}
func TestInmemUserResourceMappingService(t *testing.T) {
influxdbtesting.UserResourceMappingService(initURMServiceFunc(NewTestInmemStore), t)
}
type userResourceMappingTestFunc func(influxdbtesting.UserResourceFields, *testing.T) (influxdb.UserResourceMappingService, func())
func initURMServiceFunc(storeFn func(*testing.T) (kv.SchemaStore, func(), error), confs ...kv.ServiceConfig) userResourceMappingTestFunc {
return func(f influxdbtesting.UserResourceFields, t *testing.T) (influxdb.UserResourceMappingService, func()) {
s, closeStore, err := storeFn(t)
if err != nil {
t.Fatalf("failed to create new kv store: %v", err)
}
svc, closeSvc := initUserResourceMappingService(s, f, t, confs...)
return svc, func() {
closeSvc()
closeStore()
}
}
}
func initUserResourceMappingService(s kv.SchemaStore, f influxdbtesting.UserResourceFields, t testable, configs ...kv.ServiceConfig) (influxdb.UserResourceMappingService, func()) {
ctx := context.Background()
svc := kv.NewService(zaptest.NewLogger(t), s, configs...)
for _, o := range f.Organizations {
if err := svc.CreateOrganization(ctx, o); err != nil {
t.Fatalf("failed to create org %q", err)
}
}
for _, u := range f.Users {
if err := svc.CreateUser(ctx, u); err != nil {
t.Fatalf("failed to create user %q", err)
}
}
for _, b := range f.Buckets {
b.ID = svc.BucketIDs.ID()
if err := svc.PutBucket(ctx, b); err != nil {
t.Fatalf("failed to create bucket %q", err)
}
}
for _, m := range f.UserResourceMappings {
if err := svc.CreateUserResourceMapping(ctx, m); err != nil {
t.Fatalf("failed to populate mappings %q", err)
}
}
return svc, func() {
for _, m := range f.UserResourceMappings {
if err := svc.DeleteUserResourceMapping(ctx, m.ResourceID, m.UserID); err != nil {
t.Logf("failed to remove user resource mapping: %v", err)
}
}
for _, b := range f.Buckets {
if err := svc.DeleteBucket(ctx, b.ID); err != nil {
t.Logf("failed to delete org", err)
}
}
for _, u := range f.Users {
if err := svc.DeleteUser(ctx, u.ID); err != nil {
t.Fatalf("failed to delete user %q", err)
}
}
for _, o := range f.Organizations {
if err := svc.DeleteOrganization(ctx, o.ID); err != nil {
t.Logf("failed to delete org", err)
}
}
}
}
func BenchmarkReadURMs(b *testing.B) {
urms := influxdbtesting.UserResourceFields{
UserResourceMappings: make([]*influxdb.UserResourceMapping, 10000),
}
idgen := snowflake.NewDefaultIDGenerator()
users := make([]influxdb.ID, 10)
for i := 0; i < 10; i++ {
users[i] = idgen.ID()
}
for i := 0; i < 10000; i++ {
urms.UserResourceMappings[i] = &influxdb.UserResourceMapping{
ResourceID: idgen.ID(),
UserID: users[i%len(users)],
UserType: influxdb.Member,
ResourceType: influxdb.BucketsResourceType,
}
}
st := inmem.NewKVStore()
initUserResourceMappingService(st, urms, b)
svc := kv.NewService(zaptest.NewLogger(b), st)
b.ResetTimer()
for i := 0; i < b.N; i++ {
svc.FindUserResourceMappings(context.Background(), influxdb.UserResourceMappingFilter{
UserID: users[0],
})
}
}

View File

@ -1,622 +0,0 @@
package kv
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/influxdata/influxdb/v2"
icontext "github.com/influxdata/influxdb/v2/context"
)
var (
userBucket = []byte("usersv1")
userIndex = []byte("userindexv1")
)
var _ influxdb.UserService = (*Service)(nil)
var _ influxdb.UserOperationLogService = (*Service)(nil)
func (s *Service) userBucket(tx Tx) (Bucket, error) {
b, err := tx.Bucket([]byte(userBucket))
if err != nil {
return nil, UnexpectedUserBucketError(err)
}
return b, nil
}
func (s *Service) userIndexBucket(tx Tx) (Bucket, error) {
b, err := tx.Bucket([]byte(userIndex))
if err != nil {
return nil, UnexpectedUserIndexError(err)
}
return b, nil
}
// FindUserByID retrieves a user by id.
func (s *Service) FindUserByID(ctx context.Context, id influxdb.ID) (*influxdb.User, error) {
var u *influxdb.User
err := s.kv.View(ctx, func(tx Tx) error {
usr, err := s.findUserByID(ctx, tx, id)
if err != nil {
return err
}
u = usr
return nil
})
if err != nil {
return nil, err
}
return u, nil
}
func (s *Service) findUserByID(ctx context.Context, tx Tx, id influxdb.ID) (*influxdb.User, error) {
encodedID, err := id.Encode()
if err != nil {
return nil, InvalidUserIDError(err)
}
b, err := s.userBucket(tx)
if err != nil {
return nil, err
}
v, err := b.Get(encodedID)
if IsNotFound(err) {
return nil, ErrUserNotFound
}
if err != nil {
return nil, ErrInternalUserServiceError(err)
}
return UnmarshalUser(v)
}
// UnmarshalUser turns the stored byte slice in the kv into a *influxdb.User.
func UnmarshalUser(v []byte) (*influxdb.User, error) {
u := &influxdb.User{}
if err := json.Unmarshal(v, u); err != nil {
return nil, ErrCorruptUser(err)
}
return u, nil
}
// MarshalUser turns an *influxdb.User into a byte slice.
func MarshalUser(u *influxdb.User) ([]byte, error) {
v, err := json.Marshal(u)
if err != nil {
return nil, ErrUnprocessableUser(err)
}
return v, nil
}
// FindUserByName returns a user by name for a particular user.
func (s *Service) FindUserByName(ctx context.Context, n string) (*influxdb.User, error) {
var u *influxdb.User
err := s.kv.View(ctx, func(tx Tx) error {
usr, err := s.findUserByName(ctx, tx, n)
if err != nil {
return err
}
u = usr
return nil
})
return u, err
}
func (s *Service) findUserByName(ctx context.Context, tx Tx, n string) (*influxdb.User, error) {
b, err := s.userIndexBucket(tx)
if err != nil {
return nil, err
}
uid, err := b.Get(userIndexKey(n))
if err == ErrKeyNotFound {
return nil, ErrUserNotFound
}
if err != nil {
return nil, ErrInternalUserServiceError(err)
}
var id influxdb.ID
if err := id.Decode(uid); err != nil {
return nil, ErrCorruptUserID(err)
}
return s.findUserByID(ctx, tx, id)
}
// FindUser retrieves a user using an arbitrary user filter.
// Filters using ID, or Name should be efficient.
// Other filters will do a linear scan across users until it finds a match.
func (s *Service) FindUser(ctx context.Context, filter influxdb.UserFilter) (*influxdb.User, error) {
if filter.ID != nil {
u, err := s.FindUserByID(ctx, *filter.ID)
if err != nil {
return nil, err
}
return u, nil
}
if filter.Name != nil {
return s.FindUserByName(ctx, *filter.Name)
}
return nil, ErrUserNotFound
}
func filterUsersFn(filter influxdb.UserFilter) func(u *influxdb.User) bool {
if filter.ID != nil {
return func(u *influxdb.User) bool {
return u.ID.Valid() && u.ID == *filter.ID
}
}
if filter.Name != nil {
return func(u *influxdb.User) bool {
return u.Name == *filter.Name
}
}
return func(u *influxdb.User) bool { return true }
}
// FindUsers retrieves all users that match an arbitrary user filter.
// Filters using ID, or Name should be efficient.
// Other filters will do a linear scan across all users searching for a match.
func (s *Service) FindUsers(ctx context.Context, filter influxdb.UserFilter, opt ...influxdb.FindOptions) ([]*influxdb.User, int, error) {
if filter.ID != nil {
u, err := s.FindUserByID(ctx, *filter.ID)
if err != nil {
return nil, 0, err
}
return []*influxdb.User{u}, 1, nil
}
if filter.Name != nil {
u, err := s.FindUserByName(ctx, *filter.Name)
if err != nil {
return nil, 0, err
}
return []*influxdb.User{u}, 1, nil
}
us := []*influxdb.User{}
filterFn := filterUsersFn(filter)
err := s.kv.View(ctx, func(tx Tx) error {
return s.forEachUser(ctx, tx, func(u *influxdb.User) bool {
if filterFn(u) {
us = append(us, u)
}
return true
})
})
if err != nil {
return nil, 0, err
}
return us, len(us), nil
}
// CreateUser creates a influxdb user and sets b.ID.
func (s *Service) CreateUser(ctx context.Context, u *influxdb.User) error {
return s.kv.Update(ctx, func(tx Tx) error {
return s.createUser(ctx, tx, u)
})
}
// CreateUserTx is used when importing kv as a library
func (s *Service) CreateUserTx(ctx context.Context, tx Tx, u *influxdb.User) error {
return s.createUser(ctx, tx, u)
}
func (s *Service) createUser(ctx context.Context, tx Tx, u *influxdb.User) error {
if err := s.uniqueUserName(ctx, tx, u); err != nil {
return err
}
u.ID = s.IDGenerator.ID()
u.Status = influxdb.Active
if err := s.appendUserEventToLog(ctx, tx, u.ID, userCreatedEvent); err != nil {
return err
}
return s.putUser(ctx, tx, u)
}
// PutUser will put a user without setting an ID.
func (s *Service) PutUser(ctx context.Context, u *influxdb.User) error {
return s.kv.Update(ctx, func(tx Tx) error {
return s.putUser(ctx, tx, u)
})
}
func (s *Service) putUser(ctx context.Context, tx Tx, u *influxdb.User) error {
v, err := MarshalUser(u)
if err != nil {
return err
}
encodedID, err := u.ID.Encode()
if err != nil {
return InvalidUserIDError(err)
}
idx, err := s.userIndexBucket(tx)
if err != nil {
return err
}
if err := idx.Put(userIndexKey(u.Name), encodedID); err != nil {
return ErrInternalUserServiceError(err)
}
b, err := s.userBucket(tx)
if err != nil {
return err
}
if err := b.Put(encodedID, v); err != nil {
return ErrInternalUserServiceError(err)
}
return nil
}
func userIndexKey(n string) []byte {
return []byte(n)
}
// forEachUser will iterate through all users while fn returns true.
func (s *Service) forEachUser(ctx context.Context, tx Tx, fn func(*influxdb.User) bool) error {
b, err := s.userBucket(tx)
if err != nil {
return err
}
cur, err := b.ForwardCursor(nil)
if err != nil {
return ErrInternalUserServiceError(err)
}
for k, v := cur.Next(); k != nil; k, v = cur.Next() {
u, err := UnmarshalUser(v)
if err != nil {
return err
}
if !fn(u) {
break
}
}
return nil
}
func (s *Service) uniqueUserName(ctx context.Context, tx Tx, u *influxdb.User) error {
key := userIndexKey(u.Name)
// if the name is not unique across all users in all organizations, then,
// do not allow creation.
err := s.unique(ctx, tx, userIndex, key)
if err == NotUniqueError {
return UserAlreadyExistsError(u.Name)
}
return err
}
// UpdateUser updates a user according the parameters set on upd.
func (s *Service) UpdateUser(ctx context.Context, id influxdb.ID, upd influxdb.UserUpdate) (*influxdb.User, error) {
var u *influxdb.User
err := s.kv.Update(ctx, func(tx Tx) error {
usr, err := s.updateUser(ctx, tx, id, upd)
if err != nil {
return err
}
u = usr
return nil
})
if err != nil {
return nil, err
}
return u, nil
}
func (s *Service) updateUser(ctx context.Context, tx Tx, id influxdb.ID, upd influxdb.UserUpdate) (*influxdb.User, error) {
u, err := s.findUserByID(ctx, tx, id)
if err != nil {
return nil, err
}
if upd.Name != nil {
if err := s.removeUserFromIndex(ctx, tx, id, u.Name); err != nil {
return nil, err
}
u.Name = *upd.Name
}
if upd.Status != nil {
if *upd.Status != u.Status && *upd.Status == "inactive" {
// Disable tasks
tasks, _, err := s.findTasksByUser(ctx, tx, influxdb.TaskFilter{User: &id})
if err != nil {
return nil, err
}
status := influxdb.TaskStatusInactive
for _, task := range tasks {
_, err := s.UpdateTask(ctx, task.ID, influxdb.TaskUpdate{Status: &status})
if err != nil {
return nil, err
}
}
}
u.Status = *upd.Status
}
if err := s.appendUserEventToLog(ctx, tx, u.ID, userUpdatedEvent); err != nil {
return nil, err
}
if err := s.putUser(ctx, tx, u); err != nil {
return nil, err
}
return u, nil
}
func (s *Service) removeUserFromIndex(ctx context.Context, tx Tx, id influxdb.ID, name string) error {
// Users are indexed by name and so the user index must be pruned
// when name is modified.
idx, err := s.userIndexBucket(tx)
if err != nil {
return err
}
if err := idx.Delete(userIndexKey(name)); err != nil {
return ErrInternalUserServiceError(err)
}
return nil
}
// DeleteUser deletes a user and prunes it from the index.
func (s *Service) DeleteUser(ctx context.Context, id influxdb.ID) error {
return s.kv.Update(ctx, func(tx Tx) error {
return s.deleteUser(ctx, tx, id)
})
}
func (s *Service) deleteUser(ctx context.Context, tx Tx, id influxdb.ID) error {
u, err := s.findUserByID(ctx, tx, id)
if err != nil {
return err
}
if err := s.deleteUsersAuthorizations(ctx, tx, id); err != nil {
return err
}
encodedID, err := id.Encode()
if err != nil {
return InvalidUserIDError(err)
}
idx, err := s.userIndexBucket(tx)
if err != nil {
return err
}
if err := idx.Delete(userIndexKey(u.Name)); err != nil {
return ErrInternalUserServiceError(err)
}
b, err := s.userBucket(tx)
if err != nil {
return err
}
if err := b.Delete(encodedID); err != nil {
return ErrInternalUserServiceError(err)
}
if err := s.deleteUserResourceMappings(ctx, tx, influxdb.UserResourceMappingFilter{
UserID: id,
}); err != nil {
return err
}
return nil
}
func (s *Service) FindPermissionForUser(ctx context.Context, uid influxdb.ID) (influxdb.PermissionSet, error) {
return nil, &influxdb.Error{
Code: influxdb.EInternal,
Msg: "not implemented",
}
}
func (s *Service) deleteUsersAuthorizations(ctx context.Context, tx Tx, id influxdb.ID) error {
authFilter := influxdb.AuthorizationFilter{
UserID: &id,
}
as, err := s.findAuthorizations(ctx, tx, authFilter)
if err != nil {
return err
}
for _, a := range as {
if err := s.deleteAuthorization(ctx, tx, a.ID); err != nil {
return err
}
}
return nil
}
// GetUserOperationLog retrieves a user operation log.
func (s *Service) GetUserOperationLog(ctx context.Context, id influxdb.ID, opts influxdb.FindOptions) ([]*influxdb.OperationLogEntry, int, error) {
// TODO(desa): might be worthwhile to allocate a slice of size opts.Limit
log := []*influxdb.OperationLogEntry{}
err := s.kv.View(ctx, func(tx Tx) error {
key, err := encodeUserOperationLogKey(id)
if err != nil {
return err
}
return s.ForEachLogEntryTx(ctx, tx, key, opts, func(v []byte, t time.Time) error {
e := &influxdb.OperationLogEntry{}
if err := json.Unmarshal(v, e); err != nil {
return err
}
e.Time = t
log = append(log, e)
return nil
})
})
if err != nil && err != ErrKeyValueLogBoundsNotFound {
return nil, 0, err
}
return log, len(log), nil
}
const userOperationLogKeyPrefix = "user"
// TODO(desa): what do we want these to be?
const (
userCreatedEvent = "User Created"
userUpdatedEvent = "User Updated"
)
func encodeUserOperationLogKey(id influxdb.ID) ([]byte, error) {
buf, err := id.Encode()
if err != nil {
return nil, err
}
return append([]byte(userOperationLogKeyPrefix), buf...), nil
}
func (s *Service) appendUserEventToLog(ctx context.Context, tx Tx, id influxdb.ID, st string) error {
e := &influxdb.OperationLogEntry{
Description: st,
}
// TODO(desa): this is fragile and non explicit since it requires an authorizer to be on context. It should be
// replaced with a higher level transaction so that adding to the log can take place in the http handler
// where the userID will exist explicitly.
a, err := icontext.GetAuthorizer(ctx)
if err == nil {
// Add the user to the log if you can, but don't error if its not there.
e.UserID = a.GetUserID()
}
v, err := json.Marshal(e)
if err != nil {
return err
}
k, err := encodeUserOperationLogKey(id)
if err != nil {
return err
}
return s.AddLogEntryTx(ctx, tx, k, v, s.Now())
}
var (
// ErrUserNotFound is used when the user is not found.
ErrUserNotFound = &influxdb.Error{
Msg: "user not found",
Code: influxdb.ENotFound,
}
)
// ErrInternalUserServiceError is used when the error comes from an internal system.
func ErrInternalUserServiceError(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EInternal,
Err: err,
}
}
// UserAlreadyExistsError is used when attempting to create a user with a name
// that already exists.
func UserAlreadyExistsError(n string) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EConflict,
Msg: fmt.Sprintf("user with name %s already exists", n),
}
}
// UnexpectedUserBucketError is used when the error comes from an internal system.
func UnexpectedUserBucketError(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EInternal,
Msg: fmt.Sprintf("unexpected error retrieving user bucket; Err: %v", err),
Op: "kv/userBucket",
}
}
// UnexpectedUserIndexError is used when the error comes from an internal system.
func UnexpectedUserIndexError(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EInternal,
Msg: fmt.Sprintf("unexpected error retrieving user index; Err: %v", err),
Op: "kv/userIndex",
}
}
// InvalidUserIDError is used when a service was provided an invalid ID.
// This is some sort of internal server error.
func InvalidUserIDError(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "user id provided is invalid",
Err: err,
}
}
// ErrCorruptUserID the ID stored in the Store is corrupt.
func ErrCorruptUserID(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "corrupt ID provided",
Err: err,
}
}
// ErrCorruptUser is used when the user cannot be unmarshalled from the bytes
// stored in the kv.
func ErrCorruptUser(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EInternal,
Msg: "user could not be unmarshalled",
Err: err,
Op: "kv/UnmarshalUser",
}
}
// ErrUnprocessableUser is used when a user is not able to be processed.
func ErrUnprocessableUser(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EUnprocessableEntity,
Msg: "user could not be marshalled",
Err: err,
Op: "kv/MarshalUser",
}
}

View File

@ -1,48 +0,0 @@
package kv_test
import (
"context"
"testing"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/kv"
influxdbtesting "github.com/influxdata/influxdb/v2/testing"
"go.uber.org/zap/zaptest"
)
func TestBoltUserService(t *testing.T) {
influxdbtesting.UserService(initBoltUserService, t)
}
func initBoltUserService(f influxdbtesting.UserFields, t *testing.T) (influxdb.UserService, string, func()) {
s, closeBolt, err := NewTestBoltStore(t)
if err != nil {
t.Fatalf("failed to create new kv store: %v", err)
}
svc, op, closeSvc := initUserService(s, f, t)
return svc, op, func() {
closeSvc()
closeBolt()
}
}
func initUserService(s kv.SchemaStore, f influxdbtesting.UserFields, t *testing.T) (influxdb.UserService, string, func()) {
ctx := context.Background()
svc := kv.NewService(zaptest.NewLogger(t), s)
svc.IDGenerator = f.IDGenerator
for _, u := range f.Users {
if err := svc.PutUser(ctx, u); err != nil {
t.Fatalf("failed to populate users")
}
}
return svc, kv.OpPrefix, func() {
for _, u := range f.Users {
if err := svc.DeleteUser(ctx, u.ID); err != nil {
t.Logf("failed to remove users: %v", err)
}
}
}
}

View File

@ -108,14 +108,6 @@ func (s *Service) findVariables(ctx context.Context, tx Tx, filter influxdb.Vari
return s.findOrganizationVariables(ctx, tx, *filter.OrganizationID)
}
if filter.Organization != nil {
o, err := s.findOrganizationByName(ctx, tx, *filter.Organization)
if err != nil {
return nil, err
}
return s.findOrganizationVariables(ctx, tx, o.ID)
}
var o influxdb.FindOptions
if len(opt) > 0 {
o = opt[0]
@ -160,7 +152,17 @@ func filterVariablesFn(filter influxdb.VariableFilter) func([]byte, interface{})
// FindVariables returns all variables in the store
func (s *Service) FindVariables(ctx context.Context, filter influxdb.VariableFilter, opt ...influxdb.FindOptions) ([]*influxdb.Variable, error) {
// todo(leodido) > handle find options
if filter.Organization != nil {
o, err := s.orgs.FindOrganization(ctx, influxdb.OrganizationFilter{
Name: filter.Organization,
})
if err != nil {
return nil, err
}
filter.OrganizationID = &o.ID
}
res := []*influxdb.Variable{}
err := s.kv.View(ctx, func(tx Tx) error {
variables, err := s.findVariables(ctx, tx, filter, opt...)

View File

@ -6,6 +6,7 @@ import (
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/kv"
"github.com/influxdata/influxdb/v2/mock"
influxdbtesting "github.com/influxdata/influxdb/v2/testing"
"go.uber.org/zap/zaptest"
)
@ -29,7 +30,7 @@ func initBoltVariableService(f influxdbtesting.VariableFields, t *testing.T) (in
func initVariableService(s kv.SchemaStore, f influxdbtesting.VariableFields, t *testing.T) (influxdb.VariableService, string, func()) {
ctx := context.Background()
svc := kv.NewService(zaptest.NewLogger(t), s)
svc := kv.NewService(zaptest.NewLogger(t), s, &mock.OrganizationService{})
svc.IDGenerator = f.IDGenerator
svc.TimeGenerator = f.TimeGenerator
if svc.TimeGenerator == nil {

View File

@ -1,94 +0,0 @@
package label
import (
"context"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/kit/feature"
)
var _ influxdb.LabelService = (*LabelController)(nil)
// LabelController is a temporary system for switching the label backend between
// the old and refactored versions with a feature flag
type LabelController struct {
flagger feature.Flagger
oldLabelService influxdb.LabelService
newLabelService influxdb.LabelService
}
func NewLabelController(flagger feature.Flagger, oldLabelService, newLabelService influxdb.LabelService) *LabelController {
return &LabelController{
flagger: flagger,
oldLabelService: oldLabelService,
newLabelService: newLabelService,
}
}
func (s *LabelController) useNew(ctx context.Context) bool {
return feature.NewLabelPackage().Enabled(ctx, s.flagger)
}
func (s *LabelController) CreateLabel(ctx context.Context, l *influxdb.Label) error {
if s.useNew(ctx) {
return s.newLabelService.CreateLabel(ctx, l)
}
return s.oldLabelService.CreateLabel(ctx, l)
}
func (s *LabelController) FindLabelByID(ctx context.Context, id influxdb.ID) (*influxdb.Label, error) {
if s.useNew(ctx) {
return s.newLabelService.FindLabelByID(ctx, id)
}
return s.oldLabelService.FindLabelByID(ctx, id)
}
func (s *LabelController) FindLabels(ctx context.Context, filter influxdb.LabelFilter, opt ...influxdb.FindOptions) ([]*influxdb.Label, error) {
if s.useNew(ctx) {
return s.newLabelService.FindLabels(ctx, filter, opt...)
}
return s.oldLabelService.FindLabels(ctx, filter, opt...)
}
func (s *LabelController) FindResourceLabels(ctx context.Context, filter influxdb.LabelMappingFilter) ([]*influxdb.Label, error) {
if s.useNew(ctx) {
return s.newLabelService.FindResourceLabels(ctx, filter)
}
return s.oldLabelService.FindResourceLabels(ctx, filter)
}
func (s *LabelController) UpdateLabel(ctx context.Context, id influxdb.ID, upd influxdb.LabelUpdate) (*influxdb.Label, error) {
if s.useNew(ctx) {
return s.newLabelService.UpdateLabel(ctx, id, upd)
}
return s.oldLabelService.UpdateLabel(ctx, id, upd)
}
func (s *LabelController) DeleteLabel(ctx context.Context, id influxdb.ID) error {
if s.useNew(ctx) {
return s.newLabelService.DeleteLabel(ctx, id)
}
return s.oldLabelService.DeleteLabel(ctx, id)
}
func (s *LabelController) CreateLabelMapping(ctx context.Context, m *influxdb.LabelMapping) error {
if s.useNew(ctx) {
return s.newLabelService.CreateLabelMapping(ctx, m)
}
return s.oldLabelService.CreateLabelMapping(ctx, m)
}
func (s *LabelController) DeleteLabelMapping(ctx context.Context, m *influxdb.LabelMapping) error {
if s.useNew(ctx) {
return s.newLabelService.DeleteLabelMapping(ctx, m)
}
return s.oldLabelService.DeleteLabelMapping(ctx, m)
}

View File

@ -1,52 +0,0 @@
package label_test
import (
"context"
"testing"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/kit/feature"
"github.com/influxdata/influxdb/v2/kv"
"github.com/influxdata/influxdb/v2/label"
influxdbtesting "github.com/influxdata/influxdb/v2/testing"
"go.uber.org/zap/zaptest"
)
func TestLabelServiceController(t *testing.T) {
influxdbtesting.LabelService(initBoltLabelServiceController, t)
}
func initBoltLabelServiceController(f influxdbtesting.LabelFields, t *testing.T) (influxdb.LabelService, string, func()) {
newSvc, s, closer := initBoltLabelService(f, t)
st, _, _ := NewTestBoltStore(t)
oldSvc, _, _ := initOldLabelService(st, f, t)
flagger := feature.DefaultFlagger()
return label.NewLabelController(flagger, oldSvc, newSvc), s, closer
}
func initOldLabelService(s kv.Store, f influxdbtesting.LabelFields, t *testing.T) (influxdb.LabelService, string, func()) {
svc := kv.NewService(zaptest.NewLogger(t), s)
svc.IDGenerator = f.IDGenerator
ctx := context.Background()
for _, l := range f.Labels {
if err := svc.PutLabel(ctx, l); err != nil {
t.Fatalf("failed to populate labels: %v", err)
}
}
for _, m := range f.Mappings {
if err := svc.PutLabelMapping(ctx, m); err != nil {
t.Fatalf("failed to populate label mappings: %v", err)
}
}
return svc, kv.OpPrefix, func() {
for _, l := range f.Labels {
if err := svc.DeleteLabel(ctx, l.ID); err != nil {
t.Logf("failed to remove label: %v", err)
}
}
}
}

View File

@ -22,6 +22,10 @@ func resourceIDPath(resourceType influxdb.ResourceType, resourceID influxdb.ID,
return path.Join("/api/v2/", string(resourceType), resourceID.String(), p)
}
func resourceIDMappingPath(resourceType influxdb.ResourceType, resourceID influxdb.ID, p string, labelID influxdb.ID) string {
return path.Join("/api/v2/", string(resourceType), resourceID.String(), p, labelID.String())
}
// CreateLabel creates a new label.
func (s *LabelClientService) CreateLabel(ctx context.Context, l *influxdb.Label) error {
var lr labelResponse
@ -130,6 +134,6 @@ func (s *LabelClientService) DeleteLabelMapping(ctx context.Context, m *influxdb
}
return s.Client.
Delete(resourceIDPath(m.ResourceType, m.ResourceID, "labels")).
Delete(resourceIDMappingPath(m.ResourceType, m.ResourceID, "labels", m.LabelID)).
Do(ctx)
}

View File

@ -12,6 +12,7 @@ import (
"github.com/influxdata/influxdb/v2/kv"
"github.com/influxdata/influxdb/v2/kv/migration/all"
"github.com/influxdata/influxdb/v2/label"
"github.com/influxdata/influxdb/v2/mock"
influxdbtesting "github.com/influxdata/influxdb/v2/testing"
"go.uber.org/zap/zaptest"
)
@ -68,13 +69,19 @@ func initLabelService(s kv.Store, f influxdbtesting.LabelFields, t *testing.T) (
t.Fatalf("failed to create label store: %v", err)
}
if f.IDGenerator != nil {
st.IDGenerator = f.IDGenerator
}
svc := label.NewService(st)
ctx := context.Background()
for _, l := range f.Labels {
if err := svc.CreateLabel(ctx, l); err != nil {
t.Fatalf("failed to populate labels: %v", err)
}
mock.SetIDForFunc(&st.IDGenerator, l.ID, func() {
if err := svc.CreateLabel(ctx, l); err != nil {
t.Fatalf("failed to populate labels: %v", err)
}
})
}
for _, m := range f.Mappings {

View File

@ -66,12 +66,6 @@ func (s *Store) generateSafeID(ctx context.Context, tx kv.Tx, bucket []byte) (in
for i := 0; i < MaxIDGenerationN; i++ {
id := s.IDGenerator.ID()
// TODO: this is probably unnecessary but for testing we need to keep it in.
// After KV is cleaned out we can update the tests and remove this.
if id < ReservedIDs {
continue
}
err := s.uniqueID(ctx, tx, bucket, id)
if err == nil {
return id, nil

View File

@ -12,19 +12,11 @@ import (
func (s *Store) CreateLabel(ctx context.Context, tx kv.Tx, l *influxdb.Label) error {
// if the provided ID is invalid, or already maps to an existing Auth, then generate a new one
if !l.ID.Valid() {
id, err := s.generateSafeID(ctx, tx, labelBucket)
if err != nil {
return nil
}
l.ID = id
} else if err := uniqueID(ctx, tx, l.ID); err != nil {
id, err := s.generateSafeID(ctx, tx, labelBucket)
if err != nil {
return nil
}
l.ID = id
id, err := s.generateSafeID(ctx, tx, labelBucket)
if err != nil {
return nil
}
l.ID = id
v, err := json.Marshal(l)
if err != nil {
@ -473,29 +465,3 @@ func forEachLabel(ctx context.Context, tx kv.Tx, fn func(*influxdb.Label) bool)
return cur.Close()
}
// uniqueID returns nil if the ID provided is unique, returns an error otherwise
func uniqueID(ctx context.Context, tx kv.Tx, id influxdb.ID) error {
encodedID, err := id.Encode()
if err != nil {
return influxdb.ErrInvalidID
}
b, err := tx.Bucket(labelBucket)
if err != nil {
return ErrInternalServiceError(err)
}
_, err = b.Get(encodedID)
// if not found then the ID is unique
if kv.IsNotFound(err) {
return nil
}
// no error means this is not unique
if err == nil {
return kv.NotUniqueError
}
// any other error is some sort of internal server error
return kv.UnexpectedIndexError(err)
}

View File

@ -11,35 +11,38 @@ import (
"github.com/influxdata/influxdb/v2/kv"
"github.com/influxdata/influxdb/v2/kv/migration/all"
"github.com/influxdata/influxdb/v2/label"
"github.com/influxdata/influxdb/v2/mock"
"go.uber.org/zap/zaptest"
)
func TestLabels(t *testing.T) {
setup := func(t *testing.T, store *label.Store, tx kv.Tx) {
for i := 1; i <= 10; i++ {
err := store.CreateLabel(context.Background(), tx, &influxdb.Label{
ID: influxdb.ID(i),
Name: fmt.Sprintf("labelname%d", i),
OrgID: influxdb.ID(i),
})
mock.SetIDForFunc(&store.IDGenerator, influxdb.ID(i), func() {
err := store.CreateLabel(context.Background(), tx, &influxdb.Label{
Name: fmt.Sprintf("labelname%d", i),
OrgID: influxdb.ID(i),
})
if err != nil {
t.Fatal(err)
}
if err != nil {
t.Fatal(err)
}
})
}
}
setupForList := func(t *testing.T, store *label.Store, tx kv.Tx) {
setup(t, store, tx)
err := store.CreateLabel(context.Background(), tx, &influxdb.Label{
ID: influxdb.ID(11),
Name: fmt.Sprintf("labelname%d", 11),
OrgID: influxdb.ID(5),
mock.SetIDForFunc(&store.IDGenerator, influxdb.ID(11), func() {
err := store.CreateLabel(context.Background(), tx, &influxdb.Label{
Name: fmt.Sprintf("labelname%d", 11),
OrgID: influxdb.ID(5),
})
if err != nil {
t.Fatal(err)
}
})
if err != nil {
t.Fatal(err)
}
}
tt := []struct {

View File

@ -13,8 +13,10 @@ import (
"github.com/influxdata/influxdb/v2/notification/endpoint"
"github.com/influxdata/influxdb/v2/notification/endpoint/service"
"github.com/influxdata/influxdb/v2/pkg/pointer"
"github.com/influxdata/influxdb/v2/secret"
"github.com/influxdata/influxdb/v2/tenant"
influxTesting "github.com/influxdata/influxdb/v2/testing"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"go.uber.org/zap/zaptest"
)
@ -51,7 +53,9 @@ func newSecretService(t *testing.T, ctx context.Context, logger *zap.Logger, s k
}
orgID = &org.ID // orgID is generated
return kv.NewService(logger, s)
secretStore, err := secret.NewStore(s)
require.NoError(t, err)
return secret.NewService(secretStore)
}
// TestEndpointService_cumulativeSecrets tests that secrets are cumulatively added/updated and removed upon delete

View File

@ -14,7 +14,9 @@ import (
"github.com/influxdata/influxdb/v2/kv/migration/all"
"github.com/influxdata/influxdb/v2/notification/endpoint/service"
endpointsTesting "github.com/influxdata/influxdb/v2/notification/endpoint/service/testing"
"github.com/influxdata/influxdb/v2/secret"
"github.com/influxdata/influxdb/v2/tenant"
"github.com/stretchr/testify/require"
"go.uber.org/zap/zaptest"
)
@ -80,7 +82,6 @@ func initInmemNotificationEndpointService(f endpointsTesting.NotificationEndpoin
func initNotificationEndpointService(s kv.SchemaStore, f endpointsTesting.NotificationEndpointFields, t *testing.T) (influxdb.NotificationEndpointService, influxdb.SecretService, func()) {
ctx := context.Background()
logger := zaptest.NewLogger(t)
tenantStore := tenant.NewStore(s)
if f.IDGenerator != nil {
@ -90,7 +91,9 @@ func initNotificationEndpointService(s kv.SchemaStore, f endpointsTesting.Notifi
tenantSvc := tenant.NewService(tenantStore)
secretSvc := kv.NewService(logger, s)
secretStore, err := secret.NewStore(s)
require.NoError(t, err)
secretSvc := secret.NewService(secretStore)
store := service.NewStore(s)
store.IDGenerator = f.IDGenerator

File diff suppressed because it is too large Load Diff

View File

@ -16,7 +16,9 @@ import (
endpointservice "github.com/influxdata/influxdb/v2/notification/endpoint/service"
_ "github.com/influxdata/influxdb/v2/fluxinit/static"
"github.com/influxdata/influxdb/v2/query/fluxlang"
"github.com/influxdata/influxdb/v2/secret"
"github.com/influxdata/influxdb/v2/tenant"
"github.com/stretchr/testify/require"
"go.uber.org/zap/zaptest"
)
@ -55,7 +57,13 @@ func TestBoltNotificationRuleStore(t *testing.T) {
func initNotificationRuleStore(s kv.Store, f NotificationRuleFields, t *testing.T) (influxdb.NotificationRuleStore, influxdb.TaskService, func()) {
logger := zaptest.NewLogger(t)
kvsvc := kv.NewService(logger, s, kv.ServiceConfig{
var (
tenantStore = tenant.NewStore(s)
tenantSvc = tenant.NewService(tenantStore)
)
kvsvc := kv.NewService(logger, s, tenantSvc, kv.ServiceConfig{
FluxLanguageService: fluxlang.DefaultService,
})
kvsvc.IDGenerator = f.IDGenerator
@ -64,15 +72,14 @@ func initNotificationRuleStore(s kv.Store, f NotificationRuleFields, t *testing.
kvsvc.TimeGenerator = influxdb.RealTimeGenerator{}
}
var (
tenantStore = tenant.NewStore(s)
tenantSvc = tenant.NewService(tenantStore)
)
secretStore, err := secret.NewStore(s)
require.NoError(t, err)
secretSvc := secret.NewService(secretStore)
endpStore := endpointservice.NewStore(s)
endpStore.IDGenerator = f.IDGenerator
endpStore.TimeGenerator = f.TimeGenerator
endp := endpointservice.New(endpStore, kvsvc)
endp := endpointservice.New(endpStore, secretSvc)
svc, err := New(logger, s, kvsvc, tenantSvc, endp)
if err != nil {

View File

@ -6,6 +6,7 @@ import (
platform "github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/http"
"github.com/influxdata/influxdb/v2/http/influxdb"
"github.com/influxdata/influxdb/v2/tenant"
)
// NewBucketService creates a bucket service from a source.
@ -21,7 +22,7 @@ func NewBucketService(s *platform.Source) (platform.BucketService, error) {
if err != nil {
return nil, err
}
return &http.BucketService{Client: httpClient}, nil
return &tenant.BucketClientService{Client: httpClient}, nil
case platform.V1SourceType:
return &influxdb.BucketService{Source: s}, nil
}

Some files were not shown because too many files have changed in this diff Show More