Skip to content

Commit

Permalink
Merge pull request #6378 from TheThingsNetwork/feature/6323-limit-tec…
Browse files Browse the repository at this point in the history
…h-admin-contact-ops

Limit tech admin contact operations via IS config
  • Loading branch information
nicholaspcr committed Jul 7, 2023
2 parents d199b2e + 5245e76 commit 644376a
Show file tree
Hide file tree
Showing 22 changed files with 1,120 additions and 304 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ For details about compatibility between different releases, see the **Commitment
- RPCs and CLI command to delete a batch of end devices within an application.
- Check `ttn-lw-cli end-devices batch-delete` for more details.
- Add `UserInput` component to the Console to handle user id input fields by implementing an autosuggest.
- The Identity Server configuration has a new optional restriction regarding adminstrative and technical contacts of entities. This limits the action of an user or organization to set these contacts only to themselves, it is disabled by default but it is possible to enable it by setting `is.collaborator-rights.set-others-as-contacts` as false.

### Changed

Expand Down
8 changes: 8 additions & 0 deletions api/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,7 @@
- [Message `GetIsConfigurationResponse`](#ttn.lorawan.v3.GetIsConfigurationResponse)
- [Message `IsConfiguration`](#ttn.lorawan.v3.IsConfiguration)
- [Message `IsConfiguration.AdminRights`](#ttn.lorawan.v3.IsConfiguration.AdminRights)
- [Message `IsConfiguration.CollaboratorRights`](#ttn.lorawan.v3.IsConfiguration.CollaboratorRights)
- [Message `IsConfiguration.EndDevicePicture`](#ttn.lorawan.v3.IsConfiguration.EndDevicePicture)
- [Message `IsConfiguration.ProfilePicture`](#ttn.lorawan.v3.IsConfiguration.ProfilePicture)
- [Message `IsConfiguration.UserLogin`](#ttn.lorawan.v3.IsConfiguration.UserLogin)
Expand Down Expand Up @@ -5290,13 +5291,20 @@ OrganizationOrUserIdentifiers contains either organization or user identifiers.
| `user_rights` | [`IsConfiguration.UserRights`](#ttn.lorawan.v3.IsConfiguration.UserRights) | | |
| `user_login` | [`IsConfiguration.UserLogin`](#ttn.lorawan.v3.IsConfiguration.UserLogin) | | |
| `admin_rights` | [`IsConfiguration.AdminRights`](#ttn.lorawan.v3.IsConfiguration.AdminRights) | | |
| `collaborator_rights` | [`IsConfiguration.CollaboratorRights`](#ttn.lorawan.v3.IsConfiguration.CollaboratorRights) | | |

### <a name="ttn.lorawan.v3.IsConfiguration.AdminRights">Message `IsConfiguration.AdminRights`</a>

| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| `all` | [`google.protobuf.BoolValue`](#google.protobuf.BoolValue) | | |

### <a name="ttn.lorawan.v3.IsConfiguration.CollaboratorRights">Message `IsConfiguration.CollaboratorRights`</a>

| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| `set_others_as_contacts` | [`google.protobuf.BoolValue`](#google.protobuf.BoolValue) | | |

### <a name="ttn.lorawan.v3.IsConfiguration.EndDevicePicture">Message `IsConfiguration.EndDevicePicture`</a>

| Field | Type | Label | Description |
Expand Down
11 changes: 11 additions & 0 deletions api/api.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -17601,6 +17601,14 @@
}
}
},
"IsConfigurationCollaboratorRights": {
"type": "object",
"properties": {
"set_others_as_contacts": {
"type": "boolean"
}
}
},
"IsConfigurationEndDevicePicture": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -22305,6 +22313,9 @@
},
"admin_rights": {
"$ref": "#/definitions/IsConfigurationAdminRights"
},
"collaborator_rights": {
"$ref": "#/definitions/IsConfigurationCollaboratorRights"
}
}
},
Expand Down
5 changes: 5 additions & 0 deletions api/identityserver.proto
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@ message IsConfiguration {
reserved 10; reserved "application_limits";
reserved 11; reserved "organization_limits";
reserved 12; reserved "user_limits";

message CollaboratorRights {
google.protobuf.BoolValue set_others_as_contacts = 1;
}
CollaboratorRights collaborator_rights = 13;
}

message GetIsConfigurationResponse {
Expand Down
1 change: 1 addition & 0 deletions cmd/internal/shared/identityserver/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ func init() {
DefaultIdentityServerConfig.UserRights.CreateClients = true
DefaultIdentityServerConfig.UserRights.CreateGateways = true
DefaultIdentityServerConfig.UserRights.CreateOrganizations = true
DefaultIdentityServerConfig.CollaboratorRights.SetOthersAsContacts = true
DefaultIdentityServerConfig.LoginTokens.TokenTTL = time.Hour
DefaultIdentityServerConfig.Delete.Restore = 24 * time.Hour
}
9 changes: 9 additions & 0 deletions config/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -5372,6 +5372,15 @@
"file": "errors.go"
}
},
"error:pkg/identityserver/store:contact_info_restricted": {
"translations": {
"en": "contact information can only reference the caller"
},
"description": {
"package": "pkg/identityserver/store",
"file": "errors.go"
}
},
"error:pkg/identityserver/store:end_device_not_found": {
"translations": {
"en": "end device with id `{device_id}` not found in application with id `{application_id}`"
Expand Down
133 changes: 106 additions & 27 deletions pkg/identityserver/application_registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,18 @@ var (
)

var (
errAdminsCreateApplications = errors.DefinePermissionDenied("admins_create_applications", "applications may only be created by admins, or in organizations")
errAdminsPurgeApplications = errors.DefinePermissionDenied("admins_purge_applications", "applications may only be purged by admins")
errDevEUIIssuingNotEnabled = errors.DefineInvalidArgument("dev_eui_issuing_not_enabled", "DevEUI issuing not configured")
errAdminsCreateApplications = errors.DefinePermissionDenied(
"admins_create_applications",
"applications may only be created by admins, or in organizations",
)
errAdminsPurgeApplications = errors.DefinePermissionDenied(
"admins_purge_applications",
"applications may only be purged by admins",
)
errDevEUIIssuingNotEnabled = errors.DefineInvalidArgument(
"dev_eui_issuing_not_enabled",
"DevEUI issuing not configured",
)
)

func (is *IdentityServer) createApplication( //nolint:gocyclo
Expand All @@ -88,18 +97,24 @@ func (is *IdentityServer) createApplication( //nolint:gocyclo
return nil, err
}
} else if orgIDs := req.Collaborator.GetOrganizationIds(); orgIDs != nil {
if err = rights.RequireOrganization(ctx, orgIDs, ttnpb.Right_RIGHT_ORGANIZATION_APPLICATIONS_CREATE); err != nil {
if err = rights.RequireOrganization(
ctx, orgIDs, ttnpb.Right_RIGHT_ORGANIZATION_APPLICATIONS_CREATE,
); err != nil {
return nil, err
}
}
if req.Application.AdministrativeContact == nil {
req.Application.AdministrativeContact = req.Collaborator
} else if err := validateCollaboratorEqualsContact(req.Collaborator, req.Application.AdministrativeContact); err != nil {
} else if err := validateCollaboratorEqualsContact(
req.Collaborator, req.Application.AdministrativeContact,
); err != nil {
return nil, err
}
if req.Application.TechnicalContact == nil {
req.Application.TechnicalContact = req.Collaborator
} else if err := validateCollaboratorEqualsContact(req.Collaborator, req.Application.TechnicalContact); err != nil {
} else if err := validateCollaboratorEqualsContact(
req.Collaborator, req.Application.TechnicalContact,
); err != nil {
return nil, err
}
if err := validateContactInfo(req.Application.ContactInfo); err != nil {
Expand Down Expand Up @@ -134,7 +149,10 @@ func (is *IdentityServer) createApplication( //nolint:gocyclo
return app, nil
}

func (is *IdentityServer) getApplication(ctx context.Context, req *ttnpb.GetApplicationRequest) (app *ttnpb.Application, err error) {
func (is *IdentityServer) getApplication(
ctx context.Context,
req *ttnpb.GetApplicationRequest,
) (app *ttnpb.Application, err error) {
if err = is.RequireAuthenticated(ctx); err != nil {
return nil, err
}
Expand Down Expand Up @@ -164,7 +182,10 @@ func (is *IdentityServer) getApplication(ctx context.Context, req *ttnpb.GetAppl
return app, nil
}

func (is *IdentityServer) listApplications(ctx context.Context, req *ttnpb.ListApplicationsRequest) (apps *ttnpb.Applications, err error) {
func (is *IdentityServer) listApplications( // nolint:gocyclo
ctx context.Context,
req *ttnpb.ListApplicationsRequest,
) (apps *ttnpb.Applications, err error) {
req.FieldMask = cleanFieldMaskPaths(ttnpb.ApplicationFieldPathsNested, req.FieldMask, getPaths, nil)

authInfo, err := is.authInfo(ctx)
Expand Down Expand Up @@ -195,7 +216,9 @@ func (is *IdentityServer) listApplications(ctx context.Context, req *ttnpb.ListA
return nil, err
}
} else if orgIDs := req.Collaborator.GetOrganizationIds(); orgIDs != nil {
if err = rights.RequireOrganization(ctx, orgIDs, ttnpb.Right_RIGHT_ORGANIZATION_APPLICATIONS_LIST); err != nil {
if err = rights.RequireOrganization(
ctx, orgIDs, ttnpb.Right_RIGHT_ORGANIZATION_APPLICATIONS_LIST,
); err != nil {
return nil, err
}
}
Expand Down Expand Up @@ -227,7 +250,11 @@ func (is *IdentityServer) listApplications(ctx context.Context, req *ttnpb.ListA
if len(ids) == 0 {
return nil
}
callerMemberships, err = st.FindAccountMembershipChains(ctx, callerAccountID, "application", idStrings(ids...)...)
callerMemberships, err = st.FindAccountMembershipChains(
ctx,
callerAccountID,
"application",
idStrings(ids...)...)
if err != nil {
return err
}
Expand Down Expand Up @@ -260,8 +287,13 @@ func (is *IdentityServer) listApplications(ctx context.Context, req *ttnpb.ListA
return apps, nil
}

func (is *IdentityServer) updateApplication(ctx context.Context, req *ttnpb.UpdateApplicationRequest) (app *ttnpb.Application, err error) {
if err = rights.RequireApplication(ctx, req.Application.GetIds(), ttnpb.Right_RIGHT_APPLICATION_SETTINGS_BASIC); err != nil {
func (is *IdentityServer) updateApplication(
ctx context.Context,
req *ttnpb.UpdateApplicationRequest,
) (app *ttnpb.Application, err error) {
if err = rights.RequireApplication(
ctx, req.Application.GetIds(), ttnpb.Right_RIGHT_APPLICATION_SETTINGS_BASIC,
); err != nil {
return nil, err
}
req.FieldMask = cleanFieldMaskPaths(ttnpb.ApplicationFieldPathsNested, req.FieldMask, nil, getPaths)
Expand All @@ -273,12 +305,26 @@ func (is *IdentityServer) updateApplication(ctx context.Context, req *ttnpb.Upda
return nil, err
}
}
req.FieldMask.Paths = ttnpb.FlattenPaths(req.FieldMask.Paths, []string{"administrative_contact", "technical_contact"})

if err := is.validateContactInfoRestrictions(
ctx, req.Application.GetAdministrativeContact(), req.Application.GetTechnicalContact(),
); err != nil {
return nil, err
}

req.FieldMask.Paths = ttnpb.FlattenPaths(
req.FieldMask.Paths,
[]string{"administrative_contact", "technical_contact"},
)
err = is.store.Transact(ctx, func(ctx context.Context, st store.Store) (err error) {
if err := validateContactIsCollaborator(ctx, st, req.Application.AdministrativeContact, req.Application.GetEntityIdentifiers()); err != nil {
if err := validateContactIsCollaborator(
ctx, st, req.Application.AdministrativeContact, req.Application.GetEntityIdentifiers(),
); err != nil {
return err
}
if err := validateContactIsCollaborator(ctx, st, req.Application.TechnicalContact, req.Application.GetEntityIdentifiers()); err != nil {
if err := validateContactIsCollaborator(
ctx, st, req.Application.TechnicalContact, req.Application.GetEntityIdentifiers(),
); err != nil {
return err
}
app, err = st.UpdateApplication(ctx, req.Application, req.FieldMask.GetPaths())
Expand All @@ -297,13 +343,21 @@ func (is *IdentityServer) updateApplication(ctx context.Context, req *ttnpb.Upda
if err != nil {
return nil, err
}
events.Publish(evtUpdateApplication.NewWithIdentifiersAndData(ctx, req.Application.GetIds(), req.FieldMask.GetPaths()))
events.Publish(
evtUpdateApplication.NewWithIdentifiersAndData(ctx, req.Application.GetIds(), req.FieldMask.GetPaths()),
)
return app, nil
}

var errApplicationHasDevices = errors.DefineFailedPrecondition("application_has_devices", "application still has `{count}` devices")
var errApplicationHasDevices = errors.DefineFailedPrecondition(
"application_has_devices",
"application still has `{count}` devices",
)

func (is *IdentityServer) deleteApplication(ctx context.Context, ids *ttnpb.ApplicationIdentifiers) (*emptypb.Empty, error) {
func (is *IdentityServer) deleteApplication(
ctx context.Context,
ids *ttnpb.ApplicationIdentifiers,
) (*emptypb.Empty, error) {
if err := rights.RequireApplication(ctx, ids, ttnpb.Right_RIGHT_APPLICATION_DELETE); err != nil {
return nil, err
}
Expand All @@ -324,8 +378,13 @@ func (is *IdentityServer) deleteApplication(ctx context.Context, ids *ttnpb.Appl
return ttnpb.Empty, nil
}

func (is *IdentityServer) restoreApplication(ctx context.Context, ids *ttnpb.ApplicationIdentifiers) (*emptypb.Empty, error) {
if err := rights.RequireApplication(store.WithSoftDeleted(ctx, false), ids, ttnpb.Right_RIGHT_APPLICATION_DELETE); err != nil {
func (is *IdentityServer) restoreApplication(
ctx context.Context,
ids *ttnpb.ApplicationIdentifiers,
) (*emptypb.Empty, error) {
if err := rights.RequireApplication(
store.WithSoftDeleted(ctx, false), ids, ttnpb.Right_RIGHT_APPLICATION_DELETE,
); err != nil {
return nil, err
}
err := is.store.Transact(ctx, func(ctx context.Context, st store.Store) error {
Expand All @@ -349,7 +408,10 @@ func (is *IdentityServer) restoreApplication(ctx context.Context, ids *ttnpb.App
return ttnpb.Empty, nil
}

func (is *IdentityServer) purgeApplication(ctx context.Context, ids *ttnpb.ApplicationIdentifiers) (*emptypb.Empty, error) {
func (is *IdentityServer) purgeApplication(
ctx context.Context,
ids *ttnpb.ApplicationIdentifiers,
) (*emptypb.Empty, error) {
if !is.IsAdmin(ctx) {
return nil, errAdminsPurgeApplications.New()
}
Expand Down Expand Up @@ -385,8 +447,13 @@ func (is *IdentityServer) purgeApplication(ctx context.Context, ids *ttnpb.Appli
return ttnpb.Empty, nil
}

func (is *IdentityServer) issueDevEUI(ctx context.Context, ids *ttnpb.ApplicationIdentifiers) (*ttnpb.IssueDevEUIResponse, error) {
if err := rights.RequireApplication(store.WithSoftDeleted(ctx, false), ids, ttnpb.Right_RIGHT_APPLICATION_DEVICES_WRITE); err != nil {
func (is *IdentityServer) issueDevEUI(
ctx context.Context,
ids *ttnpb.ApplicationIdentifiers,
) (*ttnpb.IssueDevEUIResponse, error) {
if err := rights.RequireApplication(
store.WithSoftDeleted(ctx, false), ids, ttnpb.Right_RIGHT_APPLICATION_DEVICES_WRITE,
); err != nil {
return nil, err
}
if !is.config.DevEUIBlock.Enabled {
Expand Down Expand Up @@ -414,19 +481,28 @@ type applicationRegistry struct {
*IdentityServer
}

func (ar *applicationRegistry) Create(ctx context.Context, req *ttnpb.CreateApplicationRequest) (*ttnpb.Application, error) {
func (ar *applicationRegistry) Create(
ctx context.Context,
req *ttnpb.CreateApplicationRequest,
) (*ttnpb.Application, error) {
return ar.createApplication(ctx, req)
}

func (ar *applicationRegistry) Get(ctx context.Context, req *ttnpb.GetApplicationRequest) (*ttnpb.Application, error) {
return ar.getApplication(ctx, req)
}

func (ar *applicationRegistry) List(ctx context.Context, req *ttnpb.ListApplicationsRequest) (*ttnpb.Applications, error) {
func (ar *applicationRegistry) List(
ctx context.Context,
req *ttnpb.ListApplicationsRequest,
) (*ttnpb.Applications, error) {
return ar.listApplications(ctx, req)
}

func (ar *applicationRegistry) Update(ctx context.Context, req *ttnpb.UpdateApplicationRequest) (*ttnpb.Application, error) {
func (ar *applicationRegistry) Update(
ctx context.Context,
req *ttnpb.UpdateApplicationRequest,
) (*ttnpb.Application, error) {
return ar.updateApplication(ctx, req)
}

Expand All @@ -442,6 +518,9 @@ func (ar *applicationRegistry) Restore(ctx context.Context, req *ttnpb.Applicati
return ar.restoreApplication(ctx, req)
}

func (ar *applicationRegistry) IssueDevEUI(ctx context.Context, req *ttnpb.ApplicationIdentifiers) (*ttnpb.IssueDevEUIResponse, error) {
func (ar *applicationRegistry) IssueDevEUI(
ctx context.Context,
req *ttnpb.ApplicationIdentifiers,
) (*ttnpb.IssueDevEUIResponse, error) {
return ar.issueDevEUI(ctx, req)
}
Loading

0 comments on commit 644376a

Please sign in to comment.