package tenant import ( "context" "fmt" "strings" "github.com/influxdata/influxdb/v2" icontext "github.com/influxdata/influxdb/v2/context" "github.com/influxdata/influxdb/v2/kit/platform" "github.com/influxdata/influxdb/v2/kit/platform/errors" "github.com/influxdata/influxdb/v2/kv" "go.uber.org/zap" ) type OnboardService struct { service *Service authSvc influxdb.AuthorizationService alwaysAllow bool log *zap.Logger } type OnboardServiceOptionFn func(*OnboardService) // WithAlwaysAllowInitialUser configures the OnboardService to // always return true for IsOnboarding to allow multiple // initial onboard requests. func WithAlwaysAllowInitialUser() OnboardServiceOptionFn { return func(s *OnboardService) { s.alwaysAllow = true } } func WithOnboardingLogger(logger *zap.Logger) OnboardServiceOptionFn { return func(s *OnboardService) { s.log = logger } } func NewOnboardService(svc *Service, as influxdb.AuthorizationService, opts ...OnboardServiceOptionFn) influxdb.OnboardingService { s := &OnboardService{ service: svc, authSvc: as, log: zap.NewNop(), } for _, opt := range opts { opt(s) } return s } // IsOnboarding determine if onboarding request is allowed. func (s *OnboardService) IsOnboarding(ctx context.Context) (bool, error) { if s.alwaysAllow { return true, nil } allowed := false err := s.service.store.View(ctx, func(tx kv.Tx) error { // we are allowed to onboard a user if we have no users or orgs users, _ := s.service.store.ListUsers(ctx, tx, influxdb.FindOptions{Limit: 1}) orgs, _ := s.service.store.ListOrgs(ctx, tx, influxdb.FindOptions{Limit: 1}) if len(users) == 0 && len(orgs) == 0 { allowed = true } return nil }) return allowed, err } // OnboardInitialUser allows us to onboard a new user if is onboarding is allowed func (s *OnboardService) OnboardInitialUser(ctx context.Context, req *influxdb.OnboardingRequest) (*influxdb.OnboardingResults, error) { allowed, err := s.IsOnboarding(ctx) if err != nil { return nil, err } if !allowed { return nil, ErrOnboardingNotAllowed } return s.onboardUser(ctx, req, func(platform.ID, platform.ID) []influxdb.Permission { return influxdb.OperPermissions() }) } // onboardUser allows us to onboard new users. func (s *OnboardService) onboardUser(ctx context.Context, req *influxdb.OnboardingRequest, permFn func(orgID, userID platform.ID) []influxdb.Permission) (*influxdb.OnboardingResults, error) { if req == nil { return nil, &errors.Error{ Code: errors.EEmptyValue, Msg: "onboarding failed: no request body provided", } } var missingFields []string if req.User == "" { missingFields = append(missingFields, "username") } if req.Org == "" { missingFields = append(missingFields, "org") } if req.Bucket == "" { missingFields = append(missingFields, "bucket") } if len(missingFields) > 0 { return nil, &errors.Error{ Code: errors.EUnprocessableEntity, Msg: fmt.Sprintf("onboarding failed: missing required fields [%s]", strings.Join(missingFields, ",")), } } result := &influxdb.OnboardingResults{} // create a user user := &influxdb.User{ Name: req.User, Status: influxdb.Active, } if err := s.service.CreateUser(ctx, user); err != nil { return nil, err } // create users password if req.Password != "" { if err := s.service.SetPassword(ctx, user.ID, req.Password); err != nil { // Try to clean up. if cleanupErr := s.service.DeleteUser(ctx, user.ID); cleanupErr != nil { s.log.Error( "couldn't clean up user after failing to set password", zap.String("user", user.Name), zap.String("user_id", user.ID.String()), zap.Error(cleanupErr), ) } return nil, err } } // set the new user in the context ctx = icontext.SetAuthorizer(ctx, &influxdb.Authorization{ UserID: user.ID, }) // create users org org := &influxdb.Organization{ Name: req.Org, } if err := s.service.CreateOrganization(ctx, org); err != nil { return nil, err } if err := s.service.CreateUserResourceMapping(ctx, &influxdb.UserResourceMapping{ UserID: user.ID, UserType: influxdb.Owner, MappingType: influxdb.UserMappingType, ResourceType: influxdb.InstanceResourceType, ResourceID: platform.ID(1), // The instance doesn't have a resourceid }); err != nil { return nil, err } // create orgs buckets ub := &influxdb.Bucket{ OrgID: org.ID, Name: req.Bucket, Type: influxdb.BucketTypeUser, RetentionPeriod: req.RetentionPeriod(), } if err := s.service.CreateBucket(ctx, ub); err != nil { return nil, err } result.User = user result.Org = org result.Bucket = ub // bolt doesn't lock per collection or record so we have to close our transaction // before we can reach out to the auth service. result.Auth = &influxdb.Authorization{ Description: fmt.Sprintf("%s's Token", req.User), Permissions: permFn(result.Org.ID, result.User.ID), Token: req.Token, UserID: result.User.ID, OrgID: result.Org.ID, } return result, s.authSvc.CreateAuthorization(ctx, result.Auth) }