diff --git a/CHANGELOG.md b/CHANGELOG.md index f4204bb887..9dd46614d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/api/api.md b/api/api.md index 088a682b35..aab0577cf7 100644 --- a/api/api.md +++ b/api/api.md @@ -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) @@ -5290,6 +5291,7 @@ 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) | | | ### Message `IsConfiguration.AdminRights` @@ -5297,6 +5299,12 @@ OrganizationOrUserIdentifiers contains either organization or user identifiers. | ----- | ---- | ----- | ----------- | | `all` | [`google.protobuf.BoolValue`](#google.protobuf.BoolValue) | | | +### Message `IsConfiguration.CollaboratorRights` + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| `set_others_as_contacts` | [`google.protobuf.BoolValue`](#google.protobuf.BoolValue) | | | + ### Message `IsConfiguration.EndDevicePicture` | Field | Type | Label | Description | diff --git a/api/api.swagger.json b/api/api.swagger.json index 584c4bb88d..4f10dc8995 100644 --- a/api/api.swagger.json +++ b/api/api.swagger.json @@ -17601,6 +17601,14 @@ } } }, + "IsConfigurationCollaboratorRights": { + "type": "object", + "properties": { + "set_others_as_contacts": { + "type": "boolean" + } + } + }, "IsConfigurationEndDevicePicture": { "type": "object", "properties": { @@ -22305,6 +22313,9 @@ }, "admin_rights": { "$ref": "#/definitions/IsConfigurationAdminRights" + }, + "collaborator_rights": { + "$ref": "#/definitions/IsConfigurationCollaboratorRights" } } }, diff --git a/api/identityserver.proto b/api/identityserver.proto index 6fad88a0f1..fc76d8d99c 100644 --- a/api/identityserver.proto +++ b/api/identityserver.proto @@ -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 { diff --git a/cmd/internal/shared/identityserver/config.go b/cmd/internal/shared/identityserver/config.go index f9943be566..e79ed4477b 100644 --- a/cmd/internal/shared/identityserver/config.go +++ b/cmd/internal/shared/identityserver/config.go @@ -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 } diff --git a/config/messages.json b/config/messages.json index 8a995eb4c7..13e97549b4 100644 --- a/config/messages.json +++ b/config/messages.json @@ -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}`" diff --git a/pkg/identityserver/application_registry.go b/pkg/identityserver/application_registry.go index 0c125faf38..ff047af2e2 100644 --- a/pkg/identityserver/application_registry.go +++ b/pkg/identityserver/application_registry.go @@ -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 @@ -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 { @@ -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 } @@ -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) @@ -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 } } @@ -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 } @@ -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) @@ -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()) @@ -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 } @@ -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 { @@ -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() } @@ -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 { @@ -414,7 +481,10 @@ 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) } @@ -422,11 +492,17 @@ func (ar *applicationRegistry) Get(ctx context.Context, req *ttnpb.GetApplicatio 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) } @@ -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) } diff --git a/pkg/identityserver/application_registry_test.go b/pkg/identityserver/application_registry_test.go index e3d70901db..0966e92478 100644 --- a/pkg/identityserver/application_registry_test.go +++ b/pkg/identityserver/application_registry_test.go @@ -187,7 +187,61 @@ func TestApplicationsCRUD(t *testing.T) { a.So(updated.Name, should.Equal, "Updated Name") } - for _, collaborator := range []*ttnpb.OrganizationOrUserIdentifiers{nil, usr1.GetOrganizationOrUserIdentifiers()} { + t.Run("Contact Info Restrictions", func(t *testing.T) { // nolint:paralleltest + a, ctx := test.New(t) + + oldSetOtherAsContacts := is.config.CollaboratorRights.SetOthersAsContacts + t.Cleanup(func() { is.config.CollaboratorRights.SetOthersAsContacts = oldSetOtherAsContacts }) + is.config.CollaboratorRights.SetOthersAsContacts = false + + // Set usr-2 as collaborator to application. + aac := ttnpb.NewApplicationAccessClient(cc) + _, err := aac.SetCollaborator(ctx, &ttnpb.SetApplicationCollaboratorRequest{ + ApplicationIds: created.GetIds(), + Collaborator: &ttnpb.Collaborator{ + Ids: usr2.GetOrganizationOrUserIdentifiers(), + Rights: []ttnpb.Right{ttnpb.Right_RIGHT_ALL}, + }, + }, creds) + a.So(err, should.BeNil) + + // Attempt to set another collaborator as administrative contact. + _, err = reg.Update(ctx, &ttnpb.UpdateApplicationRequest{ + Application: &ttnpb.Application{ + Ids: created.GetIds(), + AdministrativeContact: usr2.GetOrganizationOrUserIdentifiers(), + }, + FieldMask: ttnpb.FieldMask("administrative_contact"), + }, creds) + a.So(errors.IsPermissionDenied(err), should.BeTrue) + + // Admin can bypass contact info restrictions. + _, err = reg.Update(ctx, &ttnpb.UpdateApplicationRequest{ + Application: &ttnpb.Application{ + Ids: created.GetIds(), + AdministrativeContact: usr1.GetOrganizationOrUserIdentifiers(), + }, + FieldMask: ttnpb.FieldMask("administrative_contact"), + }, adminCreds) + a.So(err, should.BeNil) + + is.config.CollaboratorRights.SetOthersAsContacts = true + + // Now usr-1 can set usr-2 as technical contact. + _, err = reg.Update(ctx, &ttnpb.UpdateApplicationRequest{ + Application: &ttnpb.Application{ + Ids: created.GetIds(), + Name: "Updated Name", + TechnicalContact: usr2.GetOrganizationOrUserIdentifiers(), + }, + FieldMask: ttnpb.FieldMask("technical_contact"), + }, creds) + a.So(err, should.BeNil) + }) + + for _, collaborator := range []*ttnpb.OrganizationOrUserIdentifiers{ + nil, usr1.GetOrganizationOrUserIdentifiers(), + } { list, err := reg.List(ctx, &ttnpb.ListApplicationsRequest{ FieldMask: ttnpb.FieldMask("name"), Collaborator: collaborator, diff --git a/pkg/identityserver/client_registry.go b/pkg/identityserver/client_registry.go index 49708fbe23..b15c5fbb9b 100644 --- a/pkg/identityserver/client_registry.go +++ b/pkg/identityserver/client_registry.go @@ -317,6 +317,12 @@ func (is *IdentityServer) updateClient( []string{"administrative_contact", "technical_contact"}, ) + if err := is.validateContactInfoRestrictions( + ctx, req.Client.GetAdministrativeContact(), req.Client.GetTechnicalContact(), + ); err != nil { + return nil, err + } + if err = is.RequireAdminForFieldUpdate(ctx, req.GetFieldMask().GetPaths(), []string{ "state", "state_description", "skip_authorization", "endorsed", "grants", }); err != nil { @@ -331,10 +337,14 @@ func (is *IdentityServer) updateClient( } err = is.store.Transact(ctx, func(ctx context.Context, st store.Store) (err error) { - if err := validateContactIsCollaborator(ctx, st, req.Client.AdministrativeContact, req.Client.GetEntityIdentifiers()); err != nil { + if err := validateContactIsCollaborator( + ctx, st, req.Client.AdministrativeContact, req.Client.GetEntityIdentifiers(), + ); err != nil { return err } - if err := validateContactIsCollaborator(ctx, st, req.Client.TechnicalContact, req.Client.GetEntityIdentifiers()); err != nil { + if err := validateContactIsCollaborator( + ctx, st, req.Client.TechnicalContact, req.Client.GetEntityIdentifiers(), + ); err != nil { return err } cli, err = st.UpdateClient(ctx, req.Client, req.FieldMask.GetPaths()) diff --git a/pkg/identityserver/client_registry_test.go b/pkg/identityserver/client_registry_test.go index d2cc75a4da..101b04f0c7 100644 --- a/pkg/identityserver/client_registry_test.go +++ b/pkg/identityserver/client_registry_test.go @@ -177,6 +177,57 @@ func TestClientsCRUD(t *testing.T) { a.So(updated.Name, should.Equal, "Updated Name") } + t.Run("Contact Info Restrictions", func(t *testing.T) { // nolint:paralleltest + a, ctx := test.New(t) + + oldSetOtherAsContacts := is.config.CollaboratorRights.SetOthersAsContacts + t.Cleanup(func() { is.config.CollaboratorRights.SetOthersAsContacts = oldSetOtherAsContacts }) + is.config.CollaboratorRights.SetOthersAsContacts = false + + // Set usr-2 as collaborator to client. + cac := ttnpb.NewClientAccessClient(cc) + _, err := cac.SetCollaborator(ctx, &ttnpb.SetClientCollaboratorRequest{ + ClientIds: created.GetIds(), + Collaborator: &ttnpb.Collaborator{ + Ids: usr2.GetOrganizationOrUserIdentifiers(), + Rights: []ttnpb.Right{ttnpb.Right_RIGHT_ALL}, + }, + }, creds) + a.So(err, should.BeNil) + + // Attempt to set another collaborator as administrative contact. + _, err = reg.Update(ctx, &ttnpb.UpdateClientRequest{ + Client: &ttnpb.Client{ + Ids: created.GetIds(), + AdministrativeContact: usr2.GetOrganizationOrUserIdentifiers(), + }, + FieldMask: ttnpb.FieldMask("administrative_contact"), + }, creds) + a.So(errors.IsPermissionDenied(err), should.BeTrue) + + // Admin can bypass contact info restrictions. + _, err = reg.Update(ctx, &ttnpb.UpdateClientRequest{ + Client: &ttnpb.Client{ + Ids: created.GetIds(), + AdministrativeContact: usr1.GetOrganizationOrUserIdentifiers(), + }, + FieldMask: ttnpb.FieldMask("administrative_contact"), + }, adminCreds) + a.So(err, should.BeNil) + + is.config.CollaboratorRights.SetOthersAsContacts = true + + // Now usr-1 can set usr-2 as technical contact. + _, err = reg.Update(ctx, &ttnpb.UpdateClientRequest{ + Client: &ttnpb.Client{ + Ids: created.GetIds(), + TechnicalContact: usr2.GetOrganizationOrUserIdentifiers(), + }, + FieldMask: ttnpb.FieldMask("technical_contact"), + }, creds) + a.So(err, should.BeNil) + }) + updated, err = reg.Update(ctx, &ttnpb.UpdateClientRequest{ Client: &ttnpb.Client{ Ids: created.GetIds(), @@ -210,7 +261,9 @@ func TestClientsCRUD(t *testing.T) { a.So(got.StateDescription, should.Equal, "") } - for _, collaborator := range []*ttnpb.OrganizationOrUserIdentifiers{nil, usr1.GetOrganizationOrUserIdentifiers()} { + for _, collaborator := range []*ttnpb.OrganizationOrUserIdentifiers{ + nil, usr1.GetOrganizationOrUserIdentifiers(), + } { list, err := reg.List(ctx, &ttnpb.ListClientsRequest{ FieldMask: ttnpb.FieldMask("name"), Collaborator: collaborator, diff --git a/pkg/identityserver/config.go b/pkg/identityserver/config.go index 9aee2cf3b4..07caa1e194 100644 --- a/pkg/identityserver/config.go +++ b/pkg/identityserver/config.go @@ -83,13 +83,16 @@ type Config struct { AdminRights struct { All bool `name:"all" description:"Grant all rights to admins, including _KEYS and _ALL"` } `name:"admin-rights"` + CollaboratorRights struct { + SetOthersAsContacts bool `name:"set-others-as-contacts" description:"Allow users to set other users as entity contacts"` // nolint:lll + } `name:"collaborator-rights"` LoginTokens struct { Enabled bool `name:"enabled" description:"enable users requesting login tokens"` TokenTTL time.Duration `name:"token-ttl" description:"TTL of login tokens"` } `name:"login-tokens"` Email struct { email.Config `name:",squash"` - Dir string `name:"dir" description:"Directory to write emails to if the dir provider is used (development only)"` //nolint:lll + Dir string `name:"dir" description:"Directory to write emails to if the dir provider is used (development only)"` // nolint:lll SendGrid sendgrid.Config `name:"sendgrid"` SMTP smtp.Config `name:"smtp"` Templates emailTemplatesConfig `name:"templates"` @@ -207,6 +210,9 @@ func (c Config) toProto() *ttnpb.IsConfiguration { AdminRights: &ttnpb.IsConfiguration_AdminRights{ All: &wrapperspb.BoolValue{Value: c.AdminRights.All}, }, + CollaboratorRights: &ttnpb.IsConfiguration_CollaboratorRights{ + SetOthersAsContacts: &wrapperspb.BoolValue{Value: c.CollaboratorRights.SetOthersAsContacts}, + }, } } diff --git a/pkg/identityserver/contact_info_registry.go b/pkg/identityserver/contact_info_registry.go index 210990e644..b4cb18657e 100644 --- a/pkg/identityserver/contact_info_registry.go +++ b/pkg/identityserver/contact_info_registry.go @@ -42,6 +42,32 @@ var ( ) ) +// validateContactInfoRestrictions fetches the auth info from the context and validates if the caller ID matches the +// provided `ids` in the parameters. The usage of this function should be restricted to testing the administrative and +// technical contacts in methods belonging to each entity registry. +func (is *IdentityServer) validateContactInfoRestrictions( + ctx context.Context, ids ...*ttnpb.OrganizationOrUserIdentifiers, +) error { + authInfo, err := is.authInfo(ctx) + if err != nil { + return err + } + callerID := authInfo.GetOrganizationOrUserIdentifiers() + if is.configFromContext(ctx).CollaboratorRights.SetOthersAsContacts || authInfo.IsAdmin { + return nil + } + + for _, id := range ids { + if id == nil { + continue + } + if callerID.EntityType() != id.EntityType() || callerID.IDString() != id.IDString() { + return store.ErrContactInfoRestricted.New() + } + } + return nil +} + func validateCollaboratorEqualsContact(collaborator, contact *ttnpb.OrganizationOrUserIdentifiers) error { if contact == nil { return nil @@ -52,7 +78,12 @@ func validateCollaboratorEqualsContact(collaborator, contact *ttnpb.Organization return nil } -func validateContactIsCollaborator(ctx context.Context, st store.Store, contact *ttnpb.OrganizationOrUserIdentifiers, entity *ttnpb.EntityIdentifiers) error { +func validateContactIsCollaborator( + ctx context.Context, + st store.Store, + contact *ttnpb.OrganizationOrUserIdentifiers, + entity *ttnpb.EntityIdentifiers, +) error { if contact == nil { return nil } @@ -66,7 +97,10 @@ func validateContactIsCollaborator(ctx context.Context, st store.Store, contact return nil } -func (is *IdentityServer) requestContactInfoValidation(ctx context.Context, ids *ttnpb.EntityIdentifiers) (*ttnpb.ContactInfoValidation, error) { +func (is *IdentityServer) requestContactInfoValidation( + ctx context.Context, + ids *ttnpb.EntityIdentifiers, +) (*ttnpb.ContactInfoValidation, error) { // NOTE: This does NOT check auth. Internal use only. id, err := auth.GenerateID(ctx) if err != nil { @@ -139,10 +173,15 @@ func (is *IdentityServer) requestContactInfoValidation(ctx context.Context, ids Token: validation.Token, TTL: ttl, } - go is.SendTemplateEmailToUsers(is.FromRequestContext(ctx), "validate", func(_ context.Context, data email.TemplateData) (email.TemplateData, error) { - validateData.TemplateData = data - return validateData, nil - }, &ttnpb.User{PrimaryEmailAddress: address}) + go is.SendTemplateEmailToUsers( // nolint:errcheck + is.FromRequestContext(ctx), + "validate", + func(_ context.Context, data email.TemplateData) (email.TemplateData, error) { + validateData.TemplateData = data + return validateData, nil + }, + &ttnpb.User{PrimaryEmailAddress: address}, + ) pendingContactInfo = append(pendingContactInfo, validation.ContactInfo...) validation.Token = "" // Unset tokens after sending emails } @@ -188,7 +227,10 @@ type contactInfoRegistry struct { var errNoContactInfoForEntity = errors.DefineInvalidArgument("no_contact_info", "no contact info for this entity type") -func (cir *contactInfoRegistry) RequestValidation(ctx context.Context, ids *ttnpb.EntityIdentifiers) (*ttnpb.ContactInfoValidation, error) { +func (cir *contactInfoRegistry) RequestValidation( + ctx context.Context, + ids *ttnpb.EntityIdentifiers, +) (*ttnpb.ContactInfoValidation, error) { var err error switch id := ids.GetIds().(type) { case *ttnpb.EntityIdentifiers_ApplicationIds: @@ -210,6 +252,9 @@ func (cir *contactInfoRegistry) RequestValidation(ctx context.Context, ids *ttnp return cir.requestContactInfoValidation(ctx, ids) } -func (cir *contactInfoRegistry) Validate(ctx context.Context, req *ttnpb.ContactInfoValidation) (*emptypb.Empty, error) { +func (cir *contactInfoRegistry) Validate( + ctx context.Context, + req *ttnpb.ContactInfoValidation, +) (*emptypb.Empty, error) { return cir.validateContactInfo(ctx, req) } diff --git a/pkg/identityserver/gateway_registry.go b/pkg/identityserver/gateway_registry.go index dbb8470468..a44889c174 100644 --- a/pkg/identityserver/gateway_registry.go +++ b/pkg/identityserver/gateway_registry.go @@ -65,13 +65,29 @@ var ( ) var ( - errAdminsCreateGateways = errors.DefinePermissionDenied("admins_create_gateways", "gateways may only be created by admins, or in organizations") - errGatewayEUITaken = errors.DefineAlreadyExists("gateway_eui_taken", "a gateway with EUI `{gateway_eui}` is already registered (by you or someone else) as `{gateway_id}`", "administrative_contact") - errAdminsPurgeGateways = errors.DefinePermissionDenied("admins_purge_gateways", "gateways may only be purged by admins") - errClaimAuthenticationCode = errors.DefineInvalidArgument("claim_authentication_code", "invalid claim authentication code") + errAdminsCreateGateways = errors.DefinePermissionDenied( + "admins_create_gateways", + "gateways may only be created by admins, or in organizations", + ) + errGatewayEUITaken = errors.DefineAlreadyExists( + "gateway_eui_taken", + "a gateway with EUI `{gateway_eui}` is already registered (by you or someone else) as `{gateway_id}`", + "administrative_contact", + ) + errAdminsPurgeGateways = errors.DefinePermissionDenied( + "admins_purge_gateways", + "gateways may only be purged by admins", + ) + errClaimAuthenticationCode = errors.DefineInvalidArgument( + "claim_authentication_code", + "invalid claim authentication code", + ) ) -func (is *IdentityServer) createGateway(ctx context.Context, req *ttnpb.CreateGatewayRequest) (gtw *ttnpb.Gateway, err error) { +func (is *IdentityServer) createGateway( // nolint:gocyclo + ctx context.Context, + req *ttnpb.CreateGatewayRequest, +) (gtw *ttnpb.Gateway, err error) { reqGtw := req.GetGateway() if err = blocklist.Check(ctx, reqGtw.GetIds().GetGatewayId()); err != nil { return nil, err @@ -91,7 +107,9 @@ func (is *IdentityServer) createGateway(ctx context.Context, req *ttnpb.CreateGa if req.Gateway.AdministrativeContact == nil { req.Gateway.AdministrativeContact = req.Collaborator - } else if err := validateCollaboratorEqualsContact(req.Collaborator, req.Gateway.AdministrativeContact); err != nil { + } else if err := validateCollaboratorEqualsContact( + req.Collaborator, req.Gateway.AdministrativeContact, + ); err != nil { return nil, err } if req.Gateway.TechnicalContact == nil { @@ -199,7 +217,10 @@ func (is *IdentityServer) createGateway(ctx context.Context, req *ttnpb.CreateGa return gtw, nil } -func (is *IdentityServer) getGateway(ctx context.Context, req *ttnpb.GetGatewayRequest) (gtw *ttnpb.Gateway, err error) { +func (is *IdentityServer) getGateway( // nolint:gocyclo + ctx context.Context, + req *ttnpb.GetGatewayRequest, +) (gtw *ttnpb.Gateway, err error) { if err = is.RequireAuthenticated(ctx); err != nil { return nil, err } @@ -210,7 +231,12 @@ func (is *IdentityServer) getGateway(ctx context.Context, req *ttnpb.GetGatewayR req.FieldMask.Paths = append(req.FieldMask.GetPaths(), "frequency_plan_ids") } } - req.FieldMask = cleanFieldMaskPaths(ttnpb.GatewayFieldPathsNested, req.FieldMask, getPaths, []string{"frequency_plan_id"}) + req.FieldMask = cleanFieldMaskPaths( + ttnpb.GatewayFieldPathsNested, + req.FieldMask, + getPaths, + []string{"frequency_plan_id"}, + ) if err = rights.RequireGateway(ctx, req.GetGatewayIds(), ttnpb.Right_RIGHT_GATEWAY_INFO); err != nil { if !ttnpb.HasOnlyAllowedFields(req.FieldMask.GetPaths(), ttnpb.PublicGatewayFields...) { @@ -259,7 +285,8 @@ func (is *IdentityServer) getGateway(ctx context.Context, req *ttnpb.GetGatewayR if gtw.ClaimAuthenticationCode != nil && gtw.ClaimAuthenticationCode.Secret != nil { value := gtw.ClaimAuthenticationCode.Secret.Value if gtw.ClaimAuthenticationCode.Secret.KeyId != "" { - value, err = is.KeyService().Decrypt(ctx, gtw.ClaimAuthenticationCode.Secret.Value, gtw.ClaimAuthenticationCode.Secret.KeyId) + value, err = is.KeyService(). + Decrypt(ctx, gtw.ClaimAuthenticationCode.Secret.Value, gtw.ClaimAuthenticationCode.Secret.KeyId) if err != nil { return nil, err } @@ -292,7 +319,10 @@ func (is *IdentityServer) getGateway(ctx context.Context, req *ttnpb.GetGatewayR return gtw, nil } -func (is *IdentityServer) getGatewayIdentifiersForEUI(ctx context.Context, req *ttnpb.GetGatewayIdentifiersForEUIRequest) (ids *ttnpb.GatewayIdentifiers, err error) { +func (is *IdentityServer) getGatewayIdentifiersForEUI( + ctx context.Context, + req *ttnpb.GetGatewayIdentifiersForEUIRequest, +) (ids *ttnpb.GatewayIdentifiers, err error) { if err = is.RequireAuthenticated(ctx); err != nil { return nil, err } @@ -312,14 +342,22 @@ func (is *IdentityServer) getGatewayIdentifiersForEUI(ctx context.Context, req * return ids, nil } -func (is *IdentityServer) listGateways(ctx context.Context, req *ttnpb.ListGatewaysRequest) (gtws *ttnpb.Gateways, err error) { +func (is *IdentityServer) listGateways( // nolint:gocyclo + ctx context.Context, + req *ttnpb.ListGatewaysRequest, +) (gtws *ttnpb.Gateways, err error) { // Backwards compatibility for frequency_plan_id field. if ttnpb.HasAnyField(req.FieldMask.GetPaths(), "frequency_plan_id") { if !ttnpb.HasAnyField(req.FieldMask.GetPaths(), "frequency_plan_ids") { req.FieldMask.Paths = append(req.FieldMask.GetPaths(), "frequency_plan_ids") } } - req.FieldMask = cleanFieldMaskPaths(ttnpb.GatewayFieldPathsNested, req.FieldMask, getPaths, []string{"frequency_plan_id"}) + req.FieldMask = cleanFieldMaskPaths( + ttnpb.GatewayFieldPathsNested, + req.FieldMask, + getPaths, + []string{"frequency_plan_id"}, + ) authInfo, err := is.authInfo(ctx) if err != nil { @@ -407,7 +445,9 @@ func (is *IdentityServer) listGateways(ctx context.Context, req *ttnpb.ListGatew } else if gtws.Gateways[i].LbsLnsSecret != nil { value := gtws.Gateways[i].LbsLnsSecret.Value if gtws.Gateways[i].LbsLnsSecret.KeyId != "" { - value, err = is.KeyService().Decrypt(ctx, gtws.Gateways[i].LbsLnsSecret.Value, gtws.Gateways[i].LbsLnsSecret.KeyId) + value, err = is.KeyService().Decrypt( + ctx, gtws.Gateways[i].LbsLnsSecret.Value, gtws.Gateways[i].LbsLnsSecret.KeyId, + ) if err != nil { return nil, err } @@ -442,7 +482,7 @@ func (is *IdentityServer) listGateways(ctx context.Context, req *ttnpb.ListGatew if ttnpb.HasAnyField(req.FieldMask.GetPaths(), "claim_authentication_code") { if !entityRights.IncludesAll(ttnpb.Right_RIGHT_GATEWAY_READ_SECRETS) { gtws.Gateways[i].ClaimAuthenticationCode = nil - } else if gtws.Gateways[i].ClaimAuthenticationCode != nil && gtws.Gateways[i].ClaimAuthenticationCode.Secret != nil { + } else if authCode := gtws.Gateways[i].ClaimAuthenticationCode; authCode != nil && authCode.Secret != nil { value := gtws.Gateways[i].ClaimAuthenticationCode.Secret.Value if keyID := gtws.Gateways[i].ClaimAuthenticationCode.Secret.KeyId; keyID != "" { value, err = is.KeyService().Decrypt(ctx, value, keyID) @@ -461,7 +501,10 @@ func (is *IdentityServer) listGateways(ctx context.Context, req *ttnpb.ListGatew return gtws, nil } -func (is *IdentityServer) updateGateway(ctx context.Context, req *ttnpb.UpdateGatewayRequest) (gtw *ttnpb.Gateway, err error) { +func (is *IdentityServer) updateGateway( // nolint:gocyclo + ctx context.Context, + req *ttnpb.UpdateGatewayRequest, +) (gtw *ttnpb.Gateway, err error) { reqGtw := req.GetGateway() if err = rights.RequireGateway(ctx, reqGtw.GetIds(), ttnpb.Right_RIGHT_GATEWAY_SETTINGS_BASIC); err != nil { // Allow setting the location field or the attributes field with the RIGHT_GATEWAY_LINK right. @@ -484,7 +527,12 @@ func (is *IdentityServer) updateGateway(ctx context.Context, req *ttnpb.UpdateGa reqGtw.FrequencyPlanIds = []string{reqGtw.FrequencyPlanId} } - req.FieldMask = cleanFieldMaskPaths(ttnpb.GatewayFieldPathsNested, req.FieldMask, nil, append(getPaths, "frequency_plan_id")) + req.FieldMask = cleanFieldMaskPaths( + ttnpb.GatewayFieldPathsNested, + req.FieldMask, + nil, + append(getPaths, "frequency_plan_id"), + ) if len(req.FieldMask.GetPaths()) == 0 { req.FieldMask = ttnpb.FieldMask(updatePaths...) } @@ -493,7 +541,10 @@ func (is *IdentityServer) updateGateway(ctx context.Context, req *ttnpb.UpdateGa return nil, err } } - req.FieldMask.Paths = ttnpb.FlattenPaths(req.FieldMask.Paths, []string{"administrative_contact", "technical_contact"}) + req.FieldMask.Paths = ttnpb.FlattenPaths( + req.FieldMask.Paths, + []string{"administrative_contact", "technical_contact"}, + ) if ttnpb.HasAnyField(req.FieldMask.GetPaths(), "lbs_lns_secret") { if err := rights.RequireGateway(ctx, reqGtw.GetIds(), ttnpb.Right_RIGHT_GATEWAY_WRITE_SECRETS); err != nil { @@ -558,11 +609,21 @@ func (is *IdentityServer) updateGateway(ctx context.Context, req *ttnpb.UpdateGa } } + if err := is.validateContactInfoRestrictions( + ctx, req.Gateway.GetAdministrativeContact(), req.Gateway.GetTechnicalContact(), + ); err != nil { + return nil, err + } + err = is.store.Transact(ctx, func(ctx context.Context, st store.Store) (err error) { - if err := validateContactIsCollaborator(ctx, st, req.Gateway.AdministrativeContact, req.Gateway.GetEntityIdentifiers()); err != nil { + if err := validateContactIsCollaborator( + ctx, st, req.Gateway.AdministrativeContact, req.Gateway.GetEntityIdentifiers(), + ); err != nil { return err } - if err := validateContactIsCollaborator(ctx, st, req.Gateway.TechnicalContact, req.Gateway.GetEntityIdentifiers()); err != nil { + if err := validateContactIsCollaborator( + ctx, st, req.Gateway.TechnicalContact, req.Gateway.GetEntityIdentifiers(), + ); err != nil { return err } gtw, err = st.UpdateGateway(ctx, reqGtw, req.FieldMask.GetPaths()) @@ -611,7 +672,9 @@ func (is *IdentityServer) deleteGateway(ctx context.Context, ids *ttnpb.GatewayI } func (is *IdentityServer) restoreGateway(ctx context.Context, ids *ttnpb.GatewayIdentifiers) (*emptypb.Empty, error) { - if err := rights.RequireGateway(store.WithSoftDeleted(ctx, false), ids, ttnpb.Right_RIGHT_GATEWAY_DELETE); err != nil { + if err := rights.RequireGateway( + store.WithSoftDeleted(ctx, false), ids, ttnpb.Right_RIGHT_GATEWAY_DELETE, + ); err != nil { return nil, err } err := is.store.Transact(ctx, func(ctx context.Context, st store.Store) error { @@ -669,7 +732,8 @@ func validateClaimAuthenticationCode(authCode *ttnpb.GatewayClaimAuthenticationC if authCode.Secret == nil { return errClaimAuthenticationCode.New() } - if validFrom, validTo := ttnpb.StdTime(authCode.ValidFrom), ttnpb.StdTime(authCode.ValidTo); validFrom != nil && validTo != nil { + validFrom, validTo := ttnpb.StdTime(authCode.ValidFrom), ttnpb.StdTime(authCode.ValidTo) + if validFrom != nil && validTo != nil { if validTo.Before(*validFrom) { return errClaimAuthenticationCode.New() } @@ -691,7 +755,10 @@ func (gr *gatewayRegistry) Get(ctx context.Context, req *ttnpb.GetGatewayRequest return gr.getGateway(ctx, req) } -func (gr *gatewayRegistry) GetIdentifiersForEUI(ctx context.Context, req *ttnpb.GetGatewayIdentifiersForEUIRequest) (*ttnpb.GatewayIdentifiers, error) { +func (gr *gatewayRegistry) GetIdentifiersForEUI( + ctx context.Context, + req *ttnpb.GetGatewayIdentifiersForEUIRequest, +) (*ttnpb.GatewayIdentifiers, error) { return gr.getGatewayIdentifiersForEUI(ctx, req) } diff --git a/pkg/identityserver/gateway_registry_test.go b/pkg/identityserver/gateway_registry_test.go index bbb6488380..e546ea0fa6 100644 --- a/pkg/identityserver/gateway_registry_test.go +++ b/pkg/identityserver/gateway_registry_test.go @@ -207,7 +207,60 @@ func TestGatewaysCRUD(t *testing.T) { a.So(updated.Name, should.Equal, "Updated Name") } - for _, collaborator := range []*ttnpb.OrganizationOrUserIdentifiers{nil, usr1.GetOrganizationOrUserIdentifiers()} { + t.Run("Contact Info Restrictions", func(t *testing.T) { // nolint:paralleltest + a, ctx := test.New(t) + + oldSetOtherAsContacts := is.config.CollaboratorRights.SetOthersAsContacts + t.Cleanup(func() { is.config.CollaboratorRights.SetOthersAsContacts = oldSetOtherAsContacts }) + is.config.CollaboratorRights.SetOthersAsContacts = false + + // Set usr-2 as collaborator to client. + gac := ttnpb.NewGatewayAccessClient(cc) + _, err := gac.SetCollaborator(ctx, &ttnpb.SetGatewayCollaboratorRequest{ + GatewayIds: created.GetIds(), + Collaborator: &ttnpb.Collaborator{ + Ids: usr2.GetOrganizationOrUserIdentifiers(), + Rights: []ttnpb.Right{ttnpb.Right_RIGHT_ALL}, + }, + }, creds) + a.So(err, should.BeNil) + + // Attempt to set another collaborator as administrative contact. + _, err = reg.Update(ctx, &ttnpb.UpdateGatewayRequest{ + Gateway: &ttnpb.Gateway{ + Ids: created.GetIds(), + AdministrativeContact: usr2.GetOrganizationOrUserIdentifiers(), + }, + FieldMask: ttnpb.FieldMask("administrative_contact"), + }, creds) + a.So(errors.IsPermissionDenied(err), should.BeTrue) + + // Admin can bypass contact info restrictions. + _, err = reg.Update(ctx, &ttnpb.UpdateGatewayRequest{ + Gateway: &ttnpb.Gateway{ + Ids: created.GetIds(), + AdministrativeContact: usr1.GetOrganizationOrUserIdentifiers(), + }, + FieldMask: ttnpb.FieldMask("administrative_contact"), + }, adminCreds) + a.So(err, should.BeNil) + + is.config.CollaboratorRights.SetOthersAsContacts = true + + // Now usr-1 can set usr-2 as technical contact. + _, err = reg.Update(ctx, &ttnpb.UpdateGatewayRequest{ + Gateway: &ttnpb.Gateway{ + Ids: created.GetIds(), + TechnicalContact: usr2.GetOrganizationOrUserIdentifiers(), + }, + FieldMask: ttnpb.FieldMask("technical_contact"), + }, creds) + a.So(err, should.BeNil) + }) + + for _, collaborator := range []*ttnpb.OrganizationOrUserIdentifiers{ + nil, usr1.GetOrganizationOrUserIdentifiers(), + } { list, err := reg.List(ctx, &ttnpb.ListGatewaysRequest{ FieldMask: ttnpb.FieldMask("name"), Collaborator: collaborator, diff --git a/pkg/identityserver/organization_registry.go b/pkg/identityserver/organization_registry.go index e7169122c1..d5893105be 100644 --- a/pkg/identityserver/organization_registry.go +++ b/pkg/identityserver/organization_registry.go @@ -62,12 +62,22 @@ var ( ) 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") + 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) { +func (is *IdentityServer) createOrganization( + ctx context.Context, req *ttnpb.CreateOrganizationRequest, +) (org *ttnpb.Organization, err error) { if err = blocklist.Check(ctx, req.Organization.GetIds().GetOrganizationId()); err != nil { return nil, err } @@ -84,7 +94,9 @@ func (is *IdentityServer) createOrganization(ctx context.Context, req *ttnpb.Cre if req.Organization.AdministrativeContact == nil { req.Organization.AdministrativeContact = req.Collaborator - } else if err := validateCollaboratorEqualsContact(req.Collaborator, req.Organization.AdministrativeContact); err != nil { + } else if err := validateCollaboratorEqualsContact( + req.Collaborator, req.Organization.AdministrativeContact, + ); err != nil { return nil, err } if req.Organization.TechnicalContact == nil { @@ -125,7 +137,10 @@ func (is *IdentityServer) createOrganization(ctx context.Context, req *ttnpb.Cre return org, nil } -func (is *IdentityServer) getOrganization(ctx context.Context, req *ttnpb.GetOrganizationRequest) (org *ttnpb.Organization, err error) { +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 } @@ -155,7 +170,10 @@ func (is *IdentityServer) getOrganization(ctx context.Context, req *ttnpb.GetOrg return org, nil } -func (is *IdentityServer) listOrganizations(ctx context.Context, req *ttnpb.ListOrganizationsRequest) (orgs *ttnpb.Organizations, err error) { +func (is *IdentityServer) listOrganizations( + ctx context.Context, + req *ttnpb.ListOrganizationsRequest, +) (orgs *ttnpb.Organizations, err error) { req.FieldMask = cleanFieldMaskPaths(ttnpb.OrganizationFieldPathsNested, req.FieldMask, getPaths, nil) authInfo, err := is.authInfo(ctx) @@ -202,7 +220,11 @@ func (is *IdentityServer) listOrganizations(ctx context.Context, req *ttnpb.List if len(ids) == 0 { return nil } - callerMemberships, err = st.FindAccountMembershipChains(ctx, callerAccountID, "organization", idStrings(ids...)...) + callerMemberships, err = st.FindAccountMembershipChains( + ctx, + callerAccountID, + "organization", + idStrings(ids...)...) if err != nil { return err } @@ -232,8 +254,13 @@ func (is *IdentityServer) listOrganizations(ctx context.Context, req *ttnpb.List return orgs, nil } -func (is *IdentityServer) updateOrganization(ctx context.Context, req *ttnpb.UpdateOrganizationRequest) (org *ttnpb.Organization, err error) { - if err = rights.RequireOrganization(ctx, req.Organization.GetIds(), ttnpb.Right_RIGHT_ORGANIZATION_SETTINGS_BASIC); err != nil { +func (is *IdentityServer) updateOrganization( + ctx context.Context, + req *ttnpb.UpdateOrganizationRequest, +) (org *ttnpb.Organization, err error) { + if err = rights.RequireOrganization( + ctx, req.Organization.GetIds(), ttnpb.Right_RIGHT_ORGANIZATION_SETTINGS_BASIC, + ); err != nil { return nil, err } req.FieldMask = cleanFieldMaskPaths(ttnpb.OrganizationFieldPathsNested, req.FieldMask, nil, getPaths) @@ -245,12 +272,26 @@ func (is *IdentityServer) updateOrganization(ctx context.Context, req *ttnpb.Upd return nil, err } } - req.FieldMask.Paths = ttnpb.FlattenPaths(req.FieldMask.Paths, []string{"administrative_contact", "technical_contact"}) + + if err := is.validateContactInfoRestrictions( + ctx, req.Organization.GetAdministrativeContact(), req.Organization.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.Organization.AdministrativeContact, req.Organization.GetEntityIdentifiers()); err != nil { + if err := validateContactIsCollaborator( + ctx, st, req.Organization.AdministrativeContact, req.Organization.GetEntityIdentifiers(), + ); err != nil { return err } - if err := validateContactIsCollaborator(ctx, st, req.Organization.TechnicalContact, req.Organization.GetEntityIdentifiers()); err != nil { + if err := validateContactIsCollaborator( + ctx, st, req.Organization.TechnicalContact, req.Organization.GetEntityIdentifiers(), + ); err != nil { return err } org, err = st.UpdateOrganization(ctx, req.Organization, req.FieldMask.GetPaths()) @@ -269,11 +310,16 @@ func (is *IdentityServer) updateOrganization(ctx context.Context, req *ttnpb.Upd if err != nil { return nil, err } - events.Publish(evtUpdateOrganization.NewWithIdentifiersAndData(ctx, req.Organization.GetIds(), req.FieldMask.GetPaths())) + events.Publish( + evtUpdateOrganization.NewWithIdentifiersAndData(ctx, req.Organization.GetIds(), req.FieldMask.GetPaths()), + ) return org, nil } -func (is *IdentityServer) deleteOrganization(ctx context.Context, ids *ttnpb.OrganizationIdentifiers) (*emptypb.Empty, error) { +func (is *IdentityServer) deleteOrganization( + ctx context.Context, + ids *ttnpb.OrganizationIdentifiers, +) (*emptypb.Empty, error) { if err := rights.RequireOrganization(ctx, ids, ttnpb.Right_RIGHT_ORGANIZATION_DELETE); err != nil { return nil, err } @@ -287,8 +333,13 @@ func (is *IdentityServer) deleteOrganization(ctx context.Context, ids *ttnpb.Org return ttnpb.Empty, nil } -func (is *IdentityServer) restoreOrganization(ctx context.Context, ids *ttnpb.OrganizationIdentifiers) (*emptypb.Empty, error) { - if err := rights.RequireOrganization(store.WithSoftDeleted(ctx, false), ids, ttnpb.Right_RIGHT_ORGANIZATION_DELETE); err != nil { +func (is *IdentityServer) restoreOrganization( + ctx context.Context, + ids *ttnpb.OrganizationIdentifiers, +) (*emptypb.Empty, error) { + if err := rights.RequireOrganization( + store.WithSoftDeleted(ctx, false), ids, ttnpb.Right_RIGHT_ORGANIZATION_DELETE, + ); err != nil { return nil, err } err := is.store.Transact(ctx, func(ctx context.Context, st store.Store) error { @@ -312,7 +363,10 @@ func (is *IdentityServer) restoreOrganization(ctx context.Context, ids *ttnpb.Or return ttnpb.Empty, nil } -func (is *IdentityServer) purgeOrganization(ctx context.Context, ids *ttnpb.OrganizationIdentifiers) (*emptypb.Empty, error) { +func (is *IdentityServer) purgeOrganization( + ctx context.Context, + ids *ttnpb.OrganizationIdentifiers, +) (*emptypb.Empty, error) { if !is.IsAdmin(ctx) { return nil, errAdminsPurgeOrganizations.New() } @@ -345,30 +399,50 @@ type organizationRegistry struct { *IdentityServer } -func (or *organizationRegistry) Create(ctx context.Context, req *ttnpb.CreateOrganizationRequest) (*ttnpb.Organization, error) { +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) { +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) { +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) { +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) (*emptypb.Empty, error) { +func (or *organizationRegistry) Delete( + ctx context.Context, + req *ttnpb.OrganizationIdentifiers, +) (*emptypb.Empty, error) { return or.deleteOrganization(ctx, req) } -func (or *organizationRegistry) Restore(ctx context.Context, req *ttnpb.OrganizationIdentifiers) (*emptypb.Empty, error) { +func (or *organizationRegistry) Restore( + ctx context.Context, + req *ttnpb.OrganizationIdentifiers, +) (*emptypb.Empty, error) { return or.restoreOrganization(ctx, req) } -func (or *organizationRegistry) Purge(ctx context.Context, req *ttnpb.OrganizationIdentifiers) (*emptypb.Empty, error) { +func (or *organizationRegistry) Purge( + ctx context.Context, req *ttnpb.OrganizationIdentifiers, +) (*emptypb.Empty, error) { return or.purgeOrganization(ctx, req) } diff --git a/pkg/identityserver/organization_registry_test.go b/pkg/identityserver/organization_registry_test.go index 4627683210..7a9cb1f67c 100644 --- a/pkg/identityserver/organization_registry_test.go +++ b/pkg/identityserver/organization_registry_test.go @@ -213,12 +213,66 @@ func TestOrganizationsCRUD(t *testing.T) { a.So(updated.Name, should.Equal, "Updated Name") } - for _, collaborator := range []*ttnpb.OrganizationOrUserIdentifiers{nil, usr1.GetOrganizationOrUserIdentifiers()} { + t.Run("Contact Info Restrictions", func(t *testing.T) { // nolint:paralleltest + a, ctx := test.New(t) + + oldSetOtherAsContacts := is.config.CollaboratorRights.SetOthersAsContacts + t.Cleanup(func() { is.config.CollaboratorRights.SetOthersAsContacts = oldSetOtherAsContacts }) + is.config.CollaboratorRights.SetOthersAsContacts = false + + // Set usr-2 as collaborator to client. + oac := ttnpb.NewOrganizationAccessClient(cc) + _, err := oac.SetCollaborator(ctx, &ttnpb.SetOrganizationCollaboratorRequest{ + OrganizationIds: created.GetIds(), + Collaborator: &ttnpb.Collaborator{ + Ids: usr2.GetOrganizationOrUserIdentifiers(), + Rights: []ttnpb.Right{ttnpb.Right_RIGHT_ALL}, + }, + }, creds) + a.So(err, should.BeNil) + + // Attempt to set another collaborator as administrative contact. + _, err = reg.Update(ctx, &ttnpb.UpdateOrganizationRequest{ + Organization: &ttnpb.Organization{ + Ids: created.GetIds(), + AdministrativeContact: usr2.GetOrganizationOrUserIdentifiers(), + }, + FieldMask: ttnpb.FieldMask("administrative_contact"), + }, creds) + a.So(errors.IsPermissionDenied(err), should.BeTrue) + + // Admin can bypass contact info restrictions. + _, err = reg.Update(ctx, &ttnpb.UpdateOrganizationRequest{ + Organization: &ttnpb.Organization{ + Ids: created.GetIds(), + AdministrativeContact: usr1.GetOrganizationOrUserIdentifiers(), + }, + FieldMask: ttnpb.FieldMask("administrative_contact"), + }, adminCreds) + a.So(err, should.BeNil) + + is.config.CollaboratorRights.SetOthersAsContacts = true + + // Now usr-1 can set usr-2 as technical contact. + _, err = reg.Update(ctx, &ttnpb.UpdateOrganizationRequest{ + Organization: &ttnpb.Organization{ + Ids: created.GetIds(), + TechnicalContact: usr2.GetOrganizationOrUserIdentifiers(), + }, + FieldMask: ttnpb.FieldMask("technical_contact"), + }, creds) + a.So(err, should.BeNil) + }) + + for _, collaborator := range []*ttnpb.OrganizationOrUserIdentifiers{ + nil, usr1.GetOrganizationOrUserIdentifiers(), + } { list, err := reg.List(ctx, &ttnpb.ListOrganizationsRequest{ FieldMask: ttnpb.FieldMask("name"), Collaborator: collaborator, }, creds) - if a.So(err, should.BeNil) && a.So(list, should.NotBeNil) && a.So(list.Organizations, should.HaveLength, 6) { + if a.So(err, should.BeNil) && a.So(list, should.NotBeNil) && + a.So(list.Organizations, should.HaveLength, 6) { var found bool for _, item := range list.Organizations { if item.GetIds().GetOrganizationId() == created.GetIds().GetOrganizationId() { diff --git a/pkg/identityserver/store/errors.go b/pkg/identityserver/store/errors.go index f35864fa53..12cc97076b 100644 --- a/pkg/identityserver/store/errors.go +++ b/pkg/identityserver/store/errors.go @@ -124,4 +124,8 @@ var ( "application_dev_eui_limit_reached", "application issued DevEUI limit ({dev_eui_limit}) reached", ) + + ErrContactInfoRestricted = errors.DefinePermissionDenied( + "contact_info_restricted", "contact information can only reference the caller", + ) ) diff --git a/pkg/ttnpb/identityserver.pb.go b/pkg/ttnpb/identityserver.pb.go index 468efba1ac..148e83373f 100644 --- a/pkg/ttnpb/identityserver.pb.go +++ b/pkg/ttnpb/identityserver.pb.go @@ -210,12 +210,13 @@ type IsConfiguration struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - UserRegistration *IsConfiguration_UserRegistration `protobuf:"bytes,3,opt,name=user_registration,json=userRegistration,proto3" json:"user_registration,omitempty"` - ProfilePicture *IsConfiguration_ProfilePicture `protobuf:"bytes,4,opt,name=profile_picture,json=profilePicture,proto3" json:"profile_picture,omitempty"` - EndDevicePicture *IsConfiguration_EndDevicePicture `protobuf:"bytes,5,opt,name=end_device_picture,json=endDevicePicture,proto3" json:"end_device_picture,omitempty"` - UserRights *IsConfiguration_UserRights `protobuf:"bytes,6,opt,name=user_rights,json=userRights,proto3" json:"user_rights,omitempty"` - UserLogin *IsConfiguration_UserLogin `protobuf:"bytes,7,opt,name=user_login,json=userLogin,proto3" json:"user_login,omitempty"` - AdminRights *IsConfiguration_AdminRights `protobuf:"bytes,8,opt,name=admin_rights,json=adminRights,proto3" json:"admin_rights,omitempty"` + UserRegistration *IsConfiguration_UserRegistration `protobuf:"bytes,3,opt,name=user_registration,json=userRegistration,proto3" json:"user_registration,omitempty"` + ProfilePicture *IsConfiguration_ProfilePicture `protobuf:"bytes,4,opt,name=profile_picture,json=profilePicture,proto3" json:"profile_picture,omitempty"` + EndDevicePicture *IsConfiguration_EndDevicePicture `protobuf:"bytes,5,opt,name=end_device_picture,json=endDevicePicture,proto3" json:"end_device_picture,omitempty"` + UserRights *IsConfiguration_UserRights `protobuf:"bytes,6,opt,name=user_rights,json=userRights,proto3" json:"user_rights,omitempty"` + UserLogin *IsConfiguration_UserLogin `protobuf:"bytes,7,opt,name=user_login,json=userLogin,proto3" json:"user_login,omitempty"` + AdminRights *IsConfiguration_AdminRights `protobuf:"bytes,8,opt,name=admin_rights,json=adminRights,proto3" json:"admin_rights,omitempty"` + CollaboratorRights *IsConfiguration_CollaboratorRights `protobuf:"bytes,13,opt,name=collaborator_rights,json=collaboratorRights,proto3" json:"collaborator_rights,omitempty"` } func (x *IsConfiguration) Reset() { @@ -292,6 +293,13 @@ func (x *IsConfiguration) GetAdminRights() *IsConfiguration_AdminRights { return nil } +func (x *IsConfiguration) GetCollaboratorRights() *IsConfiguration_CollaboratorRights { + if x != nil { + return x.CollaboratorRights + } + return nil +} + type GetIsConfigurationResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -795,6 +803,53 @@ func (x *IsConfiguration_AdminRights) GetAll() *wrapperspb.BoolValue { return nil } +type IsConfiguration_CollaboratorRights struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + SetOthersAsContacts *wrapperspb.BoolValue `protobuf:"bytes,1,opt,name=set_others_as_contacts,json=setOthersAsContacts,proto3" json:"set_others_as_contacts,omitempty"` +} + +func (x *IsConfiguration_CollaboratorRights) Reset() { + *x = IsConfiguration_CollaboratorRights{} + if protoimpl.UnsafeEnabled { + mi := &file_lorawan_stack_api_identityserver_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *IsConfiguration_CollaboratorRights) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IsConfiguration_CollaboratorRights) ProtoMessage() {} + +func (x *IsConfiguration_CollaboratorRights) ProtoReflect() protoreflect.Message { + mi := &file_lorawan_stack_api_identityserver_proto_msgTypes[12] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use IsConfiguration_CollaboratorRights.ProtoReflect.Descriptor instead. +func (*IsConfiguration_CollaboratorRights) Descriptor() ([]byte, []int) { + return file_lorawan_stack_api_identityserver_proto_rawDescGZIP(), []int{2, 6} +} + +func (x *IsConfiguration_CollaboratorRights) GetSetOthersAsContacts() *wrapperspb.BoolValue { + if x != nil { + return x.SetOthersAsContacts + } + return nil +} + type IsConfiguration_UserRegistration_Invitation struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -807,7 +862,7 @@ type IsConfiguration_UserRegistration_Invitation struct { func (x *IsConfiguration_UserRegistration_Invitation) Reset() { *x = IsConfiguration_UserRegistration_Invitation{} if protoimpl.UnsafeEnabled { - mi := &file_lorawan_stack_api_identityserver_proto_msgTypes[12] + mi := &file_lorawan_stack_api_identityserver_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -820,7 +875,7 @@ func (x *IsConfiguration_UserRegistration_Invitation) String() string { func (*IsConfiguration_UserRegistration_Invitation) ProtoMessage() {} func (x *IsConfiguration_UserRegistration_Invitation) ProtoReflect() protoreflect.Message { - mi := &file_lorawan_stack_api_identityserver_proto_msgTypes[12] + mi := &file_lorawan_stack_api_identityserver_proto_msgTypes[13] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -861,7 +916,7 @@ type IsConfiguration_UserRegistration_ContactInfoValidation struct { func (x *IsConfiguration_UserRegistration_ContactInfoValidation) Reset() { *x = IsConfiguration_UserRegistration_ContactInfoValidation{} if protoimpl.UnsafeEnabled { - mi := &file_lorawan_stack_api_identityserver_proto_msgTypes[13] + mi := &file_lorawan_stack_api_identityserver_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -874,7 +929,7 @@ func (x *IsConfiguration_UserRegistration_ContactInfoValidation) String() string func (*IsConfiguration_UserRegistration_ContactInfoValidation) ProtoMessage() {} func (x *IsConfiguration_UserRegistration_ContactInfoValidation) ProtoReflect() protoreflect.Message { - mi := &file_lorawan_stack_api_identityserver_proto_msgTypes[13] + mi := &file_lorawan_stack_api_identityserver_proto_msgTypes[14] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -908,7 +963,7 @@ type IsConfiguration_UserRegistration_AdminApproval struct { func (x *IsConfiguration_UserRegistration_AdminApproval) Reset() { *x = IsConfiguration_UserRegistration_AdminApproval{} if protoimpl.UnsafeEnabled { - mi := &file_lorawan_stack_api_identityserver_proto_msgTypes[14] + mi := &file_lorawan_stack_api_identityserver_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -921,7 +976,7 @@ func (x *IsConfiguration_UserRegistration_AdminApproval) String() string { func (*IsConfiguration_UserRegistration_AdminApproval) ProtoMessage() {} func (x *IsConfiguration_UserRegistration_AdminApproval) ProtoReflect() protoreflect.Message { - mi := &file_lorawan_stack_api_identityserver_proto_msgTypes[14] + mi := &file_lorawan_stack_api_identityserver_proto_msgTypes[15] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -959,7 +1014,7 @@ type IsConfiguration_UserRegistration_PasswordRequirements struct { func (x *IsConfiguration_UserRegistration_PasswordRequirements) Reset() { *x = IsConfiguration_UserRegistration_PasswordRequirements{} if protoimpl.UnsafeEnabled { - mi := &file_lorawan_stack_api_identityserver_proto_msgTypes[15] + mi := &file_lorawan_stack_api_identityserver_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -972,7 +1027,7 @@ func (x *IsConfiguration_UserRegistration_PasswordRequirements) String() string func (*IsConfiguration_UserRegistration_PasswordRequirements) ProtoMessage() {} func (x *IsConfiguration_UserRegistration_PasswordRequirements) ProtoReflect() protoreflect.Message { - mi := &file_lorawan_stack_api_identityserver_proto_msgTypes[15] + mi := &file_lorawan_stack_api_identityserver_proto_msgTypes[16] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1097,7 +1152,7 @@ var file_lorawan_stack_api_identityserver_proto_rawDesc = []byte{ 0x72, 0x69, 0x67, 0x68, 0x74, 0x73, 0x42, 0x0f, 0x0a, 0x0d, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x22, 0x1b, 0x0a, 0x19, 0x47, 0x65, 0x74, 0x49, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x22, 0x88, 0x13, 0x0a, 0x0f, 0x49, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x75, 0x65, 0x73, 0x74, 0x22, 0xd4, 0x14, 0x0a, 0x0f, 0x49, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x5d, 0x0a, 0x11, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, @@ -1130,152 +1185,165 @@ var file_lorawan_stack_api_identityserver_proto_rawDesc = []byte{ 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x49, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x52, 0x69, 0x67, 0x68, 0x74, 0x73, 0x52, 0x0b, 0x61, 0x64, 0x6d, 0x69, - 0x6e, 0x52, 0x69, 0x67, 0x68, 0x74, 0x73, 0x1a, 0xd6, 0x08, 0x0a, 0x10, 0x55, 0x73, 0x65, 0x72, - 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x5b, 0x0a, 0x0a, - 0x69, 0x6e, 0x76, 0x69, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x3b, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, - 0x33, 0x2e, 0x49, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x2e, 0x49, 0x6e, 0x76, 0x69, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x69, - 0x6e, 0x76, 0x69, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x7e, 0x0a, 0x17, 0x63, 0x6f, 0x6e, - 0x74, 0x61, 0x63, 0x74, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x5f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x46, 0x2e, 0x74, 0x74, 0x6e, - 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x49, 0x73, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x55, 0x73, 0x65, 0x72, - 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, - 0x74, 0x61, 0x63, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x15, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x63, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x56, - 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x65, 0x0a, 0x0e, 0x61, 0x64, 0x6d, - 0x69, 0x6e, 0x5f, 0x61, 0x70, 0x70, 0x72, 0x6f, 0x76, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x3e, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, - 0x76, 0x33, 0x2e, 0x49, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x2e, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x41, 0x70, 0x70, 0x72, 0x6f, 0x76, 0x61, - 0x6c, 0x52, 0x0d, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x41, 0x70, 0x70, 0x72, 0x6f, 0x76, 0x61, 0x6c, - 0x12, 0x7a, 0x0a, 0x15, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x72, 0x65, 0x71, - 0x75, 0x69, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x45, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, + 0x6e, 0x52, 0x69, 0x67, 0x68, 0x74, 0x73, 0x12, 0x63, 0x0a, 0x13, 0x63, 0x6f, 0x6c, 0x6c, 0x61, + 0x62, 0x6f, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x72, 0x69, 0x67, 0x68, 0x74, 0x73, 0x18, 0x0d, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, + 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x49, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6c, 0x6c, 0x61, 0x62, 0x6f, 0x72, 0x61, 0x74, + 0x6f, 0x72, 0x52, 0x69, 0x67, 0x68, 0x74, 0x73, 0x52, 0x12, 0x63, 0x6f, 0x6c, 0x6c, 0x61, 0x62, + 0x6f, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x52, 0x69, 0x67, 0x68, 0x74, 0x73, 0x1a, 0xd6, 0x08, 0x0a, + 0x10, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x5b, 0x0a, 0x0a, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3b, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, + 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x49, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x67, 0x69, 0x73, + 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x49, 0x6e, 0x76, 0x69, 0x74, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x52, 0x0a, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x7e, + 0x0a, 0x17, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x63, 0x74, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x5f, 0x76, + 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x46, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x49, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x2e, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x14, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, - 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x18, 0x0a, 0x07, - 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, - 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x1a, 0x7c, 0x0a, 0x0a, 0x49, 0x6e, 0x76, 0x69, 0x74, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x36, 0x0a, 0x08, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x52, 0x08, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x12, 0x36, 0x0a, 0x09, - 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x74, 0x74, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x74, 0x6f, 0x6b, 0x65, - 0x6e, 0x54, 0x74, 0x6c, 0x1a, 0x4f, 0x0a, 0x15, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x63, 0x74, 0x49, - 0x6e, 0x66, 0x6f, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x36, 0x0a, - 0x08, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x08, 0x72, 0x65, 0x71, - 0x75, 0x69, 0x72, 0x65, 0x64, 0x1a, 0x47, 0x0a, 0x0d, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x41, 0x70, - 0x70, 0x72, 0x6f, 0x76, 0x61, 0x6c, 0x12, 0x36, 0x0a, 0x08, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, - 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x52, 0x08, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x1a, 0xcf, - 0x02, 0x0a, 0x14, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x69, - 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x3b, 0x0a, 0x0a, 0x6d, 0x69, 0x6e, 0x5f, 0x6c, - 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x55, 0x49, - 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x09, 0x6d, 0x69, 0x6e, 0x4c, 0x65, - 0x6e, 0x67, 0x74, 0x68, 0x12, 0x3b, 0x0a, 0x0a, 0x6d, 0x61, 0x78, 0x5f, 0x6c, 0x65, 0x6e, 0x67, - 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x55, 0x49, 0x6e, 0x74, 0x33, - 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x09, 0x6d, 0x61, 0x78, 0x4c, 0x65, 0x6e, 0x67, 0x74, - 0x68, 0x12, 0x41, 0x0a, 0x0d, 0x6d, 0x69, 0x6e, 0x5f, 0x75, 0x70, 0x70, 0x65, 0x72, 0x63, 0x61, - 0x73, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x55, 0x49, 0x6e, 0x74, 0x33, - 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0c, 0x6d, 0x69, 0x6e, 0x55, 0x70, 0x70, 0x65, 0x72, - 0x63, 0x61, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x0a, 0x6d, 0x69, 0x6e, 0x5f, 0x64, 0x69, 0x67, 0x69, - 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x55, 0x49, 0x6e, 0x74, 0x33, - 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x09, 0x6d, 0x69, 0x6e, 0x44, 0x69, 0x67, 0x69, 0x74, - 0x73, 0x12, 0x3d, 0x0a, 0x0b, 0x6d, 0x69, 0x6e, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x69, 0x61, 0x6c, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x55, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0a, 0x6d, 0x69, 0x6e, 0x53, 0x70, 0x65, 0x63, 0x69, 0x61, 0x6c, - 0x1a, 0x92, 0x01, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x69, 0x63, 0x74, - 0x75, 0x72, 0x65, 0x12, 0x41, 0x0a, 0x0e, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x75, - 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, + 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x63, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x56, 0x61, 0x6c, + 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x15, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x63, 0x74, + 0x49, 0x6e, 0x66, 0x6f, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x65, + 0x0a, 0x0e, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x5f, 0x61, 0x70, 0x70, 0x72, 0x6f, 0x76, 0x61, 0x6c, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3e, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, + 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x49, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x67, 0x69, + 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x41, 0x70, + 0x70, 0x72, 0x6f, 0x76, 0x61, 0x6c, 0x52, 0x0d, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x41, 0x70, 0x70, + 0x72, 0x6f, 0x76, 0x61, 0x6c, 0x12, 0x7a, 0x0a, 0x15, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, + 0x64, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x45, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, + 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x49, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, + 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x14, 0x70, 0x61, 0x73, + 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x73, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x1a, 0x7c, 0x0a, 0x0a, 0x49, + 0x6e, 0x76, 0x69, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x36, 0x0a, 0x08, 0x72, 0x65, 0x71, + 0x75, 0x69, 0x72, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, - 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0d, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, - 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x3d, 0x0a, 0x0c, 0x75, 0x73, 0x65, 0x5f, 0x67, 0x72, - 0x61, 0x76, 0x61, 0x74, 0x61, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, - 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0b, 0x75, 0x73, 0x65, 0x47, 0x72, 0x61, - 0x76, 0x61, 0x74, 0x61, 0x72, 0x1a, 0x55, 0x0a, 0x10, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, - 0x63, 0x65, 0x50, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x12, 0x41, 0x0a, 0x0e, 0x64, 0x69, 0x73, - 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0d, 0x64, - 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x1a, 0xb0, 0x02, 0x0a, - 0x0a, 0x55, 0x73, 0x65, 0x72, 0x52, 0x69, 0x67, 0x68, 0x74, 0x73, 0x12, 0x4b, 0x0a, 0x13, 0x63, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x52, 0x12, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x6c, - 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x41, 0x0a, 0x0e, 0x63, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0d, 0x63, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x43, 0x0a, 0x0f, 0x63, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x73, 0x18, 0x03, + 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x08, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, + 0x64, 0x12, 0x36, 0x0a, 0x09, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x74, 0x74, 0x6c, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x08, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x54, 0x74, 0x6c, 0x1a, 0x4f, 0x0a, 0x15, 0x43, 0x6f, 0x6e, + 0x74, 0x61, 0x63, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x36, 0x0a, 0x08, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x52, 0x0e, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x73, - 0x12, 0x4d, 0x0a, 0x14, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x6f, 0x72, 0x67, 0x61, 0x6e, - 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x13, 0x63, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x1a, - 0x63, 0x0a, 0x09, 0x55, 0x73, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x56, 0x0a, 0x19, - 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, - 0x61, 0x6c, 0x73, 0x5f, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x17, 0x64, 0x69, 0x73, - 0x61, 0x62, 0x6c, 0x65, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x4c, - 0x6f, 0x67, 0x69, 0x6e, 0x1a, 0x3b, 0x0a, 0x0b, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x52, 0x69, 0x67, - 0x68, 0x74, 0x73, 0x12, 0x2c, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x52, 0x08, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x1a, 0x47, 0x0a, 0x0d, 0x41, 0x64, + 0x6d, 0x69, 0x6e, 0x41, 0x70, 0x70, 0x72, 0x6f, 0x76, 0x61, 0x6c, 0x12, 0x36, 0x0a, 0x08, 0x72, + 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x08, 0x72, 0x65, 0x71, 0x75, 0x69, + 0x72, 0x65, 0x64, 0x1a, 0xcf, 0x02, 0x0a, 0x14, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, + 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x3b, 0x0a, 0x0a, + 0x6d, 0x69, 0x6e, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x55, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x09, + 0x6d, 0x69, 0x6e, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x12, 0x3b, 0x0a, 0x0a, 0x6d, 0x61, 0x78, + 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x55, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x09, 0x6d, 0x61, 0x78, + 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x12, 0x41, 0x0a, 0x0d, 0x6d, 0x69, 0x6e, 0x5f, 0x75, 0x70, + 0x70, 0x65, 0x72, 0x63, 0x61, 0x73, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x55, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0c, 0x6d, 0x69, 0x6e, + 0x55, 0x70, 0x70, 0x65, 0x72, 0x63, 0x61, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x0a, 0x6d, 0x69, 0x6e, + 0x5f, 0x64, 0x69, 0x67, 0x69, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x55, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x09, 0x6d, 0x69, 0x6e, + 0x44, 0x69, 0x67, 0x69, 0x74, 0x73, 0x12, 0x3d, 0x0a, 0x0b, 0x6d, 0x69, 0x6e, 0x5f, 0x73, 0x70, + 0x65, 0x63, 0x69, 0x61, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x55, 0x49, + 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0a, 0x6d, 0x69, 0x6e, 0x53, 0x70, + 0x65, 0x63, 0x69, 0x61, 0x6c, 0x1a, 0x92, 0x01, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, + 0x65, 0x50, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x12, 0x41, 0x0a, 0x0e, 0x64, 0x69, 0x73, 0x61, + 0x62, 0x6c, 0x65, 0x5f, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x03, 0x61, 0x6c, - 0x6c, 0x4a, 0x04, 0x08, 0x09, 0x10, 0x0a, 0x4a, 0x04, 0x08, 0x0a, 0x10, 0x0b, 0x4a, 0x04, 0x08, - 0x0b, 0x10, 0x0c, 0x4a, 0x04, 0x08, 0x0c, 0x10, 0x0d, 0x52, 0x0e, 0x6e, 0x65, 0x74, 0x77, 0x6f, - 0x72, 0x6b, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x12, 0x61, 0x70, 0x70, 0x6c, 0x69, - 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x13, 0x6f, - 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6c, 0x69, 0x6d, 0x69, - 0x74, 0x73, 0x52, 0x0b, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x73, 0x22, - 0x63, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x49, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x45, 0x0a, - 0x0d, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, - 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x49, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0d, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x32, 0x68, 0x0a, 0x0c, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x41, 0x63, - 0x63, 0x65, 0x73, 0x73, 0x12, 0x58, 0x0a, 0x08, 0x41, 0x75, 0x74, 0x68, 0x49, 0x6e, 0x66, 0x6f, - 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x20, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, - 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x49, 0x6e, - 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x12, 0x82, 0xd3, 0xe4, 0x93, - 0x02, 0x0c, 0x12, 0x0a, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x32, 0x8b, - 0x01, 0x0a, 0x02, 0x49, 0x73, 0x12, 0x84, 0x01, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x29, 0x2e, 0x74, 0x74, 0x6e, - 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x47, 0x65, 0x74, 0x49, - 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, - 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x47, 0x65, 0x74, 0x49, 0x73, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x19, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x12, 0x11, 0x2f, 0x69, 0x73, 0x2f, 0x63, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x31, 0x5a, 0x2f, - 0x67, 0x6f, 0x2e, 0x74, 0x68, 0x65, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x6e, 0x65, 0x74, - 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2d, 0x73, 0x74, 0x61, - 0x63, 0x6b, 0x2f, 0x76, 0x33, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x74, 0x74, 0x6e, 0x70, 0x62, 0x62, - 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0d, 0x64, 0x69, + 0x73, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x3d, 0x0a, 0x0c, 0x75, + 0x73, 0x65, 0x5f, 0x67, 0x72, 0x61, 0x76, 0x61, 0x74, 0x61, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0b, 0x75, + 0x73, 0x65, 0x47, 0x72, 0x61, 0x76, 0x61, 0x74, 0x61, 0x72, 0x1a, 0x55, 0x0a, 0x10, 0x45, 0x6e, + 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x50, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x12, 0x41, + 0x0a, 0x0e, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x52, 0x0d, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x70, 0x6c, 0x6f, 0x61, + 0x64, 0x1a, 0xb0, 0x02, 0x0a, 0x0a, 0x55, 0x73, 0x65, 0x72, 0x52, 0x69, 0x67, 0x68, 0x74, 0x73, + 0x12, 0x4b, 0x0a, 0x13, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x61, 0x70, 0x70, 0x6c, 0x69, + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x12, 0x63, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x41, 0x0a, + 0x0e, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x52, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, + 0x12, 0x43, 0x0a, 0x0f, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x67, 0x61, 0x74, 0x65, 0x77, + 0x61, 0x79, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0e, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x47, 0x61, 0x74, + 0x65, 0x77, 0x61, 0x79, 0x73, 0x12, 0x4d, 0x0a, 0x14, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, + 0x6f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, + 0x13, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x1a, 0x63, 0x0a, 0x09, 0x55, 0x73, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x69, + 0x6e, 0x12, 0x56, 0x0a, 0x19, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x63, 0x72, 0x65, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x5f, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x52, 0x17, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x61, 0x6c, 0x73, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x1a, 0x3b, 0x0a, 0x0b, 0x41, 0x64, 0x6d, + 0x69, 0x6e, 0x52, 0x69, 0x67, 0x68, 0x74, 0x73, 0x12, 0x2c, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x1a, 0x65, 0x0a, 0x12, 0x43, 0x6f, 0x6c, 0x6c, 0x61, 0x62, + 0x6f, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x52, 0x69, 0x67, 0x68, 0x74, 0x73, 0x12, 0x4f, 0x0a, 0x16, + 0x73, 0x65, 0x74, 0x5f, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x73, 0x5f, 0x61, 0x73, 0x5f, 0x63, 0x6f, + 0x6e, 0x74, 0x61, 0x63, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, + 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x13, 0x73, 0x65, 0x74, 0x4f, 0x74, 0x68, + 0x65, 0x72, 0x73, 0x41, 0x73, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x63, 0x74, 0x73, 0x4a, 0x04, 0x08, + 0x09, 0x10, 0x0a, 0x4a, 0x04, 0x08, 0x0a, 0x10, 0x0b, 0x4a, 0x04, 0x08, 0x0b, 0x10, 0x0c, 0x4a, + 0x04, 0x08, 0x0c, 0x10, 0x0d, 0x52, 0x0e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x6c, + 0x69, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x12, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x13, 0x6f, 0x72, 0x67, 0x61, 0x6e, + 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x0b, + 0x75, 0x73, 0x65, 0x72, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x73, 0x22, 0x63, 0x0a, 0x1a, 0x47, + 0x65, 0x74, 0x49, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x45, 0x0a, 0x0d, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1f, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, + 0x33, 0x2e, 0x49, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x0d, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x32, 0x68, 0x0a, 0x0c, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, + 0x12, 0x58, 0x0a, 0x08, 0x41, 0x75, 0x74, 0x68, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x16, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, + 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x20, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, + 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x12, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0c, 0x12, 0x0a, + 0x2f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x32, 0x8b, 0x01, 0x0a, 0x02, 0x49, + 0x73, 0x12, 0x84, 0x01, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x29, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, + 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x47, 0x65, 0x74, 0x49, 0x73, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x2a, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, + 0x76, 0x33, 0x2e, 0x47, 0x65, 0x74, 0x49, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x19, 0x82, + 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x12, 0x11, 0x2f, 0x69, 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x6f, 0x2e, 0x74, + 0x68, 0x65, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, + 0x2f, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2d, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2f, 0x76, + 0x33, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x74, 0x74, 0x6e, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, } var ( @@ -1290,7 +1358,7 @@ func file_lorawan_stack_api_identityserver_proto_rawDescGZIP() []byte { return file_lorawan_stack_api_identityserver_proto_rawDescData } -var file_lorawan_stack_api_identityserver_proto_msgTypes = make([]protoimpl.MessageInfo, 16) +var file_lorawan_stack_api_identityserver_proto_msgTypes = make([]protoimpl.MessageInfo, 17) var file_lorawan_stack_api_identityserver_proto_goTypes = []interface{}{ (*AuthInfoResponse)(nil), // 0: ttn.lorawan.v3.AuthInfoResponse (*GetIsConfigurationRequest)(nil), // 1: ttn.lorawan.v3.GetIsConfigurationRequest @@ -1304,70 +1372,73 @@ var file_lorawan_stack_api_identityserver_proto_goTypes = []interface{}{ (*IsConfiguration_UserRights)(nil), // 9: ttn.lorawan.v3.IsConfiguration.UserRights (*IsConfiguration_UserLogin)(nil), // 10: ttn.lorawan.v3.IsConfiguration.UserLogin (*IsConfiguration_AdminRights)(nil), // 11: ttn.lorawan.v3.IsConfiguration.AdminRights - (*IsConfiguration_UserRegistration_Invitation)(nil), // 12: ttn.lorawan.v3.IsConfiguration.UserRegistration.Invitation - (*IsConfiguration_UserRegistration_ContactInfoValidation)(nil), // 13: ttn.lorawan.v3.IsConfiguration.UserRegistration.ContactInfoValidation - (*IsConfiguration_UserRegistration_AdminApproval)(nil), // 14: ttn.lorawan.v3.IsConfiguration.UserRegistration.AdminApproval - (*IsConfiguration_UserRegistration_PasswordRequirements)(nil), // 15: ttn.lorawan.v3.IsConfiguration.UserRegistration.PasswordRequirements - (*OAuthAccessToken)(nil), // 16: ttn.lorawan.v3.OAuthAccessToken - (*UserSession)(nil), // 17: ttn.lorawan.v3.UserSession - (*Rights)(nil), // 18: ttn.lorawan.v3.Rights - (*APIKey)(nil), // 19: ttn.lorawan.v3.APIKey - (*EntityIdentifiers)(nil), // 20: ttn.lorawan.v3.EntityIdentifiers - (*GatewayIdentifiers)(nil), // 21: ttn.lorawan.v3.GatewayIdentifiers - (Right)(0), // 22: ttn.lorawan.v3.Right - (*wrapperspb.BoolValue)(nil), // 23: google.protobuf.BoolValue - (*durationpb.Duration)(nil), // 24: google.protobuf.Duration - (*wrapperspb.UInt32Value)(nil), // 25: google.protobuf.UInt32Value - (*emptypb.Empty)(nil), // 26: google.protobuf.Empty + (*IsConfiguration_CollaboratorRights)(nil), // 12: ttn.lorawan.v3.IsConfiguration.CollaboratorRights + (*IsConfiguration_UserRegistration_Invitation)(nil), // 13: ttn.lorawan.v3.IsConfiguration.UserRegistration.Invitation + (*IsConfiguration_UserRegistration_ContactInfoValidation)(nil), // 14: ttn.lorawan.v3.IsConfiguration.UserRegistration.ContactInfoValidation + (*IsConfiguration_UserRegistration_AdminApproval)(nil), // 15: ttn.lorawan.v3.IsConfiguration.UserRegistration.AdminApproval + (*IsConfiguration_UserRegistration_PasswordRequirements)(nil), // 16: ttn.lorawan.v3.IsConfiguration.UserRegistration.PasswordRequirements + (*OAuthAccessToken)(nil), // 17: ttn.lorawan.v3.OAuthAccessToken + (*UserSession)(nil), // 18: ttn.lorawan.v3.UserSession + (*Rights)(nil), // 19: ttn.lorawan.v3.Rights + (*APIKey)(nil), // 20: ttn.lorawan.v3.APIKey + (*EntityIdentifiers)(nil), // 21: ttn.lorawan.v3.EntityIdentifiers + (*GatewayIdentifiers)(nil), // 22: ttn.lorawan.v3.GatewayIdentifiers + (Right)(0), // 23: ttn.lorawan.v3.Right + (*wrapperspb.BoolValue)(nil), // 24: google.protobuf.BoolValue + (*durationpb.Duration)(nil), // 25: google.protobuf.Duration + (*wrapperspb.UInt32Value)(nil), // 26: google.protobuf.UInt32Value + (*emptypb.Empty)(nil), // 27: google.protobuf.Empty } var file_lorawan_stack_api_identityserver_proto_depIdxs = []int32{ 4, // 0: ttn.lorawan.v3.AuthInfoResponse.api_key:type_name -> ttn.lorawan.v3.AuthInfoResponse.APIKeyAccess - 16, // 1: ttn.lorawan.v3.AuthInfoResponse.oauth_access_token:type_name -> ttn.lorawan.v3.OAuthAccessToken - 17, // 2: ttn.lorawan.v3.AuthInfoResponse.user_session:type_name -> ttn.lorawan.v3.UserSession + 17, // 1: ttn.lorawan.v3.AuthInfoResponse.oauth_access_token:type_name -> ttn.lorawan.v3.OAuthAccessToken + 18, // 2: ttn.lorawan.v3.AuthInfoResponse.user_session:type_name -> ttn.lorawan.v3.UserSession 5, // 3: ttn.lorawan.v3.AuthInfoResponse.gateway_token:type_name -> ttn.lorawan.v3.AuthInfoResponse.GatewayToken - 18, // 4: ttn.lorawan.v3.AuthInfoResponse.universal_rights:type_name -> ttn.lorawan.v3.Rights + 19, // 4: ttn.lorawan.v3.AuthInfoResponse.universal_rights:type_name -> ttn.lorawan.v3.Rights 6, // 5: ttn.lorawan.v3.IsConfiguration.user_registration:type_name -> ttn.lorawan.v3.IsConfiguration.UserRegistration 7, // 6: ttn.lorawan.v3.IsConfiguration.profile_picture:type_name -> ttn.lorawan.v3.IsConfiguration.ProfilePicture 8, // 7: ttn.lorawan.v3.IsConfiguration.end_device_picture:type_name -> ttn.lorawan.v3.IsConfiguration.EndDevicePicture 9, // 8: ttn.lorawan.v3.IsConfiguration.user_rights:type_name -> ttn.lorawan.v3.IsConfiguration.UserRights 10, // 9: ttn.lorawan.v3.IsConfiguration.user_login:type_name -> ttn.lorawan.v3.IsConfiguration.UserLogin 11, // 10: ttn.lorawan.v3.IsConfiguration.admin_rights:type_name -> ttn.lorawan.v3.IsConfiguration.AdminRights - 2, // 11: ttn.lorawan.v3.GetIsConfigurationResponse.configuration:type_name -> ttn.lorawan.v3.IsConfiguration - 19, // 12: ttn.lorawan.v3.AuthInfoResponse.APIKeyAccess.api_key:type_name -> ttn.lorawan.v3.APIKey - 20, // 13: ttn.lorawan.v3.AuthInfoResponse.APIKeyAccess.entity_ids:type_name -> ttn.lorawan.v3.EntityIdentifiers - 21, // 14: ttn.lorawan.v3.AuthInfoResponse.GatewayToken.gateway_ids:type_name -> ttn.lorawan.v3.GatewayIdentifiers - 22, // 15: ttn.lorawan.v3.AuthInfoResponse.GatewayToken.rights:type_name -> ttn.lorawan.v3.Right - 12, // 16: ttn.lorawan.v3.IsConfiguration.UserRegistration.invitation:type_name -> ttn.lorawan.v3.IsConfiguration.UserRegistration.Invitation - 13, // 17: ttn.lorawan.v3.IsConfiguration.UserRegistration.contact_info_validation:type_name -> ttn.lorawan.v3.IsConfiguration.UserRegistration.ContactInfoValidation - 14, // 18: ttn.lorawan.v3.IsConfiguration.UserRegistration.admin_approval:type_name -> ttn.lorawan.v3.IsConfiguration.UserRegistration.AdminApproval - 15, // 19: ttn.lorawan.v3.IsConfiguration.UserRegistration.password_requirements:type_name -> ttn.lorawan.v3.IsConfiguration.UserRegistration.PasswordRequirements - 23, // 20: ttn.lorawan.v3.IsConfiguration.ProfilePicture.disable_upload:type_name -> google.protobuf.BoolValue - 23, // 21: ttn.lorawan.v3.IsConfiguration.ProfilePicture.use_gravatar:type_name -> google.protobuf.BoolValue - 23, // 22: ttn.lorawan.v3.IsConfiguration.EndDevicePicture.disable_upload:type_name -> google.protobuf.BoolValue - 23, // 23: ttn.lorawan.v3.IsConfiguration.UserRights.create_applications:type_name -> google.protobuf.BoolValue - 23, // 24: ttn.lorawan.v3.IsConfiguration.UserRights.create_clients:type_name -> google.protobuf.BoolValue - 23, // 25: ttn.lorawan.v3.IsConfiguration.UserRights.create_gateways:type_name -> google.protobuf.BoolValue - 23, // 26: ttn.lorawan.v3.IsConfiguration.UserRights.create_organizations:type_name -> google.protobuf.BoolValue - 23, // 27: ttn.lorawan.v3.IsConfiguration.UserLogin.disable_credentials_login:type_name -> google.protobuf.BoolValue - 23, // 28: ttn.lorawan.v3.IsConfiguration.AdminRights.all:type_name -> google.protobuf.BoolValue - 23, // 29: ttn.lorawan.v3.IsConfiguration.UserRegistration.Invitation.required:type_name -> google.protobuf.BoolValue - 24, // 30: ttn.lorawan.v3.IsConfiguration.UserRegistration.Invitation.token_ttl:type_name -> google.protobuf.Duration - 23, // 31: ttn.lorawan.v3.IsConfiguration.UserRegistration.ContactInfoValidation.required:type_name -> google.protobuf.BoolValue - 23, // 32: ttn.lorawan.v3.IsConfiguration.UserRegistration.AdminApproval.required:type_name -> google.protobuf.BoolValue - 25, // 33: ttn.lorawan.v3.IsConfiguration.UserRegistration.PasswordRequirements.min_length:type_name -> google.protobuf.UInt32Value - 25, // 34: ttn.lorawan.v3.IsConfiguration.UserRegistration.PasswordRequirements.max_length:type_name -> google.protobuf.UInt32Value - 25, // 35: ttn.lorawan.v3.IsConfiguration.UserRegistration.PasswordRequirements.min_uppercase:type_name -> google.protobuf.UInt32Value - 25, // 36: ttn.lorawan.v3.IsConfiguration.UserRegistration.PasswordRequirements.min_digits:type_name -> google.protobuf.UInt32Value - 25, // 37: ttn.lorawan.v3.IsConfiguration.UserRegistration.PasswordRequirements.min_special:type_name -> google.protobuf.UInt32Value - 26, // 38: ttn.lorawan.v3.EntityAccess.AuthInfo:input_type -> google.protobuf.Empty - 1, // 39: ttn.lorawan.v3.Is.GetConfiguration:input_type -> ttn.lorawan.v3.GetIsConfigurationRequest - 0, // 40: ttn.lorawan.v3.EntityAccess.AuthInfo:output_type -> ttn.lorawan.v3.AuthInfoResponse - 3, // 41: ttn.lorawan.v3.Is.GetConfiguration:output_type -> ttn.lorawan.v3.GetIsConfigurationResponse - 40, // [40:42] is the sub-list for method output_type - 38, // [38:40] is the sub-list for method input_type - 38, // [38:38] is the sub-list for extension type_name - 38, // [38:38] is the sub-list for extension extendee - 0, // [0:38] is the sub-list for field type_name + 12, // 11: ttn.lorawan.v3.IsConfiguration.collaborator_rights:type_name -> ttn.lorawan.v3.IsConfiguration.CollaboratorRights + 2, // 12: ttn.lorawan.v3.GetIsConfigurationResponse.configuration:type_name -> ttn.lorawan.v3.IsConfiguration + 20, // 13: ttn.lorawan.v3.AuthInfoResponse.APIKeyAccess.api_key:type_name -> ttn.lorawan.v3.APIKey + 21, // 14: ttn.lorawan.v3.AuthInfoResponse.APIKeyAccess.entity_ids:type_name -> ttn.lorawan.v3.EntityIdentifiers + 22, // 15: ttn.lorawan.v3.AuthInfoResponse.GatewayToken.gateway_ids:type_name -> ttn.lorawan.v3.GatewayIdentifiers + 23, // 16: ttn.lorawan.v3.AuthInfoResponse.GatewayToken.rights:type_name -> ttn.lorawan.v3.Right + 13, // 17: ttn.lorawan.v3.IsConfiguration.UserRegistration.invitation:type_name -> ttn.lorawan.v3.IsConfiguration.UserRegistration.Invitation + 14, // 18: ttn.lorawan.v3.IsConfiguration.UserRegistration.contact_info_validation:type_name -> ttn.lorawan.v3.IsConfiguration.UserRegistration.ContactInfoValidation + 15, // 19: ttn.lorawan.v3.IsConfiguration.UserRegistration.admin_approval:type_name -> ttn.lorawan.v3.IsConfiguration.UserRegistration.AdminApproval + 16, // 20: ttn.lorawan.v3.IsConfiguration.UserRegistration.password_requirements:type_name -> ttn.lorawan.v3.IsConfiguration.UserRegistration.PasswordRequirements + 24, // 21: ttn.lorawan.v3.IsConfiguration.ProfilePicture.disable_upload:type_name -> google.protobuf.BoolValue + 24, // 22: ttn.lorawan.v3.IsConfiguration.ProfilePicture.use_gravatar:type_name -> google.protobuf.BoolValue + 24, // 23: ttn.lorawan.v3.IsConfiguration.EndDevicePicture.disable_upload:type_name -> google.protobuf.BoolValue + 24, // 24: ttn.lorawan.v3.IsConfiguration.UserRights.create_applications:type_name -> google.protobuf.BoolValue + 24, // 25: ttn.lorawan.v3.IsConfiguration.UserRights.create_clients:type_name -> google.protobuf.BoolValue + 24, // 26: ttn.lorawan.v3.IsConfiguration.UserRights.create_gateways:type_name -> google.protobuf.BoolValue + 24, // 27: ttn.lorawan.v3.IsConfiguration.UserRights.create_organizations:type_name -> google.protobuf.BoolValue + 24, // 28: ttn.lorawan.v3.IsConfiguration.UserLogin.disable_credentials_login:type_name -> google.protobuf.BoolValue + 24, // 29: ttn.lorawan.v3.IsConfiguration.AdminRights.all:type_name -> google.protobuf.BoolValue + 24, // 30: ttn.lorawan.v3.IsConfiguration.CollaboratorRights.set_others_as_contacts:type_name -> google.protobuf.BoolValue + 24, // 31: ttn.lorawan.v3.IsConfiguration.UserRegistration.Invitation.required:type_name -> google.protobuf.BoolValue + 25, // 32: ttn.lorawan.v3.IsConfiguration.UserRegistration.Invitation.token_ttl:type_name -> google.protobuf.Duration + 24, // 33: ttn.lorawan.v3.IsConfiguration.UserRegistration.ContactInfoValidation.required:type_name -> google.protobuf.BoolValue + 24, // 34: ttn.lorawan.v3.IsConfiguration.UserRegistration.AdminApproval.required:type_name -> google.protobuf.BoolValue + 26, // 35: ttn.lorawan.v3.IsConfiguration.UserRegistration.PasswordRequirements.min_length:type_name -> google.protobuf.UInt32Value + 26, // 36: ttn.lorawan.v3.IsConfiguration.UserRegistration.PasswordRequirements.max_length:type_name -> google.protobuf.UInt32Value + 26, // 37: ttn.lorawan.v3.IsConfiguration.UserRegistration.PasswordRequirements.min_uppercase:type_name -> google.protobuf.UInt32Value + 26, // 38: ttn.lorawan.v3.IsConfiguration.UserRegistration.PasswordRequirements.min_digits:type_name -> google.protobuf.UInt32Value + 26, // 39: ttn.lorawan.v3.IsConfiguration.UserRegistration.PasswordRequirements.min_special:type_name -> google.protobuf.UInt32Value + 27, // 40: ttn.lorawan.v3.EntityAccess.AuthInfo:input_type -> google.protobuf.Empty + 1, // 41: ttn.lorawan.v3.Is.GetConfiguration:input_type -> ttn.lorawan.v3.GetIsConfigurationRequest + 0, // 42: ttn.lorawan.v3.EntityAccess.AuthInfo:output_type -> ttn.lorawan.v3.AuthInfoResponse + 3, // 43: ttn.lorawan.v3.Is.GetConfiguration:output_type -> ttn.lorawan.v3.GetIsConfigurationResponse + 42, // [42:44] is the sub-list for method output_type + 40, // [40:42] is the sub-list for method input_type + 40, // [40:40] is the sub-list for extension type_name + 40, // [40:40] is the sub-list for extension extendee + 0, // [0:40] is the sub-list for field type_name } func init() { file_lorawan_stack_api_identityserver_proto_init() } @@ -1525,7 +1596,7 @@ func file_lorawan_stack_api_identityserver_proto_init() { } } file_lorawan_stack_api_identityserver_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*IsConfiguration_UserRegistration_Invitation); i { + switch v := v.(*IsConfiguration_CollaboratorRights); i { case 0: return &v.state case 1: @@ -1537,7 +1608,7 @@ func file_lorawan_stack_api_identityserver_proto_init() { } } file_lorawan_stack_api_identityserver_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*IsConfiguration_UserRegistration_ContactInfoValidation); i { + switch v := v.(*IsConfiguration_UserRegistration_Invitation); i { case 0: return &v.state case 1: @@ -1549,7 +1620,7 @@ func file_lorawan_stack_api_identityserver_proto_init() { } } file_lorawan_stack_api_identityserver_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*IsConfiguration_UserRegistration_AdminApproval); i { + switch v := v.(*IsConfiguration_UserRegistration_ContactInfoValidation); i { case 0: return &v.state case 1: @@ -1561,6 +1632,18 @@ func file_lorawan_stack_api_identityserver_proto_init() { } } file_lorawan_stack_api_identityserver_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*IsConfiguration_UserRegistration_AdminApproval); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_lorawan_stack_api_identityserver_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*IsConfiguration_UserRegistration_PasswordRequirements); i { case 0: return &v.state @@ -1585,7 +1668,7 @@ func file_lorawan_stack_api_identityserver_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_lorawan_stack_api_identityserver_proto_rawDesc, NumEnums: 0, - NumMessages: 16, + NumMessages: 17, NumExtensions: 0, NumServices: 2, }, diff --git a/pkg/ttnpb/identityserver.pb.paths.fm.go b/pkg/ttnpb/identityserver.pb.paths.fm.go index 94f06072f7..1cb7f3bbb7 100644 --- a/pkg/ttnpb/identityserver.pb.paths.fm.go +++ b/pkg/ttnpb/identityserver.pb.paths.fm.go @@ -76,6 +76,8 @@ var GetIsConfigurationRequestFieldPathsTopLevel []string var IsConfigurationFieldPathsNested = []string{ "admin_rights", "admin_rights.all", + "collaborator_rights", + "collaborator_rights.set_others_as_contacts", "end_device_picture", "end_device_picture.disable_upload", "profile_picture", @@ -107,6 +109,7 @@ var IsConfigurationFieldPathsNested = []string{ var IsConfigurationFieldPathsTopLevel = []string{ "admin_rights", + "collaborator_rights", "end_device_picture", "profile_picture", "user_login", @@ -117,6 +120,8 @@ var GetIsConfigurationResponseFieldPathsNested = []string{ "configuration", "configuration.admin_rights", "configuration.admin_rights.all", + "configuration.collaborator_rights", + "configuration.collaborator_rights.set_others_as_contacts", "configuration.end_device_picture", "configuration.end_device_picture.disable_upload", "configuration.profile_picture", @@ -263,6 +268,13 @@ var IsConfiguration_AdminRightsFieldPathsNested = []string{ var IsConfiguration_AdminRightsFieldPathsTopLevel = []string{ "all", } +var IsConfiguration_CollaboratorRightsFieldPathsNested = []string{ + "set_others_as_contacts", +} + +var IsConfiguration_CollaboratorRightsFieldPathsTopLevel = []string{ + "set_others_as_contacts", +} var IsConfiguration_UserRegistration_InvitationFieldPathsNested = []string{ "required", "token_ttl", diff --git a/pkg/ttnpb/identityserver.pb.setters.fm.go b/pkg/ttnpb/identityserver.pb.setters.fm.go index a4cccf5554..04a175ebf3 100644 --- a/pkg/ttnpb/identityserver.pb.setters.fm.go +++ b/pkg/ttnpb/identityserver.pb.setters.fm.go @@ -375,6 +375,31 @@ func (dst *IsConfiguration) SetFields(src *IsConfiguration, paths ...string) err dst.AdminRights = nil } } + case "collaborator_rights": + if len(subs) > 0 { + var newDst, newSrc *IsConfiguration_CollaboratorRights + if (src == nil || src.CollaboratorRights == nil) && dst.CollaboratorRights == nil { + continue + } + if src != nil { + newSrc = src.CollaboratorRights + } + if dst.CollaboratorRights != nil { + newDst = dst.CollaboratorRights + } else { + newDst = &IsConfiguration_CollaboratorRights{} + dst.CollaboratorRights = newDst + } + if err := newDst.SetFields(newSrc, subs...); err != nil { + return err + } + } else { + if src != nil { + dst.CollaboratorRights = src.CollaboratorRights + } else { + dst.CollaboratorRights = nil + } + } default: return fmt.Errorf("invalid field: '%s'", name) @@ -782,6 +807,26 @@ func (dst *IsConfiguration_AdminRights) SetFields(src *IsConfiguration_AdminRigh return nil } +func (dst *IsConfiguration_CollaboratorRights) SetFields(src *IsConfiguration_CollaboratorRights, paths ...string) error { + for name, subs := range _processPaths(paths) { + switch name { + case "set_others_as_contacts": + if len(subs) > 0 { + return fmt.Errorf("'set_others_as_contacts' has no subfields, but %s were specified", subs) + } + if src != nil { + dst.SetOthersAsContacts = src.SetOthersAsContacts + } else { + dst.SetOthersAsContacts = nil + } + + default: + return fmt.Errorf("invalid field: '%s'", name) + } + } + return nil +} + func (dst *IsConfiguration_UserRegistration_Invitation) SetFields(src *IsConfiguration_UserRegistration_Invitation, paths ...string) error { for name, subs := range _processPaths(paths) { switch name { diff --git a/pkg/ttnpb/identityserver.pb.validate.go b/pkg/ttnpb/identityserver.pb.validate.go index 0dbd8313fc..0b0fb26806 100644 --- a/pkg/ttnpb/identityserver.pb.validate.go +++ b/pkg/ttnpb/identityserver.pb.validate.go @@ -354,6 +354,18 @@ func (m *IsConfiguration) ValidateFields(paths ...string) error { } } + case "collaborator_rights": + + if v, ok := interface{}(m.GetCollaboratorRights()).(interface{ ValidateFields(...string) error }); ok { + if err := v.ValidateFields(subs...); err != nil { + return IsConfigurationValidationError{ + field: "collaborator_rights", + reason: "embedded message failed validation", + cause: err, + } + } + } + default: return IsConfigurationValidationError{ field: name, @@ -1385,6 +1397,100 @@ var _ interface { ErrorName() string } = IsConfiguration_AdminRightsValidationError{} +// ValidateFields checks the field values on IsConfiguration_CollaboratorRights +// with the rules defined in the proto definition for this message. If any +// rules are violated, an error is returned. +func (m *IsConfiguration_CollaboratorRights) ValidateFields(paths ...string) error { + if m == nil { + return nil + } + + if len(paths) == 0 { + paths = IsConfiguration_CollaboratorRightsFieldPathsNested + } + + for name, subs := range _processPaths(append(paths[:0:0], paths...)) { + _ = subs + switch name { + case "set_others_as_contacts": + + if v, ok := interface{}(m.GetSetOthersAsContacts()).(interface{ ValidateFields(...string) error }); ok { + if err := v.ValidateFields(subs...); err != nil { + return IsConfiguration_CollaboratorRightsValidationError{ + field: "set_others_as_contacts", + reason: "embedded message failed validation", + cause: err, + } + } + } + + default: + return IsConfiguration_CollaboratorRightsValidationError{ + field: name, + reason: "invalid field path", + } + } + } + return nil +} + +// IsConfiguration_CollaboratorRightsValidationError is the validation error +// returned by IsConfiguration_CollaboratorRights.ValidateFields if the +// designated constraints aren't met. +type IsConfiguration_CollaboratorRightsValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e IsConfiguration_CollaboratorRightsValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e IsConfiguration_CollaboratorRightsValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e IsConfiguration_CollaboratorRightsValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e IsConfiguration_CollaboratorRightsValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e IsConfiguration_CollaboratorRightsValidationError) ErrorName() string { + return "IsConfiguration_CollaboratorRightsValidationError" +} + +// Error satisfies the builtin error interface +func (e IsConfiguration_CollaboratorRightsValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sIsConfiguration_CollaboratorRights.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = IsConfiguration_CollaboratorRightsValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = IsConfiguration_CollaboratorRightsValidationError{} + // ValidateFields checks the field values on // IsConfiguration_UserRegistration_Invitation with the rules defined in the // proto definition for this message. If any rules are violated, an error is returned. diff --git a/sdk/js/generated/api.json b/sdk/js/generated/api.json index cbf4d9c838..3adf5b9118 100644 --- a/sdk/js/generated/api.json +++ b/sdk/js/generated/api.json @@ -24049,6 +24049,18 @@ "isoneof": false, "oneofdecl": "", "defaultValue": "" + }, + { + "name": "collaborator_rights", + "description": "", + "label": "", + "type": "CollaboratorRights", + "longType": "IsConfiguration.CollaboratorRights", + "fullType": "ttn.lorawan.v3.IsConfiguration.CollaboratorRights", + "ismap": false, + "isoneof": false, + "oneofdecl": "", + "defaultValue": "" } ] }, @@ -24076,6 +24088,30 @@ } ] }, + { + "name": "CollaboratorRights", + "longName": "IsConfiguration.CollaboratorRights", + "fullName": "ttn.lorawan.v3.IsConfiguration.CollaboratorRights", + "description": "", + "hasExtensions": false, + "hasFields": true, + "hasOneofs": false, + "extensions": [], + "fields": [ + { + "name": "set_others_as_contacts", + "description": "", + "label": "", + "type": "BoolValue", + "longType": "google.protobuf.BoolValue", + "fullType": "google.protobuf.BoolValue", + "ismap": false, + "isoneof": false, + "oneofdecl": "", + "defaultValue": "" + } + ] + }, { "name": "EndDevicePicture", "longName": "IsConfiguration.EndDevicePicture",