Run Ginkgo automation tests on LambdaTest cloud grid
Perform automation testing on 3000+ real desktop and mobile devices online.
// Copyright © 2020 The Things Network Foundation, The Things Industries B.V.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package identityserver
import (
"context"
"github.com/gogo/protobuf/types"
"github.com/jinzhu/gorm"
"go.thethings.network/lorawan-stack/v3/pkg/auth/rights"
"go.thethings.network/lorawan-stack/v3/pkg/errors"
"go.thethings.network/lorawan-stack/v3/pkg/events"
"go.thethings.network/lorawan-stack/v3/pkg/identityserver/blacklist"
"go.thethings.network/lorawan-stack/v3/pkg/identityserver/store"
"go.thethings.network/lorawan-stack/v3/pkg/ttnpb"
)
var (
evtCreateOrganization = events.Define(
"organization.create", "create organization",
events.WithVisibility(ttnpb.RIGHT_ORGANIZATION_INFO),
events.WithAuthFromContext(),
events.WithClientInfoFromContext(),
)
evtUpdateOrganization = events.Define(
"organization.update", "update organization",
events.WithVisibility(ttnpb.RIGHT_ORGANIZATION_INFO),
events.WithUpdatedFieldsDataType(),
events.WithAuthFromContext(),
events.WithClientInfoFromContext(),
)
evtDeleteOrganization = events.Define(
"organization.delete", "delete organization",
events.WithVisibility(ttnpb.RIGHT_ORGANIZATION_INFO),
events.WithAuthFromContext(),
events.WithClientInfoFromContext(),
)
evtPurgeOrganization = events.Define(
"organization.purge", "purge organization",
events.WithVisibility(ttnpb.RIGHT_ORGANIZATION_INFO),
events.WithAuthFromContext(),
events.WithClientInfoFromContext(),
)
)
var (
errNestedOrganizations = errors.DefineInvalidArgument("nested_organizations", "organizations can not be nested")
errAdminsCreateOrganizations = errors.DefinePermissionDenied("admins_create_organizations", "organizations may only be created by admins")
errAdminsPurgeOrganizations = errors.DefinePermissionDenied("admins_purge_organizations", "organizations may only be purged by admins")
)
func (is *IdentityServer) createOrganization(ctx context.Context, req *ttnpb.CreateOrganizationRequest) (org *ttnpb.Organization, err error) {
if err = blacklist.Check(ctx, req.OrganizationID); err != nil {
return nil, err
}
if usrIDs := req.Collaborator.GetUserIDs(); usrIDs != nil {
if !is.IsAdmin(ctx) && !is.configFromContext(ctx).UserRights.CreateOrganizations {
return nil, errAdminsCreateOrganizations
}
if err = rights.RequireUser(ctx, *usrIDs, ttnpb.RIGHT_USER_ORGANIZATIONS_CREATE); err != nil {
return nil, err
}
} else if orgIDs := req.Collaborator.GetOrganizationIDs(); orgIDs != nil {
return nil, errNestedOrganizations.New()
}
if err := validateContactInfo(req.Organization.ContactInfo); err != nil {
return nil, err
}
err = is.withDatabase(ctx, func(db *gorm.DB) (err error) {
org, err = store.GetOrganizationStore(db).CreateOrganization(ctx, &req.Organization)
if err != nil {
return err
}
if err = is.getMembershipStore(ctx, db).SetMember(
ctx,
&req.Collaborator,
org.OrganizationIdentifiers,
ttnpb.RightsFrom(ttnpb.RIGHT_ALL),
); err != nil {
return err
}
if len(req.ContactInfo) > 0 {
cleanContactInfo(req.ContactInfo)
org.ContactInfo, err = store.GetContactInfoStore(db).SetContactInfo(ctx, org.OrganizationIdentifiers, req.ContactInfo)
if err != nil {
return err
}
}
return nil
})
if err != nil {
return nil, err
}
events.Publish(evtCreateOrganization.NewWithIdentifiersAndData(ctx, req.OrganizationIdentifiers, nil))
return org, nil
}
func (is *IdentityServer) getOrganization(ctx context.Context, req *ttnpb.GetOrganizationRequest) (org *ttnpb.Organization, err error) {
if err = is.RequireAuthenticated(ctx); err != nil {
return nil, err
}
req.FieldMask.Paths = cleanFieldMaskPaths(ttnpb.OrganizationFieldPathsNested, req.FieldMask.Paths, getPaths, nil)
if err = rights.RequireOrganization(ctx, req.OrganizationIdentifiers, ttnpb.RIGHT_ORGANIZATION_INFO); err != nil {
if ttnpb.HasOnlyAllowedFields(req.FieldMask.Paths, ttnpb.PublicOrganizationFields...) {
defer func() { org = org.PublicSafe() }()
} else {
return nil, err
}
}
err = is.withDatabase(ctx, func(db *gorm.DB) (err error) {
org, err = store.GetOrganizationStore(db).GetOrganization(ctx, &req.OrganizationIdentifiers, &req.FieldMask)
if err != nil {
return err
}
if ttnpb.HasAnyField(req.FieldMask.Paths, "contact_info") {
org.ContactInfo, err = store.GetContactInfoStore(db).GetContactInfo(ctx, org.OrganizationIdentifiers)
if err != nil {
return err
}
}
return err
})
if err != nil {
return nil, err
}
return org, nil
}
func (is *IdentityServer) listOrganizations(ctx context.Context, req *ttnpb.ListOrganizationsRequest) (orgs *ttnpb.Organizations, err error) {
req.FieldMask.Paths = cleanFieldMaskPaths(ttnpb.OrganizationFieldPathsNested, req.FieldMask.Paths, getPaths, nil)
var includeIndirect bool
if req.Collaborator == nil {
authInfo, err := is.authInfo(ctx)
if err != nil {
return nil, err
}
collaborator := authInfo.GetOrganizationOrUserIdentifiers()
if collaborator == nil {
return &ttnpb.Organizations{}, nil
}
req.Collaborator = collaborator
includeIndirect = true
}
if usrIDs := req.Collaborator.GetUserIDs(); usrIDs != nil {
if err = rights.RequireUser(ctx, *usrIDs, ttnpb.RIGHT_USER_ORGANIZATIONS_LIST); err != nil {
return nil, err
}
} else if orgIDs := req.Collaborator.GetOrganizationIDs(); orgIDs != nil {
return nil, errNestedOrganizations.New()
}
ctx = store.WithOrder(ctx, req.Order)
var total uint64
paginateCtx := store.WithPagination(ctx, req.Limit, req.Page, &total)
defer func() {
if err == nil {
setTotalHeader(ctx, total)
}
}()
orgs = &ttnpb.Organizations{}
err = is.withDatabase(ctx, func(db *gorm.DB) (err error) {
ids, err := is.getMembershipStore(ctx, db).FindMemberships(paginateCtx, req.Collaborator, "organization", includeIndirect)
if err != nil {
return err
}
if len(ids) == 0 {
return nil
}
orgIDs := make([]*ttnpb.OrganizationIdentifiers, 0, len(ids))
for _, id := range ids {
if orgID := id.EntityIdentifiers().GetOrganizationIDs(); orgID != nil {
orgIDs = append(orgIDs, orgID)
}
}
orgs.Organizations, err = store.GetOrganizationStore(db).FindOrganizations(ctx, orgIDs, &req.FieldMask)
if err != nil {
return err
}
return nil
})
if err != nil {
return nil, err
}
for i, org := range orgs.Organizations {
if rights.RequireOrganization(ctx, org.OrganizationIdentifiers, ttnpb.RIGHT_ORGANIZATION_INFO) != nil {
orgs.Organizations[i] = org.PublicSafe()
}
}
return orgs, nil
}
func (is *IdentityServer) updateOrganization(ctx context.Context, req *ttnpb.UpdateOrganizationRequest) (org *ttnpb.Organization, err error) {
if err = rights.RequireOrganization(ctx, req.OrganizationIdentifiers, ttnpb.RIGHT_ORGANIZATION_SETTINGS_BASIC); err != nil {
return nil, err
}
req.FieldMask.Paths = cleanFieldMaskPaths(ttnpb.OrganizationFieldPathsNested, req.FieldMask.Paths, nil, getPaths)
if len(req.FieldMask.Paths) == 0 {
req.FieldMask.Paths = updatePaths
}
if ttnpb.HasAnyField(req.FieldMask.Paths, "contact_info") {
if err := validateContactInfo(req.Organization.ContactInfo); err != nil {
return nil, err
}
}
err = is.withDatabase(ctx, func(db *gorm.DB) (err error) {
org, err = store.GetOrganizationStore(db).UpdateOrganization(ctx, &req.Organization, &req.FieldMask)
if err != nil {
return err
}
if ttnpb.HasAnyField(req.FieldMask.Paths, "contact_info") {
cleanContactInfo(req.ContactInfo)
org.ContactInfo, err = store.GetContactInfoStore(db).SetContactInfo(ctx, org.OrganizationIdentifiers, req.ContactInfo)
if err != nil {
return err
}
}
return nil
})
if err != nil {
return nil, err
}
events.Publish(evtUpdateOrganization.NewWithIdentifiersAndData(ctx, req.OrganizationIdentifiers, req.FieldMask.Paths))
return org, nil
}
func (is *IdentityServer) deleteOrganization(ctx context.Context, ids *ttnpb.OrganizationIdentifiers) (*types.Empty, error) {
if err := rights.RequireOrganization(ctx, *ids, ttnpb.RIGHT_ORGANIZATION_DELETE); err != nil {
return nil, err
}
err := is.withDatabase(ctx, func(db *gorm.DB) error {
return store.GetOrganizationStore(db).DeleteOrganization(ctx, ids)
})
if err != nil {
return nil, err
}
events.Publish(evtDeleteOrganization.NewWithIdentifiersAndData(ctx, ids, nil))
return ttnpb.Empty, nil
}
func (is *IdentityServer) purgeOrganization(ctx context.Context, ids *ttnpb.OrganizationIdentifiers) (*types.Empty, error) {
if !is.IsAdmin(ctx) {
return nil, errAdminsPurgeOrganizations
}
err := is.withDatabase(ctx, func(db *gorm.DB) error {
err := store.GetContactInfoStore(db).DeleteEntityContactInfo(ctx, ids)
if err != nil {
return err
}
// Delete related API keys before purging the organization.
err = store.GetAPIKeyStore(db).DeleteEntityAPIKeys(ctx, ids)
if err != nil {
return err
}
err = store.GetMembershipStore(db).DeleteAccountMembers(ctx, ids.GetOrganizationOrUserIdentifiers())
if err != nil {
return err
}
return store.GetOrganizationStore(db).PurgeOrganization(ctx, ids)
})
if err != nil {
return nil, err
}
events.Publish(evtPurgeOrganization.NewWithIdentifiersAndData(ctx, ids, nil))
return ttnpb.Empty, nil
}
type organizationRegistry struct {
*IdentityServer
}
func (or *organizationRegistry) Create(ctx context.Context, req *ttnpb.CreateOrganizationRequest) (*ttnpb.Organization, error) {
return or.createOrganization(ctx, req)
}
func (or *organizationRegistry) Get(ctx context.Context, req *ttnpb.GetOrganizationRequest) (*ttnpb.Organization, error) {
return or.getOrganization(ctx, req)
}
func (or *organizationRegistry) List(ctx context.Context, req *ttnpb.ListOrganizationsRequest) (*ttnpb.Organizations, error) {
return or.listOrganizations(ctx, req)
}
func (or *organizationRegistry) Update(ctx context.Context, req *ttnpb.UpdateOrganizationRequest) (*ttnpb.Organization, error) {
return or.updateOrganization(ctx, req)
}
func (or *organizationRegistry) Delete(ctx context.Context, req *ttnpb.OrganizationIdentifiers) (*types.Empty, error) {
return or.deleteOrganization(ctx, req)
}
func (or *organizationRegistry) Purge(ctx context.Context, req *ttnpb.OrganizationIdentifiers) (*types.Empty, error) {
return or.purgeOrganization(ctx, req)
}
// Copyright © 2019 The Things Network Foundation, The Things Industries B.V.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package identityserver
import (
"context"
"github.com/gogo/protobuf/types"
"github.com/jinzhu/gorm"
"go.thethings.network/lorawan-stack/v3/pkg/auth/rights"
"go.thethings.network/lorawan-stack/v3/pkg/email"
"go.thethings.network/lorawan-stack/v3/pkg/errors"
"go.thethings.network/lorawan-stack/v3/pkg/events"
"go.thethings.network/lorawan-stack/v3/pkg/identityserver/emails"
"go.thethings.network/lorawan-stack/v3/pkg/identityserver/store"
"go.thethings.network/lorawan-stack/v3/pkg/log"
"go.thethings.network/lorawan-stack/v3/pkg/ttnpb"
)
var (
evtCreateApplicationAPIKey = events.Define(
"application.api-key.create", "create application API key",
events.WithVisibility(ttnpb.RIGHT_APPLICATION_SETTINGS_API_KEYS),
events.WithAuthFromContext(),
events.WithClientInfoFromContext(),
)
evtUpdateApplicationAPIKey = events.Define(
"application.api-key.update", "update application API key",
events.WithVisibility(ttnpb.RIGHT_APPLICATION_SETTINGS_API_KEYS),
events.WithAuthFromContext(),
events.WithClientInfoFromContext(),
)
evtDeleteApplicationAPIKey = events.Define(
"application.api-key.delete", "delete application API key",
events.WithVisibility(ttnpb.RIGHT_APPLICATION_SETTINGS_API_KEYS),
events.WithAuthFromContext(),
events.WithClientInfoFromContext(),
)
evtUpdateApplicationCollaborator = events.Define(
"application.collaborator.update", "update application collaborator",
events.WithVisibility(
ttnpb.RIGHT_APPLICATION_SETTINGS_COLLABORATORS,
ttnpb.RIGHT_USER_APPLICATIONS_LIST,
),
events.WithAuthFromContext(),
events.WithClientInfoFromContext(),
)
evtDeleteApplicationCollaborator = events.Define(
"application.collaborator.delete", "delete application collaborator",
events.WithVisibility(
ttnpb.RIGHT_APPLICATION_SETTINGS_COLLABORATORS,
ttnpb.RIGHT_USER_APPLICATIONS_LIST,
),
events.WithAuthFromContext(),
events.WithClientInfoFromContext(),
)
)
func (is *IdentityServer) listApplicationRights(ctx context.Context, ids *ttnpb.ApplicationIdentifiers) (*ttnpb.Rights, error) {
appRights, err := rights.ListApplication(ctx, *ids)
if err != nil {
return nil, err
}
return appRights.Intersect(ttnpb.AllApplicationRights), nil
}
func (is *IdentityServer) createApplicationAPIKey(ctx context.Context, req *ttnpb.CreateApplicationAPIKeyRequest) (key *ttnpb.APIKey, err error) {
// Require that caller has rights to manage API keys.
if err = rights.RequireApplication(ctx, req.ApplicationIdentifiers, ttnpb.RIGHT_APPLICATION_SETTINGS_API_KEYS); err != nil {
return nil, err
}
// Require that caller has at least the rights of the API key.
if err = rights.RequireApplication(ctx, req.ApplicationIdentifiers, req.Rights...); err != nil {
return nil, err
}
key, token, err := GenerateAPIKey(ctx, req.Name, req.Rights...)
if err != nil {
return nil, err
}
err = is.withDatabase(ctx, func(db *gorm.DB) error {
return store.GetAPIKeyStore(db).CreateAPIKey(ctx, req.ApplicationIdentifiers, key)
})
if err != nil {
return nil, err
}
key.Key = token
events.Publish(evtCreateApplicationAPIKey.NewWithIdentifiersAndData(ctx, req.ApplicationIdentifiers, nil))
err = is.SendContactsEmail(ctx, req.EntityIdentifiers(), func(data emails.Data) email.MessageData {
data.SetEntity(req.EntityIdentifiers())
return &emails.APIKeyCreated{Data: data, Identifier: key.PrettyName(), Rights: key.Rights}
})
if err != nil {
log.FromContext(ctx).WithError(err).Error("Could not send API key creation notification email")
}
return key, nil
}
func (is *IdentityServer) listApplicationAPIKeys(ctx context.Context, req *ttnpb.ListApplicationAPIKeysRequest) (keys *ttnpb.APIKeys, err error) {
if err = rights.RequireApplication(ctx, req.ApplicationIdentifiers, ttnpb.RIGHT_APPLICATION_SETTINGS_API_KEYS); err != nil {
return nil, err
}
var total uint64
ctx = store.WithPagination(ctx, req.Limit, req.Page, &total)
defer func() {
if err == nil {
setTotalHeader(ctx, total)
}
}()
keys = &ttnpb.APIKeys{}
err = is.withDatabase(ctx, func(db *gorm.DB) (err error) {
keys.APIKeys, err = store.GetAPIKeyStore(db).FindAPIKeys(ctx, req.ApplicationIdentifiers)
return err
})
if err != nil {
return nil, err
}
for _, key := range keys.APIKeys {
key.Key = ""
}
return keys, nil
}
func (is *IdentityServer) getApplicationAPIKey(ctx context.Context, req *ttnpb.GetApplicationAPIKeyRequest) (key *ttnpb.APIKey, err error) {
if err = rights.RequireApplication(ctx, req.ApplicationIdentifiers, ttnpb.RIGHT_APPLICATION_SETTINGS_API_KEYS); err != nil {
return nil, err
}
err = is.withDatabase(ctx, func(db *gorm.DB) (err error) {
_, key, err = store.GetAPIKeyStore(db).GetAPIKey(ctx, req.KeyID)
if err != nil {
return err
}
return nil
})
if err != nil {
return nil, err
}
key.Key = ""
return key, nil
}
func (is *IdentityServer) updateApplicationAPIKey(ctx context.Context, req *ttnpb.UpdateApplicationAPIKeyRequest) (key *ttnpb.APIKey, err error) {
// Require that caller has rights to manage API keys.
if err := rights.RequireApplication(ctx, req.ApplicationIdentifiers, ttnpb.RIGHT_APPLICATION_SETTINGS_API_KEYS); err != nil {
return nil, err
}
err = is.withDatabase(ctx, func(db *gorm.DB) (err error) {
if len(req.APIKey.Rights) > 0 {
_, key, err := store.GetAPIKeyStore(db).GetAPIKey(ctx, req.APIKey.ID)
if err != nil {
return err
}
newRights := ttnpb.RightsFrom(req.APIKey.Rights...)
existingRights := ttnpb.RightsFrom(key.Rights...)
// Require the caller to have all added rights.
if err := rights.RequireApplication(ctx, req.ApplicationIdentifiers, newRights.Sub(existingRights).GetRights()...); err != nil {
return err
}
// Require the caller to have all removed rights.
if err := rights.RequireApplication(ctx, req.ApplicationIdentifiers, existingRights.Sub(newRights).GetRights()...); err != nil {
return err
}
}
key, err = store.GetAPIKeyStore(db).UpdateAPIKey(ctx, req.ApplicationIdentifiers, &req.APIKey)
return err
})
if err != nil {
return nil, err
}
if key == nil { // API key was deleted.
events.Publish(evtDeleteApplicationAPIKey.NewWithIdentifiersAndData(ctx, req.ApplicationIdentifiers, nil))
return &ttnpb.APIKey{}, nil
}
key.Key = ""
events.Publish(evtUpdateApplicationAPIKey.NewWithIdentifiersAndData(ctx, req.ApplicationIdentifiers, nil))
err = is.SendContactsEmail(ctx, req.EntityIdentifiers(), func(data emails.Data) email.MessageData {
data.SetEntity(req.EntityIdentifiers())
return &emails.APIKeyChanged{Data: data, Identifier: key.PrettyName(), Rights: key.Rights}
})
if err != nil {
log.FromContext(ctx).WithError(err).Error("Could not send API key update notification email")
}
return key, nil
}
func (is *IdentityServer) getApplicationCollaborator(ctx context.Context, req *ttnpb.GetApplicationCollaboratorRequest) (*ttnpb.GetCollaboratorResponse, error) {
if err := rights.RequireApplication(ctx, req.ApplicationIdentifiers, ttnpb.RIGHT_APPLICATION_SETTINGS_COLLABORATORS); err != nil {
return nil, err
}
res := &ttnpb.GetCollaboratorResponse{
OrganizationOrUserIdentifiers: req.OrganizationOrUserIdentifiers,
}
err := is.withDatabase(ctx, func(db *gorm.DB) error {
rights, err := is.getMembershipStore(ctx, db).GetMember(
ctx,
&req.OrganizationOrUserIdentifiers,
req.ApplicationIdentifiers,
)
if err != nil {
return err
}
res.Rights = rights.GetRights()
return nil
})
if err != nil {
return nil, err
}
return res, nil
}
func (is *IdentityServer) setApplicationCollaborator(ctx context.Context, req *ttnpb.SetApplicationCollaboratorRequest) (*types.Empty, error) {
// Require that caller has rights to manage collaborators.
if err := rights.RequireApplication(ctx, req.ApplicationIdentifiers, ttnpb.RIGHT_APPLICATION_SETTINGS_COLLABORATORS); err != nil {
return nil, err
}
err := is.withDatabase(ctx, func(db *gorm.DB) error {
store := is.getMembershipStore(ctx, db)
if len(req.Collaborator.Rights) > 0 {
newRights := ttnpb.RightsFrom(req.Collaborator.Rights...)
existingRights, err := store.GetMember(
ctx,
&req.Collaborator.OrganizationOrUserIdentifiers,
req.ApplicationIdentifiers,
)
if err != nil && !errors.IsNotFound(err) {
return err
}
// Require the caller to have all added rights.
if err := rights.RequireApplication(ctx, req.ApplicationIdentifiers, newRights.Sub(existingRights).GetRights()...); err != nil {
return err
}
// Require the caller to have all removed rights.
if err := rights.RequireApplication(ctx, req.ApplicationIdentifiers, existingRights.Sub(newRights).GetRights()...); err != nil {
return err
}
}
return store.SetMember(
ctx,
&req.Collaborator.OrganizationOrUserIdentifiers,
req.ApplicationIdentifiers,
ttnpb.RightsFrom(req.Collaborator.Rights...),
)
})
if err != nil {
return nil, err
}
if len(req.Collaborator.Rights) > 0 {
events.Publish(evtUpdateApplicationCollaborator.NewWithIdentifiersAndData(ctx, ttnpb.CombineIdentifiers(req.ApplicationIdentifiers, req.Collaborator), nil))
err = is.SendContactsEmail(ctx, req.EntityIdentifiers(), func(data emails.Data) email.MessageData {
data.SetEntity(req.EntityIdentifiers())
return &emails.CollaboratorChanged{Data: data, Collaborator: req.Collaborator}
})
if err != nil {
log.FromContext(ctx).WithError(err).Error("Could not send collaborator updated notification email")
}
} else {
events.Publish(evtDeleteApplicationCollaborator.NewWithIdentifiersAndData(ctx, ttnpb.CombineIdentifiers(req.ApplicationIdentifiers, req.Collaborator), nil))
}
return ttnpb.Empty, nil
}
func (is *IdentityServer) listApplicationCollaborators(ctx context.Context, req *ttnpb.ListApplicationCollaboratorsRequest) (collaborators *ttnpb.Collaborators, err error) {
if err = rights.RequireApplication(ctx, req.ApplicationIdentifiers, ttnpb.RIGHT_APPLICATION_SETTINGS_COLLABORATORS); err != nil {
return nil, err
}
var total uint64
ctx = store.WithPagination(ctx, req.Limit, req.Page, &total)
defer func() {
if err == nil {
setTotalHeader(ctx, total)
}
}()
err = is.withDatabase(ctx, func(db *gorm.DB) error {
memberRights, err := is.getMembershipStore(ctx, db).FindMembers(ctx, req.ApplicationIdentifiers)
if err != nil {
return err
}
collaborators = &ttnpb.Collaborators{}
for member, rights := range memberRights {
collaborators.Collaborators = append(collaborators.Collaborators, &ttnpb.Collaborator{
OrganizationOrUserIdentifiers: *member,
Rights: rights.GetRights(),
})
}
return nil
})
if err != nil {
return nil, err
}
return collaborators, nil
}
type applicationAccess struct {
*IdentityServer
}
func (aa *applicationAccess) ListRights(ctx context.Context, req *ttnpb.ApplicationIdentifiers) (*ttnpb.Rights, error) {
return aa.listApplicationRights(ctx, req)
}
func (aa *applicationAccess) CreateAPIKey(ctx context.Context, req *ttnpb.CreateApplicationAPIKeyRequest) (*ttnpb.APIKey, error) {
return aa.createApplicationAPIKey(ctx, req)
}
func (aa *applicationAccess) ListAPIKeys(ctx context.Context, req *ttnpb.ListApplicationAPIKeysRequest) (*ttnpb.APIKeys, error) {
return aa.listApplicationAPIKeys(ctx, req)
}
func (aa *applicationAccess) GetAPIKey(ctx context.Context, req *ttnpb.GetApplicationAPIKeyRequest) (*ttnpb.APIKey, error) {
return aa.getApplicationAPIKey(ctx, req)
}
func (aa *applicationAccess) UpdateAPIKey(ctx context.Context, req *ttnpb.UpdateApplicationAPIKeyRequest) (*ttnpb.APIKey, error) {
return aa.updateApplicationAPIKey(ctx, req)
}
func (aa *applicationAccess) GetCollaborator(ctx context.Context, req *ttnpb.GetApplicationCollaboratorRequest) (*ttnpb.GetCollaboratorResponse, error) {
return aa.getApplicationCollaborator(ctx, req)
}
func (aa *applicationAccess) SetCollaborator(ctx context.Context, req *ttnpb.SetApplicationCollaboratorRequest) (*types.Empty, error) {
return aa.setApplicationCollaborator(ctx, req)
}
func (aa *applicationAccess) ListCollaborators(ctx context.Context, req *ttnpb.ListApplicationCollaboratorsRequest) (*ttnpb.Collaborators, error) {
return aa.listApplicationCollaborators(ctx, req)
}
// Copyright © 2019 The Things Network Foundation, The Things Industries B.V.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package identityserver
import (
"context"
"github.com/gogo/protobuf/types"
"github.com/jinzhu/gorm"
"go.thethings.network/lorawan-stack/v3/pkg/auth/rights"
"go.thethings.network/lorawan-stack/v3/pkg/email"
"go.thethings.network/lorawan-stack/v3/pkg/errors"
"go.thethings.network/lorawan-stack/v3/pkg/events"
"go.thethings.network/lorawan-stack/v3/pkg/identityserver/emails"
"go.thethings.network/lorawan-stack/v3/pkg/identityserver/store"
"go.thethings.network/lorawan-stack/v3/pkg/log"
"go.thethings.network/lorawan-stack/v3/pkg/ttnpb"
)
var (
evtCreateGatewayAPIKey = events.Define(
"gateway.api-key.create", "create gateway API key",
events.WithVisibility(ttnpb.RIGHT_GATEWAY_SETTINGS_API_KEYS),
events.WithAuthFromContext(),
events.WithClientInfoFromContext(),
)
evtUpdateGatewayAPIKey = events.Define(
"gateway.api-key.update", "update gateway API key",
events.WithVisibility(ttnpb.RIGHT_GATEWAY_SETTINGS_API_KEYS),
events.WithAuthFromContext(),
events.WithClientInfoFromContext(),
)
evtDeleteGatewayAPIKey = events.Define(
"gateway.api-key.delete", "delete gateway API key",
events.WithVisibility(ttnpb.RIGHT_GATEWAY_SETTINGS_API_KEYS),
events.WithAuthFromContext(),
events.WithClientInfoFromContext(),
)
evtUpdateGatewayCollaborator = events.Define(
"gateway.collaborator.update", "update gateway collaborator",
events.WithVisibility(
ttnpb.RIGHT_GATEWAY_SETTINGS_COLLABORATORS,
ttnpb.RIGHT_USER_GATEWAYS_LIST,
),
events.WithAuthFromContext(),
events.WithClientInfoFromContext(),
)
evtDeleteGatewayCollaborator = events.Define(
"gateway.collaborator.delete", "delete gateway collaborator",
events.WithVisibility(
ttnpb.RIGHT_GATEWAY_SETTINGS_COLLABORATORS,
ttnpb.RIGHT_USER_GATEWAYS_LIST,
),
events.WithAuthFromContext(),
events.WithClientInfoFromContext(),
)
)
func (is *IdentityServer) listGatewayRights(ctx context.Context, ids *ttnpb.GatewayIdentifiers) (*ttnpb.Rights, error) {
gtwRights, err := rights.ListGateway(ctx, *ids)
if err != nil {
return nil, err
}
return gtwRights.Intersect(ttnpb.AllGatewayRights), nil
}
func (is *IdentityServer) createGatewayAPIKey(ctx context.Context, req *ttnpb.CreateGatewayAPIKeyRequest) (key *ttnpb.APIKey, err error) {
// Require that caller has rights to manage API keys.
if err = rights.RequireGateway(ctx, req.GatewayIdentifiers, ttnpb.RIGHT_GATEWAY_SETTINGS_API_KEYS); err != nil {
return nil, err
}
// Require that caller has at least the rights of the API key.
if err = rights.RequireGateway(ctx, req.GatewayIdentifiers, req.Rights...); err != nil {
return nil, err
}
key, token, err := GenerateAPIKey(ctx, req.Name, req.Rights...)
if err != nil {
return nil, err
}
err = is.withDatabase(ctx, func(db *gorm.DB) error {
return store.GetAPIKeyStore(db).CreateAPIKey(ctx, req.GatewayIdentifiers, key)
})
if err != nil {
return nil, err
}
key.Key = token
events.Publish(evtCreateGatewayAPIKey.NewWithIdentifiersAndData(ctx, req.GatewayIdentifiers, nil))
err = is.SendContactsEmail(ctx, req.EntityIdentifiers(), func(data emails.Data) email.MessageData {
data.SetEntity(req.EntityIdentifiers())
return &emails.APIKeyCreated{Data: data, Identifier: key.PrettyName(), Rights: key.Rights}
})
if err != nil {
log.FromContext(ctx).WithError(err).Error("Could not send API key creation notification email")
}
return key, nil
}
func (is *IdentityServer) listGatewayAPIKeys(ctx context.Context, req *ttnpb.ListGatewayAPIKeysRequest) (keys *ttnpb.APIKeys, err error) {
if err = rights.RequireGateway(ctx, req.GatewayIdentifiers, ttnpb.RIGHT_GATEWAY_SETTINGS_API_KEYS); err != nil {
return nil, err
}
var total uint64
ctx = store.WithPagination(ctx, req.Limit, req.Page, &total)
defer func() {
if err == nil {
setTotalHeader(ctx, total)
}
}()
keys = &ttnpb.APIKeys{}
err = is.withDatabase(ctx, func(db *gorm.DB) (err error) {
keys.APIKeys, err = store.GetAPIKeyStore(db).FindAPIKeys(ctx, req.GatewayIdentifiers)
return err
})
if err != nil {
return nil, err
}
for _, key := range keys.APIKeys {
key.Key = ""
}
return keys, nil
}
func (is *IdentityServer) getGatewayAPIKey(ctx context.Context, req *ttnpb.GetGatewayAPIKeyRequest) (key *ttnpb.APIKey, err error) {
if err = rights.RequireGateway(ctx, req.GatewayIdentifiers, ttnpb.RIGHT_GATEWAY_SETTINGS_API_KEYS); err != nil {
return nil, err
}
err = is.withDatabase(ctx, func(db *gorm.DB) (err error) {
_, key, err = store.GetAPIKeyStore(db).GetAPIKey(ctx, req.KeyID)
if err != nil {
return err
}
return nil
})
if err != nil {
return nil, err
}
key.Key = ""
return key, nil
}
func (is *IdentityServer) updateGatewayAPIKey(ctx context.Context, req *ttnpb.UpdateGatewayAPIKeyRequest) (key *ttnpb.APIKey, err error) {
// Require that caller has rights to manage API keys.
if err = rights.RequireGateway(ctx, req.GatewayIdentifiers, ttnpb.RIGHT_GATEWAY_SETTINGS_API_KEYS); err != nil {
return nil, err
}
err = is.withDatabase(ctx, func(db *gorm.DB) (err error) {
if len(req.APIKey.Rights) > 0 {
_, key, err := store.GetAPIKeyStore(db).GetAPIKey(ctx, req.APIKey.ID)
if err != nil {
return err
}
newRights := ttnpb.RightsFrom(req.APIKey.Rights...)
existingRights := ttnpb.RightsFrom(key.Rights...)
// Require the caller to have all added rights.
if err := rights.RequireGateway(ctx, req.GatewayIdentifiers, newRights.Sub(existingRights).GetRights()...); err != nil {
return err
}
// Require the caller to have all removed rights.
if err := rights.RequireGateway(ctx, req.GatewayIdentifiers, existingRights.Sub(newRights).GetRights()...); err != nil {
return err
}
}
key, err = store.GetAPIKeyStore(db).UpdateAPIKey(ctx, req.GatewayIdentifiers, &req.APIKey)
return err
})
if err != nil {
return nil, err
}
if key == nil { // API key was deleted.
events.Publish(evtDeleteGatewayAPIKey.NewWithIdentifiersAndData(ctx, req.GatewayIdentifiers, nil))
return &ttnpb.APIKey{}, nil
}
key.Key = ""
events.Publish(evtUpdateGatewayAPIKey.NewWithIdentifiersAndData(ctx, req.GatewayIdentifiers, nil))
err = is.SendContactsEmail(ctx, req.EntityIdentifiers(), func(data emails.Data) email.MessageData {
data.SetEntity(req.EntityIdentifiers())
return &emails.APIKeyChanged{Data: data, Identifier: key.PrettyName(), Rights: key.Rights}
})
if err != nil {
log.FromContext(ctx).WithError(err).Error("Could not send API key update notification email")
}
return key, nil
}
func (is *IdentityServer) getGatewayCollaborator(ctx context.Context, req *ttnpb.GetGatewayCollaboratorRequest) (*ttnpb.GetCollaboratorResponse, error) {
if err := rights.RequireGateway(ctx, req.GatewayIdentifiers, ttnpb.RIGHT_GATEWAY_SETTINGS_COLLABORATORS); err != nil {
return nil, err
}
res := &ttnpb.GetCollaboratorResponse{
OrganizationOrUserIdentifiers: req.OrganizationOrUserIdentifiers,
}
err := is.withDatabase(ctx, func(db *gorm.DB) error {
rights, err := is.getMembershipStore(ctx, db).GetMember(
ctx,
&req.OrganizationOrUserIdentifiers,
req.GatewayIdentifiers,
)
if err != nil {
return err
}
res.Rights = rights.GetRights()
return nil
})
if err != nil {
return nil, err
}
return res, nil
}
func (is *IdentityServer) setGatewayCollaborator(ctx context.Context, req *ttnpb.SetGatewayCollaboratorRequest) (*types.Empty, error) {
// Require that caller has rights to manage collaborators.
if err := rights.RequireGateway(ctx, req.GatewayIdentifiers, ttnpb.RIGHT_GATEWAY_SETTINGS_COLLABORATORS); err != nil {
return nil, err
}
err := is.withDatabase(ctx, func(db *gorm.DB) error {
store := is.getMembershipStore(ctx, db)
if len(req.Collaborator.Rights) > 0 {
newRights := ttnpb.RightsFrom(req.Collaborator.Rights...)
existingRights, err := store.GetMember(
ctx,
&req.Collaborator.OrganizationOrUserIdentifiers,
req.GatewayIdentifiers,
)
if err != nil && !errors.IsNotFound(err) {
return err
}
// Require the caller to have all added rights.
if err := rights.RequireGateway(ctx, req.GatewayIdentifiers, newRights.Sub(existingRights).GetRights()...); err != nil {
return err
}
// Require the caller to have all removed rights.
if err := rights.RequireGateway(ctx, req.GatewayIdentifiers, existingRights.Sub(newRights).GetRights()...); err != nil {
return err
}
}
return store.SetMember(
ctx,
&req.Collaborator.OrganizationOrUserIdentifiers,
req.GatewayIdentifiers,
ttnpb.RightsFrom(req.Collaborator.Rights...),
)
})
if err != nil {
return nil, err
}
if len(req.Collaborator.Rights) > 0 {
events.Publish(evtUpdateGatewayCollaborator.NewWithIdentifiersAndData(ctx, ttnpb.CombineIdentifiers(req.GatewayIdentifiers, req.Collaborator), nil))
err = is.SendContactsEmail(ctx, req.EntityIdentifiers(), func(data emails.Data) email.MessageData {
data.SetEntity(req.EntityIdentifiers())
return &emails.CollaboratorChanged{Data: data, Collaborator: req.Collaborator}
})
if err != nil {
log.FromContext(ctx).WithError(err).Error("Could not send collaborator updated notification email")
}
} else {
events.Publish(evtDeleteGatewayCollaborator.NewWithIdentifiersAndData(ctx, ttnpb.CombineIdentifiers(req.GatewayIdentifiers, req.Collaborator), nil))
}
return ttnpb.Empty, nil
}
func (is *IdentityServer) listGatewayCollaborators(ctx context.Context, req *ttnpb.ListGatewayCollaboratorsRequest) (collaborators *ttnpb.Collaborators, err error) {
if err = rights.RequireGateway(ctx, req.GatewayIdentifiers, ttnpb.RIGHT_GATEWAY_SETTINGS_COLLABORATORS); err != nil {
return nil, err
}
var total uint64
ctx = store.WithPagination(ctx, req.Limit, req.Page, &total)
defer func() {
if err == nil {
setTotalHeader(ctx, total)
}
}()
err = is.withDatabase(ctx, func(db *gorm.DB) error {
memberRights, err := is.getMembershipStore(ctx, db).FindMembers(ctx, req.GatewayIdentifiers)
if err != nil {
return err
}
collaborators = &ttnpb.Collaborators{}
for member, rights := range memberRights {
collaborators.Collaborators = append(collaborators.Collaborators, &ttnpb.Collaborator{
OrganizationOrUserIdentifiers: *member,
Rights: rights.GetRights(),
})
}
return nil
})
if err != nil {
return nil, err
}
return collaborators, nil
}
type gatewayAccess struct {
*IdentityServer
}
func (ga *gatewayAccess) ListRights(ctx context.Context, req *ttnpb.GatewayIdentifiers) (*ttnpb.Rights, error) {
return ga.listGatewayRights(ctx, req)
}
func (ga *gatewayAccess) CreateAPIKey(ctx context.Context, req *ttnpb.CreateGatewayAPIKeyRequest) (*ttnpb.APIKey, error) {
return ga.createGatewayAPIKey(ctx, req)
}
func (ga *gatewayAccess) ListAPIKeys(ctx context.Context, req *ttnpb.ListGatewayAPIKeysRequest) (*ttnpb.APIKeys, error) {
return ga.listGatewayAPIKeys(ctx, req)
}
func (ga *gatewayAccess) GetAPIKey(ctx context.Context, req *ttnpb.GetGatewayAPIKeyRequest) (*ttnpb.APIKey, error) {
return ga.getGatewayAPIKey(ctx, req)
}
func (ga *gatewayAccess) UpdateAPIKey(ctx context.Context, req *ttnpb.UpdateGatewayAPIKeyRequest) (*ttnpb.APIKey, error) {
return ga.updateGatewayAPIKey(ctx, req)
}
func (ga *gatewayAccess) GetCollaborator(ctx context.Context, req *ttnpb.GetGatewayCollaboratorRequest) (*ttnpb.GetCollaboratorResponse, error) {
return ga.getGatewayCollaborator(ctx, req)
}
func (ga *gatewayAccess) SetCollaborator(ctx context.Context, req *ttnpb.SetGatewayCollaboratorRequest) (*types.Empty, error) {
return ga.setGatewayCollaborator(ctx, req)
}
func (ga *gatewayAccess) ListCollaborators(ctx context.Context, req *ttnpb.ListGatewayCollaboratorsRequest) (*ttnpb.Collaborators, error) {
return ga.listGatewayCollaborators(ctx, req)
}
Accelerate Your Automation Test Cycles With LambdaTest
Leverage LambdaTest’s cloud-based platform to execute your automation tests in parallel and trim down your test execution time significantly. Your first 100 automation testing minutes are on us.