diff --git a/.github/actions/test-server/action.yaml b/.github/actions/test-server/action.yaml index 7fd42a836..a1d2133f1 100644 --- a/.github/actions/test-server/action.yaml +++ b/.github/actions/test-server/action.yaml @@ -80,8 +80,11 @@ runs: run: echo "name=$CONTROLLER_NAME" >> $GITHUB_OUTPUT shell: bash - - name: Install jimmctl and yq - run: sudo snap install jimmctl --channel=3/stable && sudo snap install yq + - name: Install jimmctl, jaas plugin and yq + run: | + sudo snap install jimmctl --channel=3/stable && \ + sudo snap install jaas --channel=3/stable && + sudo snap install yq shell: bash - name: Authenticate Juju CLI @@ -97,3 +100,7 @@ runs: env: JIMM_CONTROLLER_NAME: "jimm" CONTROLLER_NAME: ${{ steps.lxd-controller.outputs.name }} + + - name: Provide service account with cloud-credentials + run: ./local/jimm/setup-service-account.sh + shell: bash diff --git a/compose-common.yaml b/compose-common.yaml index 3e905dab0..b1f9727e7 100644 --- a/compose-common.yaml +++ b/compose-common.yaml @@ -37,7 +37,7 @@ services: JIMM_OAUTH_CLIENT_SECRET: "SwjDofnbDzJDm9iyfUhEp67FfUFMY8L4" JIMM_OAUTH_SCOPES: "openid profile email" # Space separated list of scopes JIMM_DASHBOARD_FINAL_REDIRECT_URL: "https://jaas.ai" # Example URL - JIMM_ACCESS_TOKEN_EXPIRY_DURATION: 1h + JIMM_ACCESS_TOKEN_EXPIRY_DURATION: 100h JIMM_SECURE_SESSION_COOKIES: false JIMM_SESSION_COOKIE_MAX_AGE: 86400 JIMM_SESSION_SECRET_KEY: Xz2RkR9g87M75xfoumhEs5OmGziIX8D88Rk5YW8FSvkBPSgeK9t5AS9IvPDJ3NnB diff --git a/internal/jimm/cache.go b/internal/jimm/cache.go index 01ca55f9c..07fd93563 100644 --- a/internal/jimm/cache.go +++ b/internal/jimm/cache.go @@ -44,7 +44,7 @@ func (d *cacheDialer) Dial(ctx context.Context, ctl *dbmodel.Controller, mt name return d.dialer.Dial(ctx, ctl, mt, requiredPermissions) } rc := d.sfg.DoChan(ctl.Name, func() (interface{}, error) { - return d.dial(ctx, ctl) + return d.dial(ctx, ctl, requiredPermissions) }) select { case r := <-rc: @@ -57,7 +57,7 @@ func (d *cacheDialer) Dial(ctx context.Context, ctl *dbmodel.Controller, mt name } } -func (d *cacheDialer) dial(ctx context.Context, ctl *dbmodel.Controller) (interface{}, error) { +func (d *cacheDialer) dial(ctx context.Context, ctl *dbmodel.Controller, requiredPermissions map[string]string) (interface{}, error) { d.mu.Lock() capi, ok := d.conns[ctl.Name] if ok { @@ -73,7 +73,7 @@ func (d *cacheDialer) dial(ctx context.Context, ctl *dbmodel.Controller) (interf d.mu.Unlock() // We don't have a working connection to the controller, so dial one. - api, err := d.dialer.Dial(ctx, ctl, names.ModelTag{}, nil) + api, err := d.dialer.Dial(ctx, ctl, names.ModelTag{}, requiredPermissions) if err != nil { return nil, err } diff --git a/internal/jimmtest/jimm_mock.go b/internal/jimmtest/jimm_mock.go index 5eff89a5e..c51a7febc 100644 --- a/internal/jimmtest/jimm_mock.go +++ b/internal/jimmtest/jimm_mock.go @@ -21,7 +21,6 @@ import ( "github.com/canonical/jimm/v3/internal/openfga" ofganames "github.com/canonical/jimm/v3/internal/openfga/names" "github.com/canonical/jimm/v3/internal/pubsub" - "github.com/canonical/jimm/v3/pkg/api/params" jimmnames "github.com/canonical/jimm/v3/pkg/names" ) @@ -31,32 +30,25 @@ import ( // a NotImplemented error. type JIMM struct { mocks.LoginService + mocks.ModelManager AddAuditLogEntry_ func(ale *dbmodel.AuditLogEntry) AddCloudToController_ func(ctx context.Context, user *openfga.User, controllerName string, tag names.CloudTag, cloud jujuparams.Cloud, force bool) error AddController_ func(ctx context.Context, u *openfga.User, ctl *dbmodel.Controller) error AddGroup_ func(ctx context.Context, user *openfga.User, name string) (*dbmodel.GroupEntry, error) AddHostedCloud_ func(ctx context.Context, user *openfga.User, tag names.CloudTag, cloud jujuparams.Cloud, force bool) error - AddModel_ func(ctx context.Context, u *openfga.User, args *jimm.ModelCreateArgs) (*jujuparams.ModelInfo, error) AddServiceAccount_ func(ctx context.Context, u *openfga.User, clientId string) error Authenticate_ func(ctx context.Context, req *jujuparams.LoginRequest) (*openfga.User, error) AuthorizationClient_ func() *openfga.OFGAClient - ChangeModelCredential_ func(ctx context.Context, user *openfga.User, modelTag names.ModelTag, cloudCredentialTag names.CloudCredentialTag) error CheckPermission_ func(ctx context.Context, user *openfga.User, cachedPerms map[string]string, desiredPerms map[string]interface{}) (map[string]string, error) CopyServiceAccountCredential_ func(ctx context.Context, u *openfga.User, svcAcc *openfga.User, cloudCredentialTag names.CloudCredentialTag) (names.CloudCredentialTag, []jujuparams.UpdateCredentialModelResult, error) DB_ func() *db.Database - DestroyModel_ func(ctx context.Context, u *openfga.User, mt names.ModelTag, destroyStorage *bool, force *bool, maxWait *time.Duration, timeout *time.Duration) error DestroyOffer_ func(ctx context.Context, user *openfga.User, offerURL string, force bool) error - DumpModel_ func(ctx context.Context, u *openfga.User, mt names.ModelTag, simplified bool) (string, error) - DumpModelDB_ func(ctx context.Context, u *openfga.User, mt names.ModelTag) (map[string]interface{}, error) EarliestControllerVersion_ func(ctx context.Context) (version.Number, error) FindApplicationOffers_ func(ctx context.Context, user *openfga.User, filters ...jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetailsV5, error) FindAuditEvents_ func(ctx context.Context, user *openfga.User, filter db.AuditLogFilter) ([]dbmodel.AuditLogEntry, error) ForEachCloud_ func(ctx context.Context, user *openfga.User, f func(*dbmodel.Cloud) error) error - ForEachModel_ func(ctx context.Context, u *openfga.User, f func(*dbmodel.Model, jujuparams.UserAccessPermission) error) error ForEachUserCloud_ func(ctx context.Context, user *openfga.User, f func(*dbmodel.Cloud) error) error ForEachUserCloudCredential_ func(ctx context.Context, u *dbmodel.Identity, ct names.CloudTag, f func(cred *dbmodel.CloudCredential) error) error - ForEachUserModel_ func(ctx context.Context, u *openfga.User, f func(*dbmodel.Model, jujuparams.UserAccessPermission) error) error - FullModelStatus_ func(ctx context.Context, user *openfga.User, modelTag names.ModelTag, patterns []string) (*jujuparams.FullStatus, error) GetApplicationOffer_ func(ctx context.Context, user *openfga.User, offerURL string) (*jujuparams.ApplicationOfferAdminDetailsV5, error) GetApplicationOfferConsumeDetails_ func(ctx context.Context, user *openfga.User, details *jujuparams.ConsumeOfferDetails, v bakery.Version) error GetCloud_ func(ctx context.Context, u *openfga.User, tag names.CloudTag) (dbmodel.Cloud, error) @@ -73,22 +65,16 @@ type JIMM struct { GrantModelAccess_ func(ctx context.Context, user *openfga.User, mt names.ModelTag, ut names.UserTag, access jujuparams.UserAccessPermission) error GrantOfferAccess_ func(ctx context.Context, u *openfga.User, offerURL string, ut names.UserTag, access jujuparams.OfferAccessPermission) error GrantServiceAccountAccess_ func(ctx context.Context, u *openfga.User, svcAccTag jimmnames.ServiceAccountTag, entities []string) error - ImportModel_ func(ctx context.Context, user *openfga.User, controllerName string, modelTag names.ModelTag, newOwner string) error - IdentityModelDefaults_ func(ctx context.Context, user *dbmodel.Identity) (map[string]interface{}, error) InitiateMigration_ func(ctx context.Context, user *openfga.User, spec jujuparams.MigrationSpec) (jujuparams.InitiateMigrationResult, error) InitiateInternalMigration_ func(ctx context.Context, user *openfga.User, modelTag names.ModelTag, targetController string) (jujuparams.InitiateMigrationResult, error) ListApplicationOffers_ func(ctx context.Context, user *openfga.User, filters ...jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetailsV5, error) ListControllers_ func(ctx context.Context, user *openfga.User) ([]dbmodel.Controller, error) ListGroups_ func(ctx context.Context, user *openfga.User) ([]dbmodel.GroupEntry, error) - ModelDefaultsForCloud_ func(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag) (jujuparams.ModelDefaultsResult, error) - ModelInfo_ func(ctx context.Context, u *openfga.User, mt names.ModelTag) (*jujuparams.ModelInfo, error) - ModelStatus_ func(ctx context.Context, u *openfga.User, mt names.ModelTag) (*jujuparams.ModelStatus, error) Offer_ func(ctx context.Context, user *openfga.User, offer jimm.AddApplicationOfferParams) error OAuthAuthenticationService_ func() jimm.OAuthAuthenticator ParseTag_ func(ctx context.Context, key string) (*ofganames.Tag, error) PubSubHub_ func() *pubsub.Hub PurgeLogs_ func(ctx context.Context, user *openfga.User, before time.Time) (int64, error) - QueryModelsJq_ func(ctx context.Context, models []dbmodel.Model, jqQuery string) (params.CrossModelQueryResponse, error) RemoveCloud_ func(ctx context.Context, u *openfga.User, ct names.CloudTag) error RemoveCloudFromController_ func(ctx context.Context, u *openfga.User, controllerName string, ct names.CloudTag) error RemoveController_ func(ctx context.Context, user *openfga.User, controllerName string, force bool) error @@ -102,17 +88,12 @@ type JIMM struct { RevokeOfferAccess_ func(ctx context.Context, user *openfga.User, offerURL string, ut names.UserTag, access jujuparams.OfferAccessPermission) (err error) SetControllerConfig_ func(ctx context.Context, u *openfga.User, args jujuparams.ControllerConfigSet) error SetControllerDeprecated_ func(ctx context.Context, user *openfga.User, controllerName string, deprecated bool) error - SetModelDefaults_ func(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag, region string, configs map[string]interface{}) error SetIdentityModelDefaults_ func(ctx context.Context, user *dbmodel.Identity, configs map[string]interface{}) error ToJAASTag_ func(ctx context.Context, tag *ofganames.Tag, resolveUUIDs bool) (string, error) - UnsetModelDefaults_ func(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag, region string, keys []string) error UpdateApplicationOffer_ func(ctx context.Context, controller *dbmodel.Controller, offerUUID string, removed bool) error UpdateCloud_ func(ctx context.Context, u *openfga.User, ct names.CloudTag, cloud jujuparams.Cloud) error UpdateCloudCredential_ func(ctx context.Context, u *openfga.User, args jimm.UpdateCloudCredentialArgs) ([]jujuparams.UpdateCredentialModelResult, error) - UpdateMigratedModel_ func(ctx context.Context, user *openfga.User, modelTag names.ModelTag, targetControllerName string) error UserLogin_ func(ctx context.Context, identityName string) (*openfga.User, error) - ValidateModelUpgrade_ func(ctx context.Context, u *openfga.User, mt names.ModelTag, force bool) error - WatchAllModelSummaries_ func(ctx context.Context, controller *dbmodel.Controller) (_ func() error, err error) } func (j *JIMM) AddAuditLogEntry(ale *dbmodel.AuditLogEntry) { @@ -145,12 +126,6 @@ func (j *JIMM) AddHostedCloud(ctx context.Context, user *openfga.User, tag names } return j.AddHostedCloud_(ctx, user, tag, cloud, force) } -func (j *JIMM) AddModel(ctx context.Context, u *openfga.User, args *jimm.ModelCreateArgs) (_ *jujuparams.ModelInfo, err error) { - if j.AddModel_ == nil { - return nil, errors.E(errors.CodeNotImplemented) - } - return j.AddModel_(ctx, u, args) -} func (j *JIMM) AddServiceAccount(ctx context.Context, u *openfga.User, clientId string) error { if j.AddServiceAccount_ == nil { @@ -178,12 +153,7 @@ func (j *JIMM) AuthorizationClient() *openfga.OFGAClient { } return j.AuthorizationClient_() } -func (j *JIMM) ChangeModelCredential(ctx context.Context, user *openfga.User, modelTag names.ModelTag, cloudCredentialTag names.CloudCredentialTag) error { - if j.ChangeModelCredential_ == nil { - return errors.E(errors.CodeNotImplemented) - } - return j.ChangeModelCredential_(ctx, user, modelTag, cloudCredentialTag) -} + func (j *JIMM) CheckPermission(ctx context.Context, user *openfga.User, cachedPerms map[string]string, desiredPerms map[string]interface{}) (map[string]string, error) { if j.CheckPermission_ == nil { return nil, errors.E(errors.CodeNotImplemented) @@ -196,30 +166,13 @@ func (j *JIMM) DB() *db.Database { } return j.DB_() } -func (j *JIMM) DestroyModel(ctx context.Context, u *openfga.User, mt names.ModelTag, destroyStorage *bool, force *bool, maxWait *time.Duration, timeout *time.Duration) error { - if j.DestroyModel_ == nil { - return errors.E(errors.CodeNotImplemented) - } - return j.DestroyModel_(ctx, u, mt, destroyStorage, force, maxWait, timeout) -} func (j *JIMM) DestroyOffer(ctx context.Context, user *openfga.User, offerURL string, force bool) error { if j.DestroyOffer_ == nil { return errors.E(errors.CodeNotImplemented) } return j.DestroyOffer_(ctx, user, offerURL, force) } -func (j *JIMM) DumpModel(ctx context.Context, u *openfga.User, mt names.ModelTag, simplified bool) (string, error) { - if j.DumpModel_ == nil { - return "", errors.E(errors.CodeNotImplemented) - } - return j.DumpModel_(ctx, u, mt, simplified) -} -func (j *JIMM) DumpModelDB(ctx context.Context, u *openfga.User, mt names.ModelTag) (map[string]interface{}, error) { - if j.DumpModelDB_ == nil { - return nil, errors.E(errors.CodeNotImplemented) - } - return j.DumpModelDB_(ctx, u, mt) -} + func (j *JIMM) EarliestControllerVersion(ctx context.Context) (version.Number, error) { if j.EarliestControllerVersion_ == nil { return version.Number{}, errors.E(errors.CodeNotImplemented) @@ -244,12 +197,7 @@ func (j *JIMM) ForEachCloud(ctx context.Context, user *openfga.User, f func(*dbm } return j.ForEachCloud_(ctx, user, f) } -func (j *JIMM) ForEachModel(ctx context.Context, u *openfga.User, f func(*dbmodel.Model, jujuparams.UserAccessPermission) error) error { - if j.ForEachModel_ == nil { - return errors.E(errors.CodeNotImplemented) - } - return j.ForEachModel_(ctx, u, f) -} + func (j *JIMM) ForEachUserCloud(ctx context.Context, user *openfga.User, f func(*dbmodel.Cloud) error) error { if j.ForEachUserCloud_ == nil { return errors.E(errors.CodeNotImplemented) @@ -262,18 +210,7 @@ func (j *JIMM) ForEachUserCloudCredential(ctx context.Context, u *dbmodel.Identi } return j.ForEachUserCloudCredential_(ctx, u, ct, f) } -func (j *JIMM) ForEachUserModel(ctx context.Context, u *openfga.User, f func(*dbmodel.Model, jujuparams.UserAccessPermission) error) error { - if j.ForEachUserModel_ == nil { - return errors.E(errors.CodeNotImplemented) - } - return j.ForEachUserModel_(ctx, u, f) -} -func (j *JIMM) FullModelStatus(ctx context.Context, user *openfga.User, modelTag names.ModelTag, patterns []string) (*jujuparams.FullStatus, error) { - if j.FullModelStatus_ == nil { - return nil, errors.E(errors.CodeNotImplemented) - } - return j.FullModelStatus_(ctx, user, modelTag, patterns) -} + func (j *JIMM) GetApplicationOffer(ctx context.Context, user *openfga.User, offerURL string) (*jujuparams.ApplicationOfferAdminDetailsV5, error) { if j.GetApplicationOffer_ == nil { return nil, errors.E(errors.CodeNotImplemented) @@ -372,12 +309,6 @@ func (j *JIMM) GrantServiceAccountAccess(ctx context.Context, u *openfga.User, s return j.GrantServiceAccountAccess_(ctx, u, svcAccTag, entities) } -func (j *JIMM) ImportModel(ctx context.Context, user *openfga.User, controllerName string, modelTag names.ModelTag, newOwner string) error { - if j.ImportModel_ == nil { - return errors.E(errors.CodeNotImplemented) - } - return j.ImportModel_(ctx, user, controllerName, modelTag, newOwner) -} func (j *JIMM) InitiateMigration(ctx context.Context, user *openfga.User, spec jujuparams.MigrationSpec) (jujuparams.InitiateMigrationResult, error) { if j.InitiateMigration_ == nil { return jujuparams.InitiateMigrationResult{}, errors.E(errors.CodeNotImplemented) @@ -408,24 +339,7 @@ func (j *JIMM) ListGroups(ctx context.Context, user *openfga.User) ([]dbmodel.Gr } return j.ListGroups_(ctx, user) } -func (j *JIMM) ModelDefaultsForCloud(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag) (jujuparams.ModelDefaultsResult, error) { - if j.ModelDefaultsForCloud_ == nil { - return jujuparams.ModelDefaultsResult{}, errors.E(errors.CodeNotImplemented) - } - return j.ModelDefaultsForCloud_(ctx, user, cloudTag) -} -func (j *JIMM) ModelInfo(ctx context.Context, u *openfga.User, mt names.ModelTag) (*jujuparams.ModelInfo, error) { - if j.ModelInfo_ == nil { - return nil, errors.E(errors.CodeNotImplemented) - } - return j.ModelInfo_(ctx, u, mt) -} -func (j *JIMM) ModelStatus(ctx context.Context, u *openfga.User, mt names.ModelTag) (*jujuparams.ModelStatus, error) { - if j.ModelStatus_ == nil { - return nil, errors.E(errors.CodeNotImplemented) - } - return j.ModelStatus_(ctx, u, mt) -} + func (j *JIMM) Offer(ctx context.Context, user *openfga.User, offer jimm.AddApplicationOfferParams) error { if j.Offer_ == nil { return errors.E(errors.CodeNotImplemented) @@ -456,12 +370,7 @@ func (j *JIMM) PurgeLogs(ctx context.Context, user *openfga.User, before time.Ti } return j.PurgeLogs_(ctx, user, before) } -func (j *JIMM) QueryModelsJq(ctx context.Context, models []dbmodel.Model, jqQuery string) (params.CrossModelQueryResponse, error) { - if j.QueryModelsJq_ == nil { - return params.CrossModelQueryResponse{}, errors.E(errors.CodeNotImplemented) - } - return j.QueryModelsJq_(ctx, models, jqQuery) -} + func (j *JIMM) RemoveCloud(ctx context.Context, u *openfga.User, ct names.CloudTag) error { if j.RemoveCloud_ == nil { return errors.E(errors.CodeNotImplemented) @@ -540,12 +449,7 @@ func (j *JIMM) SetControllerDeprecated(ctx context.Context, user *openfga.User, } return j.SetControllerDeprecated_(ctx, user, controllerName, deprecated) } -func (j *JIMM) SetModelDefaults(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag, region string, configs map[string]interface{}) error { - if j.SetModelDefaults_ == nil { - return errors.E(errors.CodeNotImplemented) - } - return j.SetModelDefaults_(ctx, user, cloudTag, region, configs) -} + func (j *JIMM) SetIdentityModelDefaults(ctx context.Context, user *dbmodel.Identity, configs map[string]interface{}) error { if j.SetIdentityModelDefaults_ == nil { return errors.E(errors.CodeNotImplemented) @@ -558,12 +462,7 @@ func (j *JIMM) ToJAASTag(ctx context.Context, tag *ofganames.Tag, resolveUUIDs b } return j.ToJAASTag_(ctx, tag, resolveUUIDs) } -func (j *JIMM) UnsetModelDefaults(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag, region string, keys []string) error { - if j.UnsetModelDefaults_ == nil { - return errors.E(errors.CodeNotImplemented) - } - return j.UnsetModelDefaults_(ctx, user, cloudTag, region, keys) -} + func (j *JIMM) UpdateApplicationOffer(ctx context.Context, controller *dbmodel.Controller, offerUUID string, removed bool) error { if j.UpdateApplicationOffer_ == nil { return errors.E(errors.CodeNotImplemented) @@ -582,33 +481,10 @@ func (j *JIMM) UpdateCloudCredential(ctx context.Context, u *openfga.User, args } return j.UpdateCloudCredential_(ctx, u, args) } -func (j *JIMM) UpdateMigratedModel(ctx context.Context, user *openfga.User, modelTag names.ModelTag, targetControllerName string) error { - if j.UpdateMigratedModel_ == nil { - return errors.E(errors.CodeNotImplemented) - } - return j.UpdateMigratedModel_(ctx, user, modelTag, targetControllerName) -} + func (j *JIMM) UserLogin(ctx context.Context, identityName string) (*openfga.User, error) { if j.UserLogin_ == nil { return nil, errors.E(errors.CodeNotImplemented) } return j.UserLogin_(ctx, identityName) } -func (j *JIMM) IdentityModelDefaults(ctx context.Context, user *dbmodel.Identity) (map[string]interface{}, error) { - if j.IdentityModelDefaults_ == nil { - return nil, errors.E(errors.CodeNotImplemented) - } - return j.IdentityModelDefaults_(ctx, user) -} -func (j *JIMM) ValidateModelUpgrade(ctx context.Context, u *openfga.User, mt names.ModelTag, force bool) error { - if j.ValidateModelUpgrade_ == nil { - return errors.E(errors.CodeNotImplemented) - } - return j.ValidateModelUpgrade_(ctx, u, mt, force) -} -func (j *JIMM) WatchAllModelSummaries(ctx context.Context, controller *dbmodel.Controller) (_ func() error, err error) { - if j.WatchAllModelSummaries_ == nil { - return nil, errors.E(errors.CodeNotImplemented) - } - return j.WatchAllModelSummaries_(ctx, controller) -} diff --git a/internal/jimmtest/mocks/model.go b/internal/jimmtest/mocks/model.go new file mode 100644 index 000000000..37b991873 --- /dev/null +++ b/internal/jimmtest/mocks/model.go @@ -0,0 +1,167 @@ +// Copyright 2024 Canonical. +package mocks + +import ( + "context" + "time" + + jujuparams "github.com/juju/juju/rpc/params" + "github.com/juju/names/v5" + + "github.com/canonical/jimm/v3/internal/dbmodel" + "github.com/canonical/jimm/v3/internal/errors" + "github.com/canonical/jimm/v3/internal/jimm" + "github.com/canonical/jimm/v3/internal/openfga" + "github.com/canonical/jimm/v3/pkg/api/params" +) + +// ModelManager defines the mock struct used to implement the ModelManger interface. +type ModelManager struct { + AddModel_ func(ctx context.Context, u *openfga.User, args *jimm.ModelCreateArgs) (*jujuparams.ModelInfo, error) + ChangeModelCredential_ func(ctx context.Context, user *openfga.User, modelTag names.ModelTag, cloudCredentialTag names.CloudCredentialTag) error + DestroyModel_ func(ctx context.Context, u *openfga.User, mt names.ModelTag, destroyStorage *bool, force *bool, maxWait *time.Duration, timeout *time.Duration) error + DumpModel_ func(ctx context.Context, u *openfga.User, mt names.ModelTag, simplified bool) (string, error) + DumpModelDB_ func(ctx context.Context, u *openfga.User, mt names.ModelTag) (map[string]interface{}, error) + ForEachModel_ func(ctx context.Context, u *openfga.User, f func(*dbmodel.Model, jujuparams.UserAccessPermission) error) error + ForEachUserModel_ func(ctx context.Context, u *openfga.User, f func(*dbmodel.Model, jujuparams.UserAccessPermission) error) error + FullModelStatus_ func(ctx context.Context, user *openfga.User, modelTag names.ModelTag, patterns []string) (*jujuparams.FullStatus, error) + ImportModel_ func(ctx context.Context, user *openfga.User, controllerName string, modelTag names.ModelTag, newOwner string) error + IdentityModelDefaults_ func(ctx context.Context, user *dbmodel.Identity) (map[string]interface{}, error) + ModelDefaultsForCloud_ func(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag) (jujuparams.ModelDefaultsResult, error) + ModelInfo_ func(ctx context.Context, u *openfga.User, mt names.ModelTag) (*jujuparams.ModelInfo, error) + ModelStatus_ func(ctx context.Context, u *openfga.User, mt names.ModelTag) (*jujuparams.ModelStatus, error) + QueryModelsJq_ func(ctx context.Context, models []dbmodel.Model, jqQuery string) (params.CrossModelQueryResponse, error) + SetModelDefaults_ func(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag, region string, configs map[string]interface{}) error + UnsetModelDefaults_ func(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag, region string, keys []string) error + UpdateMigratedModel_ func(ctx context.Context, user *openfga.User, modelTag names.ModelTag, targetControllerName string) error + ValidateModelUpgrade_ func(ctx context.Context, u *openfga.User, mt names.ModelTag, force bool) error + WatchAllModelSummaries_ func(ctx context.Context, controller *dbmodel.Controller) (_ func() error, err error) +} + +func (j *ModelManager) AddModel(ctx context.Context, u *openfga.User, args *jimm.ModelCreateArgs) (_ *jujuparams.ModelInfo, err error) { + if j.AddModel_ == nil { + return nil, errors.E(errors.CodeNotImplemented) + } + return j.AddModel_(ctx, u, args) +} + +func (j *ModelManager) ChangeModelCredential(ctx context.Context, user *openfga.User, modelTag names.ModelTag, cloudCredentialTag names.CloudCredentialTag) error { + if j.ChangeModelCredential_ == nil { + return errors.E(errors.CodeNotImplemented) + } + return j.ChangeModelCredential_(ctx, user, modelTag, cloudCredentialTag) +} + +func (j *ModelManager) DestroyModel(ctx context.Context, u *openfga.User, mt names.ModelTag, destroyStorage *bool, force *bool, maxWait *time.Duration, timeout *time.Duration) error { + if j.DestroyModel_ == nil { + return errors.E(errors.CodeNotImplemented) + } + return j.DestroyModel_(ctx, u, mt, destroyStorage, force, maxWait, timeout) +} + +func (j *ModelManager) DumpModel(ctx context.Context, u *openfga.User, mt names.ModelTag, simplified bool) (string, error) { + if j.DumpModel_ == nil { + return "", errors.E(errors.CodeNotImplemented) + } + return j.DumpModel_(ctx, u, mt, simplified) +} +func (j *ModelManager) DumpModelDB(ctx context.Context, u *openfga.User, mt names.ModelTag) (map[string]interface{}, error) { + if j.DumpModelDB_ == nil { + return nil, errors.E(errors.CodeNotImplemented) + } + return j.DumpModelDB_(ctx, u, mt) +} + +func (j *ModelManager) ForEachModel(ctx context.Context, u *openfga.User, f func(*dbmodel.Model, jujuparams.UserAccessPermission) error) error { + if j.ForEachModel_ == nil { + return errors.E(errors.CodeNotImplemented) + } + return j.ForEachModel_(ctx, u, f) +} + +func (j *ModelManager) ForEachUserModel(ctx context.Context, u *openfga.User, f func(*dbmodel.Model, jujuparams.UserAccessPermission) error) error { + if j.ForEachUserModel_ == nil { + return errors.E(errors.CodeNotImplemented) + } + return j.ForEachUserModel_(ctx, u, f) +} + +func (j *ModelManager) FullModelStatus(ctx context.Context, user *openfga.User, modelTag names.ModelTag, patterns []string) (*jujuparams.FullStatus, error) { + if j.FullModelStatus_ == nil { + return nil, errors.E(errors.CodeNotImplemented) + } + return j.FullModelStatus_(ctx, user, modelTag, patterns) +} + +func (j *ModelManager) ImportModel(ctx context.Context, user *openfga.User, controllerName string, modelTag names.ModelTag, newOwner string) error { + if j.ImportModel_ == nil { + return errors.E(errors.CodeNotImplemented) + } + return j.ImportModel_(ctx, user, controllerName, modelTag, newOwner) +} + +func (j *ModelManager) ModelDefaultsForCloud(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag) (jujuparams.ModelDefaultsResult, error) { + if j.ModelDefaultsForCloud_ == nil { + return jujuparams.ModelDefaultsResult{}, errors.E(errors.CodeNotImplemented) + } + return j.ModelDefaultsForCloud_(ctx, user, cloudTag) +} + +func (j *ModelManager) ModelInfo(ctx context.Context, u *openfga.User, mt names.ModelTag) (*jujuparams.ModelInfo, error) { + if j.ModelInfo_ == nil { + return nil, errors.E(errors.CodeNotImplemented) + } + return j.ModelInfo_(ctx, u, mt) +} +func (j *ModelManager) ModelStatus(ctx context.Context, u *openfga.User, mt names.ModelTag) (*jujuparams.ModelStatus, error) { + if j.ModelStatus_ == nil { + return nil, errors.E(errors.CodeNotImplemented) + } + return j.ModelStatus_(ctx, u, mt) +} + +func (j *ModelManager) QueryModelsJq(ctx context.Context, models []dbmodel.Model, jqQuery string) (params.CrossModelQueryResponse, error) { + if j.QueryModelsJq_ == nil { + return params.CrossModelQueryResponse{}, errors.E(errors.CodeNotImplemented) + } + return j.QueryModelsJq_(ctx, models, jqQuery) +} + +func (j *ModelManager) SetModelDefaults(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag, region string, configs map[string]interface{}) error { + if j.SetModelDefaults_ == nil { + return errors.E(errors.CodeNotImplemented) + } + return j.SetModelDefaults_(ctx, user, cloudTag, region, configs) +} + +func (j *ModelManager) UnsetModelDefaults(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag, region string, keys []string) error { + if j.UnsetModelDefaults_ == nil { + return errors.E(errors.CodeNotImplemented) + } + return j.UnsetModelDefaults_(ctx, user, cloudTag, region, keys) +} + +func (j *ModelManager) UpdateMigratedModel(ctx context.Context, user *openfga.User, modelTag names.ModelTag, targetControllerName string) error { + if j.UpdateMigratedModel_ == nil { + return errors.E(errors.CodeNotImplemented) + } + return j.UpdateMigratedModel_(ctx, user, modelTag, targetControllerName) +} +func (j *ModelManager) IdentityModelDefaults(ctx context.Context, user *dbmodel.Identity) (map[string]interface{}, error) { + if j.IdentityModelDefaults_ == nil { + return nil, errors.E(errors.CodeNotImplemented) + } + return j.IdentityModelDefaults_(ctx, user) +} +func (j *ModelManager) ValidateModelUpgrade(ctx context.Context, u *openfga.User, mt names.ModelTag, force bool) error { + if j.ValidateModelUpgrade_ == nil { + return errors.E(errors.CodeNotImplemented) + } + return j.ValidateModelUpgrade_(ctx, u, mt, force) +} +func (j *ModelManager) WatchAllModelSummaries(ctx context.Context, controller *dbmodel.Controller) (_ func() error, err error) { + if j.WatchAllModelSummaries_ == nil { + return nil, errors.E(errors.CodeNotImplemented) + } + return j.WatchAllModelSummaries_(ctx, controller) +} diff --git a/internal/jujuapi/controllerroot.go b/internal/jujuapi/controllerroot.go index f0e620d1e..f682edf9a 100644 --- a/internal/jujuapi/controllerroot.go +++ b/internal/jujuapi/controllerroot.go @@ -24,36 +24,28 @@ import ( "github.com/canonical/jimm/v3/internal/openfga" ofganames "github.com/canonical/jimm/v3/internal/openfga/names" "github.com/canonical/jimm/v3/internal/pubsub" - "github.com/canonical/jimm/v3/pkg/api/params" jimmnames "github.com/canonical/jimm/v3/pkg/names" ) type JIMM interface { LoginService + ModelManager AddAuditLogEntry(ale *dbmodel.AuditLogEntry) AddCloudToController(ctx context.Context, user *openfga.User, controllerName string, tag names.CloudTag, cloud jujuparams.Cloud, force bool) error AddController(ctx context.Context, u *openfga.User, ctl *dbmodel.Controller) error AddHostedCloud(ctx context.Context, user *openfga.User, tag names.CloudTag, cloud jujuparams.Cloud, force bool) error AddGroup(ctx context.Context, user *openfga.User, name string) (*dbmodel.GroupEntry, error) - AddModel(ctx context.Context, u *openfga.User, args *jimm.ModelCreateArgs) (_ *jujuparams.ModelInfo, err error) AddServiceAccount(ctx context.Context, u *openfga.User, clientId string) error AuthorizationClient() *openfga.OFGAClient - ChangeModelCredential(ctx context.Context, user *openfga.User, modelTag names.ModelTag, cloudCredentialTag names.CloudCredentialTag) error CopyServiceAccountCredential(ctx context.Context, u *openfga.User, svcAcc *openfga.User, cloudCredentialTag names.CloudCredentialTag) (names.CloudCredentialTag, []jujuparams.UpdateCredentialModelResult, error) DB() *db.Database - DestroyModel(ctx context.Context, u *openfga.User, mt names.ModelTag, destroyStorage *bool, force *bool, maxWait *time.Duration, timeout *time.Duration) error DestroyOffer(ctx context.Context, user *openfga.User, offerURL string, force bool) error - DumpModel(ctx context.Context, u *openfga.User, mt names.ModelTag, simplified bool) (string, error) - DumpModelDB(ctx context.Context, u *openfga.User, mt names.ModelTag) (map[string]interface{}, error) EarliestControllerVersion(ctx context.Context) (version.Number, error) FindApplicationOffers(ctx context.Context, user *openfga.User, filters ...jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetailsV5, error) FindAuditEvents(ctx context.Context, user *openfga.User, filter db.AuditLogFilter) ([]dbmodel.AuditLogEntry, error) ForEachCloud(ctx context.Context, user *openfga.User, f func(*dbmodel.Cloud) error) error - ForEachModel(ctx context.Context, u *openfga.User, f func(*dbmodel.Model, jujuparams.UserAccessPermission) error) error ForEachUserCloud(ctx context.Context, user *openfga.User, f func(*dbmodel.Cloud) error) error ForEachUserCloudCredential(ctx context.Context, u *dbmodel.Identity, ct names.CloudTag, f func(cred *dbmodel.CloudCredential) error) error - ForEachUserModel(ctx context.Context, u *openfga.User, f func(*dbmodel.Model, jujuparams.UserAccessPermission) error) error - FullModelStatus(ctx context.Context, user *openfga.User, modelTag names.ModelTag, patterns []string) (*jujuparams.FullStatus, error) GetApplicationOffer(ctx context.Context, user *openfga.User, offerURL string) (*jujuparams.ApplicationOfferAdminDetailsV5, error) GetApplicationOfferConsumeDetails(ctx context.Context, user *openfga.User, details *jujuparams.ConsumeOfferDetails, v bakery.Version) error GetCloud(ctx context.Context, u *openfga.User, tag names.CloudTag) (dbmodel.Cloud, error) @@ -70,20 +62,14 @@ type JIMM interface { GrantModelAccess(ctx context.Context, user *openfga.User, mt names.ModelTag, ut names.UserTag, access jujuparams.UserAccessPermission) error GrantOfferAccess(ctx context.Context, u *openfga.User, offerURL string, ut names.UserTag, access jujuparams.OfferAccessPermission) error GrantServiceAccountAccess(ctx context.Context, u *openfga.User, svcAccTag jimmnames.ServiceAccountTag, tags []string) error - IdentityModelDefaults(ctx context.Context, user *dbmodel.Identity) (map[string]interface{}, error) - ImportModel(ctx context.Context, user *openfga.User, controllerName string, modelTag names.ModelTag, newOwner string) error InitiateInternalMigration(ctx context.Context, user *openfga.User, modelTag names.ModelTag, targetController string) (jujuparams.InitiateMigrationResult, error) InitiateMigration(ctx context.Context, user *openfga.User, spec jujuparams.MigrationSpec) (jujuparams.InitiateMigrationResult, error) ListApplicationOffers(ctx context.Context, user *openfga.User, filters ...jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetailsV5, error) ListGroups(ctx context.Context, user *openfga.User) ([]dbmodel.GroupEntry, error) - ModelDefaultsForCloud(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag) (jujuparams.ModelDefaultsResult, error) - ModelInfo(ctx context.Context, u *openfga.User, mt names.ModelTag) (*jujuparams.ModelInfo, error) - ModelStatus(ctx context.Context, u *openfga.User, mt names.ModelTag) (*jujuparams.ModelStatus, error) Offer(ctx context.Context, user *openfga.User, offer jimm.AddApplicationOfferParams) error ParseTag(ctx context.Context, key string) (*ofganames.Tag, error) PubSubHub() *pubsub.Hub PurgeLogs(ctx context.Context, user *openfga.User, before time.Time) (int64, error) - QueryModelsJq(ctx context.Context, models []dbmodel.Model, jqQuery string) (params.CrossModelQueryResponse, error) RenameGroup(ctx context.Context, user *openfga.User, oldName, newName string) error RemoveCloud(ctx context.Context, u *openfga.User, ct names.CloudTag) error RemoveCloudFromController(ctx context.Context, u *openfga.User, controllerName string, ct names.CloudTag) error @@ -97,16 +83,11 @@ type JIMM interface { RevokeOfferAccess(ctx context.Context, user *openfga.User, offerURL string, ut names.UserTag, access jujuparams.OfferAccessPermission) (err error) SetControllerConfig(ctx context.Context, u *openfga.User, args jujuparams.ControllerConfigSet) error SetControllerDeprecated(ctx context.Context, user *openfga.User, controllerName string, deprecated bool) error - SetModelDefaults(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag, region string, configs map[string]interface{}) error ToJAASTag(ctx context.Context, tag *ofganames.Tag, resolveUUIDs bool) (string, error) - UnsetModelDefaults(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag, region string, keys []string) error UpdateApplicationOffer(ctx context.Context, controller *dbmodel.Controller, offerUUID string, removed bool) error UpdateCloud(ctx context.Context, u *openfga.User, ct names.CloudTag, cloud jujuparams.Cloud) error UpdateCloudCredential(ctx context.Context, u *openfga.User, args jimm.UpdateCloudCredentialArgs) ([]jujuparams.UpdateCredentialModelResult, error) - UpdateMigratedModel(ctx context.Context, user *openfga.User, modelTag names.ModelTag, targetControllerName string) error UserLogin(ctx context.Context, identityName string) (*openfga.User, error) - ValidateModelUpgrade(ctx context.Context, u *openfga.User, mt names.ModelTag, force bool) error - WatchAllModelSummaries(ctx context.Context, controller *dbmodel.Controller) (_ func() error, err error) } // controllerRoot is the root for endpoints served on controller connections. diff --git a/internal/jujuapi/modelmanager.go b/internal/jujuapi/modelmanager.go index a79d19790..fec14a500 100644 --- a/internal/jujuapi/modelmanager.go +++ b/internal/jujuapi/modelmanager.go @@ -5,6 +5,7 @@ package jujuapi import ( "context" "fmt" + "time" jujuparams "github.com/juju/juju/rpc/params" "github.com/juju/names/v5" @@ -13,7 +14,9 @@ import ( "github.com/canonical/jimm/v3/internal/errors" "github.com/canonical/jimm/v3/internal/jimm" "github.com/canonical/jimm/v3/internal/jujuapi/rpc" + "github.com/canonical/jimm/v3/internal/openfga" "github.com/canonical/jimm/v3/internal/servermon" + "github.com/canonical/jimm/v3/pkg/api/params" ) func init() { @@ -52,6 +55,29 @@ func init() { } } +// ModelManager defines the model related operations that JIMM can perform. +type ModelManager interface { + AddModel(ctx context.Context, u *openfga.User, args *jimm.ModelCreateArgs) (_ *jujuparams.ModelInfo, err error) + ChangeModelCredential(ctx context.Context, user *openfga.User, modelTag names.ModelTag, cloudCredentialTag names.CloudCredentialTag) error + DestroyModel(ctx context.Context, u *openfga.User, mt names.ModelTag, destroyStorage *bool, force *bool, maxWait *time.Duration, timeout *time.Duration) error + DumpModel(ctx context.Context, u *openfga.User, mt names.ModelTag, simplified bool) (string, error) + DumpModelDB(ctx context.Context, u *openfga.User, mt names.ModelTag) (map[string]interface{}, error) + ForEachModel(ctx context.Context, u *openfga.User, f func(*dbmodel.Model, jujuparams.UserAccessPermission) error) error + ForEachUserModel(ctx context.Context, u *openfga.User, f func(*dbmodel.Model, jujuparams.UserAccessPermission) error) error + FullModelStatus(ctx context.Context, user *openfga.User, modelTag names.ModelTag, patterns []string) (*jujuparams.FullStatus, error) + IdentityModelDefaults(ctx context.Context, user *dbmodel.Identity) (map[string]interface{}, error) + ImportModel(ctx context.Context, user *openfga.User, controllerName string, modelTag names.ModelTag, newOwner string) error + ModelDefaultsForCloud(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag) (jujuparams.ModelDefaultsResult, error) + ModelInfo(ctx context.Context, u *openfga.User, mt names.ModelTag) (*jujuparams.ModelInfo, error) + ModelStatus(ctx context.Context, u *openfga.User, mt names.ModelTag) (*jujuparams.ModelStatus, error) + QueryModelsJq(ctx context.Context, models []dbmodel.Model, jqQuery string) (params.CrossModelQueryResponse, error) + SetModelDefaults(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag, region string, configs map[string]interface{}) error + UnsetModelDefaults(ctx context.Context, user *dbmodel.Identity, cloudTag names.CloudTag, region string, keys []string) error + UpdateMigratedModel(ctx context.Context, user *openfga.User, modelTag names.ModelTag, targetControllerName string) error + ValidateModelUpgrade(ctx context.Context, u *openfga.User, mt names.ModelTag, force bool) error + WatchAllModelSummaries(ctx context.Context, controller *dbmodel.Controller) (_ func() error, err error) +} + // DumpModels implements the DumpModels method of the modelmanager (version // 3 onwards) facade. The model dump is passed back as-is from the // controller without any changes from JIMM. diff --git a/internal/jujuclient/applicationoffers.go b/internal/jujuclient/applicationoffers.go index 335764453..94e2e7b26 100644 --- a/internal/jujuclient/applicationoffers.go +++ b/internal/jujuclient/applicationoffers.go @@ -209,13 +209,15 @@ func (c Connection) GetApplicationOfferConsumeDetails(ctx context.Context, user OfferURLs: []string{info.Offer.OfferURL}, BakeryVersion: v, }, - UserTag: user.String(), + // Do not include a user in the args, Juju will opt to use the user authenticated in the connection. + // There is a bug where setting the user tag does not behave as expected. + UserTag: "", } resp := jujuparams.ConsumeOfferDetailsResults{ Results: make([]jujuparams.ConsumeOfferDetailsResult, 1), } - err := c.CallHighestFacadeVersion(ctx, "ApplicationOffers", []int{4, 3}, "", "GetConsumeDetails", &args, &resp) + err := c.CallHighestFacadeVersion(ctx, "ApplicationOffers", []int{5, 4}, "", "GetConsumeDetails", &args, &resp) if err != nil { return errors.E(op, jujuerrors.Cause(err)) } diff --git a/internal/rpc/proxy.go b/internal/rpc/proxy.go index 142623a07..60d7db1ce 100644 --- a/internal/rpc/proxy.go +++ b/internal/rpc/proxy.go @@ -515,7 +515,9 @@ func checkPermissionsRequired(ctx context.Context, msg *message) (map[string]any // Check for errors that may be a result of a bulk request. for _, e := range er.Results { - zapctx.Debug(ctx, "received error", zap.Any("error", e)) + if e.Error != nil { + zapctx.Debug(ctx, "received error", zap.Any("error", e.Error)) + } if e.Error != nil && e.Error.Code == accessRequiredErrorCode { for k, v := range e.Error.Info { accessLevel, ok := v.(string) diff --git a/local/jimm/setup-service-account.sh b/local/jimm/setup-service-account.sh new file mode 100755 index 000000000..b18229a0c --- /dev/null +++ b/local/jimm/setup-service-account.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +# This script is used to setup a service account by adding a set of cloud-credentials. +# Default values below assume a lxd controller is added to JIMM. + +set -eux + +SERVICE_ACCOUNT_ID="${SERVICE_ACCOUNT_ID:-test-client-id}" +CLOUD="${CLOUD:-localhost}" +CREDENTIAL_NAME="${CREDENTIAL_NAME:-localhost}" + +juju add-service-account "$SERVICE_ACCOUNT_ID" +juju update-service-account-credential "$SERVICE_ACCOUNT_ID" "$CLOUD" "$CREDENTIAL_NAME"