Skip to content

Commit

Permalink
chore: move jwt generator
Browse files Browse the repository at this point in the history
Move the jwtGenerator into a separate package.
  • Loading branch information
kian99 committed Dec 17, 2024
1 parent 6fa9787 commit d53de57
Show file tree
Hide file tree
Showing 5 changed files with 540 additions and 503 deletions.
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 d53de57

Please sign in to comment.