Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/v3' into juju-7322/improve-usa…
Browse files Browse the repository at this point in the history
…bility-of-migrate-command
  • Loading branch information
ale8k committed Dec 18, 2024
2 parents 75158c6 + aec55fc commit d14b8f4
Show file tree
Hide file tree
Showing 19 changed files with 660 additions and 615 deletions.
2 changes: 1 addition & 1 deletion cmd/jaas/cmd/updatecredentials_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func (s *updateCredentialsSuite) TestUpdateCredentialsWithLocalCredentials(c *gc
models: []
`)

ofgaUser := openfga.NewUser(sa, s.JIMM.AuthorizationClient())
ofgaUser := openfga.NewUser(sa, s.JIMM.OpenFGAClient)
cloudCredentialTag := names.NewCloudCredentialTag("test-cloud/" + clientIDWithDomain + "/test-credentials")
cloudCredential2, err := s.JIMM.GetCloudCredential(ctx, ofgaUser, cloudCredentialTag)
c.Assert(err, gc.IsNil)
Expand Down
7 changes: 4 additions & 3 deletions cmd/jimmsrv/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,8 @@ type Params struct {

// A Service is the implementation of a JIMM server.
type Service struct {
jimm *jimm.JIMM
jimm *jimm.JIMM
jwkService *jimmjwx.JWKSService

isLeader bool
auditLogCleanupPeriod int
Expand Down Expand Up @@ -231,7 +232,7 @@ func (s *Service) WatchModelSummaries(ctx context.Context) error {

// StartJWKSRotator see internal/jimmjwx/jwks.go for details.
func (s *Service) StartJWKSRotator(ctx context.Context, checkRotateRequired <-chan time.Time, initialRotateRequiredTime time.Time) error {
return s.jimm.JWKService.StartJWKSRotator(ctx, checkRotateRequired, initialRotateRequiredTime)
return s.jwkService.StartJWKSRotator(ctx, checkRotateRequired, initialRotateRequiredTime)
}

// MonitorResources periodically updates metrics.
Expand Down Expand Up @@ -382,7 +383,7 @@ func NewService(ctx context.Context, p Params) (*Service, error) {
p.JWTExpiryDuration = 24 * time.Hour
}

jimmParameters.JWKService = jimmjwx.NewJWKSService(credentialStore)
s.jwkService = jimmjwx.NewJWKSService(credentialStore)
jimmParameters.JWTService = jimmjwx.NewJWTService(jimmjwx.JWTServiceParams{
Host: p.PublicDNSName,
Store: credentialStore,
Expand Down
22 changes: 22 additions & 0 deletions internal/dbmodel/group_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,25 @@ func TestGroupEntry(t *testing.T) {
c.Assert(result.Error, qt.IsNil)
c.Assert(ge3, qt.DeepEquals, ge)
}

// TestHardDeleteGroupEntry tests hard delete of groups, to make sure we can create a group with the same name after deleting it.
func TestHardDeleteGroupEntry(t *testing.T) {
c := qt.New(t)
db := gormDB(t)

ge := dbmodel.GroupEntry{
Name: "test-group-1",
}
c.Assert(db.Create(&ge).Error, qt.IsNil)
c.Assert(ge.ID, qt.Equals, uint(1))

c.Assert(db.Delete(ge).Error, qt.IsNil)

result := db.First(&ge)
c.Assert(result.Error, qt.ErrorMatches, "record not found")

ge1 := dbmodel.GroupEntry{
Name: "test-group-1",
}
c.Assert(db.Create(&ge1).Error, qt.IsNil)
}
159 changes: 0 additions & 159 deletions internal/jimm/access.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"fmt"
"regexp"
"strings"
"sync"

"github.com/canonical/ofga"
"github.com/google/uuid"
Expand All @@ -20,7 +19,6 @@ import (
"github.com/canonical/jimm/v3/internal/db"
"github.com/canonical/jimm/v3/internal/dbmodel"
"github.com/canonical/jimm/v3/internal/errors"
"github.com/canonical/jimm/v3/internal/jimmjwx"
"github.com/canonical/jimm/v3/internal/openfga"
ofganames "github.com/canonical/jimm/v3/internal/openfga/names"
"github.com/canonical/jimm/v3/internal/servermon"
Expand Down Expand Up @@ -147,163 +145,6 @@ func ToOfferRelation(accessLevel string) (openfga.Relation, error) {
}
}

// JWTGeneratorDatabase specifies the database interface used by the
// JWT generator.
type JWTGeneratorDatabase interface {
GetController(ctx context.Context, controller *dbmodel.Controller) error
}

// JWTGeneratorAccessChecker specifies the access checker used by the JWT
// generator to obtain user's access rights to various entities.
type JWTGeneratorAccessChecker interface {
GetUserModelAccess(context.Context, *openfga.User, names.ModelTag) (string, error)
GetUserControllerAccess(context.Context, *openfga.User, names.ControllerTag) (string, error)
GetUserCloudAccess(context.Context, *openfga.User, names.CloudTag) (string, error)
CheckPermission(context.Context, *openfga.User, map[string]string, map[string]interface{}) (map[string]string, error)
}

// JWTService specifies the service JWT generator uses to generate JWTs.
type JWTService interface {
NewJWT(context.Context, jimmjwx.JWTParams) ([]byte, error)
}

// JWTGenerator provides the necessary state and methods to authorize a user and generate JWT tokens.
type JWTGenerator struct {
database JWTGeneratorDatabase
accessChecker JWTGeneratorAccessChecker
jwtService JWTService

mu sync.Mutex
accessMapCache map[string]string
mt names.ModelTag
ct names.ControllerTag
user *openfga.User
callCount int
}

// NewJWTGenerator returns a new JwtAuthorizer struct
func NewJWTGenerator(database JWTGeneratorDatabase, accessChecker JWTGeneratorAccessChecker, jwtService JWTService) JWTGenerator {
return JWTGenerator{
database: database,
accessChecker: accessChecker,
jwtService: jwtService,
}
}

// SetTags implements TokenGenerator
func (auth *JWTGenerator) SetTags(mt names.ModelTag, ct names.ControllerTag) {
auth.mt = mt
auth.ct = ct
}

// SetTags implements TokenGenerator
func (auth *JWTGenerator) GetUser() names.UserTag {
if auth.user != nil {
return auth.user.ResourceTag()
}
return names.UserTag{}
}

// MakeLoginToken authorizes the user based on the provided login requests and returns
// a JWT containing claims about user's access to the controller, model (if applicable)
// and all clouds that the controller knows about.
func (auth *JWTGenerator) MakeLoginToken(ctx context.Context, user *openfga.User) ([]byte, error) {
const op = errors.Op("jimm.MakeLoginToken")

auth.mu.Lock()
defer auth.mu.Unlock()

if user == nil {
return nil, errors.E(op, "user not specified")
}
auth.user = user

// Recreate the accessMapCache to prevent leaking permissions across multiple login requests.
auth.accessMapCache = make(map[string]string)
var authErr error

var modelAccess string
if auth.mt.Id() == "" {
return nil, errors.E(op, "model not set")
}
modelAccess, authErr = auth.accessChecker.GetUserModelAccess(ctx, auth.user, auth.mt)
if authErr != nil {
zapctx.Error(ctx, "model access check failed", zap.Error(authErr))
return nil, authErr
}
auth.accessMapCache[auth.mt.String()] = modelAccess

if auth.ct.Id() == "" {
return nil, errors.E(op, "controller not set")
}
var controllerAccess string
controllerAccess, authErr = auth.accessChecker.GetUserControllerAccess(ctx, auth.user, auth.ct)
if authErr != nil {
return nil, authErr
}
auth.accessMapCache[auth.ct.String()] = controllerAccess

var ctl dbmodel.Controller
ctl.SetTag(auth.ct)
err := auth.database.GetController(ctx, &ctl)
if err != nil {
zapctx.Error(ctx, "failed to fetch controller", zap.Error(err))
return nil, errors.E(op, "failed to fetch controller", err)
}
clouds := make(map[names.CloudTag]bool)
for _, cloudRegion := range ctl.CloudRegions {
clouds[cloudRegion.CloudRegion.Cloud.ResourceTag()] = true
}
for cloudTag := range clouds {
accessLevel, err := auth.accessChecker.GetUserCloudAccess(ctx, auth.user, cloudTag)
if err != nil {
zapctx.Error(ctx, "cloud access check failed", zap.Error(err))
return nil, errors.E(op, "failed to check user's cloud access", err)
}
auth.accessMapCache[cloudTag.String()] = accessLevel
}

return auth.jwtService.NewJWT(ctx, jimmjwx.JWTParams{
Controller: auth.ct.Id(),
User: auth.user.Tag().String(),
Access: auth.accessMapCache,
})
}

// MakeToken assumes MakeLoginToken has already been called and checks the permissions
// specified in the permissionMap. If the logged in user has all those permissions
// a JWT will be returned with assertions confirming all those permissions.
func (auth *JWTGenerator) MakeToken(ctx context.Context, permissionMap map[string]interface{}) ([]byte, error) {
const op = errors.Op("jimm.MakeToken")

auth.mu.Lock()
defer auth.mu.Unlock()

if auth.callCount >= 10 {
return nil, errors.E(op, "Permission check limit exceeded")
}
auth.callCount++
if auth.user == nil {
return nil, errors.E(op, "User authorization missing.")
}
if permissionMap != nil {
var err error
auth.accessMapCache, err = auth.accessChecker.CheckPermission(ctx, auth.user, auth.accessMapCache, permissionMap)
if err != nil {
return nil, err
}
}
jwt, err := auth.jwtService.NewJWT(ctx, jimmjwx.JWTParams{
Controller: auth.ct.Id(),
User: auth.user.Tag().String(),
Access: auth.accessMapCache,
})
if err != nil {
return nil, err
}
return jwt, nil
}

// CheckPermission loops over the desired permissions in desiredPerms and adds these permissions
// to cachedPerms if they exist. If the user does not have any of the desired permissions then an
// error is returned.
Expand Down
Loading

0 comments on commit d14b8f4

Please sign in to comment.