Skip to content

Commit

Permalink
Merge pull request #6925 from TheThingsNetwork/issue/6909-collab-remo…
Browse files Browse the repository at this point in the history
…val-admin-tech-contact

Add `admin|tech` contact validation on collaborator removal
  • Loading branch information
nicholaspcr authored Feb 19, 2024
2 parents e0be11c + eeae4e3 commit 095b9e2
Show file tree
Hide file tree
Showing 14 changed files with 297 additions and 9 deletions.
9 changes: 9 additions & 0 deletions config/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -5903,6 +5903,15 @@
"file": "client_access.go"
}
},
"error:pkg/identityserver:collaborator_is_contact": {
"translations": {
"en": "collaborator `{collaborator_id}` is used as a contact"
},
"description": {
"package": "pkg/identityserver",
"file": "errors.go"
}
},
"error:pkg/identityserver:common_password": {
"translations": {
"en": "must not be too common"
Expand Down
17 changes: 16 additions & 1 deletion pkg/identityserver/application_access.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"go.thethings.network/lorawan-stack/v3/pkg/identityserver/store"
"go.thethings.network/lorawan-stack/v3/pkg/ttnpb"
"go.thethings.network/lorawan-stack/v3/pkg/unique"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/emptypb"
)

Expand Down Expand Up @@ -434,10 +435,24 @@ func (is *IdentityServer) deleteApplicationCollaborator(
return nil, err
}
err = is.store.Transact(ctx, func(ctx context.Context, st store.Store) error {
removedRights, err := st.GetMember(ctx, req.GetCollaboratorIds(), req.GetApplicationIds().GetEntityIdentifiers())
removedRights, err := st.GetMember(
ctx, req.GetCollaboratorIds(), req.GetApplicationIds().GetEntityIdentifiers(),
)
if err != nil {
return err
}
app, err := st.GetApplication(
ctx,
req.GetApplicationIds(),
store.FieldMask([]string{"administrative_contact", "technical_contact"}),
)
if err != nil {
return err
}
if proto.Equal(app.GetAdministrativeContact(), req.GetCollaboratorIds()) ||
proto.Equal(app.GetTechnicalContact(), req.GetCollaboratorIds()) {
return errCollaboratorIsContact.WithAttributes("collaborator_id", req.GetCollaboratorIds().IDString())
}
if removedRights.Implied().IncludesAll(ttnpb.Right_RIGHT_APPLICATION_ALL) {
memberRights, err := st.FindMembers(ctx, req.GetApplicationIds().GetEntityIdentifiers())
if err != nil {
Expand Down
51 changes: 51 additions & 0 deletions pkg/identityserver/application_access_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -532,3 +532,54 @@ func TestApplicationAccessClusterAuth(t *testing.T) {
}
}, withPrivateTestDatabase(p))
}

func TestApplicationContactRestrictions(t *testing.T) {
p := &storetest.Population{}

usr1 := p.NewUser()
usr1Key, _ := p.NewAPIKey(usr1.GetEntityIdentifiers(), ttnpb.Right_RIGHT_ALL)
usr1Creds := rpcCreds(usr1Key)

app1 := p.NewApplication(usr1.GetOrganizationOrUserIdentifiers())

usr2 := p.NewUser()
p.NewMembership(
usr2.GetOrganizationOrUserIdentifiers(),
app1.GetEntityIdentifiers(),
ttnpb.Right_RIGHT_APPLICATION_INFO,
)
// Set the user as administrative contact for the application.
app1.AdministrativeContact = usr2.GetOrganizationOrUserIdentifiers()

t.Parallel()
a, ctx := test.New(t)

testWithIdentityServer(t, func(is *IdentityServer, cc *grpc.ClientConn) {
regClt := ttnpb.NewApplicationRegistryClient(cc)
accessClt := ttnpb.NewApplicationAccessClient(cc)

// Attempt to delete a collaborator that is an administrative contact.
_, err := accessClt.DeleteCollaborator(ctx, &ttnpb.DeleteApplicationCollaboratorRequest{
ApplicationIds: app1.Ids,
CollaboratorIds: usr2.GetOrganizationOrUserIdentifiers(),
}, usr1Creds)
a.So(errors.IsFailedPrecondition(err), should.BeTrue)

// Change the administrative contact.
_, err = regClt.Update(ctx, &ttnpb.UpdateApplicationRequest{
Application: &ttnpb.Application{
Ids: app1.Ids,
AdministrativeContact: usr1.GetOrganizationOrUserIdentifiers(),
},
FieldMask: ttnpb.FieldMask("administrative_contact"),
}, usr1Creds)
a.So(err, should.BeNil)

// Attempt to delete a collaborator that is an administrative contact.
_, err = accessClt.DeleteCollaborator(ctx, &ttnpb.DeleteApplicationCollaboratorRequest{
ApplicationIds: app1.Ids,
CollaboratorIds: usr2.GetOrganizationOrUserIdentifiers(),
}, usr1Creds)
a.So(err, should.BeNil)
}, withPrivateTestDatabase(p))
}
13 changes: 13 additions & 0 deletions pkg/identityserver/client_access.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"go.thethings.network/lorawan-stack/v3/pkg/identityserver/store"
"go.thethings.network/lorawan-stack/v3/pkg/ttnpb"
"go.thethings.network/lorawan-stack/v3/pkg/unique"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/emptypb"
)

Expand Down Expand Up @@ -241,6 +242,18 @@ func (is *IdentityServer) deleteClientCollaborator(
if err != nil {
return err
}
clt, err := st.GetClient(
ctx,
req.GetClientIds(),
store.FieldMask([]string{"administrative_contact", "technical_contact"}),
)
if err != nil {
return err
}
if proto.Equal(clt.GetAdministrativeContact(), req.GetCollaboratorIds()) ||
proto.Equal(clt.GetTechnicalContact(), req.GetCollaboratorIds()) {
return errCollaboratorIsContact.WithAttributes("collaborator_id", req.GetCollaboratorIds().IDString())
}
if removedRights.Implied().IncludesAll(ttnpb.Right_RIGHT_CLIENT_ALL) {
memberRights, err := st.FindMembers(ctx, req.GetClientIds().GetEntityIdentifiers())
if err != nil {
Expand Down
51 changes: 51 additions & 0 deletions pkg/identityserver/client_access_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,3 +246,54 @@ func TestClientAccessClusterAuth(t *testing.T) {
}
}, withPrivateTestDatabase(p))
}

func TestClientContactRestrictions(t *testing.T) {
p := &storetest.Population{}

usr1 := p.NewUser()
usr1Key, _ := p.NewAPIKey(usr1.GetEntityIdentifiers(), ttnpb.Right_RIGHT_ALL)
usr1Creds := rpcCreds(usr1Key)

gtw1 := p.NewClient(usr1.GetOrganizationOrUserIdentifiers())

usr2 := p.NewUser()
p.NewMembership(
usr2.GetOrganizationOrUserIdentifiers(),
gtw1.GetEntityIdentifiers(),
ttnpb.Right_RIGHT_CLIENT_INFO,
)
// Set the user as administrative contact for the client.
gtw1.AdministrativeContact = usr2.GetOrganizationOrUserIdentifiers()

t.Parallel()
a, ctx := test.New(t)

testWithIdentityServer(t, func(is *IdentityServer, cc *grpc.ClientConn) {
regClt := ttnpb.NewClientRegistryClient(cc)
accessClt := ttnpb.NewClientAccessClient(cc)

// Attempt to delete a collaborator that is an administrative contact.
_, err := accessClt.DeleteCollaborator(ctx, &ttnpb.DeleteClientCollaboratorRequest{
ClientIds: gtw1.Ids,
CollaboratorIds: usr2.GetOrganizationOrUserIdentifiers(),
}, usr1Creds)
a.So(errors.IsFailedPrecondition(err), should.BeTrue)

// Change the administrative contact.
_, err = regClt.Update(ctx, &ttnpb.UpdateClientRequest{
Client: &ttnpb.Client{
Ids: gtw1.Ids,
AdministrativeContact: usr1.GetOrganizationOrUserIdentifiers(),
},
FieldMask: ttnpb.FieldMask("administrative_contact"),
}, usr1Creds)
a.So(err, should.BeNil)

// Attempt to delete a collaborator that is an administrative contact.
_, err = accessClt.DeleteCollaborator(ctx, &ttnpb.DeleteClientCollaboratorRequest{
ClientIds: gtw1.Ids,
CollaboratorIds: usr2.GetOrganizationOrUserIdentifiers(),
}, usr1Creds)
a.So(err, should.BeNil)
}, withPrivateTestDatabase(p))
}
21 changes: 21 additions & 0 deletions pkg/identityserver/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright © 2024 The Things Network Foundation, The Things Industries B.V.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package identityserver

import "go.thethings.network/lorawan-stack/v3/pkg/errors"

var errCollaboratorIsContact = errors.DefineFailedPrecondition(
"collaborator_is_contact", "collaborator `{collaborator_id}` is used as a contact",
)
13 changes: 13 additions & 0 deletions pkg/identityserver/gateway_access.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"go.thethings.network/lorawan-stack/v3/pkg/identityserver/store"
"go.thethings.network/lorawan-stack/v3/pkg/ttnpb"
"go.thethings.network/lorawan-stack/v3/pkg/unique"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/emptypb"
)

Expand Down Expand Up @@ -427,6 +428,18 @@ func (is *IdentityServer) deleteGatewayCollaborator(
if err != nil {
return err
}
gtw, err := st.GetGateway(
ctx,
req.GetGatewayIds(),
store.FieldMask([]string{"administrative_contact", "technical_contact"}),
)
if err != nil {
return err
}
if proto.Equal(gtw.GetAdministrativeContact(), req.GetCollaboratorIds()) ||
proto.Equal(gtw.GetTechnicalContact(), req.GetCollaboratorIds()) {
return errCollaboratorIsContact.WithAttributes("collaborator_id", req.GetCollaboratorIds().IDString())
}
if removedRights.Implied().IncludesAll(ttnpb.Right_RIGHT_GATEWAY_ALL) {
memberRights, err := st.FindMembers(ctx, req.GetGatewayIds().GetEntityIdentifiers())
if err != nil {
Expand Down
51 changes: 51 additions & 0 deletions pkg/identityserver/gateway_access_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,57 @@ func TestGatewayAccessClusterAuth(t *testing.T) {
}, withPrivateTestDatabase(p))
}

func TestGatewayContactRestrictions(t *testing.T) {
p := &storetest.Population{}

usr1 := p.NewUser()
usr1Key, _ := p.NewAPIKey(usr1.GetEntityIdentifiers(), ttnpb.Right_RIGHT_ALL)
usr1Creds := rpcCreds(usr1Key)

gtw1 := p.NewGateway(usr1.GetOrganizationOrUserIdentifiers())

usr2 := p.NewUser()
p.NewMembership(
usr2.GetOrganizationOrUserIdentifiers(),
gtw1.GetEntityIdentifiers(),
ttnpb.Right_RIGHT_GATEWAY_INFO,
)
// Set the user as administrative contact for the gateway.
gtw1.AdministrativeContact = usr2.GetOrganizationOrUserIdentifiers()

t.Parallel()
a, ctx := test.New(t)

testWithIdentityServer(t, func(is *IdentityServer, cc *grpc.ClientConn) {
regClt := ttnpb.NewGatewayRegistryClient(cc)
accessClt := ttnpb.NewGatewayAccessClient(cc)

// Attempt to delete a collaborator that is an administrative contact.
_, err := accessClt.DeleteCollaborator(ctx, &ttnpb.DeleteGatewayCollaboratorRequest{
GatewayIds: gtw1.Ids,
CollaboratorIds: usr2.GetOrganizationOrUserIdentifiers(),
}, usr1Creds)
a.So(errors.IsFailedPrecondition(err), should.BeTrue)

// Change the administrative contact.
_, err = regClt.Update(ctx, &ttnpb.UpdateGatewayRequest{
Gateway: &ttnpb.Gateway{
Ids: gtw1.Ids,
AdministrativeContact: usr1.GetOrganizationOrUserIdentifiers(),
},
FieldMask: ttnpb.FieldMask("administrative_contact"),
}, usr1Creds)
a.So(err, should.BeNil)

// Attempt to delete a collaborator that is an administrative contact.
_, err = accessClt.DeleteCollaborator(ctx, &ttnpb.DeleteGatewayCollaboratorRequest{
GatewayIds: gtw1.Ids,
CollaboratorIds: usr2.GetOrganizationOrUserIdentifiers(),
}, usr1Creds)
a.So(err, should.BeNil)
}, withPrivateTestDatabase(p))
}

func TestGatewayBatchAccess(t *testing.T) {
p := &storetest.Population{}

Expand Down
21 changes: 17 additions & 4 deletions pkg/identityserver/organization_access.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"go.thethings.network/lorawan-stack/v3/pkg/identityserver/store"
"go.thethings.network/lorawan-stack/v3/pkg/ttnpb"
"go.thethings.network/lorawan-stack/v3/pkg/unique"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/emptypb"
)

Expand Down Expand Up @@ -63,6 +64,10 @@ var (
events.WithAuthFromContext(),
events.WithClientInfoFromContext(),
)

errOrganizationNeedsCollaborator = errors.DefineFailedPrecondition(
"organization_needs_collaborator", "every organization needs at least one collaborator with all rights",
)
)

func (*IdentityServer) listOrganizationRights(
Expand Down Expand Up @@ -282,10 +287,6 @@ func (is *IdentityServer) getOrganizationCollaborator(
return res, nil
}

var errOrganizationNeedsCollaborator = errors.DefineFailedPrecondition(
"organization_needs_collaborator", "every organization needs at least one collaborator with all rights",
)

func (is *IdentityServer) setOrganizationCollaborator( //nolint:gocyclo
ctx context.Context, req *ttnpb.SetOrganizationCollaboratorRequest,
) (_ *emptypb.Empty, err error) {
Expand Down Expand Up @@ -440,6 +441,18 @@ func (is *IdentityServer) deleteOrganizationCollaborator(
if err != nil {
return err
}
org, err := st.GetOrganization(
ctx,
req.GetOrganizationIds(),
store.FieldMask([]string{"administrative_contact", "technical_contact"}),
)
if err != nil {
return err
}
if proto.Equal(org.GetAdministrativeContact(), req.GetCollaboratorIds()) ||
proto.Equal(org.GetTechnicalContact(), req.GetCollaboratorIds()) {
return errCollaboratorIsContact.WithAttributes("collaborator_id", req.GetCollaboratorIds().IDString())
}
if r.Implied().IncludesAll(ttnpb.Right_RIGHT_ORGANIZATION_ALL) {
memberRights, err := st.FindMembers(ctx, req.GetOrganizationIds().GetEntityIdentifiers())
if err != nil {
Expand Down
51 changes: 51 additions & 0 deletions pkg/identityserver/organization_access_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -528,3 +528,54 @@ func TestOrganizationAccessClusterAuth(t *testing.T) {
}
}, withPrivateTestDatabase(p))
}

func TestOrganizationContactRestrictions(t *testing.T) {
p := &storetest.Population{}

usr1 := p.NewUser()
usr1Key, _ := p.NewAPIKey(usr1.GetEntityIdentifiers(), ttnpb.Right_RIGHT_ALL)
usr1Creds := rpcCreds(usr1Key)

gtw1 := p.NewOrganization(usr1.GetOrganizationOrUserIdentifiers())

usr2 := p.NewUser()
p.NewMembership(
usr2.GetOrganizationOrUserIdentifiers(),
gtw1.GetEntityIdentifiers(),
ttnpb.Right_RIGHT_ORGANIZATION_INFO,
)
// Set the user as administrative contact for the organization.
gtw1.AdministrativeContact = usr2.GetOrganizationOrUserIdentifiers()

t.Parallel()
a, ctx := test.New(t)

testWithIdentityServer(t, func(is *IdentityServer, cc *grpc.ClientConn) {
regClt := ttnpb.NewOrganizationRegistryClient(cc)
accessClt := ttnpb.NewOrganizationAccessClient(cc)

// Attempt to delete a collaborator that is an administrative contact.
_, err := accessClt.DeleteCollaborator(ctx, &ttnpb.DeleteOrganizationCollaboratorRequest{
OrganizationIds: gtw1.Ids,
CollaboratorIds: usr2.GetOrganizationOrUserIdentifiers(),
}, usr1Creds)
a.So(errors.IsFailedPrecondition(err), should.BeTrue)

// Change the administrative contact.
_, err = regClt.Update(ctx, &ttnpb.UpdateOrganizationRequest{
Organization: &ttnpb.Organization{
Ids: gtw1.Ids,
AdministrativeContact: usr1.GetOrganizationOrUserIdentifiers(),
},
FieldMask: ttnpb.FieldMask("administrative_contact"),
}, usr1Creds)
a.So(err, should.BeNil)

// Attempt to delete a collaborator that is an administrative contact.
_, err = accessClt.DeleteCollaborator(ctx, &ttnpb.DeleteOrganizationCollaboratorRequest{
OrganizationIds: gtw1.Ids,
CollaboratorIds: usr2.GetOrganizationOrUserIdentifiers(),
}, usr1Creds)
a.So(err, should.BeNil)
}, withPrivateTestDatabase(p))
}
Loading

0 comments on commit 095b9e2

Please sign in to comment.