// Copyright (c) Mainflux // SPDX-License-Identifier: Apache-2.0 package provision import ( "encoding/json" "fmt" mflog "github.com/mainflux/mainflux/logger" "github.com/mainflux/mainflux/pkg/errors" sdk "github.com/mainflux/mainflux/pkg/sdk/go" ) const ( externalIDKey = "external_id" gateway = "gateway" Active = 1 control = "control" data = "data" export = "export" ) var ( ErrUnauthorized = errors.New("unauthorized access") ErrFailedToCreateToken = errors.New("failed to create access token") ErrEmptyThingsList = errors.New("things list in configuration empty") ErrThingUpdate = errors.New("failed to update thing") ErrEmptyChannelsList = errors.New("channels list in configuration is empty") ErrFailedChannelCreation = errors.New("failed to create channel") ErrFailedChannelRetrieval = errors.New("failed to retrieve channel") ErrFailedThingCreation = errors.New("failed to create thing") ErrFailedThingRetrieval = errors.New("failed to retrieve thing") ErrMissingCredentials = errors.New("missing credentials") ErrFailedBootstrapRetrieval = errors.New("failed to retrieve bootstrap") ErrFailedCertCreation = errors.New("failed to create certificates") ErrFailedBootstrap = errors.New("failed to create bootstrap config") ErrFailedBootstrapValidate = errors.New("failed to validate bootstrap config creation") ErrGatewayUpdate = errors.New("failed to updated gateway metadata") limit uint = 10 offset uint = 0 ) var _ Service = (*provisionService)(nil) // Service specifies Provision service API. type Service interface { // Provision is the only method this API specifies. Depending on the configuration, // the following actions will can be executed: // - create a Thing based on external_id (eg. MAC address) // - create multiple Channels // - create Bootstrap configuration // - whitelist Thing in Bootstrap configuration == connect Thing to Channels Provision(token, name, externalID, externalKey string) (Result, error) // Mapping returns current configuration used for provision // useful for using in ui to create configuration that matches // one created with Provision method. Mapping(token string) (map[string]interface{}, error) // Certs creates certificate for things that communicate over mTLS // A duration string is a possibly signed sequence of decimal numbers, // each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". // Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". Cert(token, thingID, duration string) (string, string, error) } type provisionService struct { logger mflog.Logger sdk sdk.SDK conf Config } // Result represent what is created with additional info. type Result struct { Things []sdk.Thing `json:"things,omitempty"` Channels []sdk.Channel `json:"channels,omitempty"` ClientCert map[string]string `json:"client_cert,omitempty"` ClientKey map[string]string `json:"client_key,omitempty"` CACert string `json:"ca_cert,omitempty"` Whitelisted map[string]bool `json:"whitelisted,omitempty"` Error string `json:"error,omitempty"` } // New returns new provision service. func New(cfg Config, sdk sdk.SDK, logger mflog.Logger) Service { return &provisionService{ logger: logger, conf: cfg, sdk: sdk, } } // Mapping retrieves current configuration. func (ps *provisionService) Mapping(token string) (map[string]interface{}, error) { userFilter := sdk.PageMetadata{ Email: "", Offset: uint64(offset), Limit: uint64(limit), Metadata: make(map[string]interface{}), } if _, err := ps.sdk.Users(userFilter, token); err != nil { return map[string]interface{}{}, errors.Wrap(ErrUnauthorized, err) } return ps.conf.Bootstrap.Content, nil } // Provision is provision method for creating setup according to // provision layout specified in config.toml. func (ps *provisionService) Provision(token, name, externalID, externalKey string) (res Result, err error) { var channels []sdk.Channel var things []sdk.Thing defer ps.recover(&err, &things, &channels, &token) token, err = ps.createTokenIfEmpty(token) if err != nil { return res, err } if len(ps.conf.Things) == 0 { return res, ErrEmptyThingsList } if len(ps.conf.Channels) == 0 { return res, ErrEmptyChannelsList } for _, thing := range ps.conf.Things { // If thing in configs contains metadata with external_id // set value for it from the provision request if _, ok := thing.Metadata[externalIDKey]; ok { thing.Metadata[externalIDKey] = externalID } th := sdk.Thing{ Metadata: thing.Metadata, } if name == "" { name = thing.Name } th.Name = name th, err := ps.sdk.CreateThing(th, token) if err != nil { res.Error = err.Error() return res, errors.Wrap(ErrFailedThingCreation, err) } // Get newly created thing (in order to get the key). th, err = ps.sdk.Thing(th.ID, token) if err != nil { e := errors.Wrap(err, fmt.Errorf("thing id: %s", th.ID)) return res, errors.Wrap(ErrFailedThingRetrieval, e) } things = append(things, th) } for _, channel := range ps.conf.Channels { ch := sdk.Channel{ Name: channel.Name, Metadata: sdk.Metadata(channel.Metadata), } ch, err := ps.sdk.CreateChannel(ch, token) if err != nil { return res, err } ch, err = ps.sdk.Channel(ch.ID, token) if err != nil { e := errors.Wrap(err, fmt.Errorf("channel id: %s", ch.ID)) return res, errors.Wrap(ErrFailedChannelRetrieval, e) } channels = append(channels, ch) } res = Result{ Things: things, Channels: channels, Whitelisted: map[string]bool{}, ClientCert: map[string]string{}, ClientKey: map[string]string{}, } var cert sdk.Cert var bsConfig sdk.BootstrapConfig for _, thing := range things { var chanIDs []string for _, ch := range channels { chanIDs = append(chanIDs, ch.ID) } content, err := json.Marshal(ps.conf.Bootstrap.Content) if err != nil { return Result{}, errors.Wrap(ErrFailedBootstrap, err) } if ps.conf.Bootstrap.Provision && needsBootstrap(thing) { bsReq := sdk.BootstrapConfig{ ThingID: thing.ID, ExternalID: externalID, ExternalKey: externalKey, Channels: chanIDs, CACert: res.CACert, ClientCert: cert.ClientCert, ClientKey: cert.ClientKey, Content: string(content), } bsid, err := ps.sdk.AddBootstrap(bsReq, token) if err != nil { return Result{}, errors.Wrap(ErrFailedBootstrap, err) } bsConfig, err = ps.sdk.ViewBootstrap(bsid, token) if err != nil { return Result{}, errors.Wrap(ErrFailedBootstrapValidate, err) } } if ps.conf.Bootstrap.X509Provision { var cert sdk.Cert cert, err = ps.sdk.IssueCert(thing.ID, ps.conf.Cert.TTL, token) if err != nil { e := errors.Wrap(err, fmt.Errorf("thing id: %s", thing.ID)) return res, errors.Wrap(ErrFailedCertCreation, e) } res.ClientCert[thing.ID] = cert.ClientCert res.ClientKey[thing.ID] = cert.ClientKey res.CACert = "" if needsBootstrap(thing) { if _, err = ps.sdk.UpdateBootstrapCerts(bsConfig.ThingID, cert.ClientCert, cert.ClientKey, "", token); err != nil { return Result{}, errors.Wrap(ErrFailedCertCreation, err) } } } if ps.conf.Bootstrap.AutoWhiteList { wlReq := sdk.BootstrapConfig{ ThingID: thing.ID, State: Active, } if err := ps.sdk.Whitelist(wlReq, token); err != nil { res.Error = err.Error() return res, ErrThingUpdate } res.Whitelisted[thing.ID] = true } } if err = ps.updateGateway(token, bsConfig, channels); err != nil { return res, err } return res, nil } func (ps *provisionService) Cert(token, thingID, ttl string) (string, string, error) { token, err := ps.createTokenIfEmpty(token) if err != nil { return "", "", err } th, err := ps.sdk.Thing(thingID, token) if err != nil { return "", "", errors.Wrap(ErrUnauthorized, err) } cert, err := ps.sdk.IssueCert(th.ID, ps.conf.Cert.TTL, token) return cert.ClientCert, cert.ClientKey, err } func (ps *provisionService) createTokenIfEmpty(token string) (string, error) { if token != "" { return token, nil } // If no token in request is provided // use API key provided in config file or env if ps.conf.Server.MfAPIKey != "" { return ps.conf.Server.MfAPIKey, nil } // If no API key use username and password provided to create access token. if ps.conf.Server.MfUser == "" || ps.conf.Server.MfPass == "" { return token, ErrMissingCredentials } u := sdk.User{ Credentials: sdk.Credentials{ Identity: ps.conf.Server.MfUser, Secret: ps.conf.Server.MfPass, }, } tkn, err := ps.sdk.CreateToken(u) if err != nil { return token, errors.Wrap(ErrFailedToCreateToken, err) } return tkn.AccessToken, nil } func (ps *provisionService) updateGateway(token string, bs sdk.BootstrapConfig, channels []sdk.Channel) error { var gw Gateway for _, ch := range channels { switch ch.Metadata["type"] { case control: gw.CtrlChannelID = ch.ID case data: gw.DataChannelID = ch.ID case export: gw.ExportChannelID = ch.ID } } gw.ExternalID = bs.ExternalID gw.ExternalKey = bs.ExternalKey gw.CfgID = bs.ThingID gw.Type = gateway th, sdkerr := ps.sdk.Thing(bs.ThingID, token) if sdkerr != nil { return errors.Wrap(ErrGatewayUpdate, sdkerr) } b, err := json.Marshal(gw) if err != nil { return errors.Wrap(ErrGatewayUpdate, err) } if err := json.Unmarshal(b, &th.Metadata); err != nil { return errors.Wrap(ErrGatewayUpdate, err) } if _, err := ps.sdk.UpdateThing(th, token); err != nil { return errors.Wrap(ErrGatewayUpdate, err) } return nil } func (ps *provisionService) errLog(err error) { if err != nil { ps.logger.Error(fmt.Sprintf("Error recovering: %s", err)) } } func clean(ps *provisionService, things []sdk.Thing, channels []sdk.Channel, token string) { for _, t := range things { _, err := ps.sdk.DisableThing(t.ID, token) ps.errLog(err) } for _, c := range channels { _, err := ps.sdk.DisableChannel(c.ID, token) ps.errLog(err) } } func (ps *provisionService) recover(e *error, ths *[]sdk.Thing, chs *[]sdk.Channel, tkn *string) { if e == nil { return } things, channels, token, err := *ths, *chs, *tkn, *e if errors.Contains(err, ErrFailedThingRetrieval) || errors.Contains(err, ErrFailedChannelCreation) { for _, th := range things { _, err := ps.sdk.DisableThing(th.ID, token) ps.errLog(err) } return } if errors.Contains(err, ErrFailedBootstrap) || errors.Contains(err, ErrFailedChannelRetrieval) { clean(ps, things, channels, token) return } if errors.Contains(err, ErrFailedBootstrapValidate) || errors.Contains(err, ErrFailedCertCreation) { clean(ps, things, channels, token) for _, th := range things { if needsBootstrap(th) { ps.errLog(ps.sdk.RemoveBootstrap(th.ID, token)) } } return } if errors.Contains(err, ErrFailedBootstrapValidate) || errors.Contains(err, ErrFailedCertCreation) { clean(ps, things, channels, token) for _, th := range things { if needsBootstrap(th) { bs, err := ps.sdk.ViewBootstrap(th.ID, token) ps.errLog(errors.Wrap(ErrFailedBootstrapRetrieval, err)) ps.errLog(ps.sdk.RemoveBootstrap(bs.ThingID, token)) } } } if errors.Contains(err, ErrThingUpdate) || errors.Contains(err, ErrGatewayUpdate) { clean(ps, things, channels, token) for _, th := range things { if ps.conf.Bootstrap.X509Provision && needsBootstrap(th) { _, err := ps.sdk.RevokeCert(th.ID, token) ps.errLog(err) } if needsBootstrap(th) { bs, err := ps.sdk.ViewBootstrap(th.ID, token) ps.errLog(errors.Wrap(ErrFailedBootstrapRetrieval, err)) ps.errLog(ps.sdk.RemoveBootstrap(bs.ThingID, token)) } } return } } func needsBootstrap(th sdk.Thing) bool { if th.Metadata == nil { return false } if _, ok := th.Metadata[externalIDKey]; ok { return true } return false }