Skip to content

Commit

Permalink
Merge pull request #540 from kian99/CSS-9773-add-jaas-client
Browse files Browse the repository at this point in the history
feat: add jaas client
  • Loading branch information
hmlanigan authored Aug 20, 2024
2 parents 4661867 + 960de19 commit 72bb72d
Show file tree
Hide file tree
Showing 11 changed files with 407 additions and 46 deletions.
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -199,15 +199,15 @@ require (
go.abhg.dev/goldmark/frontmatter v0.2.0 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect
golang.org/x/mod v0.18.0 // indirect
golang.org/x/mod v0.19.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/oauth2 v0.21.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/term v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.22.0 // indirect
golang.org/x/tools v0.23.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect
google.golang.org/grpc v1.63.2 // indirect
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -685,8 +685,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
Expand Down Expand Up @@ -762,8 +762,8 @@ golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
Expand Down
2 changes: 2 additions & 0 deletions internal/juju/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ type Client struct {
SSHKeys sshKeysClient
Users usersClient
Secrets secretsClient
Jaas jaasClient

isJAAS func() bool
}
Expand Down Expand Up @@ -107,6 +108,7 @@ func NewClient(ctx context.Context, config ControllerConfiguration) (*Client, er
SSHKeys: *newSSHKeysClient(sc),
Users: *newUsersClient(sc),
Secrets: *newSecretsClient(sc),
Jaas: *newJaasClient(sc),
isJAAS: func() bool { return sc.IsJAAS(defaultJAASCheck) },
}, nil
}
Expand Down
6 changes: 2 additions & 4 deletions internal/juju/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,13 @@ import (
type JujuSuite struct {
suite.Suite

testModelName string
testModelName *string

mockConnection *MockConnection
mockSharedClient *MockSharedClient
}

func (s *JujuSuite) setupMocks(t *testing.T) *gomock.Controller {
s.testModelName = "test-secret-model"

ctlr := gomock.NewController(t)

s.mockConnection = NewMockConnection(ctlr)
Expand All @@ -35,7 +33,7 @@ func (s *JujuSuite) setupMocks(t *testing.T) *gomock.Controller {
s.mockSharedClient.EXPECT().Errorf(gomock.Any(), gomock.Any()).Do(log).AnyTimes()
s.mockSharedClient.EXPECT().Tracef(gomock.Any(), gomock.Any()).Do(log).AnyTimes()
s.mockSharedClient.EXPECT().JujuLogger().Return(&jujuLoggerShim{}).AnyTimes()
s.mockSharedClient.EXPECT().GetConnection(&s.testModelName).Return(s.mockConnection, nil).AnyTimes()
s.mockSharedClient.EXPECT().GetConnection(s.testModelName).Return(s.mockConnection, nil).AnyTimes()

return ctlr
}
8 changes: 8 additions & 0 deletions internal/juju/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package juju

import (
jaasparams "github.com/canonical/jimm-go-sdk/v3/api/params"
"github.com/juju/charm/v12"
"github.com/juju/juju/api"
apiapplication "github.com/juju/juju/api/client/application"
Expand Down Expand Up @@ -76,3 +77,10 @@ type SecretAPIClient interface {
GrantSecret(uri *secrets.URI, name string, apps []string) ([]error, error)
RevokeSecret(uri *secrets.URI, name string, apps []string) ([]error, error)
}

// JaasAPIClient defines the set of methods that the JAAS API provides.
type JaasAPIClient interface {
ListRelationshipTuples(req *jaasparams.ListRelationshipTuplesRequest) (*jaasparams.ListRelationshipTuplesResponse, error)
AddRelation(req *jaasparams.AddRelationRequest) error
RemoveRelation(req *jaasparams.RemoveRelationRequest) error
}
128 changes: 128 additions & 0 deletions internal/juju/jaas.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Copyright 2024 Canonical Ltd.
// Licensed under the Apache License, Version 2.0, see LICENCE file for details.

package juju

import (
"context"
"errors"

"github.com/canonical/jimm-go-sdk/v3/api"
"github.com/canonical/jimm-go-sdk/v3/api/params"
jujuapi "github.com/juju/juju/api"
)

type jaasClient struct {
SharedClient
getJaasApiClient func(jujuapi.Connection) JaasAPIClient
}

func newJaasClient(sc SharedClient) *jaasClient {
return &jaasClient{
SharedClient: sc,
getJaasApiClient: func(conn jujuapi.Connection) JaasAPIClient {
return api.NewClient(conn)
},
}
}

// JaasTuple represents a tuple object of used by JAAS for permissions management.
type JaasTuple struct {
// Object represents the source side of the relation.
Object string
// Relation represents the level of access
Relation string
// Target represents the resource that you want `object` to have access to.
Target string
}

func toAPITuples(tuples []JaasTuple) []params.RelationshipTuple {
out := make([]params.RelationshipTuple, 0, len(tuples))
for _, tuple := range tuples {
out = append(out, toAPITuple(tuple))
}
return out
}

func toAPITuple(tuple JaasTuple) params.RelationshipTuple {
return params.RelationshipTuple{
Object: tuple.Object,
Relation: tuple.Relation,
TargetObject: tuple.Target,
}
}

// AddRelations attempts to create the provided slice of relationship tuples.
// An empty slice of tuples will return an error.
func (jc *jaasClient) AddRelations(tuples []JaasTuple) error {
if len(tuples) == 0 {
return errors.New("empty slice of tuples")
}
conn, err := jc.GetConnection(nil)
if err != nil {
return err
}
defer func() { _ = conn.Close() }()
cl := jc.getJaasApiClient(conn)
req := params.AddRelationRequest{
Tuples: toAPITuples(tuples),
}
return cl.AddRelation(&req)
}

// DeleteRelations attempts to delete the provided slice of relationship tuples.
// An empty slice of tuples will return an error.
func (jc *jaasClient) DeleteRelations(tuples []JaasTuple) error {
if len(tuples) == 0 {
return errors.New("empty slice of tuples")
}
conn, err := jc.GetConnection(nil)
if err != nil {
return err
}
defer func() { _ = conn.Close() }()
cl := jc.getJaasApiClient(conn)
req := params.RemoveRelationRequest{
Tuples: toAPITuples(tuples),
}
return cl.RemoveRelation(&req)
}

// ReadRelations attempts to read relations that match the criteria defined by `tuple`.
// An nil tuple pointer is invalid and will return an error.
func (jc *jaasClient) ReadRelations(ctx context.Context, tuple *JaasTuple) ([]params.RelationshipTuple, error) {
if tuple == nil {
return nil, errors.New("read relation tuple is nil")
}

conn, err := jc.GetConnection(nil)
if err != nil {
return nil, err
}
defer func() { _ = conn.Close() }()

client := jc.getJaasApiClient(conn)
relations := make([]params.RelationshipTuple, 0)
req := &params.ListRelationshipTuplesRequest{Tuple: toAPITuple(*tuple)}
for {
resp, err := client.ListRelationshipTuples(req)
if err != nil {
jc.Errorf(err, "call to ListRelationshipTuples failed")
return nil, err
}
if len(resp.Errors) > 0 {
jc.Errorf(err, "call to ListRelationshipTuples contained error(s)")
return nil, errors.New(resp.Errors[0])
}
relations = append(relations, resp.Tuples...)
if resp.ContinuationToken == "" {
return relations, nil
}
req.ContinuationToken = resp.ContinuationToken
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
}
}
161 changes: 161 additions & 0 deletions internal/juju/jaas_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// Copyright 2024 Canonical Ltd.
// Licensed under the Apache License, Version 2.0, see LICENCE file for details.

package juju

import (
"context"
"errors"
"testing"

"github.com/canonical/jimm-go-sdk/v3/api/params"
"github.com/juju/juju/api"
"github.com/stretchr/testify/suite"
"go.uber.org/mock/gomock"
)

type JaasSuite struct {
suite.Suite
JujuSuite

mockJaasClient *MockJaasAPIClient
}

func (s *JaasSuite) setupMocks(t *testing.T) *gomock.Controller {
ctlr := s.JujuSuite.setupMocks(t)
s.mockJaasClient = NewMockJaasAPIClient(ctlr)

return ctlr
}

func (s *JaasSuite) getJaasClient() jaasClient {
return jaasClient{
SharedClient: s.JujuSuite.mockSharedClient,
getJaasApiClient: func(connection api.Connection) JaasAPIClient {
return s.mockJaasClient
},
}
}

func (s *JaasSuite) TestAddRelations() {
defer s.setupMocks(s.T()).Finish()

tuples := []JaasTuple{
{Object: "object-1", Relation: "relation", Target: "target-1"},
{Object: "object-2", Relation: "relation", Target: "target-2"},
}
req := params.AddRelationRequest{
Tuples: toAPITuples(tuples),
}

s.mockJaasClient.EXPECT().AddRelation(
&req,
).Return(nil)

client := s.getJaasClient()
err := client.AddRelations(tuples)
s.Require().NoError(err)
}

func (s *JaasSuite) TestAddRelationsEmptySlice() {
expectedErr := errors.New("empty slice of tuples")
client := s.getJaasClient()
err := client.AddRelations([]JaasTuple{})
s.Require().Error(err)
s.Assert().Equal(expectedErr, err)
}

func (s *JaasSuite) TestDeleteRelations() {
defer s.setupMocks(s.T()).Finish()

tuples := []JaasTuple{
{Object: "object-1", Relation: "relation", Target: "target-1"},
{Object: "object-2", Relation: "relation", Target: "target-2"},
}
req := params.RemoveRelationRequest{
Tuples: toAPITuples(tuples),
}

s.mockJaasClient.EXPECT().RemoveRelation(
&req,
).Return(nil)

client := s.getJaasClient()
err := client.DeleteRelations(tuples)
s.Require().NoError(err)
}

func (s *JaasSuite) TestDeleteRelationsEmptySlice() {
expectedErr := errors.New("empty slice of tuples")
client := s.getJaasClient()
err := client.DeleteRelations([]JaasTuple{})
s.Require().Error(err)
s.Assert().Equal(expectedErr, err)
}

func (s *JaasSuite) TestReadRelations() {
defer s.setupMocks(s.T()).Finish()

tuple := JaasTuple{Object: "object-1", Relation: "relation", Target: "target-1"}
// 1st request/response has no token in the request and a token in the response indicating another page is available.
req := &params.ListRelationshipTuplesRequest{Tuple: toAPITuple(tuple)}
respWithToken := &params.ListRelationshipTuplesResponse{
Tuples: []params.RelationshipTuple{toAPITuple(tuple)},
ContinuationToken: "token",
}
s.mockJaasClient.EXPECT().ListRelationshipTuples(
req,
).Return(respWithToken, nil)
// 2nd request/response has the previous token in the request and no token in the response, indicating all pages have been consumed.
reqWithToken := &params.ListRelationshipTuplesRequest{Tuple: toAPITuple(tuple), ContinuationToken: "token"}
respWithoutToken := &params.ListRelationshipTuplesResponse{
Tuples: []params.RelationshipTuple{toAPITuple(tuple)},
ContinuationToken: "",
}
s.mockJaasClient.EXPECT().ListRelationshipTuples(
reqWithToken,
).Return(respWithoutToken, nil)

client := s.getJaasClient()
relations, err := client.ReadRelations(context.Background(), &tuple)
s.Require().NoError(err)
s.Require().Len(relations, 2)
}

func (s *JaasSuite) TestReadRelationsEmptyTuple() {
expectedErr := errors.New("read relation tuple is nil")
client := s.getJaasClient()
_, err := client.ReadRelations(context.Background(), nil)
s.Require().Error(err)
s.Assert().Equal(expectedErr, err)
}

func (s *JaasSuite) TestReadRelationsCancelledContext() {
defer s.setupMocks(s.T()).Finish()

tuple := JaasTuple{Object: "object-1", Relation: "relation", Target: "target-1"}
req := &params.ListRelationshipTuplesRequest{Tuple: toAPITuple(tuple)}
respWithToken := &params.ListRelationshipTuplesResponse{
Tuples: []params.RelationshipTuple{toAPITuple(tuple)},
ContinuationToken: "token",
}
s.mockJaasClient.EXPECT().ListRelationshipTuples(
req,
).Return(respWithToken, nil)

expectedErr := errors.New("context canceled")
ctx := context.Background()
ctx, cancelFunc := context.WithCancel(ctx)
cancelFunc()

client := s.getJaasClient()
_, err := client.ReadRelations(ctx, &tuple)
s.Require().Error(err)
s.Assert().Equal(expectedErr, err)
}

// In order for 'go test' to run this suite, we need to create
// a normal test function and pass our suite to suite.Run
func TestJaasSuite(t *testing.T) {
suite.Run(t, new(JaasSuite))
}
Loading

0 comments on commit 72bb72d

Please sign in to comment.