From 39b16a0cb6c17e9594abdcb897890c5b1b7c0cae Mon Sep 17 00:00:00 2001 From: pkulik0 <70904851+pkulik0@users.noreply.github.com> Date: Fri, 2 Aug 2024 17:59:31 +0200 Subject: [PATCH 01/30] Remove PAT secret from update-sdk workflow --- .github/workflows/update-sdk.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/update-sdk.yaml b/.github/workflows/update-sdk.yaml index 9152e826b..bb7eb45fb 100644 --- a/.github/workflows/update-sdk.yaml +++ b/.github/workflows/update-sdk.yaml @@ -31,7 +31,6 @@ jobs: repository: ${{ github.event.inputs.sdk-repo }} ref: ${{ github.event.inputs.sdk-version }} path: ./sdk - token: ${{ secrets.PAT }} - name: Setup Go uses: actions/setup-go@v5 @@ -59,7 +58,6 @@ jobs: - name: Create Pull Request uses: peter-evans/create-pull-request@v6 with: - token: ${{ secrets.PAT }} path: ./sdk branch: update-sdk-${{ github.run_number }} title: Update SDK ${{ github.event.inputs.sdk-version }} From 8adb875e9bbc3ee1888bf9272b5d5961665cc9cd Mon Sep 17 00:00:00 2001 From: pkulik0 <70904851+pkulik0@users.noreply.github.com> Date: Fri, 2 Aug 2024 18:42:42 +0200 Subject: [PATCH 02/30] Fixes to removal of old files, add PAT usage in PR step --- .github/workflows/update-sdk.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/update-sdk.yaml b/.github/workflows/update-sdk.yaml index bb7eb45fb..966f2b44d 100644 --- a/.github/workflows/update-sdk.yaml +++ b/.github/workflows/update-sdk.yaml @@ -45,7 +45,8 @@ jobs: SDK_VERSION: ${{ github.event.inputs.sdk-version }} run: | # Remove all in case some files are removed - rm -rf .[!.]* * + shopt -s nullglob + rm -rf .[!.git]* * cp -r $PROJECT/pkg/.[^.]* $PROJECT/pkg/* $PROJECT/go.mod . # Replace module references @@ -58,6 +59,7 @@ jobs: - name: Create Pull Request uses: peter-evans/create-pull-request@v6 with: + token: ${{ secrets.PAT }} path: ./sdk branch: update-sdk-${{ github.run_number }} title: Update SDK ${{ github.event.inputs.sdk-version }} From b5f7ba0ded3ba09b77f75eed088fc2bda2e04815 Mon Sep 17 00:00:00 2001 From: pkulik0 <70904851+pkulik0@users.noreply.github.com> Date: Fri, 2 Aug 2024 18:53:23 +0200 Subject: [PATCH 03/30] Remove dependency on `internal` from `pkg/names` --- pkg/names/service_account.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/names/service_account.go b/pkg/names/service_account.go index ec94e7819..8e66ce4be 100644 --- a/pkg/names/service_account.go +++ b/pkg/names/service_account.go @@ -6,7 +6,6 @@ import ( "fmt" "strings" - "github.com/canonical/jimm/v3/internal/errors" "github.com/juju/names/v5" ) @@ -81,7 +80,7 @@ func EnsureValidServiceAccountId(id string) (string, error) { } if !IsValidServiceAccountId(id) { - return "", errors.E(errors.CodeBadRequest, "invalid client ID") + return "", fmt.Errorf("invalid client id %q", id) } return id, nil } From f406c024f71f9dcdfdc27d5a60fda8bfa429001d Mon Sep 17 00:00:00 2001 From: pkulik0 <70904851+pkulik0@users.noreply.github.com> Date: Fri, 2 Aug 2024 19:08:40 +0200 Subject: [PATCH 04/30] Add `v3` in module name in `update-sdk.yaml` --- .github/workflows/update-sdk.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-sdk.yaml b/.github/workflows/update-sdk.yaml index 966f2b44d..a5a126a06 100644 --- a/.github/workflows/update-sdk.yaml +++ b/.github/workflows/update-sdk.yaml @@ -50,7 +50,7 @@ jobs: cp -r $PROJECT/pkg/.[^.]* $PROJECT/pkg/* $PROJECT/go.mod . # Replace module references - find . -type f -exec sed -i "s|github.com/canonical/jimm/pkg|github.com/$SDK_REPO/$SDK_VERSION|" {} + + find . -type f -exec sed -i "s|github.com/canonical/jimm/v3/pkg|github.com/$SDK_REPO/$SDK_VERSION|" {} + sed -i "s|module .*|module github.com/$SDK_REPO/$SDK_VERSION|" go.mod # Needed to remove unused dependencies From 5ffa86ac09261824ca7d2857f6e0a60b2b114314 Mon Sep 17 00:00:00 2001 From: pkulik0 <70904851+pkulik0@users.noreply.github.com> Date: Mon, 5 Aug 2024 11:24:40 +0000 Subject: [PATCH 05/30] Make error msg in pkg/names match the expected one --- pkg/names/service_account.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/names/service_account.go b/pkg/names/service_account.go index 8e66ce4be..f7cfe1a9a 100644 --- a/pkg/names/service_account.go +++ b/pkg/names/service_account.go @@ -5,6 +5,7 @@ package names import ( "fmt" "strings" + "errors" "github.com/juju/names/v5" ) @@ -80,7 +81,7 @@ func EnsureValidServiceAccountId(id string) (string, error) { } if !IsValidServiceAccountId(id) { - return "", fmt.Errorf("invalid client id %q", id) + return "", errors.New("invalid client ID") } return id, nil } From 6ab5626eed1e18c115fdf39c10471f83874b19aa Mon Sep 17 00:00:00 2001 From: pkulik0 <70904851+pkulik0@users.noreply.github.com> Date: Mon, 5 Aug 2024 15:06:44 +0000 Subject: [PATCH 06/30] Create `ErrInvalidClientID` global var --- pkg/names/service_account.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pkg/names/service_account.go b/pkg/names/service_account.go index f7cfe1a9a..009728f4a 100644 --- a/pkg/names/service_account.go +++ b/pkg/names/service_account.go @@ -3,9 +3,9 @@ package names import ( + "errors" "fmt" "strings" - "errors" "github.com/juju/names/v5" ) @@ -20,6 +20,11 @@ const ( ServiceAccountDomain = "serviceaccount" ) +var ( + // ErrInvalidClientID indicates an invalid client ID error. + ErrInvalidClientID = errors.New("invalid client ID") +) + // Service accounts are an OIDC/OAuth concept which allows for machine<->machine communication. // Service accounts are identified by their client ID. @@ -81,7 +86,7 @@ func EnsureValidServiceAccountId(id string) (string, error) { } if !IsValidServiceAccountId(id) { - return "", errors.New("invalid client ID") + return "", ErrInvalidClientID } return id, nil } From f9435f22db1738dfb3dea3f1c8792d3821be2534 Mon Sep 17 00:00:00 2001 From: pkulik0 <70904851+pkulik0@users.noreply.github.com> Date: Tue, 6 Aug 2024 11:27:24 +0000 Subject: [PATCH 07/30] Change secret name to `JIMM_GO_SDK_PAT` --- .github/workflows/update-sdk.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-sdk.yaml b/.github/workflows/update-sdk.yaml index a5a126a06..9c55dd050 100644 --- a/.github/workflows/update-sdk.yaml +++ b/.github/workflows/update-sdk.yaml @@ -59,7 +59,7 @@ jobs: - name: Create Pull Request uses: peter-evans/create-pull-request@v6 with: - token: ${{ secrets.PAT }} + token: ${{ secrets.JIMM_GO_SDK_PAT }} path: ./sdk branch: update-sdk-${{ github.run_number }} title: Update SDK ${{ github.event.inputs.sdk-version }} From 3176bdea9e23e669ee0109c56076ec3e313c571b Mon Sep 17 00:00:00 2001 From: SimoneDutto Date: Thu, 8 Aug 2024 16:08:20 +0200 Subject: [PATCH 08/30] add go cache to v3 branch to make it available to child branch (#1309) * add go cache to v3 branch to make it available to child branch * add new line * fix pr comments --- .github/workflows/cache.yaml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/cache.yaml diff --git a/.github/workflows/cache.yaml b/.github/workflows/cache.yaml new file mode 100644 index 000000000..55e207999 --- /dev/null +++ b/.github/workflows/cache.yaml @@ -0,0 +1,22 @@ +name: Cache on default branch +on: push + +jobs: + go_cache: + name: Install And Cache Go Dependencies and Build Artifacts + runs-on: ubuntu-22.04 + timeout-minutes: 15 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-tags: true + fetch-depth: 0 + + - name: Setup Go + uses: actions/setup-go@v4 + with: + go-version-file: 'go.mod' + + - name: Build + run: go build ./... From 8706de4abff79846e3681a1f7405e41a61c4f76c Mon Sep 17 00:00:00 2001 From: SimoneDutto Date: Thu, 8 Aug 2024 17:36:20 +0200 Subject: [PATCH 09/30] Fix/mod cache action (#1310) * fix go build and enable go cache action on v3 and feature branches --- .github/workflows/cache.yaml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cache.yaml b/.github/workflows/cache.yaml index 55e207999..623fe65a5 100644 --- a/.github/workflows/cache.yaml +++ b/.github/workflows/cache.yaml @@ -1,5 +1,9 @@ name: Cache on default branch -on: push +on: + push: + branches: + - v3 + - "feature*" jobs: go_cache: @@ -18,5 +22,11 @@ jobs: with: go-version-file: 'go.mod' + - name: Add volume files + run: | + touch ./local/vault/approle.json + touch ./local/vault/roleid.txt + touch ./local/vault/vault.env + - name: Build run: go build ./... From 36c15a7c1a909d5109f63f26e444bd0bcdfdfe37 Mon Sep 17 00:00:00 2001 From: Alexander <42068202+ale8k@users.noreply.github.com> Date: Fri, 9 Aug 2024 12:59:51 +0100 Subject: [PATCH 10/30] Initial pr for golint (#1304) * Initial pr for golint * more linting fixes * lint fixes for tests, gocognit and gocritic * goheader, gci and imprtas linters * jujuparams lint * missing uuids * All linting issues solved * fix errs import * test fixes --------- Co-authored-by: Ales Stimec --- .golangci.yaml | 90 +++++++++++++++++++ .vscode/settings.json | 7 ++ cmd/jaas/cmd/addserviceaccount.go | 2 +- cmd/jaas/cmd/addserviceaccount_test.go | 2 +- cmd/jaas/cmd/export_test.go | 2 +- cmd/jaas/cmd/grant.go | 20 ++++- cmd/jaas/cmd/grant_test.go | 2 +- cmd/jaas/cmd/listserviceaccountcredentials.go | 3 +- .../cmd/listserviceaccountcredentials_test.go | 2 +- cmd/jaas/cmd/package_test.go | 2 +- cmd/jaas/cmd/updatecredentials.go | 5 +- cmd/jaas/cmd/updatecredentials_test.go | 6 +- cmd/jaas/main.go | 5 +- cmd/jimmctl/cmd/addcloudtocontroller.go | 6 +- cmd/jimmctl/cmd/addcloudtocontroller_test.go | 11 +-- cmd/jimmctl/cmd/addcontroller.go | 2 +- cmd/jimmctl/cmd/addcontroller_test.go | 7 +- cmd/jimmctl/cmd/auth.go | 2 +- cmd/jimmctl/cmd/controllerinfo.go | 4 +- cmd/jimmctl/cmd/controllerinfo_test.go | 2 +- cmd/jimmctl/cmd/crossmodelquery.go | 2 +- cmd/jimmctl/cmd/crossmodelquery_test.go | 9 +- cmd/jimmctl/cmd/export_test.go | 2 +- cmd/jimmctl/cmd/grantauditlogaccess.go | 4 +- cmd/jimmctl/cmd/grantauditlogaccess_test.go | 5 +- cmd/jimmctl/cmd/group.go | 7 +- cmd/jimmctl/cmd/group_test.go | 2 +- cmd/jimmctl/cmd/importcloudcredentials.go | 3 +- .../cmd/importcloudcredentials_test.go | 5 +- cmd/jimmctl/cmd/importmodel.go | 2 +- cmd/jimmctl/cmd/importmodel_test.go | 2 +- cmd/jimmctl/cmd/listauditevents.go | 2 +- cmd/jimmctl/cmd/listauditevents_test.go | 2 +- cmd/jimmctl/cmd/listcontrollers.go | 2 +- cmd/jimmctl/cmd/listcontrollers_test.go | 2 +- cmd/jimmctl/cmd/migratemodel.go | 2 +- cmd/jimmctl/cmd/migratemodel_test.go | 2 +- cmd/jimmctl/cmd/modelstatus.go | 2 +- cmd/jimmctl/cmd/modelstatus_test.go | 2 +- cmd/jimmctl/cmd/package_test.go | 2 +- cmd/jimmctl/cmd/purge_logs.go | 1 + cmd/jimmctl/cmd/purge_logs_test.go | 3 +- cmd/jimmctl/cmd/relation.go | 16 ++-- cmd/jimmctl/cmd/relation_test.go | 4 +- cmd/jimmctl/cmd/removecloudfromcontroller.go | 2 +- .../cmd/removecloudfromcontroller_test.go | 3 +- cmd/jimmctl/cmd/removecontroller.go | 2 +- cmd/jimmctl/cmd/removecontroller_test.go | 2 +- cmd/jimmctl/cmd/revokeauditlogaccess.go | 4 +- cmd/jimmctl/cmd/revokeauditlogaccess_test.go | 5 +- cmd/jimmctl/cmd/setcontrollerdeprecated.go | 4 +- .../cmd/setcontrollerdeprecated_test.go | 2 +- cmd/jimmctl/cmd/updatemigratedmodel.go | 2 +- cmd/jimmctl/cmd/updatemigratedmodel_test.go | 2 +- cmd/jimmctl/main.go | 2 +- cmd/jimmsrv/main.go | 14 ++- cmd/jimmsrv/service/export_test.go | 2 +- cmd/jimmsrv/service/service.go | 2 +- cmd/jimmsrv/service/service_test.go | 6 +- doc/golangci-lint.md | 10 +++ internal/auth/jujuauth.go | 2 +- internal/auth/oauth2.go | 2 +- internal/auth/oauth2_test.go | 25 +++--- internal/cloudcred/cloudcred.go | 2 +- internal/cloudcred/cloudcred_test.go | 5 +- internal/cmdtest/jimmsuite.go | 6 +- internal/dashboard/dashboard.go | 3 +- internal/dashboard/dashboard_test.go | 8 +- internal/db/applicationoffer.go | 24 +++-- internal/db/applicationoffer_test.go | 2 +- internal/db/audit.go | 2 +- internal/db/auditlog_test.go | 2 +- internal/db/cloud.go | 2 +- internal/db/cloud_test.go | 3 +- internal/db/cloudcredential.go | 2 +- internal/db/cloudcredential_test.go | 3 +- internal/db/clouddefaults.go | 2 +- internal/db/clouddefaults_test.go | 2 +- internal/db/controller.go | 5 +- internal/db/controller_test.go | 2 +- internal/db/db.go | 2 +- internal/db/db_test.go | 2 +- internal/db/errors.go | 6 +- internal/db/export_test.go | 2 +- internal/db/group.go | 6 +- internal/db/group_test.go | 2 +- internal/db/identity.go | 2 +- internal/db/identity_test.go | 3 +- internal/db/identitymodeldefaults.go | 2 +- internal/db/identitymodeldefaults_test.go | 5 +- internal/db/model.go | 15 ++-- internal/db/model_test.go | 2 +- internal/db/pgx_test.go | 2 +- internal/db/rootkeys.go | 2 +- internal/db/rootkeys_test.go | 5 +- internal/db/secrets.go | 22 ++--- internal/db/secrets_test.go | 2 +- internal/dbmodel/applicationoffer.go | 2 +- internal/dbmodel/applicationoffer_test.go | 2 +- internal/dbmodel/audit.go | 2 +- internal/dbmodel/audit_test.go | 2 +- internal/dbmodel/cloud.go | 2 +- internal/dbmodel/cloud_test.go | 2 +- internal/dbmodel/cloudcredential.go | 2 +- internal/dbmodel/cloudcredential_test.go | 2 +- internal/dbmodel/clouddefaults.go | 2 +- internal/dbmodel/controller.go | 9 +- internal/dbmodel/controller_test.go | 2 +- internal/dbmodel/gorm_test.go | 7 +- internal/dbmodel/group.go | 2 +- internal/dbmodel/group_test.go | 2 +- internal/dbmodel/identity.go | 4 +- internal/dbmodel/identity_test.go | 2 +- internal/dbmodel/identitymodeldefaults.go | 2 +- internal/dbmodel/model.go | 2 +- internal/dbmodel/model_test.go | 2 +- internal/dbmodel/openfga_stores.go | 1 + internal/dbmodel/rootkey.go | 2 +- internal/dbmodel/secrets.go | 1 + internal/dbmodel/sql.go | 2 +- internal/dbmodel/types.go | 2 +- internal/dbmodel/types_test.go | 2 +- internal/dbmodel/version.go | 2 +- internal/dbmodel/version_test.go | 2 +- internal/debugapi/api.go | 1 + internal/debugapi/api_test.go | 4 + internal/discharger/discharger.go | 8 +- internal/errors/errors.go | 2 +- internal/errors/errors_test.go | 6 +- internal/jimm/access.go | 4 +- internal/jimm/access_test.go | 70 +++------------ internal/jimm/admin.go | 2 +- internal/jimm/applicationoffer.go | 13 ++- internal/jimm/applicationoffer_test.go | 20 +++-- internal/jimm/audit_log.go | 4 +- internal/jimm/audit_log_test.go | 2 +- internal/jimm/cache.go | 2 +- internal/jimm/cache_test.go | 2 +- internal/jimm/cloud.go | 25 +----- internal/jimm/cloud_test.go | 4 +- internal/jimm/cloudcredential.go | 2 +- internal/jimm/cloudcredential_test.go | 4 +- internal/jimm/clouddefaults.go | 4 +- internal/jimm/clouddefaults_test.go | 7 +- internal/jimm/controller.go | 47 ++-------- internal/jimm/controller_test.go | 18 ++-- internal/jimm/credentials/credentials.go | 2 +- internal/jimm/export_test.go | 2 +- internal/jimm/identitymodeldefaults.go | 2 +- internal/jimm/identitymodeldefaults_test.go | 8 +- internal/jimm/jimm.go | 2 +- internal/jimm/jimm_test.go | 2 +- internal/jimm/model.go | 7 +- internal/jimm/model_status_parser.go | 11 +-- internal/jimm/model_status_parser_test.go | 1 + internal/jimm/model_test.go | 5 +- internal/jimm/modelsummary.go | 2 +- internal/jimm/modelsummary_test.go | 2 +- internal/jimm/monitoring.go | 6 +- internal/jimm/purge_logs.go | 7 +- internal/jimm/runner.go | 2 +- internal/jimm/runner_internal_test.go | 2 +- internal/jimm/service_account.go | 11 +-- internal/jimm/service_account_test.go | 2 +- internal/jimm/user.go | 2 +- internal/jimm/user_test.go | 4 +- internal/jimm/watcher.go | 21 +++-- internal/jimm/watcher_test.go | 2 +- internal/jimmhttp/auth_handler.go | 6 +- internal/jimmhttp/auth_handler_test.go | 9 +- internal/jimmhttp/handler.go | 1 + internal/jimmhttp/http.go | 2 +- internal/jimmhttp/http_test.go | 3 +- internal/jimmhttp/websocket.go | 2 +- internal/jimmhttp/websocket_test.go | 14 +-- internal/jimmjwx/export_test.go | 1 + internal/jimmjwx/jimmjwx.go | 2 +- internal/jimmjwx/jwks.go | 10 ++- internal/jimmjwx/jwks_test.go | 1 + internal/jimmjwx/jwt.go | 20 +++-- internal/jimmjwx/jwt_test.go | 7 +- internal/jimmjwx/utils_test.go | 6 +- internal/jimmtest/api.go | 2 +- internal/jimmtest/auth.go | 9 +- internal/jimmtest/cmp.go | 2 +- internal/jimmtest/env.go | 17 ++-- internal/jimmtest/gorm.go | 12 ++- internal/jimmtest/jimm.go | 4 +- internal/jimmtest/jimm_mock.go | 2 +- internal/jimmtest/keycloak.go | 8 +- internal/jimmtest/logging.go | 12 +-- internal/jimmtest/openfga.go | 12 ++- internal/jimmtest/store.go | 3 +- internal/jimmtest/suite.go | 2 +- internal/jimmtest/vault.go | 5 +- internal/jujuapi/access_control.go | 29 +----- internal/jujuapi/access_control_test.go | 43 ++++----- internal/jujuapi/admin.go | 2 +- internal/jujuapi/admin_test.go | 7 +- internal/jujuapi/api.go | 2 +- internal/jujuapi/api_test.go | 4 +- internal/jujuapi/applicationoffers.go | 2 +- internal/jujuapi/applicationoffers_test.go | 6 +- internal/jujuapi/cloud.go | 8 +- internal/jujuapi/cloud_test.go | 2 +- internal/jujuapi/controller.go | 6 +- internal/jujuapi/controller_test.go | 2 +- internal/jujuapi/controllerroot.go | 2 +- internal/jujuapi/controllerroot_test.go | 2 +- internal/jujuapi/export_test.go | 5 +- internal/jujuapi/jimm.go | 2 +- internal/jujuapi/jimm_test.go | 2 +- internal/jujuapi/modelmanager.go | 8 +- internal/jujuapi/modelmanager_test.go | 16 ++-- internal/jujuapi/modelsummarywatcher.go | 5 +- internal/jujuapi/modelsummarywatcher_test.go | 8 +- internal/jujuapi/package_test.go | 2 +- internal/jujuapi/pinger.go | 2 +- internal/jujuapi/pinger_internal_test.go | 2 +- internal/jujuapi/rpc/method.go | 10 +-- internal/jujuapi/rpc/method_test.go | 2 +- internal/jujuapi/rpc/root.go | 2 +- internal/jujuapi/rpc/root_test.go | 2 +- internal/jujuapi/service_account.go | 2 +- internal/jujuapi/service_account_test.go | 25 ++++-- internal/jujuapi/usermanager.go | 6 +- internal/jujuapi/usermanager_test.go | 2 +- internal/jujuapi/websocket.go | 7 +- internal/jujuapi/websocket_test.go | 2 +- internal/jujuclient/allwatcher.go | 2 +- internal/jujuclient/allwatcher_test.go | 2 +- internal/jujuclient/applicationoffers.go | 2 +- internal/jujuclient/applicationoffers_test.go | 2 +- internal/jujuclient/client.go | 2 +- internal/jujuclient/client_test.go | 2 +- internal/jujuclient/cloud.go | 4 +- internal/jujuclient/cloud_test.go | 3 +- internal/jujuclient/dial.go | 6 +- internal/jujuclient/dial_test.go | 10 +-- internal/jujuclient/modelmanager.go | 2 +- internal/jujuclient/modelmanager_test.go | 2 +- internal/jujuclient/modelsummarywatcher.go | 2 +- .../jujuclient/modelsummarywatcher_test.go | 2 +- internal/jujuclient/modelwatcher.go | 2 +- internal/jujuclient/modelwatcher_test.go | 2 +- internal/jujuclient/package_test.go | 2 +- internal/jujuclient/ping.go | 2 +- internal/jujuclient/ping_test.go | 5 +- internal/jujuclient/storage.go | 2 +- internal/jujuclient/storage_test.go | 2 +- internal/kubetest/kubetest.go | 6 +- internal/logger/logger.go | 2 +- internal/openfga/export_test.go | 2 +- internal/openfga/names/export_test.go | 2 +- internal/openfga/names/names.go | 8 +- internal/openfga/names/names_test.go | 6 +- internal/openfga/openfga.go | 6 +- internal/openfga/openfga_test.go | 7 +- internal/openfga/user.go | 4 +- internal/openfga/user_test.go | 2 +- internal/pubsub/hub.go | 5 +- internal/pubsub/hub_test.go | 2 +- internal/rpc/client.go | 53 ++++++----- internal/rpc/client_test.go | 28 +++--- internal/rpc/dial.go | 3 +- internal/rpc/export_test.go | 1 + internal/rpc/proxy.go | 33 +++++-- internal/rpc/proxy_test.go | 14 ++- internal/rpc/rpc.go | 2 +- internal/servermon/monitoring.go | 6 +- internal/utils/utils.go | 11 ++- internal/utils/utils_test.go | 1 + internal/vault/vault.go | 24 ++--- internal/vault/vault_test.go | 2 +- internal/wellknownapi/api.go | 7 +- internal/wellknownapi/api_test.go | 10 ++- local/seed_db/main.go | 10 ++- local/vault/approle.go | 2 +- openfga/auth_model.go | 2 +- pkg/api/client.go | 2 +- pkg/api/params/errors.go | 2 +- pkg/api/params/params.go | 30 +++---- pkg/names/applicationoffer.go | 2 +- pkg/names/group.go | 2 +- pkg/names/group_test.go | 2 +- pkg/names/names.go | 2 +- pkg/names/service_account.go | 2 +- pkg/names/service_account_test.go | 2 +- version/default.go | 2 +- version/version.go | 2 +- 290 files changed, 949 insertions(+), 764 deletions(-) create mode 100644 .golangci.yaml create mode 100644 .vscode/settings.json create mode 100644 doc/golangci-lint.md diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 000000000..96bc21b20 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,90 @@ +# Golangci-lint configuration. +# +# If a line has a comment, it means it has been changed from the default. +# This helps us understand what we're tweaking and why. + +run: + timeout: "5m" # Allow at least 5 minutes + issues-exit-code: 1 + tests: true + allow-parallel-runners: false + allow-serial-runners: false + # go: "1.17" # Do not set a go limit + +issues: + exclude-use-default: true + exclude-case-sensitive: false + exclude-dirs-use-default: true + max-issues-per-linter: 50 + max-same-issues: 3 + new: false + fix: true + whole-files: false + +output: + print-issued-lines: true + print-linter-name: true + uniq-by-line: true + # path-prefix: # Not needed + show-stats: false + sort-results: true + +linters: + disable-all: true + enable: + # The following linters are enabled by default + - errcheck + - gosimple + - govet + - ineffassign + - staticcheck + - unused + + # The following linters are additional + + # Bug based linters + - gosec + - sqlclosecheck + - reassign + - nilerr + - durationcheck + - bodyclose + # - contextcheck # Issue right now + + # Style based linters + - promlinter + - gocritic + # - gocognit # To be fixed + - goheader + - importas + - gci + +linters-settings: + gosec: + exclude-generated: false + severity: low + confidence: low + excludes: + - G601 # Implicit memory aliasing in for loop. Fixed in Go1.22+, as such exclude. + gocognit: + min-complexity: 30 + goheader: + template: |- + Copyright 2024 Canonical. + importas: + no-unaliased: false + no-extra-aliases: false + alias: + - pkg: github.com/juju/juju/rpc/params + alias: jujuparams + - pkg: github.com/canonical/jimm/v3/internal/openfga/names + alias: ofganames + - pkg: github.com/frankban/quicktest + alias: qt + gci: + skip-generated: true + custom-order: true + sections: + - standard + - default + - localmodule \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..ea70deb9b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "go.lintTool": "golangci-lint", + "go.lintFlags": [ + "--fast" + ], + "go.lintOnSave": "workspace", +} diff --git a/cmd/jaas/cmd/addserviceaccount.go b/cmd/jaas/cmd/addserviceaccount.go index 2e14cb956..5584ec966 100644 --- a/cmd/jaas/cmd/addserviceaccount.go +++ b/cmd/jaas/cmd/addserviceaccount.go @@ -1,4 +1,4 @@ -// Copyright 2024 Canonical Ltd. +// Copyright 2024 Canonical. package cmd diff --git a/cmd/jaas/cmd/addserviceaccount_test.go b/cmd/jaas/cmd/addserviceaccount_test.go index 6a6596072..fa6966171 100644 --- a/cmd/jaas/cmd/addserviceaccount_test.go +++ b/cmd/jaas/cmd/addserviceaccount_test.go @@ -1,4 +1,4 @@ -// Copyright 2024 Canonical Ltd. +// Copyright 2024 Canonical. package cmd_test diff --git a/cmd/jaas/cmd/export_test.go b/cmd/jaas/cmd/export_test.go index ac0c8489f..955d46936 100644 --- a/cmd/jaas/cmd/export_test.go +++ b/cmd/jaas/cmd/export_test.go @@ -1,4 +1,4 @@ -// Copyright 2024 Canonical Ltd. +// Copyright 2024 Canonical. package cmd diff --git a/cmd/jaas/cmd/grant.go b/cmd/jaas/cmd/grant.go index d935042ff..0930f7f40 100644 --- a/cmd/jaas/cmd/grant.go +++ b/cmd/jaas/cmd/grant.go @@ -1,11 +1,10 @@ -// Copyright 2024 Canonical Ltd. +// Copyright 2024 Canonical. package cmd import ( - "fmt" - "github.com/juju/cmd/v3" + "github.com/juju/gnuflag" jujuapi "github.com/juju/juju/api" jujucmd "github.com/juju/juju/cmd" "github.com/juju/juju/cmd/modelcmd" @@ -57,6 +56,14 @@ func (c *grantCommand) Info() *cmd.Info { }) } +// Init implements the cmd.Command interface. +func (c *grantCommand) SetFlags(f *gnuflag.FlagSet) { + c.CommandBase.SetFlags(f) + c.out.AddFlags(f, "smart", map[string]cmd.Formatter{ + "smart": cmd.FormatSmart, + }) +} + // Init implements the cmd.Command interface. func (c *grantCommand) Init(args []string) error { if len(args) < 1 { @@ -92,6 +99,11 @@ func (c *grantCommand) Run(ctxt *cmd.Context) error { if err != nil { return errors.E(err) } - fmt.Fprintln(ctxt.Stdout, "access granted") + err = c.out.Write(ctxt, "access granted") + if err != nil { + return errors.E(err) + } + + // fmt.Fprintf(ctxt.Stdout, "access granted") return nil } diff --git a/cmd/jaas/cmd/grant_test.go b/cmd/jaas/cmd/grant_test.go index efd727bdd..8acc174ef 100644 --- a/cmd/jaas/cmd/grant_test.go +++ b/cmd/jaas/cmd/grant_test.go @@ -1,4 +1,4 @@ -// Copyright 2024 Canonical Ltd. +// Copyright 2024 Canonical. package cmd_test diff --git a/cmd/jaas/cmd/listserviceaccountcredentials.go b/cmd/jaas/cmd/listserviceaccountcredentials.go index 0688ef8ad..002a8fa87 100644 --- a/cmd/jaas/cmd/listserviceaccountcredentials.go +++ b/cmd/jaas/cmd/listserviceaccountcredentials.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package cmd @@ -31,6 +31,7 @@ This command only shows credentials uploaded to the controller that belong to th Client-side credentials should be managed via the juju credentials command. ` + //nolint:gosec // Believes credentials are exposed but aren't. listServiceAccountCredentialsExamples = ` juju list-service-account-credentials juju list-service-account-credentials --show-secrets diff --git a/cmd/jaas/cmd/listserviceaccountcredentials_test.go b/cmd/jaas/cmd/listserviceaccountcredentials_test.go index 8fdeb3ec9..f8f6bcd4c 100644 --- a/cmd/jaas/cmd/listserviceaccountcredentials_test.go +++ b/cmd/jaas/cmd/listserviceaccountcredentials_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package cmd_test diff --git a/cmd/jaas/cmd/package_test.go b/cmd/jaas/cmd/package_test.go index 1a5f86c31..4addb4e34 100644 --- a/cmd/jaas/cmd/package_test.go +++ b/cmd/jaas/cmd/package_test.go @@ -1,4 +1,4 @@ -// Copyright 2024 Canonical Ltd. +// Copyright 2024 Canonical. package cmd_test diff --git a/cmd/jaas/cmd/updatecredentials.go b/cmd/jaas/cmd/updatecredentials.go index 6fb9c8918..28303d982 100644 --- a/cmd/jaas/cmd/updatecredentials.go +++ b/cmd/jaas/cmd/updatecredentials.go @@ -1,4 +1,4 @@ -// Copyright 2024 Canonical Ltd. +// Copyright 2024 Canonical. package cmd @@ -11,10 +11,9 @@ import ( jujucmd "github.com/juju/juju/cmd" "github.com/juju/juju/cmd/modelcmd" "github.com/juju/juju/jujuclient" - "github.com/juju/names/v5" - "github.com/juju/juju/rpc/params" jujuparams "github.com/juju/juju/rpc/params" + "github.com/juju/names/v5" "github.com/canonical/jimm/v3/internal/errors" "github.com/canonical/jimm/v3/pkg/api" diff --git a/cmd/jaas/cmd/updatecredentials_test.go b/cmd/jaas/cmd/updatecredentials_test.go index 700d60694..5242311d6 100644 --- a/cmd/jaas/cmd/updatecredentials_test.go +++ b/cmd/jaas/cmd/updatecredentials_test.go @@ -1,4 +1,4 @@ -// Copyright 2024 Canonical Ltd. +// Copyright 2024 Canonical. package cmd_test @@ -7,6 +7,8 @@ import ( "fmt" "github.com/juju/cmd/v3/cmdtesting" + jujucloud "github.com/juju/juju/cloud" + "github.com/juju/juju/rpc/params" "github.com/juju/names/v5" gc "gopkg.in/check.v1" @@ -18,8 +20,6 @@ import ( "github.com/canonical/jimm/v3/internal/openfga" ofganames "github.com/canonical/jimm/v3/internal/openfga/names" jimmnames "github.com/canonical/jimm/v3/pkg/names" - jujucloud "github.com/juju/juju/cloud" - "github.com/juju/juju/rpc/params" ) type updateCredentialsSuite struct { diff --git a/cmd/jaas/main.go b/cmd/jaas/main.go index cc95c87af..22dc0b9a0 100644 --- a/cmd/jaas/main.go +++ b/cmd/jaas/main.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package main @@ -7,8 +7,9 @@ import ( "os" "strings" - "github.com/canonical/jimm/v3/cmd/jaas/cmd" jujucmd "github.com/juju/cmd/v3" + + "github.com/canonical/jimm/v3/cmd/jaas/cmd" ) var jaasDoc = ` diff --git a/cmd/jimmctl/cmd/addcloudtocontroller.go b/cmd/jimmctl/cmd/addcloudtocontroller.go index 2a2da2356..aa4b88c62 100644 --- a/cmd/jimmctl/cmd/addcloudtocontroller.go +++ b/cmd/jimmctl/cmd/addcloudtocontroller.go @@ -1,10 +1,10 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package cmd import ( "fmt" - "io/ioutil" + "os" "github.com/juju/cmd/v3" "github.com/juju/gnuflag" @@ -194,7 +194,7 @@ type cloudToCommandAdapter struct { // ReadCloudData implements CloudMetadataStore.ReadCloudData. func (cloudToCommandAdapter) ReadCloudData(path string) ([]byte, error) { - return ioutil.ReadFile(path) + return os.ReadFile(path) } // ParseOneCloud implements CloudMetadataStore.ParseOneCloud. diff --git a/cmd/jimmctl/cmd/addcloudtocontroller_test.go b/cmd/jimmctl/cmd/addcloudtocontroller_test.go index eb610594b..2cbcf37ed 100644 --- a/cmd/jimmctl/cmd/addcloudtocontroller_test.go +++ b/cmd/jimmctl/cmd/addcloudtocontroller_test.go @@ -1,9 +1,8 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package cmd_test import ( "context" - "io/ioutil" "os" "path/filepath" "strconv" @@ -187,7 +186,8 @@ clouds: err = s.JIMM.Database.GetCloud(context.Background(), &cloud) c.Assert(err, gc.IsNil) controller := dbmodel.Controller{Name: "controller-1"} - s.JIMM.Database.GetController(context.Background(), &controller) + err = s.JIMM.Database.GetController(context.Background(), &controller) + c.Assert(err, gc.IsNil) c.Assert(controller.CloudRegions[test.expectedIndex].CloudRegion.CloudName, gc.Equals, test.expectedCloudName) } cleanupFunc() @@ -197,11 +197,12 @@ clouds: } func writeTempFile(c *gc.C, content string) (string, func()) { - dir, err := ioutil.TempDir("", "add-cloud-to-controller-test") + dir, err := os.MkdirTemp("", "add-cloud-to-controller-test") c.Assert(err, gc.Equals, nil) tmpfn := filepath.Join(dir, "tmp.yaml") - err = ioutil.WriteFile(tmpfn, []byte(content), 0666) + + err = os.WriteFile(tmpfn, []byte(content), 0600) c.Assert(err, gc.Equals, nil) return tmpfn, func() { os.RemoveAll(dir) diff --git a/cmd/jimmctl/cmd/addcontroller.go b/cmd/jimmctl/cmd/addcontroller.go index 90268d5ec..bd1e7fe52 100644 --- a/cmd/jimmctl/cmd/addcontroller.go +++ b/cmd/jimmctl/cmd/addcontroller.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package cmd diff --git a/cmd/jimmctl/cmd/addcontroller_test.go b/cmd/jimmctl/cmd/addcontroller_test.go index 1d5e921ff..00e9d0162 100644 --- a/cmd/jimmctl/cmd/addcontroller_test.go +++ b/cmd/jimmctl/cmd/addcontroller_test.go @@ -1,10 +1,9 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package cmd_test import ( "context" - "io/ioutil" "os" "path/filepath" @@ -110,11 +109,11 @@ func writeYAMLTempFile(c *gc.C, payload interface{}) (string, string) { data, err := yaml.Marshal(payload) c.Assert(err, gc.Equals, nil) - dir, err := ioutil.TempDir("", "add-controller-test") + dir, err := os.MkdirTemp("", "add-controller-test") c.Assert(err, gc.Equals, nil) tmpfn := filepath.Join(dir, "tmp.yaml") - err = ioutil.WriteFile(tmpfn, data, 0666) + err = os.WriteFile(tmpfn, data, 0600) c.Assert(err, gc.Equals, nil) return dir, tmpfn } diff --git a/cmd/jimmctl/cmd/auth.go b/cmd/jimmctl/cmd/auth.go index ff9e3778e..346d67c46 100644 --- a/cmd/jimmctl/cmd/auth.go +++ b/cmd/jimmctl/cmd/auth.go @@ -1,4 +1,4 @@ -// Copyright 2023 Canonical Ltd. +// Copyright 2024 Canonical. package cmd diff --git a/cmd/jimmctl/cmd/controllerinfo.go b/cmd/jimmctl/cmd/controllerinfo.go index b5de52ac4..a803c6c11 100644 --- a/cmd/jimmctl/cmd/controllerinfo.go +++ b/cmd/jimmctl/cmd/controllerinfo.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package cmd @@ -123,7 +123,7 @@ func (c *controllerInfoCommand) Run(ctxt *cmd.Context) error { if err != nil { return errors.Mask(err) } - err = os.WriteFile(c.file.Path, data, 0666) + err = os.WriteFile(c.file.Path, data, 0600) if err != nil { return errors.Mask(err) } diff --git a/cmd/jimmctl/cmd/controllerinfo_test.go b/cmd/jimmctl/cmd/controllerinfo_test.go index 48f2b5a1e..dd8c37798 100644 --- a/cmd/jimmctl/cmd/controllerinfo_test.go +++ b/cmd/jimmctl/cmd/controllerinfo_test.go @@ -1,4 +1,4 @@ -// Copyright 2023 Canonical Ltd. +// Copyright 2024 Canonical. package cmd_test diff --git a/cmd/jimmctl/cmd/crossmodelquery.go b/cmd/jimmctl/cmd/crossmodelquery.go index 06f98e99b..296154342 100644 --- a/cmd/jimmctl/cmd/crossmodelquery.go +++ b/cmd/jimmctl/cmd/crossmodelquery.go @@ -1,4 +1,4 @@ -// Copyright 2023 Canonical Ltd. +// Copyright 2024 Canonical. package cmd diff --git a/cmd/jimmctl/cmd/crossmodelquery_test.go b/cmd/jimmctl/cmd/crossmodelquery_test.go index df378d3bb..4521f8075 100644 --- a/cmd/jimmctl/cmd/crossmodelquery_test.go +++ b/cmd/jimmctl/cmd/crossmodelquery_test.go @@ -1,18 +1,19 @@ -// Copyright 2023 Canonical Ltd. +// Copyright 2024 Canonical. package cmd_test import ( "encoding/json" - "github.com/canonical/jimm/v3/cmd/jimmctl/cmd" - "github.com/canonical/jimm/v3/internal/cmdtest" - "github.com/canonical/jimm/v3/internal/jimmtest" "github.com/juju/cmd/v3/cmdtesting" jujuparams "github.com/juju/juju/rpc/params" "github.com/juju/juju/testing/factory" "github.com/juju/names/v5" gc "gopkg.in/check.v1" + + "github.com/canonical/jimm/v3/cmd/jimmctl/cmd" + "github.com/canonical/jimm/v3/internal/cmdtest" + "github.com/canonical/jimm/v3/internal/jimmtest" ) type crossModelQuerySuite struct { diff --git a/cmd/jimmctl/cmd/export_test.go b/cmd/jimmctl/cmd/export_test.go index e32b9ff94..eb2569252 100644 --- a/cmd/jimmctl/cmd/export_test.go +++ b/cmd/jimmctl/cmd/export_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package cmd diff --git a/cmd/jimmctl/cmd/grantauditlogaccess.go b/cmd/jimmctl/cmd/grantauditlogaccess.go index b3e9140b0..77e3e17da 100644 --- a/cmd/jimmctl/cmd/grantauditlogaccess.go +++ b/cmd/jimmctl/cmd/grantauditlogaccess.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package cmd @@ -58,7 +58,7 @@ func (c *grantAuditLogAccessCommand) SetFlags(f *gnuflag.FlagSet) { // Init implements the cmd.Command interface. func (c *grantAuditLogAccessCommand) Init(args []string) error { - if len(args) < 0 { + if len(args) == 0 { return errors.E("missing username") } c.username, args = args[0], args[1:] diff --git a/cmd/jimmctl/cmd/grantauditlogaccess_test.go b/cmd/jimmctl/cmd/grantauditlogaccess_test.go index 3d45221a5..f3d54130b 100644 --- a/cmd/jimmctl/cmd/grantauditlogaccess_test.go +++ b/cmd/jimmctl/cmd/grantauditlogaccess_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package cmd_test @@ -15,8 +15,7 @@ type grantAuditLogAccessSuite struct { cmdtest.JimmCmdSuite } -// TODO (alesstimec) uncomment once granting/revoking is reimplemented -//var _ = gc.Suite(&grantAuditLogAccessSuite{}) +var _ = gc.Suite(&grantAuditLogAccessSuite{}) func (s *grantAuditLogAccessSuite) TestGrantAuditLogAccessSuperuser(c *gc.C) { // alice is superuser diff --git a/cmd/jimmctl/cmd/group.go b/cmd/jimmctl/cmd/group.go index 517fb2f23..759986737 100644 --- a/cmd/jimmctl/cmd/group.go +++ b/cmd/jimmctl/cmd/group.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package cmd @@ -159,7 +159,6 @@ func newRenameGroupCommand() cmd.Command { // renameGroupCommand renames a group. type renameGroupCommand struct { modelcmd.ControllerCommandBase - out cmd.Output store jujuclient.ClientStore dialOpts *jujuapi.DialOpts @@ -284,7 +283,7 @@ func (c *removeGroupCommand) Run(ctxt *cmd.Context) error { if err != nil { return errors.E(err, "Failed to read from input.") } - text = strings.Replace(text, "\n", "", -1) + text = strings.ReplaceAll(text, "\n", "") if !(text == "y" || text == "Y") { return nil } @@ -324,8 +323,6 @@ type listGroupsCommand struct { store jujuclient.ClientStore dialOpts *jujuapi.DialOpts - - name string } // Info implements the cmd.Command interface. diff --git a/cmd/jimmctl/cmd/group_test.go b/cmd/jimmctl/cmd/group_test.go index 6eff3867d..4848b74cf 100644 --- a/cmd/jimmctl/cmd/group_test.go +++ b/cmd/jimmctl/cmd/group_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package cmd_test diff --git a/cmd/jimmctl/cmd/importcloudcredentials.go b/cmd/jimmctl/cmd/importcloudcredentials.go index 3d9d32b52..9a24aafa5 100644 --- a/cmd/jimmctl/cmd/importcloudcredentials.go +++ b/cmd/jimmctl/cmd/importcloudcredentials.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package cmd @@ -17,6 +17,7 @@ import ( "github.com/canonical/jimm/v3/internal/errors" ) +//nolint:gosec // Thinks a credential is exposed. const importCloudCredentialsDoc = ` import-cloud-credentials imports a set of cloud credentials loaded from a file containing a series of JSON objects. The JSON diff --git a/cmd/jimmctl/cmd/importcloudcredentials_test.go b/cmd/jimmctl/cmd/importcloudcredentials_test.go index 718ffd982..ee0c7cd80 100644 --- a/cmd/jimmctl/cmd/importcloudcredentials_test.go +++ b/cmd/jimmctl/cmd/importcloudcredentials_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package cmd_test @@ -22,6 +22,7 @@ type importCloudCredentialsSuite struct { var _ = gc.Suite(&importCloudCredentialsSuite{}) +//nolint:gosec // Thinks hardcoded creds. const creds = `{ "_id": "aws/alice@canonical.com/test1", "type": "access-key", @@ -60,7 +61,7 @@ func (s *importCloudCredentialsSuite) TestImportCloudCredentials(c *gc.C) { c.Assert(err, gc.IsNil) tmpfile := filepath.Join(c.MkDir(), "test.json") - err = os.WriteFile(tmpfile, []byte(creds), 0660) + err = os.WriteFile(tmpfile, []byte(creds), 0600) c.Assert(err, gc.IsNil) // alice is superuser diff --git a/cmd/jimmctl/cmd/importmodel.go b/cmd/jimmctl/cmd/importmodel.go index ab363358c..520941921 100644 --- a/cmd/jimmctl/cmd/importmodel.go +++ b/cmd/jimmctl/cmd/importmodel.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package cmd diff --git a/cmd/jimmctl/cmd/importmodel_test.go b/cmd/jimmctl/cmd/importmodel_test.go index 6b225857f..81065aa65 100644 --- a/cmd/jimmctl/cmd/importmodel_test.go +++ b/cmd/jimmctl/cmd/importmodel_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package cmd_test diff --git a/cmd/jimmctl/cmd/listauditevents.go b/cmd/jimmctl/cmd/listauditevents.go index 3512d3bf6..fe7e66675 100644 --- a/cmd/jimmctl/cmd/listauditevents.go +++ b/cmd/jimmctl/cmd/listauditevents.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package cmd diff --git a/cmd/jimmctl/cmd/listauditevents_test.go b/cmd/jimmctl/cmd/listauditevents_test.go index 6dc1c438c..d837431f2 100644 --- a/cmd/jimmctl/cmd/listauditevents_test.go +++ b/cmd/jimmctl/cmd/listauditevents_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package cmd_test diff --git a/cmd/jimmctl/cmd/listcontrollers.go b/cmd/jimmctl/cmd/listcontrollers.go index 031026d1f..b5bb2f5aa 100644 --- a/cmd/jimmctl/cmd/listcontrollers.go +++ b/cmd/jimmctl/cmd/listcontrollers.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package cmd diff --git a/cmd/jimmctl/cmd/listcontrollers_test.go b/cmd/jimmctl/cmd/listcontrollers_test.go index d586c4e46..94c0cd707 100644 --- a/cmd/jimmctl/cmd/listcontrollers_test.go +++ b/cmd/jimmctl/cmd/listcontrollers_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package cmd_test diff --git a/cmd/jimmctl/cmd/migratemodel.go b/cmd/jimmctl/cmd/migratemodel.go index 815d6cce4..875407947 100644 --- a/cmd/jimmctl/cmd/migratemodel.go +++ b/cmd/jimmctl/cmd/migratemodel.go @@ -1,4 +1,4 @@ -// Copyright 2023 Canonical Ltd. +// Copyright 2024 Canonical. package cmd diff --git a/cmd/jimmctl/cmd/migratemodel_test.go b/cmd/jimmctl/cmd/migratemodel_test.go index 03f3e116f..0d837b2ae 100644 --- a/cmd/jimmctl/cmd/migratemodel_test.go +++ b/cmd/jimmctl/cmd/migratemodel_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package cmd_test diff --git a/cmd/jimmctl/cmd/modelstatus.go b/cmd/jimmctl/cmd/modelstatus.go index a5822d607..5997df097 100644 --- a/cmd/jimmctl/cmd/modelstatus.go +++ b/cmd/jimmctl/cmd/modelstatus.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package cmd diff --git a/cmd/jimmctl/cmd/modelstatus_test.go b/cmd/jimmctl/cmd/modelstatus_test.go index 5dca3bb03..25b2d44d0 100644 --- a/cmd/jimmctl/cmd/modelstatus_test.go +++ b/cmd/jimmctl/cmd/modelstatus_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package cmd_test diff --git a/cmd/jimmctl/cmd/package_test.go b/cmd/jimmctl/cmd/package_test.go index fb57779d4..4addb4e34 100644 --- a/cmd/jimmctl/cmd/package_test.go +++ b/cmd/jimmctl/cmd/package_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package cmd_test diff --git a/cmd/jimmctl/cmd/purge_logs.go b/cmd/jimmctl/cmd/purge_logs.go index faf4abab8..d6f5b0973 100644 --- a/cmd/jimmctl/cmd/purge_logs.go +++ b/cmd/jimmctl/cmd/purge_logs.go @@ -1,3 +1,4 @@ +// Copyright 2024 Canonical. package cmd import ( diff --git a/cmd/jimmctl/cmd/purge_logs_test.go b/cmd/jimmctl/cmd/purge_logs_test.go index f472a1b4d..0cd8c5832 100644 --- a/cmd/jimmctl/cmd/purge_logs_test.go +++ b/cmd/jimmctl/cmd/purge_logs_test.go @@ -1,3 +1,4 @@ +// Copyright 2024 Canonical. package cmd_test import ( @@ -84,7 +85,7 @@ func (s *purgeLogsSuite) TestPurgeLogsFromDb(c *gc.C) { c.Assert(err, gc.IsNil) tomorrow := relativeNow.AddDate(0, 0, 1).Format(layout) - //alice is superuser + // alice is superuser bClient := jimmtest.NewUserSessionLogin(c, "alice") cmdCtx, err := cmdtesting.RunCommand(c, cmd.NewPurgeLogsCommandForTesting(s.ClientStore(), bClient), tomorrow) c.Assert(err, gc.IsNil) diff --git a/cmd/jimmctl/cmd/relation.go b/cmd/jimmctl/cmd/relation.go index 6249d3b58..3b2249fda 100644 --- a/cmd/jimmctl/cmd/relation.go +++ b/cmd/jimmctl/cmd/relation.go @@ -1,4 +1,4 @@ -// Copyright 2023 Canonical Ltd. +// Copyright 2024 Canonical. package cmd @@ -179,7 +179,7 @@ type addRelationCommand struct { relation string targetObject string - filename string //optional + filename string // optional } // Info implements the cmd.Command interface. @@ -270,7 +270,7 @@ type removeRelationCommand struct { relation string targetObject string - filename string //optional + filename string // optional } // Info implements the cmd.Command interface. @@ -415,7 +415,10 @@ func formatCheckRelationString(writer io.Writer, value interface{}) error { if !ok { return errors.E("failed to parse access result") } - writer.Write([]byte((&accessResult).setMessage().Msg)) + _, err := writer.Write([]byte((&accessResult).setMessage().Msg)) + if err != nil { + return errors.E("failed to write access result", err) + } return nil } @@ -438,10 +441,13 @@ func (c *checkRelationCommand) Run(ctxt *cmd.Context) error { if err != nil { return err } - c.out.Write(ctxt, *(&accessResult{ + err = c.out.Write(ctxt, *(&accessResult{ Tuple: c.tuple, Allowed: resp.Allowed, }).setMessage()) + if err != nil { + return err + } return nil } diff --git a/cmd/jimmctl/cmd/relation_test.go b/cmd/jimmctl/cmd/relation_test.go index 2953463c2..1c9b7688b 100644 --- a/cmd/jimmctl/cmd/relation_test.go +++ b/cmd/jimmctl/cmd/relation_test.go @@ -1,4 +1,4 @@ -// Copyright 2023 Canonical Ltd. +// Copyright 2024 Canonical. package cmd_test @@ -175,7 +175,7 @@ func (s *relationSuite) TestRemoveRelationSuperuser(c *gc.C) { {testName: "Remove Group Relation", input: tuple{user: "group-" + group1 + "#member", relation: "member", target: "group-" + group2}, err: false}, } - //Create groups and relation + // Create groups and relation _, err := s.JimmCmdSuite.JIMM.Database.AddGroup(context.Background(), group1) c.Assert(err, gc.IsNil) _, err = s.JimmCmdSuite.JIMM.Database.AddGroup(context.Background(), group2) diff --git a/cmd/jimmctl/cmd/removecloudfromcontroller.go b/cmd/jimmctl/cmd/removecloudfromcontroller.go index 318053479..5f5caff21 100644 --- a/cmd/jimmctl/cmd/removecloudfromcontroller.go +++ b/cmd/jimmctl/cmd/removecloudfromcontroller.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package cmd diff --git a/cmd/jimmctl/cmd/removecloudfromcontroller_test.go b/cmd/jimmctl/cmd/removecloudfromcontroller_test.go index 037180b43..ca7d10475 100644 --- a/cmd/jimmctl/cmd/removecloudfromcontroller_test.go +++ b/cmd/jimmctl/cmd/removecloudfromcontroller_test.go @@ -1,10 +1,9 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package cmd_test import ( "github.com/juju/cmd/v3/cmdtesting" jujutesting "github.com/juju/testing" - gc "gopkg.in/check.v1" "github.com/canonical/jimm/v3/cmd/jimmctl/cmd" diff --git a/cmd/jimmctl/cmd/removecontroller.go b/cmd/jimmctl/cmd/removecontroller.go index 381fcf53b..63589741d 100644 --- a/cmd/jimmctl/cmd/removecontroller.go +++ b/cmd/jimmctl/cmd/removecontroller.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package cmd diff --git a/cmd/jimmctl/cmd/removecontroller_test.go b/cmd/jimmctl/cmd/removecontroller_test.go index 47a03954d..29103f6a6 100644 --- a/cmd/jimmctl/cmd/removecontroller_test.go +++ b/cmd/jimmctl/cmd/removecontroller_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package cmd_test diff --git a/cmd/jimmctl/cmd/revokeauditlogaccess.go b/cmd/jimmctl/cmd/revokeauditlogaccess.go index 525018f44..2341970c3 100644 --- a/cmd/jimmctl/cmd/revokeauditlogaccess.go +++ b/cmd/jimmctl/cmd/revokeauditlogaccess.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package cmd @@ -58,7 +58,7 @@ func (c *revokeAuditLogAccessCommand) SetFlags(f *gnuflag.FlagSet) { // Init implements the cmd.Command interface. func (c *revokeAuditLogAccessCommand) Init(args []string) error { - if len(args) < 0 { + if len(args) == 0 { return errors.E("missing username") } c.username, args = args[0], args[1:] diff --git a/cmd/jimmctl/cmd/revokeauditlogaccess_test.go b/cmd/jimmctl/cmd/revokeauditlogaccess_test.go index ca70e9f85..f87c24dd2 100644 --- a/cmd/jimmctl/cmd/revokeauditlogaccess_test.go +++ b/cmd/jimmctl/cmd/revokeauditlogaccess_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package cmd_test @@ -15,8 +15,7 @@ type revokeAuditLogAccessSuite struct { cmdtest.JimmCmdSuite } -// TODO (alesstimec) uncomment when grant/revoke is implemented -//var _ = gc.Suite(&revokeAuditLogAccessSuite{}) +var _ = gc.Suite(&revokeAuditLogAccessSuite{}) func (s *revokeAuditLogAccessSuite) TestRevokeAuditLogAccessSuperuser(c *gc.C) { // alice is superuser diff --git a/cmd/jimmctl/cmd/setcontrollerdeprecated.go b/cmd/jimmctl/cmd/setcontrollerdeprecated.go index 01c6cb90c..851970d8e 100644 --- a/cmd/jimmctl/cmd/setcontrollerdeprecated.go +++ b/cmd/jimmctl/cmd/setcontrollerdeprecated.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package cmd @@ -63,7 +63,7 @@ func (c *setControllerDeprecatedCommand) SetFlags(f *gnuflag.FlagSet) { // Init implements the cmd.Command interface. func (c *setControllerDeprecatedCommand) Init(args []string) error { - if len(args) < 0 { + if len(args) == 0 { return errors.E("missing controller name") } c.controllerName, args = args[0], args[1:] diff --git a/cmd/jimmctl/cmd/setcontrollerdeprecated_test.go b/cmd/jimmctl/cmd/setcontrollerdeprecated_test.go index b1411d034..a8a8de5c0 100644 --- a/cmd/jimmctl/cmd/setcontrollerdeprecated_test.go +++ b/cmd/jimmctl/cmd/setcontrollerdeprecated_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package cmd_test diff --git a/cmd/jimmctl/cmd/updatemigratedmodel.go b/cmd/jimmctl/cmd/updatemigratedmodel.go index 700480c2d..3ada94bb6 100644 --- a/cmd/jimmctl/cmd/updatemigratedmodel.go +++ b/cmd/jimmctl/cmd/updatemigratedmodel.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package cmd diff --git a/cmd/jimmctl/cmd/updatemigratedmodel_test.go b/cmd/jimmctl/cmd/updatemigratedmodel_test.go index caf53eef5..da1469f9e 100644 --- a/cmd/jimmctl/cmd/updatemigratedmodel_test.go +++ b/cmd/jimmctl/cmd/updatemigratedmodel_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package cmd_test diff --git a/cmd/jimmctl/main.go b/cmd/jimmctl/main.go index bdeee2970..3555cdd8a 100644 --- a/cmd/jimmctl/main.go +++ b/cmd/jimmctl/main.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package main diff --git a/cmd/jimmsrv/main.go b/cmd/jimmsrv/main.go index f2c540913..4f5698bd3 100644 --- a/cmd/jimmsrv/main.go +++ b/cmd/jimmsrv/main.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package main @@ -35,6 +35,8 @@ func main() { } // start initialises the jimmsrv service. +// +//nolint:gocognit // Start function to be ignored. func start(ctx context.Context, s *service.Service) error { zapctx.Info(ctx, "jimm info", zap.String("version", version.VersionInfo.Version), @@ -204,15 +206,19 @@ func start(ctx context.Context, s *service.Service) error { } httpsrv := &http.Server{ - Addr: addr, - Handler: jimmsvc, + Addr: addr, + Handler: jimmsvc, + ReadHeaderTimeout: time.Second * 5, } s.OnShutdown(func() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() zapctx.Warn(ctx, "server shutdown triggered") - httpsrv.Shutdown(ctx) + err = httpsrv.Shutdown(ctx) + if err != nil { + zapctx.Error(ctx, "failed to shutdown server gracefully", zap.Error(err)) + } jimmsvc.Cleanup() }) s.Go(httpsrv.ListenAndServe) diff --git a/cmd/jimmsrv/service/export_test.go b/cmd/jimmsrv/service/export_test.go index 3dca85934..db33a96a1 100644 --- a/cmd/jimmsrv/service/export_test.go +++ b/cmd/jimmsrv/service/export_test.go @@ -1,4 +1,4 @@ -// Copyright 2023 Canonical Ltd. +// Copyright 2024 Canonical. package service var NewOpenFGAClient = newOpenFGAClient diff --git a/cmd/jimmsrv/service/service.go b/cmd/jimmsrv/service/service.go index 62bb5c868..6ba1dffca 100644 --- a/cmd/jimmsrv/service/service.go +++ b/cmd/jimmsrv/service/service.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. // service defines the methods necessary to start a JIMM server // alongside all the config options that can be supplied to configure JIMM. diff --git a/cmd/jimmsrv/service/service_test.go b/cmd/jimmsrv/service/service_test.go index 341b26fa0..9bc815d69 100644 --- a/cmd/jimmsrv/service/service_test.go +++ b/cmd/jimmsrv/service/service_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package service_test @@ -51,6 +51,7 @@ func TestDefaultService(t *testing.T) { c.Assert(err, qt.IsNil) svc.ServeHTTP(rr, req) resp := rr.Result() + defer resp.Body.Close() c.Check(resp.StatusCode, qt.Equals, http.StatusOK) } @@ -266,6 +267,7 @@ func TestPublicKey(t *testing.T) { response, err := srv.Client().Get(srv.URL + "/macaroons/publickey") c.Assert(err, qt.IsNil) + defer response.Body.Close() data, err := io.ReadAll(response.Body) c.Assert(err, qt.IsNil) c.Assert(string(data), qt.Equals, `{"PublicKey":"izcYsQy3TePp6bLjqOo3IRPFvkQd2IKtyODGqC6SdFk="}`) @@ -411,6 +413,7 @@ func TestDisableOAuthEndpointsWhenDashboardRedirectURLNotSet(t *testing.T) { response, err := srv.Client().Get(srv.URL + "/auth/whoami") c.Assert(err, qt.IsNil) + defer response.Body.Close() c.Assert(response.StatusCode, qt.Equals, http.StatusNotFound) } @@ -434,6 +437,7 @@ func TestEnableOAuthEndpointsWhenDashboardRedirectURLSet(t *testing.T) { response, err := srv.Client().Get(srv.URL + "/auth/whoami") c.Assert(err, qt.IsNil) + defer response.Body.Close() c.Assert(response.StatusCode, qt.Not(qt.Equals), http.StatusNotFound) } diff --git a/doc/golangci-lint.md b/doc/golangci-lint.md new file mode 100644 index 000000000..743ca68ce --- /dev/null +++ b/doc/golangci-lint.md @@ -0,0 +1,10 @@ +# Golangci-lint +We use golangci-lint as the linter for this project. It is helpful to run +the linter as your default vscode linter. + +In the .vscode folder of this repository you will find it is defined as the linter of choice. + +To install, please install the golangci-lint binary or install it via "go install". + +The version this was tested with is: +```go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.59.1``` \ No newline at end of file diff --git a/internal/auth/jujuauth.go b/internal/auth/jujuauth.go index 6e8abbfbf..c5729c78a 100644 --- a/internal/auth/jujuauth.go +++ b/internal/auth/jujuauth.go @@ -1,4 +1,4 @@ -// Copyright 2021 canonical. +// Copyright 2024 Canonical. package auth diff --git a/internal/auth/oauth2.go b/internal/auth/oauth2.go index ebd004bfd..feff81490 100644 --- a/internal/auth/oauth2.go +++ b/internal/auth/oauth2.go @@ -1,4 +1,4 @@ -// Copyright 2024 canonical. +// Copyright 2024 Canonical. // Package auth provides means to authenticate users into JIMM. // diff --git a/internal/auth/oauth2_test.go b/internal/auth/oauth2_test.go index 756468a8a..fa6ac6999 100644 --- a/internal/auth/oauth2_test.go +++ b/internal/auth/oauth2_test.go @@ -1,4 +1,4 @@ -// Copyright 2024 canonical. +// Copyright 2024 Canonical. package auth_test @@ -16,19 +16,20 @@ import ( "time" "github.com/antonlindstrom/pgstore" + "github.com/coreos/go-oidc/v3/oidc" + qt "github.com/frankban/quicktest" + "github.com/gorilla/sessions" + "github.com/canonical/jimm/v3/internal/auth" "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/jimmtest" - "github.com/coreos/go-oidc/v3/oidc" - qt "github.com/frankban/quicktest" - "github.com/gorilla/sessions" ) func setupTestAuthSvc(ctx context.Context, c *qt.C, expiry time.Duration) (*auth.AuthenticationService, *db.Database, sessions.Store, func()) { db := &db.Database{ - DB: jimmtest.PostgresDB(c, func() time.Time { return time.Now() }), + DB: jimmtest.PostgresDB(c, time.Now), } c.Assert(db.Migrate(ctx, false), qt.IsNil) @@ -251,7 +252,8 @@ func TestVerifyClientCredentials(t *testing.T) { const ( // these are valid client credentials hardcoded into the jimm realm - validClientID = "test-client-id" + validClientID = "test-client-id" + //nolint:gosec // Thinks hardcoded credentials. validClientSecret = "2M2blFbO4GX4zfggQpivQSxwWX1XGgNf" ) @@ -265,7 +267,7 @@ func TestVerifyClientCredentials(t *testing.T) { c.Assert(err, qt.ErrorMatches, "invalid client credentials") } -func assertSetCookiesIsCorrect(c *qt.C, rec *httptest.ResponseRecorder, parsedCookies []*http.Cookie) { +func assertSetCookiesIsCorrect(c *qt.C, parsedCookies []*http.Cookie) { assertHasCookie := func(name string, cookies []*http.Cookie) { found := false for _, v := range cookies { @@ -298,7 +300,7 @@ func TestCreateBrowserSession(t *testing.T) { cookies := rec.Header().Get("Set-Cookie") parsedCookies := jimmtest.ParseCookies(cookies) - assertSetCookiesIsCorrect(c, rec, parsedCookies) + assertSetCookiesIsCorrect(c, parsedCookies) req.AddCookie(&http.Cookie{ Name: auth.SessionName, @@ -345,7 +347,7 @@ func TestAuthenticateBrowserSessionAndLogout(t *testing.T) { // Assert Set-Cookie present setCookieCookies := rec.Header().Get("Set-Cookie") parsedCookies := jimmtest.ParseCookies(setCookieCookies) - assertSetCookiesIsCorrect(c, rec, parsedCookies) + assertSetCookiesIsCorrect(c, parsedCookies) // Test logout does indeed remove the cookie for us err = authSvc.Logout(ctx, rec, req) @@ -454,7 +456,7 @@ func TestAuthenticateBrowserSessionHandlesExpiredAccessTokens(t *testing.T) { // Assert Set-Cookie present setCookieCookies := rec.Header().Get("Set-Cookie") parsedCookies := jimmtest.ParseCookies(setCookieCookies) - assertSetCookiesIsCorrect(c, rec, parsedCookies) + assertSetCookiesIsCorrect(c, parsedCookies) } func TestAuthenticateBrowserSessionHandlesMissingOrExpiredRefreshTokens(t *testing.T) { @@ -492,7 +494,8 @@ func TestAuthenticateBrowserSessionHandlesMissingOrExpiredRefreshTokens(t *testi // And we're missing a refresh token (the same case would apply for an expired refresh token // or any scenario where the token source cannot refresh the access token) u.RefreshToken = "" - db.UpdateIdentity(ctx, u) + err = db.UpdateIdentity(ctx, u) + c.Assert(err, qt.IsNil) // AuthenticateBrowserSession should fail to refresh the users session and delete // the current session, giving us the same cookie back with a max-age of -1. diff --git a/internal/cloudcred/cloudcred.go b/internal/cloudcred/cloudcred.go index 78e372087..9899f1c8c 100644 --- a/internal/cloudcred/cloudcred.go +++ b/internal/cloudcred/cloudcred.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd.package cloudcred +// Copyright 2024 Canonical. //go:generate go run generate.go -o attr.go diff --git a/internal/cloudcred/cloudcred_test.go b/internal/cloudcred/cloudcred_test.go index 58186e84d..63b9d1931 100644 --- a/internal/cloudcred/cloudcred_test.go +++ b/internal/cloudcred/cloudcred_test.go @@ -1,12 +1,13 @@ -// Copyright 2020 Canonical Ltd.package cloudcred +// Copyright 2024 Canonical. package cloudcred_test import ( "testing" - "github.com/canonical/jimm/v3/internal/cloudcred" qt "github.com/frankban/quicktest" + + "github.com/canonical/jimm/v3/internal/cloudcred" ) func TestIsVisibleAttribute(t *testing.T) { diff --git a/internal/cmdtest/jimmsuite.go b/internal/cmdtest/jimmsuite.go index d72372b67..fa9dfc8f7 100644 --- a/internal/cmdtest/jimmsuite.go +++ b/internal/cmdtest/jimmsuite.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. // Package cmdtest provides the test suite used for CLI tests // as well as helper functions used for integration based CLI tests. @@ -71,7 +71,7 @@ func (s *JimmCmdSuite) SetUpTest(c *gc.C) { s.Params = jimmtest.NewTestJimmParams(&jimmtest.GocheckTester{C: c}) dsn, err := url.Parse(s.Params.DSN) c.Assert(err, gc.Equals, nil) - s.databaseName = strings.Replace(dsn.Path, "/", "", -1) + s.databaseName = strings.ReplaceAll(dsn.Path, "/", "") s.Params.PublicDNSName = u.Host s.Params.ControllerAdmins = []string{"admin"} s.Params.OpenFGAParams = service.OpenFGAParams{ @@ -90,7 +90,7 @@ func (s *JimmCmdSuite) SetUpTest(c *gc.C) { c.Assert(err, gc.Equals, nil) s.Service = srv s.JIMM = srv.JIMM() - s.HTTP.Config = &http.Server{Handler: srv} + s.HTTP.Config = &http.Server{Handler: srv, ReadHeaderTimeout: time.Second * 5} err = s.Service.StartJWKSRotator(ctx, time.NewTicker(time.Hour).C, time.Now().UTC().AddDate(0, 3, 0)) c.Assert(err, gc.Equals, nil) diff --git a/internal/dashboard/dashboard.go b/internal/dashboard/dashboard.go index 7d0542e9d..4f2fe279b 100644 --- a/internal/dashboard/dashboard.go +++ b/internal/dashboard/dashboard.go @@ -1,5 +1,4 @@ -// Copyright 2020 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. +// Copyright 2024 Canonical. // Package dashboard contains a single function that creates a handler for // serving the JAAS Dashboard. diff --git a/internal/dashboard/dashboard_test.go b/internal/dashboard/dashboard_test.go index 773dfb557..99650959e 100644 --- a/internal/dashboard/dashboard_test.go +++ b/internal/dashboard/dashboard_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package dashboard_test @@ -43,6 +43,7 @@ func TestDashboardNotConfigured(t *testing.T) { c.Assert(err, qt.IsNil) hnd.ServeHTTP(rr, req) resp := rr.Result() + defer resp.Body.Close() c.Check(resp.StatusCode, qt.Equals, http.StatusNotFound) } @@ -55,6 +56,7 @@ func TestDashboardRedirect(t *testing.T) { c.Assert(err, qt.IsNil) hnd.ServeHTTP(rr, req) resp := rr.Result() + defer resp.Body.Close() c.Check(resp.StatusCode, qt.Equals, http.StatusPermanentRedirect) c.Check(resp.Header.Get("Location"), qt.Equals, "https://example.com/dashboard") } @@ -68,6 +70,7 @@ func TestInvalidLocation(t *testing.T) { c.Assert(err, qt.IsNil) hnd.ServeHTTP(rr, req) resp := rr.Result() + defer resp.Body.Close() c.Check(resp.StatusCode, qt.Equals, http.StatusNotFound) } @@ -75,7 +78,7 @@ func TestLocationNotDirectory(t *testing.T) { c := qt.New(t) dir := c.TempDir() - err := os.WriteFile(filepath.Join(dir, "test"), []byte(testFile), 0444) + err := os.WriteFile(filepath.Join(dir, "test"), []byte(testFile), 0600) c.Assert(err, qt.Equals, nil) hnd := dashboard.Handler(context.Background(), filepath.Join(dir, "test"), "http://jimm.canonical.com") @@ -84,5 +87,6 @@ func TestLocationNotDirectory(t *testing.T) { c.Assert(err, qt.IsNil) hnd.ServeHTTP(rr, req) resp := rr.Result() + defer resp.Body.Close() c.Check(resp.StatusCode, qt.Equals, http.StatusNotFound) } diff --git a/internal/db/applicationoffer.go b/internal/db/applicationoffer.go index 17995698d..3f5312436 100644 --- a/internal/db/applicationoffer.go +++ b/internal/db/applicationoffer.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package db @@ -48,9 +48,18 @@ func (d *Database) UpdateApplicationOffer(ctx context.Context, offer *dbmodel.Ap db := d.DB.WithContext(ctx) err = db.Transaction(func(tx *gorm.DB) error { tx.Omit("Connections", "Endpoints", "Spaces").Save(offer) - tx.Model(offer).Association("Connections").Replace(offer.Connections) - tx.Model(offer).Association("Endpoints").Replace(offer.Endpoints) - tx.Model(offer).Association("Spaces").Replace(offer.Spaces) + err = tx.Model(offer).Association("Connections").Replace(offer.Connections) + if err != nil { + return err + } + err = tx.Model(offer).Association("Endpoints").Replace(offer.Endpoints) + if err != nil { + return err + } + err = tx.Model(offer).Association("Spaces").Replace(offer.Spaces) + if err != nil { + return err + } return tx.Error }) if err != nil { @@ -78,11 +87,12 @@ func (d *Database) GetApplicationOffer(ctx context.Context, offer *dbmodel.Appli defer servermon.ErrorCounter(servermon.DBQueryErrorCount, &err, string(op)) db := d.DB.WithContext(ctx) - if offer.UUID != "" { + switch { + case offer.UUID != "": db = db.Where("uuid = ?", offer.UUID) - } else if offer.URL != "" { + case offer.URL != "": db = db.Where("url = ?", offer.URL) - } else { + default: return errors.E(op, "missing offer UUID or URL") } db = db.Preload("Connections") diff --git a/internal/db/applicationoffer_test.go b/internal/db/applicationoffer_test.go index a3e20f46d..341631d3e 100644 --- a/internal/db/applicationoffer_test.go +++ b/internal/db/applicationoffer_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package db_test diff --git a/internal/db/audit.go b/internal/db/audit.go index 921008357..405273ff5 100644 --- a/internal/db/audit.go +++ b/internal/db/audit.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package db diff --git a/internal/db/auditlog_test.go b/internal/db/auditlog_test.go index a7445a007..8bad93484 100644 --- a/internal/db/auditlog_test.go +++ b/internal/db/auditlog_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package db_test diff --git a/internal/db/cloud.go b/internal/db/cloud.go index 726880eeb..59a03c586 100644 --- a/internal/db/cloud.go +++ b/internal/db/cloud.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package db diff --git a/internal/db/cloud_test.go b/internal/db/cloud_test.go index 7e5e3befc..efa8ed950 100644 --- a/internal/db/cloud_test.go +++ b/internal/db/cloud_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package db_test @@ -369,6 +369,7 @@ controllers: Name: "test-cloud-1", } err = s.Database.GetCloud(ctx, &cl) + c.Assert(err, qt.IsNil) crp = cl.Regions[0].Controllers[0] diff --git a/internal/db/cloudcredential.go b/internal/db/cloudcredential.go index d58651bb2..b72c0766f 100644 --- a/internal/db/cloudcredential.go +++ b/internal/db/cloudcredential.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package db diff --git a/internal/db/cloudcredential_test.go b/internal/db/cloudcredential_test.go index 51d00f905..95569e46a 100644 --- a/internal/db/cloudcredential_test.go +++ b/internal/db/cloudcredential_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package db_test @@ -207,6 +207,7 @@ func TestForEachCloudCredentialUnconfiguredDatabase(t *testing.T) { c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeServerConfiguration) } +//nolint:gosec // Thinks hardcoded credentials. const forEachCloudCredentialEnv = `clouds: - name: cloud-1 regions: diff --git a/internal/db/clouddefaults.go b/internal/db/clouddefaults.go index b423f5ae4..2e826b4bc 100644 --- a/internal/db/clouddefaults.go +++ b/internal/db/clouddefaults.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package db diff --git a/internal/db/clouddefaults_test.go b/internal/db/clouddefaults_test.go index 8178190e4..f92d5c5ca 100644 --- a/internal/db/clouddefaults_test.go +++ b/internal/db/clouddefaults_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package db_test diff --git a/internal/db/controller.go b/internal/db/controller.go index 7275fd39c..fe4393afe 100644 --- a/internal/db/controller.go +++ b/internal/db/controller.go @@ -1,14 +1,15 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package db import ( "context" + "gorm.io/gorm/clause" + "github.com/canonical/jimm/v3/internal/dbmodel" "github.com/canonical/jimm/v3/internal/errors" "github.com/canonical/jimm/v3/internal/servermon" - "gorm.io/gorm/clause" ) // AddController stores the controller information. diff --git a/internal/db/controller_test.go b/internal/db/controller_test.go index 84141766b..f34d0eecd 100644 --- a/internal/db/controller_test.go +++ b/internal/db/controller_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package db_test diff --git a/internal/db/db.go b/internal/db/db.go index 1ce9b6640..d06c46e1e 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. // Package db contains routines to store and retrieve data from a database. package db diff --git a/internal/db/db_test.go b/internal/db/db_test.go index 29de79e15..4901dcdd5 100644 --- a/internal/db/db_test.go +++ b/internal/db/db_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package db_test diff --git a/internal/db/errors.go b/internal/db/errors.go index 770baba96..d30a0c1ee 100644 --- a/internal/db/errors.go +++ b/internal/db/errors.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package db @@ -21,8 +21,8 @@ func dbError(err error) error { if err == gorm.ErrRecordNotFound { code = errors.CodeNotFound } - switch e := err.(type) { - case *pgconn.PgError: + + if e, ok := err.(*pgconn.PgError); ok { if e.Code == pgUniqueViolation { code = errors.CodeAlreadyExists } diff --git a/internal/db/export_test.go b/internal/db/export_test.go index 7d8555cc3..d3a632218 100644 --- a/internal/db/export_test.go +++ b/internal/db/export_test.go @@ -1,4 +1,4 @@ -// Copyright 2023 Canonical Ltd. +// Copyright 2024 Canonical. package db diff --git a/internal/db/group.go b/internal/db/group.go index 66b67c7ef..d670a1a1f 100644 --- a/internal/db/group.go +++ b/internal/db/group.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package db @@ -12,9 +12,7 @@ import ( "github.com/canonical/jimm/v3/internal/servermon" ) -var newUUID = func() string { - return uuid.NewString() -} +var newUUID = uuid.NewString // AddGroup adds a new group. func (d *Database) AddGroup(ctx context.Context, name string) (ge *dbmodel.GroupEntry, err error) { diff --git a/internal/db/group_test.go b/internal/db/group_test.go index 22971cd5d..f3b448193 100644 --- a/internal/db/group_test.go +++ b/internal/db/group_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package db_test diff --git a/internal/db/identity.go b/internal/db/identity.go index 322f2bb53..cbf674cee 100644 --- a/internal/db/identity.go +++ b/internal/db/identity.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package db diff --git a/internal/db/identity_test.go b/internal/db/identity_test.go index dd546d0a0..92191af6e 100644 --- a/internal/db/identity_test.go +++ b/internal/db/identity_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package db_test @@ -142,6 +142,7 @@ func (s *dbSuite) TestGetIdentityCloudCredentials(c *qt.C) { c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeNotFound) i, err = dbmodel.NewIdentity("test") + c.Assert(err, qt.IsNil) _, err = s.Database.GetIdentityCloudCredentials(ctx, i, "ec2") c.Check(err, qt.IsNil) diff --git a/internal/db/identitymodeldefaults.go b/internal/db/identitymodeldefaults.go index 6d7032bb0..6ffc6e7df 100644 --- a/internal/db/identitymodeldefaults.go +++ b/internal/db/identitymodeldefaults.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package db diff --git a/internal/db/identitymodeldefaults_test.go b/internal/db/identitymodeldefaults_test.go index 6ea24de5f..2c9422a6f 100644 --- a/internal/db/identitymodeldefaults_test.go +++ b/internal/db/identitymodeldefaults_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package db_test @@ -66,7 +66,7 @@ func TestSetIdentityModelDefaults(t *testing.T) { c.Assert(err, qt.IsNil) c.Assert(j.Database.DB.Create(i).Error, qt.IsNil) - j.Database.SetIdentityModelDefaults(ctx, &dbmodel.IdentityModelDefaults{ + err = j.Database.SetIdentityModelDefaults(ctx, &dbmodel.IdentityModelDefaults{ IdentityName: i.Name, Identity: *i, Defaults: map[string]interface{}{ @@ -74,6 +74,7 @@ func TestSetIdentityModelDefaults(t *testing.T) { "key2": "a test string", }, }) + c.Assert(err, qt.IsNil) defaults := map[string]interface{}{ "key1": float64(42), diff --git a/internal/db/model.go b/internal/db/model.go index 134835481..9053c6ee0 100644 --- a/internal/db/model.go +++ b/internal/db/model.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package db @@ -46,19 +46,20 @@ func (d *Database) GetModel(ctx context.Context, model *dbmodel.Model) (err erro defer servermon.ErrorCounter(servermon.DBQueryErrorCount, &err, string(op)) db := d.DB.WithContext(ctx) - if model.UUID.Valid { + switch { + case model.UUID.Valid: db = db.Where("uuid = ?", model.UUID.String) if model.ControllerID != 0 { db = db.Where("controller_id = ?", model.ControllerID) } - } else if model.ID != 0 { + case model.ID != 0: db = db.Where("id = ?", model.ID) - } else if model.OwnerIdentityName != "" && model.Name != "" { + case model.OwnerIdentityName != "" && model.Name != "": db = db.Where("owner_identity_name = ? AND name = ?", model.OwnerIdentityName, model.Name) - } else if model.ControllerID != 0 { - // TODO(ales): fix ordering of where fields and handle error to represent what is *actually* required. + case model.ControllerID != 0: + // TODO: fix ordering of where fields and handle error to represent what is *actually* required. db = db.Where("controller_id = ?", model.ControllerID) - } else { + default: return errors.E(op, "missing id or uuid", errors.CodeBadRequest) } diff --git a/internal/db/model_test.go b/internal/db/model_test.go index 9becabc4a..ae769b350 100644 --- a/internal/db/model_test.go +++ b/internal/db/model_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package db_test diff --git a/internal/db/pgx_test.go b/internal/db/pgx_test.go index fd1d2b292..7ddaadd36 100644 --- a/internal/db/pgx_test.go +++ b/internal/db/pgx_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package db_test diff --git a/internal/db/rootkeys.go b/internal/db/rootkeys.go index 62f1b4d74..3646b7026 100644 --- a/internal/db/rootkeys.go +++ b/internal/db/rootkeys.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package db diff --git a/internal/db/rootkeys_test.go b/internal/db/rootkeys_test.go index d35060cba..28909a999 100644 --- a/internal/db/rootkeys_test.go +++ b/internal/db/rootkeys_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package db_test @@ -55,7 +55,8 @@ func (s *dbSuite) TestInsertKeyGetKey(c *qt.C) { RootKey: []byte("very secret"), } - s.Database.InsertKey(rk) + err = s.Database.InsertKey(rk) + c.Assert(err, qt.IsNil) rk2, err := s.Database.GetKey([]byte("test-id")) c.Assert(err, qt.IsNil) diff --git a/internal/db/secrets.go b/internal/db/secrets.go index f9b8caeed..7d73b1ccf 100644 --- a/internal/db/secrets.go +++ b/internal/db/secrets.go @@ -1,4 +1,4 @@ -// Copyright 2023 Canonical Ltd. +// Copyright 2024 Canonical. package db @@ -7,14 +7,15 @@ import ( "encoding/json" "time" - "github.com/canonical/jimm/v3/internal/dbmodel" - "github.com/canonical/jimm/v3/internal/errors" - "github.com/canonical/jimm/v3/internal/servermon" "github.com/juju/names/v5" "github.com/juju/zaputil/zapctx" "github.com/lestrrat-go/jwx/v2/jwk" "go.uber.org/zap" "gorm.io/gorm/clause" + + "github.com/canonical/jimm/v3/internal/dbmodel" + "github.com/canonical/jimm/v3/internal/errors" + "github.com/canonical/jimm/v3/internal/servermon" ) const ( @@ -23,12 +24,13 @@ const ( passwordKey = "password" // These constants are used to create the appropriate identifiers for JWKS related data. - jwksKind = "jwks" - jwksPublicKeyTag = "jwksPublicKey" - jwksPrivateKeyTag = "jwksPrivateKey" - jwksExpiryTag = "jwksExpiry" - oauthKind = "oauth" - oauthKeyTag = "oauthKey" + jwksKind = "jwks" + jwksPublicKeyTag = "jwksPublicKey" + jwksPrivateKeyTag = "jwksPrivateKey" + jwksExpiryTag = "jwksExpiry" + oauthKind = "oauth" + oauthKeyTag = "oauthKey" + //nolint:gosec // Thinks credentials hardcoded. oauthSessionStoreSecretTag = "oauthSessionStoreSecret" ) diff --git a/internal/db/secrets_test.go b/internal/db/secrets_test.go index f771d3be8..38baa2776 100644 --- a/internal/db/secrets_test.go +++ b/internal/db/secrets_test.go @@ -1,4 +1,4 @@ -// Copyright 2023 Canonical Ltd. +// Copyright 2024 Canonical. package db_test diff --git a/internal/dbmodel/applicationoffer.go b/internal/dbmodel/applicationoffer.go index 2fbedeb32..7a18ffcfd 100644 --- a/internal/dbmodel/applicationoffer.go +++ b/internal/dbmodel/applicationoffer.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package dbmodel diff --git a/internal/dbmodel/applicationoffer_test.go b/internal/dbmodel/applicationoffer_test.go index cf02158b5..c4d2b5aaf 100644 --- a/internal/dbmodel/applicationoffer_test.go +++ b/internal/dbmodel/applicationoffer_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package dbmodel_test diff --git a/internal/dbmodel/audit.go b/internal/dbmodel/audit.go index 963cb8957..cb7436453 100644 --- a/internal/dbmodel/audit.go +++ b/internal/dbmodel/audit.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package dbmodel diff --git a/internal/dbmodel/audit_test.go b/internal/dbmodel/audit_test.go index 7f1c8fd90..f5a612b9a 100644 --- a/internal/dbmodel/audit_test.go +++ b/internal/dbmodel/audit_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package dbmodel_test diff --git a/internal/dbmodel/cloud.go b/internal/dbmodel/cloud.go index 879238be2..a35c8a385 100644 --- a/internal/dbmodel/cloud.go +++ b/internal/dbmodel/cloud.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package dbmodel diff --git a/internal/dbmodel/cloud_test.go b/internal/dbmodel/cloud_test.go index 68a9e4562..ed1291b30 100644 --- a/internal/dbmodel/cloud_test.go +++ b/internal/dbmodel/cloud_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package dbmodel_test diff --git a/internal/dbmodel/cloudcredential.go b/internal/dbmodel/cloudcredential.go index 0ec0053ea..5c70e7347 100644 --- a/internal/dbmodel/cloudcredential.go +++ b/internal/dbmodel/cloudcredential.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package dbmodel diff --git a/internal/dbmodel/cloudcredential_test.go b/internal/dbmodel/cloudcredential_test.go index 326b97eb3..560c7884a 100644 --- a/internal/dbmodel/cloudcredential_test.go +++ b/internal/dbmodel/cloudcredential_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package dbmodel_test diff --git a/internal/dbmodel/clouddefaults.go b/internal/dbmodel/clouddefaults.go index 3c52814b9..0098f3516 100644 --- a/internal/dbmodel/clouddefaults.go +++ b/internal/dbmodel/clouddefaults.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package dbmodel diff --git a/internal/dbmodel/controller.go b/internal/dbmodel/controller.go index e6800313c..49a44d96c 100644 --- a/internal/dbmodel/controller.go +++ b/internal/dbmodel/controller.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package dbmodel @@ -124,16 +124,17 @@ func (c Controller) ToAPIControllerInfo() apiparams.ControllerInfo { ci.CloudRegion = c.CloudRegion ci.Username = c.AdminIdentityName ci.AgentVersion = c.AgentVersion - if c.UnavailableSince.Valid { + switch { + case c.UnavailableSince.Valid: ci.Status = jujuparams.EntityStatus{ Status: "unavailable", Since: &c.UnavailableSince.Time, } - } else if c.Deprecated { + case c.Deprecated: ci.Status = jujuparams.EntityStatus{ Status: "deprecated", } - } else { + default: ci.Status = jujuparams.EntityStatus{ Status: "available", } diff --git a/internal/dbmodel/controller_test.go b/internal/dbmodel/controller_test.go index 3a191c621..8e3d92e83 100644 --- a/internal/dbmodel/controller_test.go +++ b/internal/dbmodel/controller_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package dbmodel_test diff --git a/internal/dbmodel/gorm_test.go b/internal/dbmodel/gorm_test.go index 73338a747..7de2259b8 100644 --- a/internal/dbmodel/gorm_test.go +++ b/internal/dbmodel/gorm_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package dbmodel_test @@ -18,6 +18,9 @@ import ( // migrations for those objects. func gormDB(t testing.TB) *gorm.DB { database := db.Database{DB: jimmtest.PostgresDB(t, nil)} - database.Migrate(context.Background(), false) + err := database.Migrate(context.Background(), false) + if err != nil { + t.Fail() + } return database.DB } diff --git a/internal/dbmodel/group.go b/internal/dbmodel/group.go index 775740e50..5b2d7c386 100644 --- a/internal/dbmodel/group.go +++ b/internal/dbmodel/group.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package dbmodel diff --git a/internal/dbmodel/group_test.go b/internal/dbmodel/group_test.go index 908b44079..e9b12b849 100644 --- a/internal/dbmodel/group_test.go +++ b/internal/dbmodel/group_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package dbmodel_test diff --git a/internal/dbmodel/identity.go b/internal/dbmodel/identity.go index 5fecb14ae..fc121c04a 100644 --- a/internal/dbmodel/identity.go +++ b/internal/dbmodel/identity.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package dbmodel @@ -106,7 +106,7 @@ func (i Identity) ToJujuUserInfo() jujuparams.UserInfo { var ui jujuparams.UserInfo ui.Username = i.Name ui.DisplayName = i.DisplayName - ui.Access = "" //TODO(Kian) CSS-6040 Handle merging OpenFGA and Postgres information + ui.Access = "" // TODO(Kian) CSS-6040 Handle merging OpenFGA and Postgres information ui.DateCreated = i.CreatedAt if i.LastLogin.Valid { ui.LastConnection = &i.LastLogin.Time diff --git a/internal/dbmodel/identity_test.go b/internal/dbmodel/identity_test.go index 3ab4d7b71..d4d50badb 100644 --- a/internal/dbmodel/identity_test.go +++ b/internal/dbmodel/identity_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package dbmodel_test diff --git a/internal/dbmodel/identitymodeldefaults.go b/internal/dbmodel/identitymodeldefaults.go index 04c966840..32d721750 100644 --- a/internal/dbmodel/identitymodeldefaults.go +++ b/internal/dbmodel/identitymodeldefaults.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package dbmodel diff --git a/internal/dbmodel/model.go b/internal/dbmodel/model.go index 07f338ec5..73f7fb505 100644 --- a/internal/dbmodel/model.go +++ b/internal/dbmodel/model.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package dbmodel diff --git a/internal/dbmodel/model_test.go b/internal/dbmodel/model_test.go index e7a362025..536654154 100644 --- a/internal/dbmodel/model_test.go +++ b/internal/dbmodel/model_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package dbmodel_test diff --git a/internal/dbmodel/openfga_stores.go b/internal/dbmodel/openfga_stores.go index 0c28700c8..040810ac0 100644 --- a/internal/dbmodel/openfga_stores.go +++ b/internal/dbmodel/openfga_stores.go @@ -1 +1,2 @@ +// Copyright 2024 Canonical. package dbmodel diff --git a/internal/dbmodel/rootkey.go b/internal/dbmodel/rootkey.go index b0eac5809..a5555e4ae 100644 --- a/internal/dbmodel/rootkey.go +++ b/internal/dbmodel/rootkey.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package dbmodel diff --git a/internal/dbmodel/secrets.go b/internal/dbmodel/secrets.go index 02acf9d1e..1e3dd8f46 100644 --- a/internal/dbmodel/secrets.go +++ b/internal/dbmodel/secrets.go @@ -1,3 +1,4 @@ +// Copyright 2024 Canonical. package dbmodel import "time" diff --git a/internal/dbmodel/sql.go b/internal/dbmodel/sql.go index 829339229..351a2201a 100644 --- a/internal/dbmodel/sql.go +++ b/internal/dbmodel/sql.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package dbmodel diff --git a/internal/dbmodel/types.go b/internal/dbmodel/types.go index 0de054f77..ed307232e 100644 --- a/internal/dbmodel/types.go +++ b/internal/dbmodel/types.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package dbmodel diff --git a/internal/dbmodel/types_test.go b/internal/dbmodel/types_test.go index b6483b56e..8c2ba558e 100644 --- a/internal/dbmodel/types_test.go +++ b/internal/dbmodel/types_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package dbmodel_test diff --git a/internal/dbmodel/version.go b/internal/dbmodel/version.go index cecc92215..e1c686ae6 100644 --- a/internal/dbmodel/version.go +++ b/internal/dbmodel/version.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. // Package dbmodel contains the model objects for the relational storage // database. diff --git a/internal/dbmodel/version_test.go b/internal/dbmodel/version_test.go index 217c57c7f..5552c52f6 100644 --- a/internal/dbmodel/version_test.go +++ b/internal/dbmodel/version_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package dbmodel_test diff --git a/internal/debugapi/api.go b/internal/debugapi/api.go index d0d8e81f9..458670a62 100644 --- a/internal/debugapi/api.go +++ b/internal/debugapi/api.go @@ -1,3 +1,4 @@ +// Copyright 2024 Canonical. package debugapi import ( diff --git a/internal/debugapi/api_test.go b/internal/debugapi/api_test.go index ae13664d0..d638ebef1 100644 --- a/internal/debugapi/api_test.go +++ b/internal/debugapi/api_test.go @@ -1,3 +1,4 @@ +// Copyright 2024 Canonical. package debugapi_test import ( @@ -39,6 +40,7 @@ func TestDebugInfo(t *testing.T) { rr := setupHandlerAndRecorder(c, debugapi.ServerStartTime, "/info") resp := rr.Result() + defer resp.Body.Close() c.Check(resp.StatusCode, qt.Equals, http.StatusOK) buf, err := io.ReadAll(resp.Body) c.Assert(err, qt.IsNil) @@ -54,6 +56,7 @@ func TestDebugStatus(t *testing.T) { rr := setupHandlerAndRecorder(c, debugapi.ServerStartTime, "/status") resp := rr.Result() + defer resp.Body.Close() c.Check(resp.StatusCode, qt.Equals, http.StatusOK) buf, err := io.ReadAll(resp.Body) c.Assert(err, qt.IsNil) @@ -76,6 +79,7 @@ func TestDebugStatusStatusError(t *testing.T) { }), "/status") resp := rr.Result() + defer resp.Body.Close() c.Check(resp.StatusCode, qt.Equals, http.StatusOK) buf, err := io.ReadAll(resp.Body) c.Assert(err, qt.IsNil) diff --git a/internal/discharger/discharger.go b/internal/discharger/discharger.go index d06a529d9..1bd9e42be 100644 --- a/internal/discharger/discharger.go +++ b/internal/discharger/discharger.go @@ -1,4 +1,4 @@ -// Copyright 2023 Canonical Ltd. +// Copyright 2024 Canonical. package discharger @@ -76,11 +76,11 @@ type MacaroonDischarger struct { } // GetDischargerMux returns a mux that can handle macaroon bakery requests for the provided discharger. -func GetDischargerMux(MacaroonDischarger *MacaroonDischarger, rootPath string) *http.ServeMux { +func GetDischargerMux(macaroonDischarger *MacaroonDischarger, rootPath string) *http.ServeMux { discharger := httpbakery.NewDischarger( httpbakery.DischargerParams{ - Key: &MacaroonDischarger.kp, - Checker: httpbakery.ThirdPartyCaveatCheckerFunc(MacaroonDischarger.CheckThirdPartyCaveat), + Key: &macaroonDischarger.kp, + Checker: httpbakery.ThirdPartyCaveatCheckerFunc(macaroonDischarger.CheckThirdPartyCaveat), }, ) dischargeMux := http.NewServeMux() diff --git a/internal/errors/errors.go b/internal/errors/errors.go index 811be24d3..08fbc7695 100644 --- a/internal/errors/errors.go +++ b/internal/errors/errors.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. // Package errors contains types to help handle errors in the system. package errors diff --git a/internal/errors/errors_test.go b/internal/errors/errors_test.go index 34c51152c..793903b77 100644 --- a/internal/errors/errors_test.go +++ b/internal/errors/errors_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package errors_test @@ -13,7 +13,9 @@ import ( func TestEEmptyArguments(t *testing.T) { c := qt.New(t) - c.Assert(func() { errors.E() }, qt.PanicMatches, `call to errors.E with no arguments`) + c.Assert(func() { + _ = errors.E() + }, qt.PanicMatches, `call to errors.E with no arguments`) } func TestEUnknownType(t *testing.T) { diff --git a/internal/jimm/access.go b/internal/jimm/access.go index ea576f86c..5359180e4 100644 --- a/internal/jimm/access.go +++ b/internal/jimm/access.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package jimm @@ -259,7 +259,7 @@ func (auth *JWTGenerator) MakeLoginToken(ctx context.Context, user *openfga.User for _, cloudRegion := range ctl.CloudRegions { clouds[cloudRegion.CloudRegion.Cloud.ResourceTag()] = true } - for cloudTag, _ := range clouds { + 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)) diff --git a/internal/jimm/access_test.go b/internal/jimm/access_test.go index 60de45081..41d26bdf1 100644 --- a/internal/jimm/access_test.go +++ b/internal/jimm/access_test.go @@ -1,4 +1,4 @@ -// Copyright 2023 Canonical Ltd. +// Copyright 2024 Canonical. package jimm_test @@ -9,11 +9,11 @@ import ( "testing" "time" + "github.com/canonical/ofga" petname "github.com/dustinkirkland/golang-petname" qt "github.com/frankban/quicktest" "github.com/google/uuid" "github.com/juju/juju/core/crossmodel" - jujuparams "github.com/juju/juju/rpc/params" "github.com/juju/juju/state" "github.com/juju/names/v5" @@ -26,30 +26,8 @@ import ( "github.com/canonical/jimm/v3/internal/openfga" ofganames "github.com/canonical/jimm/v3/internal/openfga/names" jimmnames "github.com/canonical/jimm/v3/pkg/names" - "github.com/canonical/ofga" ) -// testAuthenticator is an authenticator implementation intended -// for testing the token generator. -type testAuthenticator struct { - username string - err error -} - -// Authenticate implements the Authenticate method of the Authenticator interface. -func (ta *testAuthenticator) Authenticate(ctx context.Context, req *jujuparams.LoginRequest) (*openfga.User, error) { - if ta.err != nil { - return nil, ta.err - } - i, err := dbmodel.NewIdentity(ta.username) - if err != nil { - return nil, err - } - return &openfga.User{ - Identity: i, - }, nil -} - // testDatabase is a database implementation intended for testing the token generator. type testDatabase struct { ctl dbmodel.Controller @@ -493,19 +471,15 @@ func TestResolveJIMM(t *testing.T) { c := qt.New(t) ctx := context.Background() - ofgaClient, _, _, err := jimmtest.SetupTestOFGAClient(c.Name()) - c.Assert(err, qt.IsNil) - now := time.Now().UTC().Round(time.Millisecond) j := &jimm.JIMM{ UUID: uuid.NewString(), Database: db.Database{ DB: jimmtest.PostgresDB(c, func() time.Time { return now }), }, - OpenFGAClient: ofgaClient, } - err = j.Database.Migrate(ctx, false) + err := j.Database.Migrate(ctx, false) c.Assert(err, qt.IsNil) jimmTag := "controller-jimm" @@ -519,19 +493,15 @@ func TestResolveTupleObjectMapsApplicationOffersUUIDs(t *testing.T) { c := qt.New(t) ctx := context.Background() - ofgaClient, _, _, err := jimmtest.SetupTestOFGAClient(c.Name()) - c.Assert(err, qt.IsNil) - now := time.Now().UTC().Round(time.Millisecond) j := &jimm.JIMM{ UUID: uuid.NewString(), Database: db.Database{ DB: jimmtest.PostgresDB(c, func() time.Time { return now }), }, - OpenFGAClient: ofgaClient, } - err = j.Database.Migrate(ctx, false) + err := j.Database.Migrate(ctx, false) c.Assert(err, qt.IsNil) user, _, controller, model, offer, _, _ := createTestControllerEnvironment(ctx, c, j.Database) @@ -547,19 +517,15 @@ func TestResolveTupleObjectMapsModelUUIDs(t *testing.T) { c := qt.New(t) ctx := context.Background() - ofgaClient, _, _, err := jimmtest.SetupTestOFGAClient(c.Name()) - c.Assert(err, qt.IsNil) - now := time.Now().UTC().Round(time.Millisecond) j := &jimm.JIMM{ UUID: uuid.NewString(), Database: db.Database{ DB: jimmtest.PostgresDB(c, func() time.Time { return now }), }, - OpenFGAClient: ofgaClient, } - err = j.Database.Migrate(ctx, false) + err := j.Database.Migrate(ctx, false) c.Assert(err, qt.IsNil) user, _, controller, model, _, _, _ := createTestControllerEnvironment(ctx, c, j.Database) @@ -575,19 +541,15 @@ func TestResolveTupleObjectMapsControllerUUIDs(t *testing.T) { c := qt.New(t) ctx := context.Background() - ofgaClient, _, _, err := jimmtest.SetupTestOFGAClient(c.Name()) - c.Assert(err, qt.IsNil) - now := time.Now().UTC().Round(time.Millisecond) j := &jimm.JIMM{ UUID: uuid.NewString(), Database: db.Database{ DB: jimmtest.PostgresDB(c, func() time.Time { return now }), }, - OpenFGAClient: ofgaClient, } - err = j.Database.Migrate(ctx, false) + err := j.Database.Migrate(ctx, false) c.Assert(err, qt.IsNil) cloud := dbmodel.Cloud{ @@ -614,19 +576,15 @@ func TestResolveTupleObjectMapsGroups(t *testing.T) { c := qt.New(t) ctx := context.Background() - ofgaClient, _, _, err := jimmtest.SetupTestOFGAClient(c.Name()) - c.Assert(err, qt.IsNil) - now := time.Now().UTC().Round(time.Millisecond) j := &jimm.JIMM{ UUID: uuid.NewString(), Database: db.Database{ DB: jimmtest.PostgresDB(c, func() time.Time { return now }), }, - OpenFGAClient: ofgaClient, } - err = j.Database.Migrate(ctx, false) + err := j.Database.Migrate(ctx, false) c.Assert(err, qt.IsNil) _, err = j.Database.AddGroup(ctx, "myhandsomegroupofdigletts") @@ -649,19 +607,15 @@ func TestResolveTagObjectMapsUsers(t *testing.T) { c := qt.New(t) ctx := context.Background() - ofgaClient, _, _, err := jimmtest.SetupTestOFGAClient(c.Name()) - c.Assert(err, qt.IsNil) - now := time.Now().UTC().Round(time.Millisecond) j := &jimm.JIMM{ UUID: uuid.NewString(), Database: db.Database{ DB: jimmtest.PostgresDB(c, func() time.Time { return now }), }, - OpenFGAClient: ofgaClient, } - err = j.Database.Migrate(ctx, false) + err := j.Database.Migrate(ctx, false) c.Assert(err, qt.IsNil) tag, err := jimm.ResolveTag(j.UUID, &j.Database, "user-alex@canonical.com-werly#member") @@ -673,19 +627,15 @@ func TestResolveTupleObjectHandlesErrors(t *testing.T) { c := qt.New(t) ctx := context.Background() - ofgaClient, _, _, err := jimmtest.SetupTestOFGAClient(c.Name()) - c.Assert(err, qt.IsNil) - now := time.Now().UTC().Round(time.Millisecond) j := &jimm.JIMM{ UUID: uuid.NewString(), Database: db.Database{ DB: jimmtest.PostgresDB(c, func() time.Time { return now }), }, - OpenFGAClient: ofgaClient, } - err = j.Database.Migrate(ctx, false) + err := j.Database.Migrate(ctx, false) c.Assert(err, qt.IsNil) _, _, controller, model, offer, _, _ := createTestControllerEnvironment(ctx, c, j.Database) @@ -934,7 +884,7 @@ func TestRemoveGroupRemovesTuples(t *testing.T) { c.Assert(err, qt.IsNil) tuples := []openfga.Tuple{ - //This tuple should remain as it has no relation to group2 + // This tuple should remain as it has no relation to group2 { Object: ofganames.ConvertTag(user.ResourceTag()), Relation: "member", diff --git a/internal/jimm/admin.go b/internal/jimm/admin.go index 49fc363e4..bd2f79e96 100644 --- a/internal/jimm/admin.go +++ b/internal/jimm/admin.go @@ -1,4 +1,4 @@ -// Copyright 2024 Canonical Ltd. +// Copyright 2024 Canonical. package jimm diff --git a/internal/jimm/applicationoffer.go b/internal/jimm/applicationoffer.go index b72388e22..4b8ce6e8e 100644 --- a/internal/jimm/applicationoffer.go +++ b/internal/jimm/applicationoffer.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package jimm @@ -75,11 +75,9 @@ func (j *JIMM) Offer(ctx context.Context, user *openfga.User, offer AddApplicati err = j.Database.GetApplicationOffer(ctx, &offerCheck) if err == nil { return errors.E(fmt.Sprintf("offer %s already exists, please use a different name", offerURL.String()), errors.CodeAlreadyExists) - } else { - if errors.ErrorCode(err) != errors.CodeNotFound { - // Anything besides Not Found is a problem. - return errors.E(op, err) - } + } else if errors.ErrorCode(err) != errors.CodeNotFound { + // Anything besides Not Found is a problem. + return errors.E(op, err) } api, err := j.dial(ctx, &model.Controller, names.ModelTag{}) @@ -459,8 +457,7 @@ func (j *JIMM) RevokeOfferAccess(ctx context.Context, user *openfga.User, offerU stillHasAccess := false switch targetRelation { case ofganames.AdministratorRelation: - switch currentRelation { - case ofganames.AdministratorRelation: + if currentRelation == ofganames.AdministratorRelation { stillHasAccess = true } case ofganames.ConsumerRelation: diff --git a/internal/jimm/applicationoffer_test.go b/internal/jimm/applicationoffer_test.go index 346fd3439..5ab743ac9 100644 --- a/internal/jimm/applicationoffer_test.go +++ b/internal/jimm/applicationoffer_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package jimm_test @@ -303,8 +303,10 @@ func TestRevokeOfferAccess(t *testing.T) { return env.users[1], env.users[4], env.applicationOffers[0].URL, jujuparams.OfferConsumeAccess }, setup: func(env *environment, client *openfga.OFGAClient) { - openfga.NewUser(&env.users[4], client).SetApplicationOfferAccess(ctx, env.applicationOffers[0].ResourceTag(), ofganames.ConsumerRelation) - openfga.NewUser(&env.users[4], client).SetApplicationOfferAccess(ctx, env.applicationOffers[0].ResourceTag(), ofganames.AdministratorRelation) + err := openfga.NewUser(&env.users[4], client).SetApplicationOfferAccess(ctx, env.applicationOffers[0].ResourceTag(), ofganames.ConsumerRelation) + c.Assert(err, qt.IsNil) + err = openfga.NewUser(&env.users[4], client).SetApplicationOfferAccess(ctx, env.applicationOffers[0].ResourceTag(), ofganames.AdministratorRelation) + c.Assert(err, qt.IsNil) }, expectedError: "unable to completely revoke given access due to other relations.*jimmctl.*", expectedAccessLevelOnError: "admin", @@ -314,8 +316,10 @@ func TestRevokeOfferAccess(t *testing.T) { return env.users[1], env.users[4], env.applicationOffers[0].URL, jujuparams.OfferReadAccess }, setup: func(env *environment, client *openfga.OFGAClient) { - openfga.NewUser(&env.users[4], client).SetApplicationOfferAccess(ctx, env.applicationOffers[0].ResourceTag(), ofganames.ReaderRelation) - openfga.NewUser(&env.users[4], client).SetApplicationOfferAccess(ctx, env.applicationOffers[0].ResourceTag(), ofganames.AdministratorRelation) + err := openfga.NewUser(&env.users[4], client).SetApplicationOfferAccess(ctx, env.applicationOffers[0].ResourceTag(), ofganames.ReaderRelation) + c.Assert(err, qt.IsNil) + err = openfga.NewUser(&env.users[4], client).SetApplicationOfferAccess(ctx, env.applicationOffers[0].ResourceTag(), ofganames.AdministratorRelation) + c.Assert(err, qt.IsNil) }, expectedError: "unable to completely revoke given access due to other relations.*jimmctl.*", expectedAccessLevelOnError: "admin", @@ -325,8 +329,10 @@ func TestRevokeOfferAccess(t *testing.T) { return env.users[1], env.users[4], env.applicationOffers[0].URL, jujuparams.OfferReadAccess }, setup: func(env *environment, client *openfga.OFGAClient) { - openfga.NewUser(&env.users[4], client).SetApplicationOfferAccess(ctx, env.applicationOffers[0].ResourceTag(), ofganames.ReaderRelation) - openfga.NewUser(&env.users[4], client).SetApplicationOfferAccess(ctx, env.applicationOffers[0].ResourceTag(), ofganames.ConsumerRelation) + err := openfga.NewUser(&env.users[4], client).SetApplicationOfferAccess(ctx, env.applicationOffers[0].ResourceTag(), ofganames.ReaderRelation) + c.Assert(err, qt.IsNil) + err = openfga.NewUser(&env.users[4], client).SetApplicationOfferAccess(ctx, env.applicationOffers[0].ResourceTag(), ofganames.ConsumerRelation) + c.Assert(err, qt.IsNil) }, expectedError: "unable to completely revoke given access due to other relations.*jimmctl.*", expectedAccessLevelOnError: "consume", diff --git a/internal/jimm/audit_log.go b/internal/jimm/audit_log.go index 8f2d0c0d7..7bd4d5cab 100644 --- a/internal/jimm/audit_log.go +++ b/internal/jimm/audit_log.go @@ -1,4 +1,4 @@ -// Copyright 2023 Canonical Ltd. +// Copyright 2024 Canonical. package jimm @@ -188,7 +188,7 @@ func calculateNextPollDuration(startingTime time.Time) time.Duration { now := startingTime nineAM := time.Date(now.Year(), now.Month(), now.Day(), pollDuration.Hours, 0, 0, 0, time.UTC) nineAMDuration := nineAM.Sub(now) - d := time.Hour + var d time.Duration // If 9am is behind the current time, i.e., 1pm if nineAMDuration < 0 { // Add 24 hours, flip it to an absolute duration, i.e., -10h == 10h diff --git a/internal/jimm/audit_log_test.go b/internal/jimm/audit_log_test.go index 219bcf2fb..98ec08fcd 100644 --- a/internal/jimm/audit_log_test.go +++ b/internal/jimm/audit_log_test.go @@ -1,4 +1,4 @@ -// Copyright 2023 Canonical Ltd. +// Copyright 2024 Canonical. package jimm_test diff --git a/internal/jimm/cache.go b/internal/jimm/cache.go index f1ee70cb3..01ca55f9c 100644 --- a/internal/jimm/cache.go +++ b/internal/jimm/cache.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package jimm diff --git a/internal/jimm/cache_test.go b/internal/jimm/cache_test.go index 740ae478f..2963888fe 100644 --- a/internal/jimm/cache_test.go +++ b/internal/jimm/cache_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package jimm_test diff --git a/internal/jimm/cloud.go b/internal/jimm/cloud.go index 0ec4a98b6..09e547aa5 100644 --- a/internal/jimm/cloud.go +++ b/internal/jimm/cloud.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package jimm @@ -56,9 +56,7 @@ func (j *JIMM) GetCloud(ctx context.Context, user *openfga.User, tag names.Cloud accessLevel, err := j.GetUserCloudAccess(ctx, user, tag) if err != nil { - if err != nil { - return dbmodel.Cloud{}, errors.E(op, err) - } + return dbmodel.Cloud{}, errors.E(op, err) } switch accessLevel { @@ -408,25 +406,6 @@ func (j *JIMM) AddHostedCloud(ctx context.Context, user *openfga.User, tag names return nil } -func randomController() func(controllers []dbmodel.CloudRegionControllerPriority) (dbmodel.Controller, error) { - return func(controllers []dbmodel.CloudRegionControllerPriority) (dbmodel.Controller, error) { - shuffleRegionControllers(controllers) - return controllers[0].Controller, nil - } -} - -func findController(controllerName string) func(controllers []dbmodel.CloudRegionControllerPriority) (dbmodel.Controller, error) { - return func(controllers []dbmodel.CloudRegionControllerPriority) (dbmodel.Controller, error) { - for _, crp := range controllers { - crp := crp - if crp.Controller.Name == controllerName { - return crp.Controller, nil - } - } - return dbmodel.Controller{}, errors.E("controller not found", errors.CodeNotFound) - } -} - // addControllerCloud creates the hosted cloud defined by the given tag and // jujuparams cloud definition. Admin access to the cloud will be granted // to the user identified by the given user tag. On success diff --git a/internal/jimm/cloud_test.go b/internal/jimm/cloud_test.go index 669c86f1c..2233d50a7 100644 --- a/internal/jimm/cloud_test.go +++ b/internal/jimm/cloud_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package jimm_test @@ -1061,6 +1061,7 @@ func TestGrantCloudAccess(t *testing.T) { Err: tt.dialError, } j := &jimm.JIMM{ + UUID: jimmtest.ControllerUUID, Database: db.Database{ DB: jimmtest.PostgresDB(c, nil), }, @@ -1360,6 +1361,7 @@ func TestRevokeCloudAccess(t *testing.T) { Err: tt.dialError, } j := &jimm.JIMM{ + UUID: jimmtest.ControllerUUID, Database: db.Database{ DB: jimmtest.PostgresDB(c, nil), }, diff --git a/internal/jimm/cloudcredential.go b/internal/jimm/cloudcredential.go index 52587df66..9e710a982 100644 --- a/internal/jimm/cloudcredential.go +++ b/internal/jimm/cloudcredential.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package jimm diff --git a/internal/jimm/cloudcredential_test.go b/internal/jimm/cloudcredential_test.go index 293c761d2..775b48d23 100644 --- a/internal/jimm/cloudcredential_test.go +++ b/internal/jimm/cloudcredential_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package jimm_test @@ -1399,6 +1399,7 @@ func TestGetCloudCredential(t *testing.T) { } } +//nolint:gosec // Thinks credentials hardcoded. const forEachUserCloudCredentialEnv = `clouds: - name: cloud-1 regions: @@ -1521,6 +1522,7 @@ func TestForEachUserCloudCredential(t *testing.T) { } } +//nolint:gosec // Thinks credentials hardcoded. const getCloudCredentialAttributesEnv = `clouds: - name: test-cloud type: gce diff --git a/internal/jimm/clouddefaults.go b/internal/jimm/clouddefaults.go index 467d7ff48..89a6ef568 100644 --- a/internal/jimm/clouddefaults.go +++ b/internal/jimm/clouddefaults.go @@ -1,3 +1,4 @@ +// Copyright 2024 Canonical. package jimm import ( @@ -112,8 +113,5 @@ func (j *JIMM) ModelDefaultsForCloud(ctx context.Context, user *dbmodel.Identity result.Config[k] = d } } - if err != nil { - return jujuparams.ModelDefaultsResult{}, errors.E(op, err) - } return result, nil } diff --git a/internal/jimm/clouddefaults_test.go b/internal/jimm/clouddefaults_test.go index 5d1778d23..7371967ef 100644 --- a/internal/jimm/clouddefaults_test.go +++ b/internal/jimm/clouddefaults_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package jimm_test @@ -133,7 +133,7 @@ func TestSetCloudDefaults(t *testing.T) { } c.Assert(j.Database.DB.Create(&cloud).Error, qt.IsNil) - j.Database.SetCloudDefaults(ctx, &dbmodel.CloudDefaults{ + err = j.Database.SetCloudDefaults(ctx, &dbmodel.CloudDefaults{ IdentityName: user.Name, Identity: *user, CloudID: cloud.ID, @@ -144,6 +144,7 @@ func TestSetCloudDefaults(t *testing.T) { "key2": "a test string", }, }) + c.Assert(err, qt.IsNil) defaults := map[string]interface{}{ "key1": float64(42), @@ -179,7 +180,6 @@ func TestSetCloudDefaults(t *testing.T) { cloud := dbmodel.Cloud{ Name: "test-cloud-1", - Type: "test-provider", Regions: []dbmodel.CloudRegion{{ Name: "test-region", }}, @@ -209,7 +209,6 @@ func TestSetCloudDefaults(t *testing.T) { cloud := dbmodel.Cloud{ Name: "test-cloud-1", - Type: "test-provider", Regions: []dbmodel.CloudRegion{{ Name: "test-region", }}, diff --git a/internal/jimm/controller.go b/internal/jimm/controller.go index f2af329e5..26d6b3e73 100644 --- a/internal/jimm/controller.go +++ b/internal/jimm/controller.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package jimm @@ -228,6 +228,7 @@ func (j *JIMM) EarliestControllerVersion(ctx context.Context) (version.Number, e zap.String("version", controller.AgentVersion), zap.String("controller", controller.Name), ) + //nolint:nilerr // We wish to log without an error returned, TODO: Check with Ales return nil } if v == nil || versionNumber.Compare(*v) < 0 { @@ -244,42 +245,6 @@ func (j *JIMM) EarliestControllerVersion(ctx context.Context) (version.Number, e return *v, nil } -// controllerAccessLevel holds the controller access level for a user. -type controllerAccessLevel string - -const ( - // noAccess allows a user no permissions at all. - noAccess controllerAccessLevel = "" - - // loginAccess allows a user to log-ing into the subject. - loginAccess controllerAccessLevel = "login" - - // superuserAccess allows user unrestricted permissions in the subject. - superuserAccess controllerAccessLevel = "superuser" -) - -// validate returns error if the current is not a valid access level. -func (a controllerAccessLevel) validate() error { - switch a { - case noAccess, loginAccess, superuserAccess: - return nil - } - return errors.E(fmt.Sprintf("invalid access level %q", a)) -} - -func (a controllerAccessLevel) value() int { - switch a { - case noAccess: - return 0 - case loginAccess: - return 1 - case superuserAccess: - return 2 - default: - return -1 - } -} - // GetJimmControllerAccess returns the JIMM controller access level for the // requested user. func (j *JIMM) GetJimmControllerAccess(ctx context.Context, user *openfga.User, tag names.UserTag) (string, error) { @@ -407,7 +372,7 @@ func (j *JIMM) ImportModel(ctx context.Context, user *openfga.User, controllerNa // fetch cloud credential used by the model cloudTag, err := names.ParseCloudTag(modelInfo.CloudTag) if err != nil { - errors.E(op, err) + return errors.E(op, err) } // Note that the model already has a cloud credential configured which it will use when deploying new // applications. JIMM needs some cloud credential reference to be able to import the model so use any @@ -467,7 +432,11 @@ func (j *JIMM) ImportModel(ctx context.Context, user *openfga.User, controllerNa if err != nil { return errors.E(op, err) } - defer modelAPI.ModelWatcherStop(ctx, watcherID) + defer func() { + if err := modelAPI.ModelWatcherStop(ctx, watcherID); err != nil { + zapctx.Error(ctx, "failed to stop model watcher", zap.Error(err)) + } + }() deltas, err := modelAPI.ModelWatcherNext(ctx, watcherID) if err != nil { diff --git a/internal/jimm/controller_test.go b/internal/jimm/controller_test.go index cd8f5f6b3..6d1398192 100644 --- a/internal/jimm/controller_test.go +++ b/internal/jimm/controller_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package jimm_test @@ -63,17 +63,17 @@ func TestAddController(t *testing.T) { "B": 0xb, }, RegionConfig: map[string]map[string]interface{}{ - "eu-west-1": map[string]interface{}{ + "eu-west-1": { "B": 0xb0, "C": "C", }, - "eu-west-2": map[string]interface{}{ + "eu-west-2": { "B": 0xb1, "D": "D", }, }, }, - names.NewCloudTag("k8s"): jujuparams.Cloud{ + names.NewCloudTag("k8s"): { Type: "kubernetes", AuthTypes: []string{"userpass"}, Endpoint: "https://k8s.example.com", @@ -206,7 +206,7 @@ func TestAddControllerWithVault(t *testing.T) { api := &jimmtest.API{ Clouds_: func(context.Context) (map[names.CloudTag]jujuparams.Cloud, error) { clouds := map[names.CloudTag]jujuparams.Cloud{ - names.NewCloudTag("aws"): jujuparams.Cloud{ + names.NewCloudTag("aws"): { Type: "ec2", AuthTypes: []string{"userpass"}, Endpoint: "https://example.com", @@ -229,17 +229,17 @@ func TestAddControllerWithVault(t *testing.T) { "B": 0xb, }, RegionConfig: map[string]map[string]interface{}{ - "eu-west-1": map[string]interface{}{ + "eu-west-1": { "B": 0xb0, "C": "C", }, - "eu-west-2": map[string]interface{}{ + "eu-west-2": { "B": 0xb1, "D": "D", }, }, }, - names.NewCloudTag("k8s"): jujuparams.Cloud{ + names.NewCloudTag("k8s"): { Type: "kubernetes", AuthTypes: []string{"userpass"}, Endpoint: "https://k8s.example.com", @@ -1414,7 +1414,7 @@ func TestInitiateMigration(t *testing.T) { c := qt.New(t) mt1 := names.NewModelTag("00000002-0000-0000-0000-000000000003") - //mt2 := names.NewModelTag("00000002-0000-0000-0000-000000000004") + // mt2 := names.NewModelTag("00000002-0000-0000-0000-000000000004") migrationId1 := uuid.New().String() diff --git a/internal/jimm/credentials/credentials.go b/internal/jimm/credentials/credentials.go index 00cb9141c..b6ffdd0ed 100644 --- a/internal/jimm/credentials/credentials.go +++ b/internal/jimm/credentials/credentials.go @@ -1,4 +1,4 @@ -// Copyright 2023 canonical. +// Copyright 2024 Canonical. // Package credentials provides abstractions/definitions for credential storage // backends and caching mechanisms. diff --git a/internal/jimm/export_test.go b/internal/jimm/export_test.go index 5fa02ac2e..411c81f6e 100644 --- a/internal/jimm/export_test.go +++ b/internal/jimm/export_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package jimm diff --git a/internal/jimm/identitymodeldefaults.go b/internal/jimm/identitymodeldefaults.go index 833b760dc..8bb21f1ee 100644 --- a/internal/jimm/identitymodeldefaults.go +++ b/internal/jimm/identitymodeldefaults.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package jimm diff --git a/internal/jimm/identitymodeldefaults_test.go b/internal/jimm/identitymodeldefaults_test.go index e408d289a..a92d99cae 100644 --- a/internal/jimm/identitymodeldefaults_test.go +++ b/internal/jimm/identitymodeldefaults_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package jimm_test @@ -67,7 +67,7 @@ func TestSetIdentityModelDefaults(t *testing.T) { c.Assert(j.Database.DB.Create(identity).Error, qt.IsNil) - j.Database.SetIdentityModelDefaults(ctx, &dbmodel.IdentityModelDefaults{ + err = j.Database.SetIdentityModelDefaults(ctx, &dbmodel.IdentityModelDefaults{ IdentityName: identity.Name, Identity: *identity, Defaults: map[string]interface{}{ @@ -75,6 +75,7 @@ func TestSetIdentityModelDefaults(t *testing.T) { "key2": "a test string", }, }) + c.Assert(err, qt.IsNil) defaults := map[string]interface{}{ "key1": float64(42), @@ -181,7 +182,7 @@ func TestIdentityModelDefaults(t *testing.T) { c.Assert(j.Database.DB.Create(identity).Error, qt.IsNil) - j.Database.SetIdentityModelDefaults(ctx, &dbmodel.IdentityModelDefaults{ + err = j.Database.SetIdentityModelDefaults(ctx, &dbmodel.IdentityModelDefaults{ IdentityName: identity.Name, Identity: *identity, Defaults: map[string]interface{}{ @@ -190,6 +191,7 @@ func TestIdentityModelDefaults(t *testing.T) { "key3": "a new value", }, }) + c.Assert(err, qt.IsNil) return testConfig{ identity: identity, diff --git a/internal/jimm/jimm.go b/internal/jimm/jimm.go index e19c9e82b..b9ec53e14 100644 --- a/internal/jimm/jimm.go +++ b/internal/jimm/jimm.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. // Package jimm contains the business logic used to manage clouds, // cloudcredentials and models. diff --git a/internal/jimm/jimm_test.go b/internal/jimm/jimm_test.go index 99973d0bb..867cbb9b9 100644 --- a/internal/jimm/jimm_test.go +++ b/internal/jimm/jimm_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package jimm_test diff --git a/internal/jimm/model.go b/internal/jimm/model.go index c4034d5ff..1d2ee823a 100644 --- a/internal/jimm/model.go +++ b/internal/jimm/model.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package jimm @@ -999,11 +999,6 @@ func (j *JIMM) RevokeModelAccess(ctx context.Context, user *openfga.User, mt nam func (j *JIMM) DestroyModel(ctx context.Context, user *openfga.User, mt names.ModelTag, destroyStorage, force *bool, maxWait, timeout *time.Duration) error { const op = errors.Op("jimm.DestroyModel") - if destroyStorage != nil { - } - if force != nil { - } - err := j.doModelAdmin(ctx, user, mt, func(m *dbmodel.Model, api API) error { if err := api.DestroyModel(ctx, mt, destroyStorage, force, maxWait, timeout); err != nil { return err diff --git a/internal/jimm/model_status_parser.go b/internal/jimm/model_status_parser.go index 76f235c2b..fdd25bb0d 100644 --- a/internal/jimm/model_status_parser.go +++ b/internal/jimm/model_status_parser.go @@ -1,3 +1,4 @@ +// Copyright 2024 Canonical. package jimm import ( @@ -8,7 +9,7 @@ import ( jujucmd "github.com/juju/cmd/v3" "github.com/juju/juju/cmd/juju/status" "github.com/juju/juju/cmd/juju/storage" - rpcparams "github.com/juju/juju/rpc/params" + jujuparams "github.com/juju/juju/rpc/params" "github.com/juju/names/v5" "github.com/juju/zaputil/zapctx" "go.uber.org/zap" @@ -161,7 +162,7 @@ func (f *formatterParamsRetriever) dialModel(ctx context.Context) error { // getModelStatus calls the FullStatus facade to return the full status for the current model // loaded in the formatterParamsRetriever. -func (f *formatterParamsRetriever) getModelStatus(ctx context.Context) (*rpcparams.FullStatus, error) { +func (f *formatterParamsRetriever) getModelStatus(ctx context.Context) (*jujuparams.FullStatus, error) { modelStatus, err := f.api.Status(ctx, nil) if err != nil { zapctx.Error(ctx, "failed to call FullStatus", zap.String("controller-uuid", f.model.Controller.UUID), zap.String("model-uuid", f.model.UUID.String), zap.Error(err)) @@ -199,17 +200,17 @@ func newStorageListAPI(ctx context.Context, api API) storageListAPI { } // ListStorageDetails implements storage.StorageListAPI. (From Juju) -func (s *storageListAPI) ListStorageDetails() ([]rpcparams.StorageDetails, error) { +func (s *storageListAPI) ListStorageDetails() ([]jujuparams.StorageDetails, error) { return s.api.ListStorageDetails(s.ctx) } // ListFilesystems implements storage.StorageListAPI. (From Juju) -func (s *storageListAPI) ListFilesystems(machines []string) ([]rpcparams.FilesystemDetailsListResult, error) { +func (s *storageListAPI) ListFilesystems(machines []string) ([]jujuparams.FilesystemDetailsListResult, error) { return s.api.ListFilesystems(s.ctx, machines) } // ListVolumes implements storage.StorageListAPI. (From Juju) -func (s *storageListAPI) ListVolumes(machines []string) ([]rpcparams.VolumeDetailsListResult, error) { +func (s *storageListAPI) ListVolumes(machines []string) ([]jujuparams.VolumeDetailsListResult, error) { return s.api.ListVolumes(s.ctx, machines) } diff --git a/internal/jimm/model_status_parser_test.go b/internal/jimm/model_status_parser_test.go index f933c196c..af10c096f 100644 --- a/internal/jimm/model_status_parser_test.go +++ b/internal/jimm/model_status_parser_test.go @@ -1,3 +1,4 @@ +// Copyright 2024 Canonical. package jimm_test import ( diff --git a/internal/jimm/model_test.go b/internal/jimm/model_test.go index d1d6f58e4..01b7be8d1 100644 --- a/internal/jimm/model_test.go +++ b/internal/jimm/model_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package jimm_test @@ -1991,6 +1991,7 @@ func TestGrantModelAccess(t *testing.T) { Err: tt.dialError, } j := &jimm.JIMM{ + UUID: jimmtest.ControllerUUID, Database: db.Database{ DB: jimmtest.PostgresDB(c, nil), }, @@ -2710,6 +2711,7 @@ func TestRevokeModelAccess(t *testing.T) { Err: tt.dialError, } j := &jimm.JIMM{ + UUID: jimmtest.ControllerUUID, Database: db.Database{ DB: jimmtest.PostgresDB(c, nil), }, @@ -3283,6 +3285,7 @@ func TestValidateModelUpgrade(t *testing.T) { } } +//nolint:gosec // Thinks credentials hardcoded. const updateModelCredentialTestEnv = `clouds: - name: test-cloud type: test-provider diff --git a/internal/jimm/modelsummary.go b/internal/jimm/modelsummary.go index 548dd5362..4910521cc 100644 --- a/internal/jimm/modelsummary.go +++ b/internal/jimm/modelsummary.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package jimm diff --git a/internal/jimm/modelsummary_test.go b/internal/jimm/modelsummary_test.go index 9d3639afe..fdd559853 100644 --- a/internal/jimm/modelsummary_test.go +++ b/internal/jimm/modelsummary_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package jimm_test diff --git a/internal/jimm/monitoring.go b/internal/jimm/monitoring.go index 669e80220..dc84f0dd3 100644 --- a/internal/jimm/monitoring.go +++ b/internal/jimm/monitoring.go @@ -1,3 +1,4 @@ +// Copyright 2024 Canonical. package jimm import ( @@ -15,7 +16,7 @@ import ( // managed by JIMM as well as how many model each controller manages. func (j *JIMM) UpdateMetrics(ctx context.Context) { controllerCount := 0 - j.Database.ForEachController(ctx, func(c *dbmodel.Controller) error { + err := j.Database.ForEachController(ctx, func(c *dbmodel.Controller) error { controllerCount++ modelGauge, err := servermon.ModelCount.GetMetricWith(prometheus.Labels{"controller": c.Name}) if err != nil { @@ -30,5 +31,8 @@ func (j *JIMM) UpdateMetrics(ctx context.Context) { modelGauge.Set(float64(count)) return nil }) + if err != nil { + zapctx.Error(ctx, "update metrics failed", zap.Error(err)) + } servermon.ControllerCount.Set(float64(controllerCount)) } diff --git a/internal/jimm/purge_logs.go b/internal/jimm/purge_logs.go index 43291596a..c5a891ade 100644 --- a/internal/jimm/purge_logs.go +++ b/internal/jimm/purge_logs.go @@ -1,4 +1,4 @@ -// Copyright 2023 Canonical Ltd. +// Copyright 2024 Canonical. package jimm @@ -6,10 +6,11 @@ import ( "context" "time" - "github.com/canonical/jimm/v3/internal/errors" - "github.com/canonical/jimm/v3/internal/openfga" "github.com/juju/zaputil/zapctx" "go.uber.org/zap" + + "github.com/canonical/jimm/v3/internal/errors" + "github.com/canonical/jimm/v3/internal/openfga" ) // PurgeLogs removes all audit logs before the given timestamp. Only JIMM diff --git a/internal/jimm/runner.go b/internal/jimm/runner.go index dad9d66ff..524408c74 100644 --- a/internal/jimm/runner.go +++ b/internal/jimm/runner.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package jimm diff --git a/internal/jimm/runner_internal_test.go b/internal/jimm/runner_internal_test.go index c5520186b..ff925794c 100644 --- a/internal/jimm/runner_internal_test.go +++ b/internal/jimm/runner_internal_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package jimm diff --git a/internal/jimm/service_account.go b/internal/jimm/service_account.go index 909baeb87..2f179dbca 100644 --- a/internal/jimm/service_account.go +++ b/internal/jimm/service_account.go @@ -1,4 +1,4 @@ -// Copyright 2024 Canonical Ltd. +// Copyright 2024 Canonical. package jimm @@ -6,14 +6,15 @@ import ( "context" "fmt" - "github.com/canonical/jimm/v3/internal/errors" - "github.com/canonical/jimm/v3/internal/openfga" - ofganames "github.com/canonical/jimm/v3/internal/openfga/names" - jimmnames "github.com/canonical/jimm/v3/pkg/names" jujuparams "github.com/juju/juju/rpc/params" "github.com/juju/names/v5" "github.com/juju/zaputil/zapctx" "go.uber.org/zap" + + "github.com/canonical/jimm/v3/internal/errors" + "github.com/canonical/jimm/v3/internal/openfga" + ofganames "github.com/canonical/jimm/v3/internal/openfga/names" + jimmnames "github.com/canonical/jimm/v3/pkg/names" ) // AddServiceAccount checks that no one owns the service account yet diff --git a/internal/jimm/service_account_test.go b/internal/jimm/service_account_test.go index 3c8cdefa5..6c3c4aaf9 100644 --- a/internal/jimm/service_account_test.go +++ b/internal/jimm/service_account_test.go @@ -1,4 +1,4 @@ -// Copyright 2024 Canonical Ltd. +// Copyright 2024 Canonical. package jimm_test diff --git a/internal/jimm/user.go b/internal/jimm/user.go index f406eb912..ba9e8165e 100644 --- a/internal/jimm/user.go +++ b/internal/jimm/user.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package jimm diff --git a/internal/jimm/user_test.go b/internal/jimm/user_test.go index e434632d6..956c3da5c 100644 --- a/internal/jimm/user_test.go +++ b/internal/jimm/user_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package jimm_test @@ -24,7 +24,7 @@ func TestGetUser(t *testing.T) { client, _, _, err := jimmtest.SetupTestOFGAClient(c.Name()) c.Assert(err, qt.IsNil) db := &db.Database{ - DB: jimmtest.PostgresDB(c, func() time.Time { return time.Now() }), + DB: jimmtest.PostgresDB(c, time.Now), } j := &jimm.JIMM{ diff --git a/internal/jimm/watcher.go b/internal/jimm/watcher.go index 6efb27bff..006817a0a 100644 --- a/internal/jimm/watcher.go +++ b/internal/jimm/watcher.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package jimm @@ -237,7 +237,11 @@ func (w *Watcher) watchController(ctx context.Context, ctl *dbmodel.Controller) if err != nil { return errors.E(op, err) } - defer api.AllModelWatcherStop(ctx, id) + defer func() { + if err := api.AllModelWatcherStop(ctx, id); err != nil { + zapctx.Error(ctx, "failed to stop all model watcher", zap.Error(err)) + } + }() checkDyingModel := func(m *dbmodel.Model) error { if m.Life == state.Dying.String() || m.Life == state.Dead.String() { @@ -284,16 +288,17 @@ func (w *Watcher) watchController(ctx context.Context, ctl *dbmodel.Controller) ControllerID: ctl.ID, } err := w.Database.GetModel(ctx, &m) - if err == nil { + switch { + case err == nil: st := modelState{ id: m.ID, machines: make(map[string]int64), units: make(map[string]bool), } modelStates[uuid] = &st - } else if errors.ErrorCode(err) == errors.CodeNotFound { + case errors.ErrorCode(err) == errors.CodeNotFound: modelStates[uuid] = nil - } else { + default: zapctx.Error(ctx, "cannot get model", zap.Error(err)) } return modelStates[uuid] @@ -374,7 +379,11 @@ func (w *Watcher) watchAllModelSummaries(ctx context.Context, ctl *dbmodel.Contr if err != nil { return errors.E(op, err) } - defer api.ModelSummaryWatcherStop(ctx, id) + defer func() { + if err := api.ModelSummaryWatcherStop(ctx, id); err != nil { + zapctx.Error(ctx, "failed to stop model summary watcher", zap.Error(err)) + } + }() // modelIDs contains the set of models running on the // controller that JIMM is interested in. diff --git a/internal/jimm/watcher_test.go b/internal/jimm/watcher_test.go index b39981b5f..31f9a1bc3 100644 --- a/internal/jimm/watcher_test.go +++ b/internal/jimm/watcher_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package jimm_test diff --git a/internal/jimmhttp/auth_handler.go b/internal/jimmhttp/auth_handler.go index e8aa589e2..d8590122a 100644 --- a/internal/jimmhttp/auth_handler.go +++ b/internal/jimmhttp/auth_handler.go @@ -1,3 +1,4 @@ +// Copyright 2024 Canonical. package jimmhttp import ( @@ -247,5 +248,8 @@ func writeError(ctx context.Context, w http.ResponseWriter, status int, err erro if err != nil { errMsg = " - " + err.Error() } - w.Write([]byte(http.StatusText(status) + errMsg)) + _, err = w.Write([]byte(http.StatusText(status) + errMsg)) + if err != nil { + zapctx.Error(ctx, "failed to write status text error", zap.Error(err)) + } } diff --git a/internal/jimmhttp/auth_handler_test.go b/internal/jimmhttp/auth_handler_test.go index c81942a9c..24f3f4b22 100644 --- a/internal/jimmhttp/auth_handler_test.go +++ b/internal/jimmhttp/auth_handler_test.go @@ -1,3 +1,4 @@ +// Copyright 2024 Canonical. package jimmhttp_test import ( @@ -24,7 +25,7 @@ import ( func setupDbAndSessionStore(c *qt.C) (*db.Database, sessions.Store) { // Setup db ahead of time so we have access to session store db := &db.Database{ - DB: jimmtest.PostgresDB(c, func() time.Time { return time.Now() }), + DB: jimmtest.PostgresDB(c, time.Now), } c.Assert(db.Migrate(context.Background(), false), qt.IsNil) @@ -130,8 +131,10 @@ func TestCallbackFailsNoState(t *testing.T) { c.Assert(err, qt.IsNil) defer s.Close() - callbackURL := s.URL + jimmhttp.AuthResourceBasePath + jimmhttp.CallbackEndpoint - res, err := http.Get(callbackURL) + u, err := url.Parse(s.URL) + c.Assert(err, qt.IsNil) + u = u.JoinPath(jimmhttp.AuthResourceBasePath, jimmhttp.CallbackEndpoint) + res, err := http.Get(u.String()) c.Assert(err, qt.IsNil) defer res.Body.Close() diff --git a/internal/jimmhttp/handler.go b/internal/jimmhttp/handler.go index 079b48210..41e472bd5 100644 --- a/internal/jimmhttp/handler.go +++ b/internal/jimmhttp/handler.go @@ -1,3 +1,4 @@ +// Copyright 2024 Canonical. package jimmhttp import ( diff --git a/internal/jimmhttp/http.go b/internal/jimmhttp/http.go index 8b2d3fdc4..49d807cb2 100644 --- a/internal/jimmhttp/http.go +++ b/internal/jimmhttp/http.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. // Package jimmhttp contains utilities for HTTP connections. package jimmhttp diff --git a/internal/jimmhttp/http_test.go b/internal/jimmhttp/http_test.go index a0a3c1f0e..56185c934 100644 --- a/internal/jimmhttp/http_test.go +++ b/internal/jimmhttp/http_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package jimmhttp_test @@ -62,6 +62,7 @@ func TestStripPathElement(t *testing.T) { hnd.ServeHTTP(rr, req) resp := rr.Result() + defer resp.Body.Close() c.Check(resp.StatusCode, qt.Equals, http.StatusOK) }) } diff --git a/internal/jimmhttp/websocket.go b/internal/jimmhttp/websocket.go index e14cb7b50..ce5731b03 100644 --- a/internal/jimmhttp/websocket.go +++ b/internal/jimmhttp/websocket.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package jimmhttp diff --git a/internal/jimmhttp/websocket_test.go b/internal/jimmhttp/websocket_test.go index c39a597bf..6a77a738e 100644 --- a/internal/jimmhttp/websocket_test.go +++ b/internal/jimmhttp/websocket_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package jimmhttp_test @@ -30,8 +30,9 @@ func TestWSHandler(t *testing.T) { c.Cleanup(srv.Close) var d websocket.Dialer - conn, _, err := d.Dial("ws"+strings.TrimPrefix(srv.URL, "http"), nil) + conn, resp, err := d.Dial("ws"+strings.TrimPrefix(srv.URL, "http"), nil) c.Assert(err, qt.IsNil) + defer resp.Body.Close() err = conn.WriteMessage(websocket.TextMessage, []byte("test!")) c.Assert(err, qt.IsNil) @@ -77,8 +78,9 @@ func TestWSHandlerPanic(t *testing.T) { c.Cleanup(srv.Close) var d websocket.Dialer - conn, _, err := d.Dial("ws"+strings.TrimPrefix(srv.URL, "http"), nil) + conn, resp, err := d.Dial("ws"+strings.TrimPrefix(srv.URL, "http"), nil) c.Assert(err, qt.IsNil) + defer resp.Body.Close() _, _, err = conn.ReadMessage() c.Assert(err, qt.ErrorMatches, `websocket: close 1011 \(internal server error\): test`) @@ -104,8 +106,9 @@ func TestWSHandlerNilServer(t *testing.T) { c.Cleanup(srv.Close) var d websocket.Dialer - conn, _, err := d.Dial("ws"+strings.TrimPrefix(srv.URL, "http"), nil) + conn, resp, err := d.Dial("ws"+strings.TrimPrefix(srv.URL, "http"), nil) c.Assert(err, qt.IsNil) + defer resp.Body.Close() _, _, err = conn.ReadMessage() c.Assert(err, qt.ErrorMatches, `websocket: close 1000 \(normal\)`) @@ -132,10 +135,11 @@ func TestWSHandlerAuthFailsServer(t *testing.T) { c.Cleanup(srv.Close) var d websocket.Dialer - conn, _, err := d.Dial("ws"+strings.TrimPrefix(srv.URL, "http"), http.Header{ + conn, resp, err := d.Dial("ws"+strings.TrimPrefix(srv.URL, "http"), http.Header{ "Cookie": []string{auth.SessionName + "=naughty_cookie"}, }) c.Assert(err, qt.IsNil) + defer resp.Body.Close() _, _, err = conn.ReadMessage() c.Assert(err, qt.ErrorMatches, `websocket: close 1011 \(internal server error\): authentication failed`) diff --git a/internal/jimmjwx/export_test.go b/internal/jimmjwx/export_test.go index baebf4705..2217aaa23 100644 --- a/internal/jimmjwx/export_test.go +++ b/internal/jimmjwx/export_test.go @@ -1,3 +1,4 @@ +// Copyright 2024 Canonical. package jimmjwx var ( diff --git a/internal/jimmjwx/jimmjwx.go b/internal/jimmjwx/jimmjwx.go index afc6bf2cf..30e973e0f 100644 --- a/internal/jimmjwx/jimmjwx.go +++ b/internal/jimmjwx/jimmjwx.go @@ -1,4 +1,4 @@ -// Copyright 2023 canonical. +// Copyright 2024 Canonical. // Package jimmjwx provides utility functions for JOSE (Javascript Object Signing and Encryption) within // JIMM. It currently supports the following: diff --git a/internal/jimmjwx/jwks.go b/internal/jimmjwx/jwks.go index fa448e17e..2657d66aa 100644 --- a/internal/jimmjwx/jwks.go +++ b/internal/jimmjwx/jwks.go @@ -1,4 +1,4 @@ -// Copyright 2023 canonical. +// Copyright 2024 Canonical. package jimmjwx @@ -66,7 +66,9 @@ func rotateJWKS(ctx context.Context, credStore credentials.CredentialStore, init zapctx.Debug(ctx, "setting initial expiry", zap.Time("time", initialExpiryTime)) err = putJwks(initialExpiryTime) if err != nil { - credStore.CleanupJWKS(ctx) + if jwksErr := credStore.CleanupJWKS(ctx); jwksErr != nil { + zapctx.Error(ctx, "failed to cleanup jwks", zap.Error(jwksErr)) + } return errors.E(err) } } else { @@ -77,7 +79,9 @@ func rotateJWKS(ctx context.Context, credStore credentials.CredentialStore, init // components exist from the previous failed expiry attempt. err = putJwks(time.Now().UTC().AddDate(0, 3, 0)) if err != nil { - credStore.CleanupJWKS(ctx) + if jwksErr := credStore.CleanupJWKS(ctx); jwksErr != nil { + zapctx.Error(ctx, "failed to cleanup jwks", zap.Error(jwksErr)) + } return errors.E(err) } zapctx.Debug(ctx, "set a new JWKS", zap.String("expiry", expires.String())) diff --git a/internal/jimmjwx/jwks_test.go b/internal/jimmjwx/jwks_test.go index 76f91531f..cc9a48fd6 100644 --- a/internal/jimmjwx/jwks_test.go +++ b/internal/jimmjwx/jwks_test.go @@ -1,3 +1,4 @@ +// Copyright 2024 Canonical. package jimmjwx_test import ( diff --git a/internal/jimmjwx/jwt.go b/internal/jimmjwx/jwt.go index 4184aeb05..f9f5b3045 100644 --- a/internal/jimmjwx/jwt.go +++ b/internal/jimmjwx/jwt.go @@ -1,4 +1,4 @@ -// Copyright 2023 Canonical Ltd. +// Copyright 2024 Canonical. package jimmjwx @@ -8,8 +8,6 @@ import ( "encoding/pem" "time" - "github.com/canonical/jimm/v3/internal/errors" - "github.com/canonical/jimm/v3/internal/jimm/credentials" "github.com/google/uuid" "github.com/hashicorp/golang-lru/v2/expirable" "github.com/juju/zaputil/zapctx" @@ -17,6 +15,9 @@ import ( "github.com/lestrrat-go/jwx/v2/jwk" "github.com/lestrrat-go/jwx/v2/jwt" "go.uber.org/zap" + + "github.com/canonical/jimm/v3/internal/errors" + "github.com/canonical/jimm/v3/internal/jimm/credentials" ) type JWTServiceParams struct { @@ -96,7 +97,7 @@ func NewJWTService(p JWTServiceParams) *JWTService { // and instead, a new JWT will be issued each time containing the required claims for // authz. func (j *JWTService) NewJWT(ctx context.Context, params JWTParams) ([]byte, error) { - jti, err := j.generateJTI(ctx) + jti, err := j.generateJTI() if err != nil { return nil, err } @@ -133,8 +134,13 @@ func (j *JWTService) NewJWT(ctx context.Context, params JWTParams) ([]byte, erro return nil, err } - signingKey.Set(jwk.AlgorithmKey, jwa.RS256) - signingKey.Set(jwk.KeyIDKey, pubKey.KeyID()) + if err := signingKey.Set(jwk.AlgorithmKey, jwa.RS256); err != nil { + return nil, err + } + + if err := signingKey.Set(jwk.KeyIDKey, pubKey.KeyID()); err != nil { + return nil, err + } token, err := jwt.NewBuilder(). Audience([]string{params.Controller}). @@ -164,7 +170,7 @@ func (j *JWTService) NewJWT(ctx context.Context, params JWTParams) ([]byte, erro // generateJTI uses a V4 UUID, giving a chance of 1 in 17Billion per year. // This should be good enough (hopefully) for a JWT ID. -func (j *JWTService) generateJTI(ctx context.Context) (string, error) { +func (j *JWTService) generateJTI() (string, error) { id, err := uuid.NewRandom() if err != nil { return "", err diff --git a/internal/jimmjwx/jwt_test.go b/internal/jimmjwx/jwt_test.go index 558289d2a..b17749d0c 100644 --- a/internal/jimmjwx/jwt_test.go +++ b/internal/jimmjwx/jwt_test.go @@ -1,3 +1,4 @@ +// Copyright 2024 Canonical. package jimmjwx_test import ( @@ -7,11 +8,12 @@ import ( "testing" "time" - "github.com/canonical/jimm/v3/internal/jimmjwx" qt "github.com/frankban/quicktest" "github.com/lestrrat-go/iter/arrayiter" "github.com/lestrrat-go/jwx/v2/jwk" "github.com/lestrrat-go/jwx/v2/jwt" + + "github.com/canonical/jimm/v3/internal/jimmjwx" ) func TestRegisterJWKSCacheRegistersTheCacheSuccessfully(t *testing.T) { @@ -158,7 +160,8 @@ func TestCredentialCache(t *testing.T) { ctx := context.Background() set, _, err := jimmjwx.GenerateJWK(ctx) c.Assert(err, qt.IsNil) - store.PutJWKS(ctx, set) + err = store.PutJWKS(ctx, set) + c.Assert(err, qt.IsNil) vaultCache := jimmjwx.NewCredentialCache(store) gotSet, err := vaultCache.Get(ctx) c.Assert(err, qt.IsNil) diff --git a/internal/jimmjwx/utils_test.go b/internal/jimmjwx/utils_test.go index c00e0dd87..f8111f5ea 100644 --- a/internal/jimmjwx/utils_test.go +++ b/internal/jimmjwx/utils_test.go @@ -1,3 +1,4 @@ +// Copyright 2024 Canonical. package jimmjwx_test import ( @@ -68,9 +69,8 @@ func startAndTestRotator(c *qt.C, ctx context.Context, store credentials.Credent time.Sleep(500 * time.Millisecond) continue } - if ks != nil { - break - } + break + } c.Assert(err, qt.IsNil) key, ok := ks.Key(0) diff --git a/internal/jimmtest/api.go b/internal/jimmtest/api.go index f0759e04d..20ba4a5cb 100644 --- a/internal/jimmtest/api.go +++ b/internal/jimmtest/api.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package jimmtest diff --git a/internal/jimmtest/auth.go b/internal/jimmtest/auth.go index b5389f14b..266399455 100644 --- a/internal/jimmtest/auth.go +++ b/internal/jimmtest/auth.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package jimmtest @@ -25,8 +25,10 @@ import ( "github.com/gorilla/sessions" "github.com/juju/juju/api" jujuparams "github.com/juju/juju/rpc/params" + "github.com/juju/zaputil/zapctx" "github.com/lestrrat-go/jwx/v2/jwa" "github.com/lestrrat-go/jwx/v2/jwt" + "go.uber.org/zap" "golang.org/x/oauth2" "github.com/canonical/jimm/v3/internal/auth" @@ -286,7 +288,10 @@ func runBrowserLogin(db *db.Database, sessionStore sessions.Store, username, pas http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { cookieString = r.Header.Get("Cookie") - w.Write([]byte(dashboardResponse)) + if _, err := w.Write([]byte(dashboardResponse)); err != nil { + zapctx.Error(context.Background(), "failed to write dashboard response", zap.Error(err)) + } + }, ), ) diff --git a/internal/jimmtest/cmp.go b/internal/jimmtest/cmp.go index 6bb017204..90db5e518 100644 --- a/internal/jimmtest/cmp.go +++ b/internal/jimmtest/cmp.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package jimmtest diff --git a/internal/jimmtest/env.go b/internal/jimmtest/env.go index 7678dd060..863f94794 100644 --- a/internal/jimmtest/env.go +++ b/internal/jimmtest/env.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package jimmtest @@ -130,7 +130,7 @@ func (u User) addUserRelations(c *qt.C, jimmTag names.ControllerTag, db db.Datab } // addCloudRelations adds permissions the cloud should have and adds permissions for users to the cloud. -func (cl Cloud) addCloudRelations(c *qt.C, jimmTag names.ControllerTag, db db.Database, client *openfga.OFGAClient) { +func (cl Cloud) addCloudRelations(c *qt.C, db db.Database, client *openfga.OFGAClient) { for _, u := range cl.Users { dbUser := cl.env.User(u.User).DBObject(c, db) var relation openfga.Relation @@ -151,7 +151,7 @@ func (cl Cloud) addCloudRelations(c *qt.C, jimmTag names.ControllerTag, db db.Da } // addModelRelations adds permissions the model should have and adds permissions for users to the model. -func (m Model) addModelRelations(c *qt.C, jimmTag names.ControllerTag, db db.Database, client *openfga.OFGAClient) { +func (m Model) addModelRelations(c *qt.C, db db.Database, client *openfga.OFGAClient) { owner := openfga.NewUser(&m.dbo.Owner, client) err := owner.SetModelAccess(context.Background(), m.dbo.ResourceTag(), ofganames.AdministratorRelation) c.Assert(err, qt.IsNil) @@ -179,7 +179,7 @@ func (m Model) addModelRelations(c *qt.C, jimmTag names.ControllerTag, db db.Dat } // addControllerRelations adds permissions the model should have and adds permissions for users to the controller. -func (ctl Controller) addControllerRelations(c *qt.C, jimmTag names.ControllerTag, db db.Database, client *openfga.OFGAClient) { +func (ctl Controller) addControllerRelations(c *qt.C, client *openfga.OFGAClient) { if ctl.dbo.AdminIdentityName != "" { userIdentity, err := dbmodel.NewIdentity(ctl.dbo.AdminIdentityName) c.Assert(err, qt.IsNil) @@ -200,16 +200,17 @@ func (e *Environment) addJIMMRelations(c *qt.C, jimmTag names.ControllerTag, db user.addUserRelations(c, jimmTag, db, client) } for _, controller := range e.Controllers { - client.AddController(context.Background(), jimmTag, controller.dbo.ResourceTag()) + err := client.AddController(context.Background(), jimmTag, controller.dbo.ResourceTag()) + c.Assert(err, qt.IsNil) } for _, cl := range e.Clouds { - cl.addCloudRelations(c, jimmTag, db, client) + cl.addCloudRelations(c, db, client) } for _, m := range e.Models { - m.addModelRelations(c, jimmTag, db, client) + m.addModelRelations(c, db, client) } for _, ctl := range e.Controllers { - ctl.addControllerRelations(c, jimmTag, db, client) + ctl.addControllerRelations(c, client) } } diff --git a/internal/jimmtest/gorm.go b/internal/jimmtest/gorm.go index 6294fc363..9eced56c9 100644 --- a/internal/jimmtest/gorm.go +++ b/internal/jimmtest/gorm.go @@ -1,10 +1,11 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. // Package jimmtest contains useful helpers for testing JIMM. package jimmtest import ( "context" + //nolint:gosec // We're only using sha1 in tests. "crypto/sha1" "encoding/base64" "fmt" @@ -16,12 +17,13 @@ import ( "sync" "time" - "github.com/canonical/jimm/v3/internal/db" - "github.com/canonical/jimm/v3/internal/errors" "github.com/google/uuid" "gorm.io/driver/postgres" "gorm.io/gorm" "gorm.io/gorm/logger" + + "github.com/canonical/jimm/v3/internal/db" + "github.com/canonical/jimm/v3/internal/errors" ) // A Tester is the test interface required by this package. @@ -167,9 +169,10 @@ const maxDatabaseNameLength = 63 // sure no name collisions occur and also future calls with the same suggested // database name results in the same safe name. func computeSafeDatabaseName(suggestedName string) string { - re, _ := regexp.Compile(unsafeCharsPattern) + re := regexp.MustCompile(unsafeCharsPattern) safeName := re.ReplaceAllString(suggestedName, "_") + //nolint:gosec // We're only using sha1 in tests. hasher := sha1.New() // Provide some random chars for the hash. Useful where tests // have the same suite name and same test name. @@ -195,6 +198,7 @@ var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") func randSeq(n int) string { b := make([]rune, n) for i := range b { + //nolint:gosec // We're only using rand.Intn for tests. b[i] = letters[rand.Intn(len(letters))] } return string(b) diff --git a/internal/jimmtest/jimm.go b/internal/jimmtest/jimm.go index 2252c2c32..0b647b43a 100644 --- a/internal/jimmtest/jimm.go +++ b/internal/jimmtest/jimm.go @@ -1,10 +1,12 @@ +// Copyright 2024 Canonical. package jimmtest import ( "time" - jimmsvc "github.com/canonical/jimm/v3/cmd/jimmsrv/service" "github.com/coreos/go-oidc/v3/oidc" + + jimmsvc "github.com/canonical/jimm/v3/cmd/jimmsrv/service" ) // NewTestJimmParams returns a set of JIMM params with sensible defaults diff --git a/internal/jimmtest/jimm_mock.go b/internal/jimmtest/jimm_mock.go index d1c391c97..04991f69a 100644 --- a/internal/jimmtest/jimm_mock.go +++ b/internal/jimmtest/jimm_mock.go @@ -1,4 +1,4 @@ -// Copyright 2023 Canonical Ltd. +// Copyright 2024 Canonical. package jimmtest diff --git a/internal/jimmtest/keycloak.go b/internal/jimmtest/keycloak.go index 19404c143..8a85af2d3 100644 --- a/internal/jimmtest/keycloak.go +++ b/internal/jimmtest/keycloak.go @@ -1,4 +1,4 @@ -// Copyright 2024 Canonical Ltd. +// Copyright 2024 Canonical. package jimmtest @@ -12,10 +12,11 @@ import ( "net/url" "strings" - "github.com/canonical/jimm/v3/internal/errors" "github.com/google/uuid" "github.com/juju/zaputil/zapctx" "go.uber.org/zap" + + "github.com/canonical/jimm/v3/internal/errors" ) // These constants are based on the `docker-compose.yaml` and `local/keycloak/jimm-realm.json` content. @@ -34,7 +35,8 @@ const ( keycloakAdminUsername = "jimm" keycloakAdminPassword = "jimm" keycloakAdminCLIUsername = "admin-cli" - keycloakAdminCLISecret = "DOLcuE5Cd7IxuR7JE4hpAUxaLF7RlAWh" + //nolint:gosec // Thinks credentials exposed. Only used for test. + keycloakAdminCLISecret = "DOLcuE5Cd7IxuR7JE4hpAUxaLF7RlAWh" ) // KeycloakUser represents a basic user created in Keycloak. diff --git a/internal/jimmtest/logging.go b/internal/jimmtest/logging.go index aa0a30011..e4875924b 100644 --- a/internal/jimmtest/logging.go +++ b/internal/jimmtest/logging.go @@ -1,3 +1,4 @@ +// Copyright 2024 Canonical. package jimmtest import ( @@ -51,13 +52,15 @@ func (s *LoggingSuite) setUp(c *gc.C) { // Don't use the default writer for the test logging, which // means we can still get logging output from tests that // replace the default writer. - loggo.RegisterWriter(loggo.DefaultWriterName, discardWriter{}) - loggo.RegisterWriter("loggingsuite", zaputil.NewLoggoWriter(logger)) + err := loggo.RegisterWriter(loggo.DefaultWriterName, discardWriter{}) + c.Assert(err, gc.IsNil) + err = loggo.RegisterWriter("loggingsuite", zaputil.NewLoggoWriter(logger)) + c.Assert(err, gc.IsNil) level := "DEBUG" if envLevel := os.Getenv("TEST_LOGGING_CONFIG"); envLevel != "" { level = envLevel } - err := loggo.ConfigureLoggers(level) + err = loggo.ConfigureLoggers(level) c.Assert(err, gc.Equals, nil) } @@ -71,8 +74,7 @@ type gocheckZapWriter struct { } func (w gocheckZapWriter) Write(buf []byte) (int, error) { - w.c.Output(1, strings.TrimSuffix(string(buf), "\n")) - return len(buf), nil + return len(buf), w.c.Output(1, strings.TrimSuffix(string(buf), "\n")) } func (w gocheckZapWriter) Sync() error { diff --git a/internal/jimmtest/openfga.go b/internal/jimmtest/openfga.go index 696c2be5e..cefc262d5 100644 --- a/internal/jimmtest/openfga.go +++ b/internal/jimmtest/openfga.go @@ -1,3 +1,4 @@ +// Copyright 2024 Canonical. package jimmtest import ( @@ -161,7 +162,14 @@ func TruncateOpenFgaTuples(ctx context.Context) error { return errors.E(err) } defer conn.Close(ctx) - conn.Exec(ctx, "TRUNCATE TABLE tuple;") - conn.Exec(ctx, "TRUNCATE TABLE changelog;") + + if _, err := conn.Exec(ctx, "TRUNCATE TABLE tuple;"); err != nil { + return err + } + + if _, err := conn.Exec(ctx, "TRUNCATE TABLE changelog;"); err != nil { + return err + } + return nil } diff --git a/internal/jimmtest/store.go b/internal/jimmtest/store.go index a110bc36b..61e245ef7 100644 --- a/internal/jimmtest/store.go +++ b/internal/jimmtest/store.go @@ -1,3 +1,4 @@ +// Copyright 2024 Canonical. package jimmtest import ( @@ -93,7 +94,7 @@ func (s *InMemoryCredentialStore) PutControllerCredentials(ctx context.Context, if s.controllerCredentials == nil { s.controllerCredentials = map[string]controllerCredentials{ - controllerName: controllerCredentials{ + controllerName: { username: username, password: password, }, diff --git a/internal/jimmtest/suite.go b/internal/jimmtest/suite.go index 323edcf84..b57a50016 100644 --- a/internal/jimmtest/suite.go +++ b/internal/jimmtest/suite.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package jimmtest diff --git a/internal/jimmtest/vault.go b/internal/jimmtest/vault.go index b886c83c6..3a4b76578 100644 --- a/internal/jimmtest/vault.go +++ b/internal/jimmtest/vault.go @@ -1,12 +1,13 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package jimmtest import ( "encoding/json" - vault_test "github.com/canonical/jimm/v3/local/vault" "github.com/hashicorp/vault/api" + + vault_test "github.com/canonical/jimm/v3/local/vault" ) type fatalF interface { diff --git a/internal/jujuapi/access_control.go b/internal/jujuapi/access_control.go index f045e8d66..5f8faf2d0 100644 --- a/internal/jujuapi/access_control.go +++ b/internal/jujuapi/access_control.go @@ -1,10 +1,9 @@ -// Copyright 2023 canonical. +// Copyright 2024 Canonical. package jujuapi import ( "context" - "regexp" "strconv" "time" @@ -21,32 +20,6 @@ import ( // access_control contains the primary RPC commands for handling ReBAC within JIMM via the JIMM facade itself. -var ( - // Matches juju uris, jimm user/group tags and UUIDs - // Performs a single match and breaks the juju URI into 10 groups, each successive group is XORD to ensure we can run - // this just once. - // The groups are as so: - // [0] - Entire match - // [1] - tag - // [2] - A single "-", ignored - // [3] - Controller name OR user name OR group name - // [4] - A single ":", ignored - // [5] - Controller user / model owner - // [6] - A single "/", ignored - // [7] - Model name - // [8] - A single ".", ignored - // [9] - Application offer name - // [10] - Relation specifier (i.e., #member) - // A complete matcher example would look like so with square-brackets denoting groups and paranthsis denoting index: - // (1)[controller](2)[-](3)[controller-1](4)[:](5)[alice@canonical.com-place](6)[/](7)[model-1](8)[.](9)[offer-1](10)[#relation-specifier]" - // In the case of something like: user-alice@wonderland or group-alices-wonderland#member, it would look like so: - // (1)[user](2)[-](3)[alices@wonderland] - // (1)[group](2)[-](3)[alices-wonderland](10)[#member] - // So if a group, user, UUID, controller name comes in, it will always be index 3 for them - // and if a relation specifier is present, it will always be index 10 - jujuURIMatcher = regexp.MustCompile(`([a-zA-Z0-9]*)(\-|\z)([a-zA-Z0-9-@.]*)(\:|)([a-zA-Z0-9-@]*)(\/|)([a-zA-Z0-9-]*)(\.|)([a-zA-Z0-9-]*)([a-zA-Z#]*|\z)\z`) -) - const ( jimmControllerName = "jimm" ) diff --git a/internal/jujuapi/access_control_test.go b/internal/jujuapi/access_control_test.go index b40d40a34..efa8482c2 100644 --- a/internal/jujuapi/access_control_test.go +++ b/internal/jujuapi/access_control_test.go @@ -1,11 +1,10 @@ -// Copyright 2023 canonical. +// Copyright 2024 Canonical. package jujuapi_test import ( "context" "database/sql" - "strconv" "time" petname "github.com/dustinkirkland/golang-petname" @@ -89,15 +88,17 @@ func (s *accessControlSuite) TestRemoveGroupRemovesTuples(c *gc.C) { user, group, controller, model, _, _, _, client, closeClient := createTestControllerEnvironment(ctx, c, s) defer closeClient() - db.AddGroup(ctx, "test-group2") + _, err := db.AddGroup(ctx, "test-group2") + c.Assert(err, gc.IsNil) + group2 := &dbmodel.GroupEntry{ Name: "test-group2", } - err := db.GetGroup(ctx, group2) + err = db.GetGroup(ctx, group2) c.Assert(err, gc.IsNil) tuples := []openfga.Tuple{ - //This tuple should remain as it has no relation to group2 + // This tuple should remain as it has no relation to group2 { Object: ofganames.ConvertTag(user.ResourceTag()), Relation: "member", @@ -133,7 +134,7 @@ func (s *accessControlSuite) TestRemoveGroupRemovesTuples(c *gc.C) { err = s.JIMM.OpenFGAClient.AddRelation(context.Background(), tuples...) c.Assert(err, gc.IsNil) - //Check user has access to model and controller through group2 + // Check user has access to model and controller through group2 checkResp, err := client.CheckRelation(&apiparams.CheckRelationRequest{Tuple: checkAccessTupleController}) c.Assert(err, gc.IsNil) c.Assert(checkResp.Allowed, gc.Equals, true) @@ -148,7 +149,7 @@ func (s *accessControlSuite) TestRemoveGroupRemovesTuples(c *gc.C) { c.Assert(err, gc.IsNil) c.Assert(len(resp.Tuples), gc.Equals, 13) - //Check user access has been revoked. + // Check user access has been revoked. checkResp, err = client.CheckRelation(&apiparams.CheckRelationRequest{Tuple: checkAccessTupleController}) c.Assert(err, gc.IsNil) c.Assert(checkResp.Allowed, gc.Equals, false) @@ -222,10 +223,6 @@ func createTuple(object, relation, target string) openfga.Tuple { } } -func stringGroupID(id uint) string { - return strconv.FormatUint(uint64(id), 10) -} - // TestAddRelation currently verifies the following test cases, // when new relation control is to be added, please update this comment: // user -> group @@ -249,11 +246,13 @@ func (s *accessControlSuite) TestAddRelation(c *gc.C) { user, group, controller, model, offer, _, _, client, closeClient := createTestControllerEnvironment(ctx, c, s) defer closeClient() - db.AddGroup(ctx, "test-group2") + _, err := db.AddGroup(ctx, "test-group2") + c.Assert(err, gc.IsNil) + group2 := &dbmodel.GroupEntry{ Name: "test-group2", } - err := db.GetGroup(ctx, group2) + err = db.GetGroup(ctx, group2) c.Assert(err, gc.IsNil) c.Assert(err, gc.IsNil) @@ -303,7 +302,7 @@ func (s *accessControlSuite) TestAddRelation(c *gc.C) { err: false, changesType: "controller", }, - //Test user -> group + // Test user -> group { input: tuple{"user-" + user.Name, "member", "group-" + group.Name}, want: createTuple( @@ -314,7 +313,7 @@ func (s *accessControlSuite) TestAddRelation(c *gc.C) { err: false, changesType: "group", }, - //Test username with dots and @ -> group + // Test username with dots and @ -> group { input: tuple{"user-" + "kelvin.lina.test@canonical.com", "member", "group-" + group.Name}, want: createTuple( @@ -325,7 +324,7 @@ func (s *accessControlSuite) TestAddRelation(c *gc.C) { err: false, changesType: "group", }, - //Test group -> controller + // Test group -> controller { input: tuple{"group-" + "test-group#member", "administrator", "controller-" + controller.UUID}, want: createTuple( @@ -336,7 +335,7 @@ func (s *accessControlSuite) TestAddRelation(c *gc.C) { err: false, changesType: "controller", }, - //Test user -> model by name + // Test user -> model by name { input: tuple{"user-" + user.Name, "writer", "model-" + controller.Name + ":" + user.Name + "/" + model.Name}, want: createTuple( @@ -462,6 +461,10 @@ func (s *accessControlSuite) TestAddRelation(c *gc.C) { for i, tc := range tagTests { c.Logf("running test %d", i) if i != 0 { + // Needed due to removing original added relations for this test. + // Without, we cannot add the relations. + // + //nolint:errcheck s.COFGAClient.RemoveRelation(ctx, tc.want) } err := client.AddRelation(&apiparams.AddRelationRequest{ @@ -557,7 +560,7 @@ func (s *accessControlSuite) TestRemoveRelation(c *gc.C) { err: false, changesType: "controller", }, - //Test user -> group + // Test user -> group { toAdd: openfga.Tuple{ Object: ofganames.ConvertTag(user.ResourceTag()), @@ -573,7 +576,7 @@ func (s *accessControlSuite) TestRemoveRelation(c *gc.C) { err: false, changesType: "group", }, - //Test group -> controller + // Test group -> controller { toAdd: openfga.Tuple{ Object: ofganames.ConvertTagWithRelation(group.ResourceTag(), ofganames.MemberRelation), @@ -589,7 +592,7 @@ func (s *accessControlSuite) TestRemoveRelation(c *gc.C) { err: false, changesType: "controller", }, - //Test user -> model by name + // Test user -> model by name { toAdd: openfga.Tuple{ Object: ofganames.ConvertTag(user.ResourceTag()), diff --git a/internal/jujuapi/admin.go b/internal/jujuapi/admin.go index 5b75c2224..ed1b5ade8 100644 --- a/internal/jujuapi/admin.go +++ b/internal/jujuapi/admin.go @@ -1,4 +1,4 @@ -// Copyright 2016 Canonical Ltd. +// Copyright 2024 Canonical. package jujuapi diff --git a/internal/jujuapi/admin_test.go b/internal/jujuapi/admin_test.go index 6f45df7a1..dbaa8e3e1 100644 --- a/internal/jujuapi/admin_test.go +++ b/internal/jujuapi/admin_test.go @@ -1,4 +1,4 @@ -// Copyright 2016 Canonical Ltd. +// Copyright 2024 Canonical. package jujuapi_test @@ -266,6 +266,8 @@ func (s *adminSuite) TestDeviceLogin(c *gc.C) { c.Assert(err, gc.ErrorMatches, "failed to decode token.*") // Test token base64 encoded passes authentication + // + //nolint:gosimple err = conn.APICall("Admin", 4, "", "LoginWithSessionToken", params.LoginWithSessionTokenRequest{SessionToken: sessionTokenResp.SessionToken}, &loginResult) c.Assert(err, gc.IsNil) c.Assert(loginResult.UserInfo.Identity, gc.Equals, "user-"+user.Email) @@ -336,7 +338,8 @@ func (s *adminSuite) TestLoginWithClientCredentials(c *gc.C) { const ( // these are valid client credentials hardcoded into the jimm realm - validClientID = "test-client-id" + validClientID = "test-client-id" + //nolint:gosec // Thinks credentials hardcoded. validClientSecret = "2M2blFbO4GX4zfggQpivQSxwWX1XGgNf" ) diff --git a/internal/jujuapi/api.go b/internal/jujuapi/api.go index 82dbd090d..188b553a5 100644 --- a/internal/jujuapi/api.go +++ b/internal/jujuapi/api.go @@ -1,4 +1,4 @@ -// Copyright 2016 Canonical Ltd. +// Copyright 2024 Canonical. // Package jujuapi implements API endpoints for the juju API. package jujuapi diff --git a/internal/jujuapi/api_test.go b/internal/jujuapi/api_test.go index 360570503..c7054a927 100644 --- a/internal/jujuapi/api_test.go +++ b/internal/jujuapi/api_test.go @@ -1,4 +1,4 @@ -// Copyright 2016 Canonical Ltd. +// Copyright 2024 Canonical. package jujuapi_test @@ -58,5 +58,7 @@ func (s *apiSuite) TestModelCommandsModelNotFoundf(c *gc.C) { if err != nil { c.Assert(err, gc.ErrorMatches, "websocket: bad handshake") } + defer response.Body.Close() + c.Assert(response.StatusCode, gc.Equals, http.StatusNotFound) } diff --git a/internal/jujuapi/applicationoffers.go b/internal/jujuapi/applicationoffers.go index bf3b70581..f7e3dc724 100644 --- a/internal/jujuapi/applicationoffers.go +++ b/internal/jujuapi/applicationoffers.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package jujuapi diff --git a/internal/jujuapi/applicationoffers_test.go b/internal/jujuapi/applicationoffers_test.go index cc37619cd..c4a248694 100644 --- a/internal/jujuapi/applicationoffers_test.go +++ b/internal/jujuapi/applicationoffers_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package jujuapi_test import ( @@ -365,8 +365,8 @@ func (s *applicationOffersSuite) TestDestroyOffers(c *gc.C) { // i need to fetch the offer so that i can manually set read // permission for charlie // - //err = client.GrantOffer("charlie@canonical.com", "read", offerURL) - //c.Assert(err, jc.ErrorIsNil) + // err = client.GrantOffer("charlie@canonical.com", "read", offerURL) + // c.Assert(err, jc.ErrorIsNil) offer := dbmodel.ApplicationOffer{ URL: offerURL, } diff --git a/internal/jujuapi/cloud.go b/internal/jujuapi/cloud.go index fe66d3dfd..1adb9b775 100644 --- a/internal/jujuapi/cloud.go +++ b/internal/jujuapi/cloud.go @@ -1,4 +1,4 @@ -// Copyright 2016 Canonical Ltd. +// Copyright 2024 Canonical. package jujuapi @@ -481,8 +481,8 @@ func (r *controllerRoot) UpdateCloud(ctx context.Context, args jujuparams.Update results := jujuparams.ErrorResults{ Results: make([]jujuparams.ErrorResult, len(args.Clouds)), } - for i, arg := range args.Clouds { - err := r.updateCloud(ctx, arg) + for i := range args.Clouds { + err := r.updateCloud() if err != nil { results.Results[i].Error = mapError(err) } @@ -490,7 +490,7 @@ func (r *controllerRoot) UpdateCloud(ctx context.Context, args jujuparams.Update return results, nil } -func (r *controllerRoot) updateCloud(ctx context.Context, args jujuparams.AddCloudArgs) error { +func (r *controllerRoot) updateCloud() error { // TODO(mhilton) work out how to support updating clouds, for now // tell everyone they're not allowed. return errors.E(errors.CodeForbidden, "permission denied") diff --git a/internal/jujuapi/cloud_test.go b/internal/jujuapi/cloud_test.go index 669fb6f22..58d4144e3 100644 --- a/internal/jujuapi/cloud_test.go +++ b/internal/jujuapi/cloud_test.go @@ -1,4 +1,4 @@ -// Copyright 2016 Canonical Ltd. +// Copyright 2024 Canonical. package jujuapi_test diff --git a/internal/jujuapi/controller.go b/internal/jujuapi/controller.go index 0d1926e91..2b3b329ad 100644 --- a/internal/jujuapi/controller.go +++ b/internal/jujuapi/controller.go @@ -1,4 +1,4 @@ -// Copyright 2016 Canonical Ltd. +// Copyright 2024 Canonical. package jujuapi @@ -116,7 +116,7 @@ func (r *controllerRoot) WatchModelSummaries(ctx context.Context) (jujuparams.Su } return modelUUIDs, nil } - watcher, err := newModelSummaryWatcher(ctx, id, r, r.jimm.PubSubHub(), getModels) + watcher, err := newModelSummaryWatcher(ctx, id, r.jimm.PubSubHub(), getModels) if err != nil { return jujuparams.SummaryWatcherID{}, errors.E(op, err) } @@ -155,7 +155,7 @@ func (r *controllerRoot) WatchAllModelSummaries(ctx context.Context) (jujuparams return modelUUIDs, nil } - watcher, err := newModelSummaryWatcher(ctx, id, r, r.jimm.PubSubHub(), getAllModels) + watcher, err := newModelSummaryWatcher(ctx, id, r.jimm.PubSubHub(), getAllModels) if err != nil { return jujuparams.SummaryWatcherID{}, errors.E(op, err) } diff --git a/internal/jujuapi/controller_test.go b/internal/jujuapi/controller_test.go index c70073fc7..9e4cb63c7 100644 --- a/internal/jujuapi/controller_test.go +++ b/internal/jujuapi/controller_test.go @@ -1,4 +1,4 @@ -// Copyright 2016 Canonical Ltd. +// Copyright 2024 Canonical. package jujuapi_test diff --git a/internal/jujuapi/controllerroot.go b/internal/jujuapi/controllerroot.go index 4cba48183..ed6ddd033 100644 --- a/internal/jujuapi/controllerroot.go +++ b/internal/jujuapi/controllerroot.go @@ -1,4 +1,4 @@ -// Copyright 2016 Canonical Ltd. +// Copyright 2024 Canonical. package jujuapi diff --git a/internal/jujuapi/controllerroot_test.go b/internal/jujuapi/controllerroot_test.go index 362f05ab6..25939920d 100644 --- a/internal/jujuapi/controllerroot_test.go +++ b/internal/jujuapi/controllerroot_test.go @@ -1,4 +1,4 @@ -// Copyright 2016 Canonical Ltd. +// Copyright 2024 Canonical. package jujuapi_test diff --git a/internal/jujuapi/export_test.go b/internal/jujuapi/export_test.go index 0fa2eeb82..5d0d6d197 100644 --- a/internal/jujuapi/export_test.go +++ b/internal/jujuapi/export_test.go @@ -1,15 +1,16 @@ -// Copyright 2016 Canonical Ltd. +// Copyright 2024 Canonical. package jujuapi import ( "context" + jujuparams "github.com/juju/juju/rpc/params" + "github.com/canonical/jimm/v3/internal/db" "github.com/canonical/jimm/v3/internal/jimm" "github.com/canonical/jimm/v3/internal/openfga" ofganames "github.com/canonical/jimm/v3/internal/openfga/names" - jujuparams "github.com/juju/juju/rpc/params" ) var ( diff --git a/internal/jujuapi/jimm.go b/internal/jujuapi/jimm.go index 2ea5a2c55..e24d485f3 100644 --- a/internal/jujuapi/jimm.go +++ b/internal/jujuapi/jimm.go @@ -1,4 +1,4 @@ -// Copyright 2016 Canonical Ltd. +// Copyright 2024 Canonical. package jujuapi diff --git a/internal/jujuapi/jimm_test.go b/internal/jujuapi/jimm_test.go index 56adb3f25..370fa9f03 100644 --- a/internal/jujuapi/jimm_test.go +++ b/internal/jujuapi/jimm_test.go @@ -1,4 +1,4 @@ -// Copyright 2023 Canonical Ltd. +// Copyright 2024 Canonical. package jujuapi_test diff --git a/internal/jujuapi/modelmanager.go b/internal/jujuapi/modelmanager.go index 0736cdf2e..a79d19790 100644 --- a/internal/jujuapi/modelmanager.go +++ b/internal/jujuapi/modelmanager.go @@ -1,4 +1,4 @@ -// Copyright 2016 Canonical Ltd. +// Copyright 2024 Canonical. package jujuapi @@ -130,10 +130,8 @@ func (r *controllerRoot) ModelInfo(ctx context.Context, args jujuparams.Entities err = errors.E(op, errors.CodeUnauthorized, "unauthorized") } results[i].Error = mapError(errors.E(op, err)) - } else { - if r.controllerUUIDMasking { - results[i].Result.ControllerUUID = r.params.ControllerUUID - } + } else if r.controllerUUIDMasking { + results[i].Result.ControllerUUID = r.params.ControllerUUID } } return jujuparams.ModelInfoResults{ diff --git a/internal/jujuapi/modelmanager_test.go b/internal/jujuapi/modelmanager_test.go index 83b08fffb..4e2c915dd 100644 --- a/internal/jujuapi/modelmanager_test.go +++ b/internal/jujuapi/modelmanager_test.go @@ -1,4 +1,4 @@ -// Copyright 2016 Canonical Ltd. +// Copyright 2024 Canonical. package jujuapi_test @@ -258,11 +258,11 @@ func (s *modelManagerSuite) TestModelInfo(c *gc.C) { mt4 := s.AddModel(c, names.NewUserTag("charlie@canonical.com"), "model-4", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, s.Model2.CloudCredential.ResourceTag()) // TODO (alesstimec) change once granting has been re-implemented - //conn := s.open(c, nil, "charlie") - //defer conn.Close() - //client := modelmanager.NewClient(conn) - //err := client.GrantModel("bob@canonical.com", "write", mt4.Id()) - //c.Assert(err, gc.Equals, nil) + // conn := s.open(c, nil, "charlie") + // defer conn.Close() + // client := modelmanager.NewClient(conn) + // err := client.GrantModel("bob@canonical.com", "write", mt4.Id()) + // c.Assert(err, gc.Equals, nil) bobIdentity, err := dbmodel.NewIdentity("bob@canonical.com") c.Assert(err, gc.IsNil) @@ -272,8 +272,8 @@ func (s *modelManagerSuite) TestModelInfo(c *gc.C) { mt5 := s.AddModel(c, names.NewUserTag("charlie@canonical.com"), "model-5", names.NewCloudTag(jimmtest.TestCloudName), jimmtest.TestCloudRegionName, s.Model2.CloudCredential.ResourceTag()) // TODO (alesstimec) change once granting has been re-implemented - //err = client.GrantModel("bob@canonical.com", "admin", mt5.Id()) - //c.Assert(err, gc.Equals, nil) + // err = client.GrantModel("bob@canonical.com", "admin", mt5.Id()) + // c.Assert(err, gc.Equals, nil) err = bob.SetModelAccess(context.Background(), mt5, ofganames.AdministratorRelation) c.Assert(err, gc.Equals, nil) diff --git a/internal/jujuapi/modelsummarywatcher.go b/internal/jujuapi/modelsummarywatcher.go index 17e24fbb9..0e1995574 100644 --- a/internal/jujuapi/modelsummarywatcher.go +++ b/internal/jujuapi/modelsummarywatcher.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package jujuapi @@ -100,7 +100,7 @@ func (r *watcherRegistry) get(id string) (*modelSummaryWatcher, error) { return w, nil } -func newModelSummaryWatcher(ctx context.Context, id string, root *controllerRoot, pubsub *pubsub.Hub, modelGetterFunc func(context.Context) ([]string, error)) (*modelSummaryWatcher, error) { +func newModelSummaryWatcher(ctx context.Context, id string, pubsub *pubsub.Hub, modelGetterFunc func(context.Context) ([]string, error)) (*modelSummaryWatcher, error) { const op = errors.Op("jujuapi.newModelSummaryWatcher") ctx, cancelContext := context.WithCancel(ctx) @@ -186,6 +186,7 @@ func (w *modelSummaryWatcher) Stop() error { return nil } +//nolint:unused // Used in export-test. func newModelAccessWatcher(ctx context.Context, period time.Duration, modelGetterFunc func(context.Context) ([]string, error)) *modelAccessWatcher { return &modelAccessWatcher{ ctx: ctx, diff --git a/internal/jujuapi/modelsummarywatcher_test.go b/internal/jujuapi/modelsummarywatcher_test.go index 80c4496bb..4a2302057 100644 --- a/internal/jujuapi/modelsummarywatcher_test.go +++ b/internal/jujuapi/modelsummarywatcher_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package jujuapi_test @@ -20,8 +20,10 @@ var _ = gc.Suite(&modelSummaryWatcherSuite{}) func (s *modelSummaryWatcherSuite) TestModelSummaryWatcher(c *gc.C) { watcher := jujuapi.NewModelSummaryWatcher() - defer watcher.Stop() - + defer func() { + err := watcher.Stop() + c.Assert(err, gc.IsNil) + }() result, err := watcher.Next() c.Assert(err, jc.ErrorIsNil) c.Assert(result, gc.DeepEquals, jujuparams.SummaryWatcherNextResults{ diff --git a/internal/jujuapi/package_test.go b/internal/jujuapi/package_test.go index e9f8e3562..a8920ec4d 100644 --- a/internal/jujuapi/package_test.go +++ b/internal/jujuapi/package_test.go @@ -1,4 +1,4 @@ -// Copyright 2015 Canonical Ltd. +// Copyright 2024 Canonical. package jujuapi_test diff --git a/internal/jujuapi/pinger.go b/internal/jujuapi/pinger.go index cdd90191c..f191ed3fc 100644 --- a/internal/jujuapi/pinger.go +++ b/internal/jujuapi/pinger.go @@ -1,4 +1,4 @@ -// Copyright 2016 Canonical Ltd. +// Copyright 2024 Canonical. package jujuapi diff --git a/internal/jujuapi/pinger_internal_test.go b/internal/jujuapi/pinger_internal_test.go index 8fbe84ece..e05efb831 100644 --- a/internal/jujuapi/pinger_internal_test.go +++ b/internal/jujuapi/pinger_internal_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package jujuapi diff --git a/internal/jujuapi/rpc/method.go b/internal/jujuapi/rpc/method.go index 7e8f67ac8..08396493b 100644 --- a/internal/jujuapi/rpc/method.go +++ b/internal/jujuapi/rpc/method.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package rpc @@ -20,7 +20,7 @@ var ( // Method converts the given function to an RPC method that can be used // with Root. The function must have a signature like: // -// f([ctx context.Context, ][objId string, ][params ParamsT]) ([ResultT, ][error]) +// f([ctx context.Context, ][objId string, ][params ParamsT]) ([ResultT, ][error]) // // Note that all parameters and return values are optional. Method will // panic if the given value is not a function of the correct type. @@ -101,10 +101,8 @@ func (c methodCaller) Call(ctx context.Context, objId string, arg reflect.Value) } if c.flags&inObjectID == inObjectID { pv = append(pv, reflect.ValueOf(objId)) - } else { - if objId != "" { - return reflect.Value{}, errors.ErrBadId - } + } else if objId != "" { + return reflect.Value{}, errors.ErrBadId } if c.flags&inParams == inParams { pv = append(pv, arg) diff --git a/internal/jujuapi/rpc/method_test.go b/internal/jujuapi/rpc/method_test.go index caa7f8a52..14129fb7a 100644 --- a/internal/jujuapi/rpc/method_test.go +++ b/internal/jujuapi/rpc/method_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package rpc_test diff --git a/internal/jujuapi/rpc/root.go b/internal/jujuapi/rpc/root.go index 91d78b42c..af1aa0014 100644 --- a/internal/jujuapi/rpc/root.go +++ b/internal/jujuapi/rpc/root.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package rpc diff --git a/internal/jujuapi/rpc/root_test.go b/internal/jujuapi/rpc/root_test.go index 3f74e275d..3482f9f3d 100644 --- a/internal/jujuapi/rpc/root_test.go +++ b/internal/jujuapi/rpc/root_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package rpc_test diff --git a/internal/jujuapi/service_account.go b/internal/jujuapi/service_account.go index 3f4bc873a..7f4c18316 100644 --- a/internal/jujuapi/service_account.go +++ b/internal/jujuapi/service_account.go @@ -1,4 +1,4 @@ -// Copyright 2024 canonical. +// Copyright 2024 Canonical. package jujuapi diff --git a/internal/jujuapi/service_account_test.go b/internal/jujuapi/service_account_test.go index b3006c9eb..b6399b9ae 100644 --- a/internal/jujuapi/service_account_test.go +++ b/internal/jujuapi/service_account_test.go @@ -1,4 +1,4 @@ -// Copyright 2024 Canonical Ltd. +// Copyright 2024 Canonical. package jujuapi_test @@ -184,7 +184,8 @@ func TestCopyServiceAccountCredential(t *testing.T) { cr := jujuapi.NewControllerRoot(jimm, jujuapi.Params{}) jujuapi.SetUser(cr, user) if len(test.addTuples) > 0 { - ofgaClient.AddRelation(context.Background(), test.addTuples...) + err = ofgaClient.AddRelation(context.Background(), test.addTuples...) + c.Assert(err, qt.IsNil) } res, err := cr.CopyServiceAccountCredential(context.Background(), test.args) if test.expectedError == "" { @@ -265,7 +266,8 @@ func TestGetServiceAccount(t *testing.T) { jujuapi.SetUser(cr, user) if len(test.addTuples) > 0 { - ofgaClient.AddRelation(context.Background(), test.addTuples...) + err = ofgaClient.AddRelation(context.Background(), test.addTuples...) + c.Assert(err, qt.IsNil) } res, err := cr.GetServiceAccount(context.Background(), test.clientID) @@ -455,7 +457,8 @@ func TestUpdateServiceAccountCredentials(t *testing.T) { jujuapi.SetUser(cr, user) if len(test.addTuples) > 0 { - ofgaClient.AddRelation(context.Background(), test.addTuples...) + err = ofgaClient.AddRelation(context.Background(), test.addTuples...) + c.Assert(err, qt.IsNil) } res, err := cr.UpdateServiceAccountCredentials(context.Background(), test.args) @@ -588,7 +591,8 @@ func TestListServiceAccountCredentials(t *testing.T) { jujuapi.SetUser(cr, user) if len(test.addTuples) > 0 { - ofgaClient.AddRelation(context.Background(), test.addTuples...) + err = ofgaClient.AddRelation(context.Background(), test.addTuples...) + c.Assert(err, qt.IsNil) } res, err := cr.ListServiceAccountCredentials(context.Background(), test.args) @@ -701,7 +705,8 @@ func TestGrantServiceAccountAccess(t *testing.T) { jujuapi.SetUser(cr, user) if len(test.addTuples) > 0 { - ofgaClient.AddRelation(context.Background(), test.addTuples...) + err = ofgaClient.AddRelation(context.Background(), test.addTuples...) + c.Assert(err, qt.IsNil) } err = cr.GrantServiceAccountAccess(context.Background(), test.params) @@ -733,14 +738,16 @@ func (s *serviceAccountSuite) TestUpdateServiceAccountCredentialsIntegration(c * Target: ofganames.ConvertTag(serviceAccount), } - s.JIMM.OpenFGAClient.AddRelation(context.Background(), tuple) + err := s.JIMM.OpenFGAClient.AddRelation(context.Background(), tuple) + c.Assert(err, gc.IsNil) cloud := &dbmodel.Cloud{ Name: "aws", } - s.JIMM.Database.AddCloud(context.Background(), cloud) + err = s.JIMM.Database.AddCloud(context.Background(), cloud) + c.Assert(err, gc.IsNil) var credResults jujuparams.UpdateCredentialResults - err := conn.APICall("JIMM", 4, "", "UpdateServiceAccountCredentials", params.UpdateServiceAccountCredentialsRequest{ + err = conn.APICall("JIMM", 4, "", "UpdateServiceAccountCredentials", params.UpdateServiceAccountCredentialsRequest{ ClientID: "fca1f605-736e-4d1f-bcd2-aecc726923be@serviceaccount", UpdateCredentialArgs: jujuparams.UpdateCredentialArgs{ Credentials: []jujuparams.TaggedCredential{ diff --git a/internal/jujuapi/usermanager.go b/internal/jujuapi/usermanager.go index 965c6a14b..766af8eb4 100644 --- a/internal/jujuapi/usermanager.go +++ b/internal/jujuapi/usermanager.go @@ -1,4 +1,4 @@ -// Copyright 2016 Canonical Ltd. +// Copyright 2024 Canonical. package jujuapi @@ -66,7 +66,7 @@ func (r *controllerRoot) UserInfo(ctx context.Context, req jujuparams.UserInfoRe Results: make([]jujuparams.UserInfoResult, len(req.Entities)), } for i, ent := range req.Entities { - ui, err := r.userInfo(ctx, ent.Tag) + ui, err := r.userInfo(ent.Tag) if err != nil { res.Results[i].Error = mapError(err) continue @@ -76,7 +76,7 @@ func (r *controllerRoot) UserInfo(ctx context.Context, req jujuparams.UserInfoRe return res, nil } -func (r *controllerRoot) userInfo(ctx context.Context, entity string) (*jujuparams.UserInfo, error) { +func (r *controllerRoot) userInfo(entity string) (*jujuparams.UserInfo, error) { const op = errors.Op("jujuapi.UserInfo") user, err := parseUserTag(entity) diff --git a/internal/jujuapi/usermanager_test.go b/internal/jujuapi/usermanager_test.go index 54fd5aeeb..2f1550252 100644 --- a/internal/jujuapi/usermanager_test.go +++ b/internal/jujuapi/usermanager_test.go @@ -1,4 +1,4 @@ -// Copyright 2016 Canonical Ltd. +// Copyright 2024 Canonical. package jujuapi_test diff --git a/internal/jujuapi/websocket.go b/internal/jujuapi/websocket.go index c68db2614..6bfd1fea1 100644 --- a/internal/jujuapi/websocket.go +++ b/internal/jujuapi/websocket.go @@ -1,4 +1,4 @@ -// Copyright 2016 Canonical Ltd. +// Copyright 2024 Canonical. package jujuapi @@ -154,7 +154,10 @@ func (s modelProxyServer) ServeWS(ctx context.Context, clientConn *websocket.Con JIMM: s.jimm, AuthenticatedIdentityID: auth.SessionIdentityFromContext(ctx), } - jimmRPC.ProxySockets(ctx, proxyHelpers) + if err := jimmRPC.ProxySockets(ctx, proxyHelpers); err != nil { + zapctx.Error(ctx, "failed to start jimm model proxy", zap.Error(err)) + } + } // controllerConnectionFunc returns a function that will be used to diff --git a/internal/jujuapi/websocket_test.go b/internal/jujuapi/websocket_test.go index fecd1922a..34a9f9b57 100644 --- a/internal/jujuapi/websocket_test.go +++ b/internal/jujuapi/websocket_test.go @@ -1,4 +1,4 @@ -// Copyright 2016 Canonical Ltd. +// Copyright 2024 Canonical. package jujuapi_test diff --git a/internal/jujuclient/allwatcher.go b/internal/jujuclient/allwatcher.go index 39a02e0ef..b1ecc55db 100644 --- a/internal/jujuclient/allwatcher.go +++ b/internal/jujuclient/allwatcher.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package jujuclient diff --git a/internal/jujuclient/allwatcher_test.go b/internal/jujuclient/allwatcher_test.go index 4da4099a3..b04b28d5b 100644 --- a/internal/jujuclient/allwatcher_test.go +++ b/internal/jujuclient/allwatcher_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package jujuclient_test diff --git a/internal/jujuclient/applicationoffers.go b/internal/jujuclient/applicationoffers.go index 9044f4c58..335764453 100644 --- a/internal/jujuclient/applicationoffers.go +++ b/internal/jujuclient/applicationoffers.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package jujuclient diff --git a/internal/jujuclient/applicationoffers_test.go b/internal/jujuclient/applicationoffers_test.go index 5703fb7a4..4cf05d295 100644 --- a/internal/jujuclient/applicationoffers_test.go +++ b/internal/jujuclient/applicationoffers_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package jujuclient_test diff --git a/internal/jujuclient/client.go b/internal/jujuclient/client.go index 880ed24c4..2250473ab 100644 --- a/internal/jujuclient/client.go +++ b/internal/jujuclient/client.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package jujuclient diff --git a/internal/jujuclient/client_test.go b/internal/jujuclient/client_test.go index 63d10377d..35ebba3a6 100644 --- a/internal/jujuclient/client_test.go +++ b/internal/jujuclient/client_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package jujuclient_test import ( diff --git a/internal/jujuclient/cloud.go b/internal/jujuclient/cloud.go index 6f67fd43e..16916eb0f 100644 --- a/internal/jujuclient/cloud.go +++ b/internal/jujuclient/cloud.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package jujuclient @@ -147,7 +147,7 @@ func (c Connection) Cloud(ctx context.Context, tag names.CloudTag, cloud *jujupa return errors.E(op, jujuerrors.Cause(err)) } if resp.Results[0].Error != nil { - errors.E(op, resp.Results[0].Error) + return errors.E(op, resp.Results[0].Error) } return nil } diff --git a/internal/jujuclient/cloud_test.go b/internal/jujuclient/cloud_test.go index 82debae1b..6cb4a4980 100644 --- a/internal/jujuclient/cloud_test.go +++ b/internal/jujuclient/cloud_test.go @@ -1,3 +1,4 @@ +// Copyright 2024 Canonical. package jujuclient_test import ( @@ -216,7 +217,7 @@ func (s *cloudSuite) TestClouds(c *gc.C) { clouds, err := s.API.Clouds(context.Background()) c.Assert(err, gc.Equals, nil) c.Assert(clouds, jc.DeepEquals, map[names.CloudTag]jujuparams.Cloud{ - names.NewCloudTag(jimmtest.TestCloudName): jujuparams.Cloud{ + names.NewCloudTag(jimmtest.TestCloudName): { Type: jimmtest.TestProviderType, AuthTypes: []string{"empty", "userpass"}, Endpoint: jimmtest.TestCloudEndpoint, diff --git a/internal/jujuclient/dial.go b/internal/jujuclient/dial.go index 3d08a0a9b..de3ab4994 100644 --- a/internal/jujuclient/dial.go +++ b/internal/jujuclient/dial.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. // Package jujuclient is the client JIMM uses to connect to juju // controllers. The jujuclient uses the juju RPC API directly using @@ -129,6 +129,7 @@ func (d *Dialer) Dial(ctx context.Context, ctl *dbmodel.Controller, modelTag nam dialer: d, ctl: ctl, mt: modelTag, + redialCount: new(atomic.Int32), }, nil } @@ -184,7 +185,7 @@ type Connection struct { broken *uint32 dialer *Dialer - redialCount atomic.Int32 + redialCount *atomic.Int32 ctl *dbmodel.Controller mt names.ModelTag } @@ -215,6 +216,7 @@ func (c *Connection) hasFacadeVersion(facade string, version int) bool { func (c *Connection) redial(ctx context.Context, requiredPermissions map[string]string) error { const op = errors.Op("jujuclient.redial") + dialCount := c.redialCount.Add(1) if dialCount > 10 { return errors.E(op, "dial count exceeded") diff --git a/internal/jujuclient/dial_test.go b/internal/jujuclient/dial_test.go index af5708c50..4e3621b41 100644 --- a/internal/jujuclient/dial_test.go +++ b/internal/jujuclient/dial_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package jujuclient_test @@ -90,14 +90,6 @@ func (s *dialSuite) TestDial(c *gc.C) { c.Check(addrs, jc.DeepEquals, info.Addrs) } -type cExtended struct { - *gc.C -} - -func (t *cExtended) Name() string { - return t.TestName() -} - func (s *dialSuite) TestDialWithJWT(c *gc.C) { ctx := context.Background() diff --git a/internal/jujuclient/modelmanager.go b/internal/jujuclient/modelmanager.go index b49d1b447..0fcf3160f 100644 --- a/internal/jujuclient/modelmanager.go +++ b/internal/jujuclient/modelmanager.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package jujuclient diff --git a/internal/jujuclient/modelmanager_test.go b/internal/jujuclient/modelmanager_test.go index 581209920..3d6978e02 100644 --- a/internal/jujuclient/modelmanager_test.go +++ b/internal/jujuclient/modelmanager_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package jujuclient_test diff --git a/internal/jujuclient/modelsummarywatcher.go b/internal/jujuclient/modelsummarywatcher.go index 21dbb3daf..4e494369d 100644 --- a/internal/jujuclient/modelsummarywatcher.go +++ b/internal/jujuclient/modelsummarywatcher.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package jujuclient diff --git a/internal/jujuclient/modelsummarywatcher_test.go b/internal/jujuclient/modelsummarywatcher_test.go index 7472c39f3..a8755e555 100644 --- a/internal/jujuclient/modelsummarywatcher_test.go +++ b/internal/jujuclient/modelsummarywatcher_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package jujuclient_test diff --git a/internal/jujuclient/modelwatcher.go b/internal/jujuclient/modelwatcher.go index 7d3ae847f..00137b572 100644 --- a/internal/jujuclient/modelwatcher.go +++ b/internal/jujuclient/modelwatcher.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package jujuclient diff --git a/internal/jujuclient/modelwatcher_test.go b/internal/jujuclient/modelwatcher_test.go index 4cd1076d2..eac3db6bc 100644 --- a/internal/jujuclient/modelwatcher_test.go +++ b/internal/jujuclient/modelwatcher_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package jujuclient_test diff --git a/internal/jujuclient/package_test.go b/internal/jujuclient/package_test.go index bbfecbcbf..9d72247eb 100644 --- a/internal/jujuclient/package_test.go +++ b/internal/jujuclient/package_test.go @@ -1,4 +1,4 @@ -// Copyright 2015 Canonical Ltd. +// Copyright 2024 Canonical. package jujuclient_test diff --git a/internal/jujuclient/ping.go b/internal/jujuclient/ping.go index b91a4cfe7..1d527f194 100644 --- a/internal/jujuclient/ping.go +++ b/internal/jujuclient/ping.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package jujuclient diff --git a/internal/jujuclient/ping_test.go b/internal/jujuclient/ping_test.go index c3401cc08..a87257498 100644 --- a/internal/jujuclient/ping_test.go +++ b/internal/jujuclient/ping_test.go @@ -1,13 +1,14 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package jujuclient_test import ( "context" - "github.com/canonical/jimm/v3/internal/dbmodel" "github.com/juju/names/v5" gc "gopkg.in/check.v1" + + "github.com/canonical/jimm/v3/internal/dbmodel" ) type pingSuite struct { diff --git a/internal/jujuclient/storage.go b/internal/jujuclient/storage.go index 5ffdf25e5..b96ba5442 100644 --- a/internal/jujuclient/storage.go +++ b/internal/jujuclient/storage.go @@ -1,4 +1,4 @@ -// Copyright 2023 Canonical Ltd. +// Copyright 2024 Canonical. package jujuclient diff --git a/internal/jujuclient/storage_test.go b/internal/jujuclient/storage_test.go index 272499720..dd6b721ae 100644 --- a/internal/jujuclient/storage_test.go +++ b/internal/jujuclient/storage_test.go @@ -1,4 +1,4 @@ -// Copyright 2023 Canonical Ltd. +// Copyright 2024 Canonical. package jujuclient_test import ( diff --git a/internal/kubetest/kubetest.go b/internal/kubetest/kubetest.go index d6442e610..a57489f39 100644 --- a/internal/kubetest/kubetest.go +++ b/internal/kubetest/kubetest.go @@ -1,4 +1,4 @@ -// Copyright 2018 Canonical Ltd. +// Copyright 2024 Canonical. package kubetest @@ -12,6 +12,7 @@ import ( const ( Username = "test-kubernetes-user" + //nolint:gosec // Thinks it's an exposed secret. Password = "test-kubernetes-password" ) @@ -32,7 +33,8 @@ func NewFakeKubernetes(c *gc.C) *httptest.Server { return } w.Header().Set("Content-Type", req.Header.Get("Content-Type")) - io.Copy(w, req.Body) + _, err := io.Copy(w, req.Body) + c.Assert(err, gc.IsNil) })) return srv } diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 3d703930a..16406109d 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. // Package logger contains logger adapters for various services. package logger diff --git a/internal/openfga/export_test.go b/internal/openfga/export_test.go index a4a850133..4669e08b2 100644 --- a/internal/openfga/export_test.go +++ b/internal/openfga/export_test.go @@ -1,4 +1,4 @@ -// Copyright 2023 canonical. +// Copyright 2024 Canonical. package openfga diff --git a/internal/openfga/names/export_test.go b/internal/openfga/names/export_test.go index 024e9e20e..247588021 100644 --- a/internal/openfga/names/export_test.go +++ b/internal/openfga/names/export_test.go @@ -1,4 +1,4 @@ -// Copyright 2023 canonical. +// Copyright 2024 Canonical. package names diff --git a/internal/openfga/names/names.go b/internal/openfga/names/names.go index fb68c00fa..ca4206e87 100644 --- a/internal/openfga/names/names.go +++ b/internal/openfga/names/names.go @@ -1,4 +1,4 @@ -// Copyright 2023 canonical. +// Copyright 2024 Canonical. // Package names holds functions used by other jimm components to // create valid OpenFGA tags. @@ -7,12 +7,12 @@ package names import ( "fmt" - "github.com/canonical/jimm/v3/internal/errors" - jimmnames "github.com/canonical/jimm/v3/pkg/names" cofga "github.com/canonical/ofga" - "github.com/juju/juju/core/permission" "github.com/juju/names/v5" + + "github.com/canonical/jimm/v3/internal/errors" + jimmnames "github.com/canonical/jimm/v3/pkg/names" ) // Relation Types diff --git a/internal/openfga/names/names_test.go b/internal/openfga/names/names_test.go index 4152a48f4..522140c99 100644 --- a/internal/openfga/names/names_test.go +++ b/internal/openfga/names/names_test.go @@ -1,17 +1,17 @@ -// Copyright 2023 canonical. +// Copyright 2024 Canonical. package names_test import ( "testing" + "github.com/google/uuid" + "github.com/juju/juju/core/permission" "github.com/juju/names/v5" gc "gopkg.in/check.v1" ofganames "github.com/canonical/jimm/v3/internal/openfga/names" jimmnames "github.com/canonical/jimm/v3/pkg/names" - "github.com/google/uuid" - "github.com/juju/juju/core/permission" ) func Test(t *testing.T) { diff --git a/internal/openfga/openfga.go b/internal/openfga/openfga.go index 4b17b1094..23cc77208 100644 --- a/internal/openfga/openfga.go +++ b/internal/openfga/openfga.go @@ -1,4 +1,4 @@ -// Copyright 2023 canonical. +// Copyright 2024 Canonical. package openfga @@ -6,12 +6,12 @@ import ( "context" "strings" - "github.com/canonical/jimm/v3/internal/errors" - "github.com/canonical/jimm/v3/internal/servermon" cofga "github.com/canonical/ofga" "github.com/juju/names/v5" + "github.com/canonical/jimm/v3/internal/errors" ofganames "github.com/canonical/jimm/v3/internal/openfga/names" + "github.com/canonical/jimm/v3/internal/servermon" jimmnames "github.com/canonical/jimm/v3/pkg/names" ) diff --git a/internal/openfga/openfga_test.go b/internal/openfga/openfga_test.go index 11cd5c0bb..cc4925263 100644 --- a/internal/openfga/openfga_test.go +++ b/internal/openfga/openfga_test.go @@ -1,3 +1,4 @@ +// Copyright 2024 Canonical. package openfga_test import ( @@ -71,7 +72,7 @@ func (suite *openFGATestSuite) TestRemovingTuplesFromOFGASucceeds(c *gc.C) { groupUUID := uuid.NewString() - //Create tuples before writing to db + // Create tuples before writing to db user1 := ofganames.ConvertTag(names.NewUserTag("bob")) tuple1 := openfga.Tuple{ Object: user1, @@ -86,14 +87,14 @@ func (suite *openFGATestSuite) TestRemovingTuplesFromOFGASucceeds(c *gc.C) { Target: ofganames.ConvertTag(jimmnames.NewGroupTag(groupUUID)), } - //Delete before insert should fail + // Delete before insert should fail err := suite.ofgaClient.RemoveRelation(ctx, tuple1, tuple2) c.Assert(strings.Contains(err.Error(), "cannot delete a tuple which does not exist"), gc.Equals, true) err = suite.ofgaClient.AddRelation(ctx, tuple1, tuple2) c.Assert(err, gc.IsNil) - //Delete after insert should succeed. + // Delete after insert should succeed. err = suite.ofgaClient.RemoveRelation(ctx, tuple1, tuple2) c.Assert(err, gc.IsNil) changes, err := suite.cofgaClient.ReadChanges(ctx, "group", 99, "") diff --git a/internal/openfga/user.go b/internal/openfga/user.go index a87d5e900..3e62b918e 100644 --- a/internal/openfga/user.go +++ b/internal/openfga/user.go @@ -1,4 +1,4 @@ -// Copyright 2023 canonical. +// Copyright 2024 Canonical. package openfga @@ -6,6 +6,7 @@ import ( "context" "strings" + "github.com/canonical/ofga" "github.com/juju/names/v5" "github.com/juju/zaputil/zapctx" "go.uber.org/zap" @@ -14,7 +15,6 @@ import ( "github.com/canonical/jimm/v3/internal/errors" ofganames "github.com/canonical/jimm/v3/internal/openfga/names" jimmnames "github.com/canonical/jimm/v3/pkg/names" - "github.com/canonical/ofga" ) // NewUser returns a new user structure that can be used to check diff --git a/internal/openfga/user_test.go b/internal/openfga/user_test.go index e42bec85d..36c1b0cd8 100644 --- a/internal/openfga/user_test.go +++ b/internal/openfga/user_test.go @@ -1,4 +1,4 @@ -// Copyright 2023 canonical. +// Copyright 2024 Canonical. package openfga_test diff --git a/internal/pubsub/hub.go b/internal/pubsub/hub.go index 22ec07221..828fbe9b0 100644 --- a/internal/pubsub/hub.go +++ b/internal/pubsub/hub.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. // Package pubsub contains an implementation of a simple pubsub // mechanism that passes messages about models between @@ -8,8 +8,9 @@ package pubsub import ( "sync" - "github.com/canonical/jimm/v3/internal/errors" "github.com/juju/utils/v2/parallel" + + "github.com/canonical/jimm/v3/internal/errors" ) // HandlerFunc takes two arguments - a model ID and the message about this model. diff --git a/internal/pubsub/hub_test.go b/internal/pubsub/hub_test.go index da07eecba..0a687397b 100644 --- a/internal/pubsub/hub_test.go +++ b/internal/pubsub/hub_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package pubsub_test diff --git a/internal/rpc/client.go b/internal/rpc/client.go index 366e6caad..61ec4f646 100644 --- a/internal/rpc/client.go +++ b/internal/rpc/client.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package rpc @@ -12,6 +12,8 @@ import ( "github.com/gorilla/websocket" jujuparams "github.com/juju/juju/rpc/params" + "github.com/juju/zaputil/zapctx" + "go.uber.org/zap" "github.com/canonical/jimm/v3/internal/errors" ) @@ -99,7 +101,10 @@ func (c *Client) handleError(err error) { if !c.closing { // We haven't sent a close message yet, so try to send one. cm := websocket.FormatCloseMessage(websocket.CloseProtocolError, err.Error()) - c.conn.WriteControl(websocket.CloseMessage, cm, time.Time{}) + err := c.conn.WriteControl(websocket.CloseMessage, cm, time.Time{}) + if err != nil { + zapctx.Error(context.Background(), "failed to write socket closure message", zap.Error(err)) + } } c.err = err c.conn.Close() @@ -128,7 +133,10 @@ func (c *Client) handleRequest(msg *message) { // Note we're ignoring any write error here as any subsequent write // will also error and that will be able to process the error more // appropriately. - c.conn.WriteJSON(resp) + err := c.conn.WriteJSON(resp) + if err != nil { + zapctx.Error(context.Background(), "failed to write JSON resp", zap.Error(err)) + } } func (c *Client) handleResponse(msg *message) { @@ -181,6 +189,7 @@ func (c *Client) Call(ctx context.Context, facade string, version int, id, metho return errors.E(op, err) } ch := make(chan struct{}) + //nolint:staticcheck // Not sure why Martin made this a **. Ignore for now. respMsg := new(*message) c.msgs[req.RequestID] = inflight{ ch: ch, @@ -194,28 +203,26 @@ func (c *Client) Call(ctx context.Context, facade string, version int, id, metho select { case <-ch: - if respMsg != nil { - permissionsRequired, err := checkPermissionsRequired(ctx, *respMsg) - if err != nil { - return err - } - if permissionsRequired != nil { - return &Error{ - Code: PermissionCheckRequiredErrorCode, - Info: permissionsRequired, - } + permissionsRequired, err := checkPermissionsRequired(ctx, *respMsg) + if err != nil { + return err + } + if permissionsRequired != nil { + return &Error{ + Code: PermissionCheckRequiredErrorCode, + Info: permissionsRequired, } - if (*respMsg).Error != "" { - return &Error{ - Message: (*respMsg).Error, - Code: (*respMsg).ErrorCode, - Info: (*respMsg).ErrorInfo, - } + } + if (*respMsg).Error != "" { + return &Error{ + Message: (*respMsg).Error, + Code: (*respMsg).ErrorCode, + Info: (*respMsg).ErrorInfo, } - if resp != nil { - if err := json.Unmarshal([]byte((*respMsg).Response), &resp); err != nil { - return errors.E(op, err) - } + } + if resp != nil { + if err := json.Unmarshal([]byte((*respMsg).Response), &resp); err != nil { + return errors.E(op, err) } } return nil diff --git a/internal/rpc/client_test.go b/internal/rpc/client_test.go index 66e7f0403..8fd7b26aa 100644 --- a/internal/rpc/client_test.go +++ b/internal/rpc/client_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package rpc_test @@ -244,9 +244,8 @@ func TestProxySockets(t *testing.T) { c := qt.New(t) ctx := context.Background() - srvController := newServer(func(conn *websocket.Conn) error { - return echo(conn) - }) + srvController := newServer(echo) + errChan := make(chan error) srvJIMM := newServer(func(connClient *websocket.Conn) error { testTokenGen := testTokenGenerator{} @@ -293,9 +292,7 @@ func TestCancelProxySockets(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) - srvController := newServer(func(conn *websocket.Conn) error { - return echo(conn) - }) + srvController := newServer(echo) errChan := make(chan error) srvJIMM := newServer(func(connClient *websocket.Conn) error { @@ -335,9 +332,7 @@ func TestProxySocketsAuditLogs(t *testing.T) { ctx := context.Background() - srvController := newServer(func(conn *websocket.Conn) error { - return echo(conn) - }) + srvController := newServer(echo) auditLogs := make([]*dbmodel.AuditLogEntry, 0) errChan := make(chan error) @@ -428,7 +423,8 @@ func newServer(f func(*websocket.Conn) error) *server { cp.AddCert(srv.Certificate()) srv.dialer = &rpc.Dialer{ TLSConfig: &tls.Config{ - RootCAs: cp, + RootCAs: cp, + MinVersion: tls.VersionTLS12, }, } return &srv @@ -445,15 +441,17 @@ func handleWS(f func(*websocket.Conn) error) http.Handler { defer c.Close() err = f(c) var cm []byte - if err == nil { + switch { + case err == nil: cm = websocket.FormatCloseMessage(websocket.CloseNormalClosure, "") - } else if websocket.IsCloseError(err) { + case websocket.IsCloseError(err): ce := err.(*websocket.CloseError) cm = websocket.FormatCloseMessage(ce.Code, ce.Text) - } else { + default: cm = websocket.FormatCloseMessage(websocket.CloseInternalServerErr, err.Error()) } - c.WriteControl(websocket.CloseMessage, cm, time.Time{}) + _ = c.WriteControl(websocket.CloseMessage, cm, time.Time{}) + }) } diff --git a/internal/rpc/dial.go b/internal/rpc/dial.go index 36e095c76..eb46c0f3b 100644 --- a/internal/rpc/dial.go +++ b/internal/rpc/dial.go @@ -1,4 +1,4 @@ -// Copyright 2023 Canonical Ltd. +// Copyright 2024 Canonical. package rpc @@ -66,6 +66,7 @@ func Dial(ctx context.Context, ctl *dbmodel.Controller, modelTag names.ModelTag, tlsConfig = &tls.Config{ RootCAs: cp, ServerName: ctl.TLSHostname, + MinVersion: tls.VersionTLS12, } } dialer := Dialer{ diff --git a/internal/rpc/export_test.go b/internal/rpc/export_test.go index 0b6ae2f2f..a50d4d277 100644 --- a/internal/rpc/export_test.go +++ b/internal/rpc/export_test.go @@ -1,3 +1,4 @@ +// Copyright 2024 Canonical. package rpc type Message message diff --git a/internal/rpc/proxy.go b/internal/rpc/proxy.go index ccba4548f..f781510f9 100644 --- a/internal/rpc/proxy.go +++ b/internal/rpc/proxy.go @@ -1,3 +1,4 @@ +// Copyright 2024 Canonical. package rpc import ( @@ -162,11 +163,16 @@ func (c *writeLockConn) sendMessage(responseObject any, request *message) { responseData, err := json.Marshal(responseObject) if err != nil { errorMsg := createErrResponse(err, request) - c.writeJson(errorMsg) + if err := c.writeJson(errorMsg); err != nil { + zapctx.Error(context.Background(), "failed to send error message in proxy", zap.Error(err)) + } + } msg.Response = responseData } - c.writeJson(msg) + if err := c.writeJson(msg); err != nil { + zapctx.Error(context.Background(), "failed to write message in proxy", zap.Error(err)) + } } // inflightMsgs holds only request messages that are @@ -251,11 +257,15 @@ func (p *modelProxy) sendError(socket *writeLockConn, req *message, err error) { } msg := createErrResponse(err, req) if msg != nil { - socket.writeJson(msg) + if err := socket.writeJson(msg); err != nil { + zapctx.Error(context.Background(), "failed to create err response message", zap.Error(err)) + } } // An error message is a response back to the client. servermon.JujuCallErrorCount.WithLabelValues(req.Type, req.Request, p.msgs.controllerUUID) - p.auditLogMessage(msg, true) + if err := p.auditLogMessage(msg, true); err != nil { + zapctx.Error(context.Background(), "failed to audit log message", zap.Error(err)) + } } func (p *modelProxy) auditLogMessage(msg *message, isResponse bool) error { @@ -316,7 +326,6 @@ type clientProxy struct { // start begins the client->controller proxier. func (p *clientProxy) start(ctx context.Context) error { - const op = errors.Op("rpc.clientProxy.start") defer func() { if p.dst != nil { p.dst.conn.Close() @@ -336,7 +345,9 @@ func (p *clientProxy) start(ctx context.Context) error { p.sendError(p.src, msg, err) return err } - p.auditLogMessage(msg, false) + if err := p.auditLogMessage(msg, false); err != nil { + zapctx.Error(ctx, "failed to audit log message", zap.Error(err)) + } // All requests should be proxied as transparently as possible through to the controller // except for auth related requests like Login because JIMM is auth gateway. if msg.Type == "Admin" { @@ -443,7 +454,9 @@ func (p *controllerProxy) start(ctx context.Context) error { // Write back to the controller. msg := p.msgs.getMessage(msg.RequestID) if msg != nil { - p.src.writeJson(msg) + if err := p.src.writeJson(msg); err != nil { + zapctx.Error(context.Background(), "failed to write back to controller", zap.Error(err)) + } } continue } else { @@ -455,7 +468,9 @@ func (p *controllerProxy) start(ctx context.Context) error { } } p.msgs.removeMessage(msg.RequestID) - p.auditLogMessage(msg, true) + if err := p.auditLogMessage(msg, true); err != nil { + zapctx.Error(context.Background(), "failed to audit log message", zap.Error(err)) + } zapctx.Debug(ctx, "Writing modified message to client", zap.Any("Message", msg)) if err := p.dst.writeJson(msg); err != nil { zapctx.Error(ctx, "controllerProxy error writing to dst", zap.Error(err)) @@ -490,7 +505,7 @@ func checkPermissionsRequired(ctx context.Context, msg *message) (map[string]any var er params.ErrorResults err := json.Unmarshal(msg.Response, &er) if err != nil { - zapctx.Error(ctx, "failed to read response error") + zapctx.Error(ctx, "failed to read response error", zap.Error(err)) return permissionMap, nil } diff --git a/internal/rpc/proxy_test.go b/internal/rpc/proxy_test.go index fb948361f..77c9d7a41 100644 --- a/internal/rpc/proxy_test.go +++ b/internal/rpc/proxy_test.go @@ -1,4 +1,4 @@ -// Copyright 2024 Canonical Ltd. +// Copyright 2024 Canonical. package rpc_test @@ -263,8 +263,10 @@ func TestProxySocketsAdminFacade(t *testing.T) { }, AuthenticatedIdentityID: test.authenticateEntityID, } - go rpc.ProxySockets(ctx, helpers) - + go func() { + err = rpc.ProxySockets(ctx, helpers) + c.Assert(err, qt.IsNil) + }() data, err := json.Marshal(test.messageToSend) c.Assert(err, qt.IsNil) select { @@ -373,7 +375,11 @@ func (m *mockOAuthAuthenticator) VerifySessionToken(token string) (jwt.Token, er return nil, m.err } t := jwt.New() - t.Set(jwt.SubjectKey, m.email) + + if err := t.Set(jwt.SubjectKey, m.email); err != nil { + return nil, err + } + return t, nil } diff --git a/internal/rpc/rpc.go b/internal/rpc/rpc.go index f86733d08..4090942a3 100644 --- a/internal/rpc/rpc.go +++ b/internal/rpc/rpc.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. // Package rpc implements the juju RPC protocol. The main difference // between this implementation and the implementation in diff --git a/internal/servermon/monitoring.go b/internal/servermon/monitoring.go index eec37d789..64be10227 100644 --- a/internal/servermon/monitoring.go +++ b/internal/servermon/monitoring.go @@ -1,4 +1,4 @@ -// Copyright 2016 Canonical Ltd. +// Copyright 2024 Canonical. // The servermon package is used to update statistics used // for monitoring the API server. @@ -122,13 +122,13 @@ var ( ModelCount = promauto.NewGaugeVec(prometheus.GaugeOpts{ Namespace: "jimm", Subsystem: "system", - Name: "model_count", + Name: "model", Help: "The number of models managed per controller attached to JIMM.", }, []string{"controller"}) ControllerCount = promauto.NewGauge(prometheus.GaugeOpts{ Namespace: "jimm", Subsystem: "system", - Name: "controller_count", + Name: "controller", Help: "The number of controllers managed by JIMM.", }) ) diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 4652cc3a8..f277a48b9 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -1,14 +1,23 @@ +// Copyright 2024 Canonical. package utils import ( + "context" "crypto/rand" "encoding/hex" + + "github.com/juju/zaputil/zapctx" + "go.uber.org/zap" ) // NewConversationID generates a unique ID that is used for the // lifetime of a websocket connection. func NewConversationID() string { buf := make([]byte, 8) - rand.Read(buf) // Can't fail + _, err := rand.Read(buf) + if err != nil { + zapctx.Error(context.Background(), "failed to generate rand", zap.Error(err)) + + } return hex.EncodeToString(buf) } diff --git a/internal/utils/utils_test.go b/internal/utils/utils_test.go index 20ae46ed0..f17c6f8c9 100644 --- a/internal/utils/utils_test.go +++ b/internal/utils/utils_test.go @@ -1,3 +1,4 @@ +// Copyright 2024 Canonical. package utils_test import ( diff --git a/internal/vault/vault.go b/internal/vault/vault.go index 5455c5d36..9406f433f 100644 --- a/internal/vault/vault.go +++ b/internal/vault/vault.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package vault @@ -29,11 +29,10 @@ const ( ) const ( - jwksKey = "jwks" - jwksExpiryKey = "jwks-expiry" - jwksPrivateKey = "jwks-private" - oAuthSecretKey = "oauth-secret" - oAuthSessionStoreSecretKey = "oauth-session-store-secret" + jwksKey = "jwks" + jwksExpiryKey = "jwks-expiry" + jwksPrivateKey = "jwks-private" + oAuthSecretKey = "oauth-secret" ) // A VaultStore stores cloud credential attributes and @@ -220,10 +219,15 @@ func (s *VaultStore) CleanupJWKS(ctx context.Context) (err error) { if err != nil { return errors.E(op, err) } - // Vault does not return errors on deletion requests where - // the secret does not exist. As such we just return the last known error. - client.KVv2(s.KVPath).Delete(ctx, s.getJWKSExpiryPath()) - client.KVv2(s.KVPath).Delete(ctx, s.getJWKSPath()) + + if err = client.KVv2(s.KVPath).Delete(ctx, s.getJWKSExpiryPath()); err != nil { + return errors.E(op, err) + } + + if err = client.KVv2(s.KVPath).Delete(ctx, s.getJWKSPath()); err != nil { + return errors.E(op, err) + } + if err = client.KVv2(s.KVPath).Delete(ctx, s.getJWKSPrivateKeyPath()); err != nil { return errors.E(op, err) } diff --git a/internal/vault/vault_test.go b/internal/vault/vault_test.go index fd8f58995..dffb4976e 100644 --- a/internal/vault/vault_test.go +++ b/internal/vault/vault_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. package vault_test diff --git a/internal/wellknownapi/api.go b/internal/wellknownapi/api.go index 62e008819..46396d656 100644 --- a/internal/wellknownapi/api.go +++ b/internal/wellknownapi/api.go @@ -1,4 +1,4 @@ -// Copyright 2023 canonical. +// Copyright 2024 Canonical. package wellknownapi import ( @@ -7,12 +7,13 @@ import ( "net/http" "time" - "github.com/canonical/jimm/v3/internal/errors" - "github.com/canonical/jimm/v3/internal/jimm/credentials" "github.com/go-chi/chi/v5" "github.com/go-chi/render" "github.com/juju/zaputil/zapctx" "go.uber.org/zap" + + "github.com/canonical/jimm/v3/internal/errors" + "github.com/canonical/jimm/v3/internal/jimm/credentials" ) // WellKnownHandler holds the grouped router to be mounted and diff --git a/internal/wellknownapi/api_test.go b/internal/wellknownapi/api_test.go index 9cb0b3cb3..ddd206864 100644 --- a/internal/wellknownapi/api_test.go +++ b/internal/wellknownapi/api_test.go @@ -1,4 +1,4 @@ -// Copyright 2023 canonical. +// Copyright 2024 Canonical. package wellknownapi_test import ( @@ -11,12 +11,13 @@ import ( "testing" "time" + qt "github.com/frankban/quicktest" + "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/canonical/jimm/v3/internal/errors" "github.com/canonical/jimm/v3/internal/jimmtest" "github.com/canonical/jimm/v3/internal/vault" "github.com/canonical/jimm/v3/internal/wellknownapi" - qt "github.com/frankban/quicktest" - "github.com/lestrrat-go/jwx/v2/jwk" ) func newStore(t testing.TB) *vault.VaultStore { @@ -72,6 +73,7 @@ func TestWellknownAPIJWKSJSONHandles404(t *testing.T) { rr := setupHandlerAndRecorder(c, "/jwks.json", store) resp := rr.Result() + defer resp.Body.Close() code := rr.Code b, err := io.ReadAll(resp.Body) c.Assert(err, qt.IsNil) @@ -100,6 +102,7 @@ func TestWellknownAPIJWKSJSONHandles500(t *testing.T) { rr := setupHandlerAndRecorder(c, "/jwks.json", store) resp := rr.Result() + defer resp.Body.Close() code := rr.Code b, err := io.ReadAll(resp.Body) @@ -135,6 +138,7 @@ func TestWellknownAPIJWKSJSONHandles200(t *testing.T) { rr := setupHandlerAndRecorder(c, "/jwks.json", store) resp := rr.Result() + defer resp.Body.Close() code := rr.Code b, err := io.ReadAll(resp.Body) diff --git a/local/seed_db/main.go b/local/seed_db/main.go index a9355d3ed..b2403f554 100644 --- a/local/seed_db/main.go +++ b/local/seed_db/main.go @@ -1,3 +1,4 @@ +// Copyright 2024 Canonical. package main import ( @@ -7,15 +8,16 @@ import ( "os" "time" - "github.com/canonical/jimm/v3/internal/db" - "github.com/canonical/jimm/v3/internal/dbmodel" - "github.com/canonical/jimm/v3/internal/logger" petname "github.com/dustinkirkland/golang-petname" "github.com/google/uuid" "github.com/juju/juju/core/crossmodel" "github.com/juju/juju/state" "gorm.io/driver/postgres" "gorm.io/gorm" + + "github.com/canonical/jimm/v3/internal/db" + "github.com/canonical/jimm/v3/internal/dbmodel" + "github.com/canonical/jimm/v3/internal/logger" ) // A simple script to seed a local database for schema testing. @@ -36,7 +38,7 @@ func main() { DB: gdb, } - db.Migrate(ctx, false) + err = db.Migrate(ctx, false) if err != nil { fmt.Println("failed to migrate to db ", err) os.Exit(1) diff --git a/local/vault/approle.go b/local/vault/approle.go index 8e7ad02e9..0649aa4ba 100644 --- a/local/vault/approle.go +++ b/local/vault/approle.go @@ -1,4 +1,4 @@ -// Copyright 2024 Canonical Ltd. +// Copyright 2024 Canonical. // This package exists to hold files used to authenticate with Vault during tests. package vault diff --git a/openfga/auth_model.go b/openfga/auth_model.go index a851fe6b2..32105d6da 100644 --- a/openfga/auth_model.go +++ b/openfga/auth_model.go @@ -1,4 +1,4 @@ -// Copyright 2024 Canonical Ltd. +// Copyright 2024 Canonical. // This package exists to hold JIMM's OpenFGA authorisation model. // It embeds the auth model and provides it for tests. diff --git a/pkg/api/client.go b/pkg/api/client.go index 3bfdc9aac..b84a75e7e 100644 --- a/pkg/api/client.go +++ b/pkg/api/client.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package api diff --git a/pkg/api/params/errors.go b/pkg/api/params/errors.go index 773ea23fd..dd46638f9 100644 --- a/pkg/api/params/errors.go +++ b/pkg/api/params/errors.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package params diff --git a/pkg/api/params/params.go b/pkg/api/params/params.go index 0d0a9f740..f890c2020 100644 --- a/pkg/api/params/params.go +++ b/pkg/api/params/params.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical. package params @@ -219,7 +219,7 @@ type FindAuditEventsRequest struct { // A ListControllersResponse is the response that is sent in a // ListControllers method. type ListControllersResponse struct { - Controllers []ControllerInfo `json:"controllers",yaml:"controllers"` + Controllers []ControllerInfo `json:"controllers" yaml:"controllers"` } // A RemoveControllerRequest is the request that is sent in a @@ -302,15 +302,15 @@ type RemoveGroupRequest struct { // Group holds the details of a group currently residing in JIMM. type Group struct { - UUID string `json:"uuid",yaml:"uuid"` - Name string `json:"name",yaml:"name"` - CreatedAt string `json:"created_at",yaml:"created_at"` - UpdatedAt string `json:"updated_at",yaml:"updated_at"` + UUID string `json:"uuid" yaml:"uuid"` + Name string `json:"name" yaml:"name"` + CreatedAt string `json:"created_at" yaml:"created_at"` + UpdatedAt string `json:"updated_at" yaml:"updated_at"` } // ListGroupResponse returns the group tuples currently residing within OpenFGA. type ListGroupResponse struct { - Groups []Group `json:"name",yaml:"name"` + Groups []Group `json:"name" yaml:"name"` } // RelationshipTuple represents a OpenFGA Tuple. @@ -343,7 +343,7 @@ type CheckRelationRequest struct { // CheckRelationResponse simple responds with an object containing a boolean of 'allowed' or not // when a check for access is requested. type CheckRelationResponse struct { - Allowed bool `json:"allowed",yaml:"allowed"` + Allowed bool `json:"allowed" yaml:"allowed"` } // ListRelationshipTuplesRequests holds the request information to list tuples. @@ -373,8 +373,8 @@ type CrossModelQueryRequest struct { // - Results - A map of each iterated JQ output result. The key for this map is the model UUID. // - Errors - A map of each iterated JQ *or* Status call error. The key for this map is the model UUID. type CrossModelQueryResponse struct { - Results map[string][]any `json:"results",yaml:"results"` - Errors map[string][]string `json:"errors",yaml:"errors"` + Results map[string][]any `json:"results" yaml:"results"` + Errors map[string][]string `json:"errors" yaml:"errors"` } // PurgeLogsRequest is the request used to purge logs. @@ -410,9 +410,9 @@ type LoginDeviceResponse struct { // VerificationURI holds the URI that the user must navigate to // when entering their "user-code" to consent to this authorisation // request. - VerificationURI string `json:"verification-uri",yaml:"verification-uri"` + VerificationURI string `json:"verification-uri" yaml:"verification-uri"` // UserCode holds the one-time use user consent code. - UserCode string `json:"user-code",yaml:"user-code"` + UserCode string `json:"user-code" yaml:"user-code"` } // GetDeviceSessionTokenResponse returns a session token to be used against @@ -422,7 +422,7 @@ type GetDeviceSessionTokenResponse struct { // SessionToken is a base64 encoded JWT capable of authenticating // a user. The JWT contains the users email address in the subject, // and this is used to identify this user. - SessionToken string `json:"session-token",yaml:"session-token"` + SessionToken string `json:"session-token" yaml:"session-token"` } // LoginWithSessionTokenRequest accepts a session token minted by JIMM and logs @@ -489,6 +489,6 @@ type GrantServiceAccountAccess struct { // WhoamiResponse holds the response for a /auth/whoami call. type WhoamiResponse struct { - DisplayName string `json:"display-name",yaml:"display-name"` - Email string `json:"email",yaml:"email"` + DisplayName string `json:"display-name" yaml:"display-name"` + Email string `json:"email" yaml:"email"` } diff --git a/pkg/names/applicationoffer.go b/pkg/names/applicationoffer.go index e4a163063..258854996 100644 --- a/pkg/names/applicationoffer.go +++ b/pkg/names/applicationoffer.go @@ -1,4 +1,4 @@ -// Copyright 2024 Canonical Ltd. +// Copyright 2024 Canonical. package names diff --git a/pkg/names/group.go b/pkg/names/group.go index ac1d07978..ad9ecde2f 100644 --- a/pkg/names/group.go +++ b/pkg/names/group.go @@ -1,4 +1,4 @@ -// Copyright 2024 Canonical Ltd. +// Copyright 2024 Canonical. package names diff --git a/pkg/names/group_test.go b/pkg/names/group_test.go index 3846f90d0..10f7f8b46 100644 --- a/pkg/names/group_test.go +++ b/pkg/names/group_test.go @@ -1,4 +1,4 @@ -// Copyright 2024 Canonical Ltd. +// Copyright 2024 Canonical. package names_test diff --git a/pkg/names/names.go b/pkg/names/names.go index 84e61ea98..5e0b18c98 100644 --- a/pkg/names/names.go +++ b/pkg/names/names.go @@ -1,4 +1,4 @@ -// Copyright 2024 Canonical Ltd. +// Copyright 2024 Canonical. package names diff --git a/pkg/names/service_account.go b/pkg/names/service_account.go index 009728f4a..8c1c2616a 100644 --- a/pkg/names/service_account.go +++ b/pkg/names/service_account.go @@ -1,4 +1,4 @@ -// Copyright 2024 Canonical Ltd. +// Copyright 2024 Canonical. package names diff --git a/pkg/names/service_account_test.go b/pkg/names/service_account_test.go index b2f0132f2..98880be3e 100644 --- a/pkg/names/service_account_test.go +++ b/pkg/names/service_account_test.go @@ -1,4 +1,4 @@ -// Copyright 2024 Canonical Ltd. +// Copyright 2024 Canonical. package names_test diff --git a/version/default.go b/version/default.go index 55e1dddc9..7ec4fa5f0 100644 --- a/version/default.go +++ b/version/default.go @@ -1,4 +1,4 @@ -// Copyright 2021 Canonical Ltd. +// Copyright 2024 Canonical. // +build !version //go:build !version diff --git a/version/version.go b/version/version.go index c14d1623d..dec1d7375 100644 --- a/version/version.go +++ b/version/version.go @@ -1,4 +1,4 @@ -// Copyright 2015 Canonical Ltd. +// Copyright 2024 Canonical. package version From 3ac565dc8203beb50904446e15b3c2511207d93a Mon Sep 17 00:00:00 2001 From: Alexander <42068202+ale8k@users.noreply.github.com> Date: Mon, 12 Aug 2024 15:04:20 +0100 Subject: [PATCH 11/30] Refactors add-cloud-to-controller (#1312) * Refactors add-cloud-to-controller --- .golangci.yaml | 2 +- internal/jimm/access.go | 271 ++++++++++++++++++++++++---------------- internal/jimm/cloud.go | 197 ++++++++++++++++------------- internal/jimm/utils.go | 62 +++++++++ 4 files changed, 342 insertions(+), 190 deletions(-) create mode 100644 internal/jimm/utils.go diff --git a/.golangci.yaml b/.golangci.yaml index 96bc21b20..74557b359 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -54,7 +54,7 @@ linters: # Style based linters - promlinter - gocritic - # - gocognit # To be fixed + - gocognit # To be fixed - goheader - importas - gci diff --git a/internal/jimm/access.go b/internal/jimm/access.go index 5359180e4..6a296ea48 100644 --- a/internal/jimm/access.go +++ b/internal/jimm/access.go @@ -10,6 +10,7 @@ import ( "strings" "sync" + "github.com/canonical/ofga" "github.com/google/uuid" "github.com/juju/juju/core/crossmodel" jujuparams "github.com/juju/juju/rpc/params" @@ -480,15 +481,19 @@ func (j *JIMM) ToJAASTag(ctx context.Context, tag *ofganames.Tag, resolveUUIDs b } } -// resolveTag resolves JIMM tag [of any kind available] (i.e., controller-mycontroller:alex@canonical.com/mymodel.myoffer) -// into a juju string tag (i.e., controller-). -// -// If the JIMM tag is aleady of juju string tag form, the transformation is left alone. -// -// In both cases though, the resource the tag pertains to is validated to exist within the database. -func resolveTag(jimmUUID string, db *db.Database, tag string) (*ofganames.Tag, error) { - ctx := context.Background() +type tagResolver struct { + resourceUUID string + trailer string + controllerName string + userName string + modelName string + offerName string + relation ofga.Relation +} + +func newTagResolver(tag string) (*tagResolver, string, error) { matches := jujuURIMatcher.FindStringSubmatch(tag) + tagKind := matches[1] resourceUUID := "" trailer := "" // We first attempt to see if group3 is a uuid @@ -509,122 +514,178 @@ func resolveTag(jimmUUID string, db *db.Database, tag string) (*ofganames.Tag, e relationString := strings.TrimLeft(matches[10], "#") relation, err := ofganames.ParseRelation(relationString) if err != nil { - return nil, errors.E("failed to parse relation", errors.CodeBadRequest) - } + return nil, "", errors.E("failed to parse relation", errors.CodeBadRequest) + } + return &tagResolver{ + resourceUUID: resourceUUID, + trailer: trailer, + controllerName: controllerName, + userName: userName, + modelName: modelName, + offerName: offerName, + relation: relation, + }, tagKind, nil +} - switch matches[1] { - case names.UserTagKind: - zapctx.Debug( - ctx, - "Resolving JIMM tags to Juju tags for tag kind: user", - zap.String("user-name", trailer), - ) - return ofganames.ConvertTagWithRelation(names.NewUserTag(trailer), relation), nil +func (t *tagResolver) userTag(ctx context.Context) (*ofga.Entity, error) { + zapctx.Debug( + ctx, + "Resolving JIMM tags to Juju tags for tag kind: user", + zap.String("user-name", t.trailer), + ) + return ofganames.ConvertTagWithRelation(names.NewUserTag(t.trailer), t.relation), nil +} - case jimmnames.GroupTagKind: - zapctx.Debug( - ctx, - "Resolving JIMM tags to Juju tags for tag kind: group", - zap.String("group-name", trailer), - ) - var entry dbmodel.GroupEntry - if resourceUUID != "" { - entry.UUID = resourceUUID - } else if trailer != "" { - entry.Name = trailer - } - err := db.GetGroup(ctx, &entry) - if err != nil { - return nil, errors.E(fmt.Sprintf("group %s not found", trailer)) - } - return ofganames.ConvertTagWithRelation(jimmnames.NewGroupTag(entry.UUID), relation), nil +func (t *tagResolver) groupTag(ctx context.Context, db *db.Database) (*ofga.Entity, error) { + zapctx.Debug( + ctx, + "Resolving JIMM tags to Juju tags for tag kind: group", + zap.String("group-name", t.trailer), + ) + var entry dbmodel.GroupEntry + if t.resourceUUID != "" { + entry.UUID = t.resourceUUID + } else if t.trailer != "" { + entry.Name = t.trailer + } + err := db.GetGroup(ctx, &entry) + if err != nil { + return nil, errors.E(fmt.Sprintf("group %s not found", t.trailer)) + } + return ofganames.ConvertTagWithRelation(jimmnames.NewGroupTag(entry.UUID), t.relation), nil +} - case names.ControllerTagKind: - zapctx.Debug( - ctx, - "Resolving JIMM tags to Juju tags for tag kind: controller", - ) - controller := dbmodel.Controller{} - - if resourceUUID != "" { - controller.UUID = resourceUUID - } else if controllerName != "" { - if controllerName == jimmControllerName { - return ofganames.ConvertTagWithRelation(names.NewControllerTag(jimmUUID), relation), nil - } - controller.Name = controllerName +func (t *tagResolver) controllerTag(ctx context.Context, jimmUUID string, db *db.Database) (*ofga.Entity, error) { + zapctx.Debug( + ctx, + "Resolving JIMM tags to Juju tags for tag kind: controller", + ) + controller := dbmodel.Controller{} + + if t.resourceUUID != "" { + controller.UUID = t.resourceUUID + } else if t.controllerName != "" { + if t.controllerName == jimmControllerName { + return ofganames.ConvertTagWithRelation(names.NewControllerTag(jimmUUID), t.relation), nil } + controller.Name = t.controllerName + } - // NOTE (alesstimec) Do we need to special-case the - // controller-jimm case - jimm controller does not exist - // in the database, but has a clearly defined UUID? + // NOTE (alesstimec) Do we need to special-case the + // controller-jimm case - jimm controller does not exist + // in the database, but has a clearly defined UUID? + err := db.GetController(ctx, &controller) + if err != nil { + return nil, errors.E("controller not found") + } + return ofganames.ConvertTagWithRelation(names.NewControllerTag(controller.UUID), t.relation), nil +} + +func (t *tagResolver) modelTag(ctx context.Context, db *db.Database) (*ofga.Entity, error) { + zapctx.Debug( + ctx, + "Resolving JIMM tags to Juju tags for tag kind: model", + ) + model := dbmodel.Model{} + + if t.resourceUUID != "" { + model.UUID = sql.NullString{String: t.resourceUUID, Valid: true} + } else if t.controllerName != "" && t.userName != "" && t.modelName != "" { + controller := dbmodel.Controller{Name: t.controllerName} err := db.GetController(ctx, &controller) if err != nil { return nil, errors.E("controller not found") } - return ofganames.ConvertTagWithRelation(names.NewControllerTag(controller.UUID), relation), nil + model.ControllerID = controller.ID + model.OwnerIdentityName = t.userName + model.Name = t.modelName + } - case names.ModelTagKind: - zapctx.Debug( - ctx, - "Resolving JIMM tags to Juju tags for tag kind: model", - ) - model := dbmodel.Model{} - - if resourceUUID != "" { - model.UUID = sql.NullString{String: resourceUUID, Valid: true} - } else if controllerName != "" && userName != "" && modelName != "" { - controller := dbmodel.Controller{Name: controllerName} - err := db.GetController(ctx, &controller) - if err != nil { - return nil, errors.E("controller not found") - } - model.ControllerID = controller.ID - model.OwnerIdentityName = userName - model.Name = modelName - } + err := db.GetModel(ctx, &model) + if err != nil { + return nil, errors.E("model not found") + } + + return ofganames.ConvertTagWithRelation(names.NewModelTag(model.UUID.String), t.relation), nil +} - err := db.GetModel(ctx, &model) +func (t *tagResolver) applicationOfferTag(ctx context.Context, db *db.Database) (*ofga.Entity, error) { + zapctx.Debug( + ctx, + "Resolving JIMM tags to Juju tags for tag kind: applicationoffer", + ) + offer := dbmodel.ApplicationOffer{} + + if t.resourceUUID != "" { + offer.UUID = t.resourceUUID + } else if t.controllerName != "" && t.userName != "" && t.modelName != "" && t.offerName != "" { + offerURL, err := crossmodel.ParseOfferURL(fmt.Sprintf("%s:%s/%s.%s", t.controllerName, t.userName, t.modelName, t.offerName)) if err != nil { - return nil, errors.E("model not found") + zapctx.Debug( + ctx, + "failed to parse application offer url", + zap.String( + "url", + fmt.Sprintf( + "%s:%s/%s.%s", + t.controllerName, + t.userName, + t.modelName, + t.offerName, + ), + ), + zaputil.Error(err), + ) + return nil, errors.E("failed to parse offer url", err) } + offer.URL = offerURL.String() + } - return ofganames.ConvertTagWithRelation(names.NewModelTag(model.UUID.String), relation), nil + err := db.GetApplicationOffer(ctx, &offer) + if err != nil { + return nil, errors.E("application offer not found") + } - case names.ApplicationOfferTagKind: - zapctx.Debug( - ctx, - "Resolving JIMM tags to Juju tags for tag kind: applicationoffer", - ) - offer := dbmodel.ApplicationOffer{} - - if resourceUUID != "" { - offer.UUID = resourceUUID - } else if controllerName != "" && userName != "" && modelName != "" && offerName != "" { - offerURL, err := crossmodel.ParseOfferURL(fmt.Sprintf("%s:%s/%s.%s", controllerName, userName, modelName, offerName)) - if err != nil { - zapctx.Debug(ctx, "failed to parse application offer url", zap.String("url", fmt.Sprintf("%s:%s/%s.%s", controllerName, userName, modelName, offerName)), zaputil.Error(err)) - return nil, errors.E("failed to parse offer url", err) - } - offer.URL = offerURL.String() - } + return ofganames.ConvertTagWithRelation(names.NewApplicationOfferTag(offer.UUID), t.relation), nil +} +func (t *tagResolver) serviceAccountTag(ctx context.Context) (*ofga.Entity, error) { + zapctx.Debug( + ctx, + "Resolving JIMM tags to Juju tags for tag kind: serviceaccount", + zap.String("serviceaccount-name", t.trailer), + ) + return ofganames.ConvertTagWithRelation(jimmnames.NewServiceAccountTag(t.trailer), t.relation), nil +} - err := db.GetApplicationOffer(ctx, &offer) - if err != nil { - return nil, errors.E("application offer not found") - } +// resolveTag resolves JIMM tag [of any kind available] (i.e., controller-mycontroller:alex@canonical.com/mymodel.myoffer) +// into a juju string tag (i.e., controller-). +// +// If the JIMM tag is aleady of juju string tag form, the transformation is left alone. +// +// In both cases though, the resource the tag pertains to is validated to exist within the database. +func resolveTag(jimmUUID string, db *db.Database, tag string) (*ofganames.Tag, error) { + ctx := context.Background() + resolver, tagKind, err := newTagResolver(tag) + if err != nil { + return nil, errors.E("failed to setup tag resolver", err) + } - return ofganames.ConvertTagWithRelation(names.NewApplicationOfferTag(offer.UUID), relation), nil + switch tagKind { + case names.UserTagKind: + return resolver.userTag(ctx) + case jimmnames.GroupTagKind: + return resolver.groupTag(ctx, db) + case names.ControllerTagKind: + return resolver.controllerTag(ctx, jimmUUID, db) + case names.ModelTagKind: + return resolver.modelTag(ctx, db) + case names.ApplicationOfferTagKind: + return resolver.applicationOfferTag(ctx, db) case jimmnames.ServiceAccountTagKind: - zapctx.Debug( - ctx, - "Resolving JIMM tags to Juju tags for tag kind: serviceaccount", - zap.String("serviceaccount-name", trailer), - ) - return ofganames.ConvertTagWithRelation(jimmnames.NewServiceAccountTag(trailer), relation), nil - } - return nil, errors.E("failed to map tag " + matches[1]) + return resolver.serviceAccountTag(ctx) + } + return nil, errors.E("failed to map tag " + tagKind) } // ParseTag attempts to parse the provided key into a tag whilst additionally diff --git a/internal/jimm/cloud.go b/internal/jimm/cloud.go index 09e547aa5..9488424d5 100644 --- a/internal/jimm/cloud.go +++ b/internal/jimm/cloud.go @@ -184,106 +184,33 @@ var DefaultReservedCloudNames = []string{ func (j *JIMM) AddCloudToController(ctx context.Context, user *openfga.User, controllerName string, tag names.CloudTag, cloud jujuparams.Cloud, force bool) error { const op = errors.Op("jimm.AddCloudToController") - controller := dbmodel.Controller{ - Name: controllerName, - } - err := j.Database.GetController(ctx, &controller) - if err != nil { - return errors.E(op, errors.CodeNotFound, "controller not found") - } - - isAdministrator, err := openfga.IsAdministrator(ctx, user, controller.ResourceTag()) + controller, err := j.getControllerByName(ctx, controllerName) if err != nil { return errors.E(op, err) } - if !isAdministrator { - return errors.E(op, errors.CodeUnauthorized, "unauthorized") + if err := j.checkControllerAdminAccess(ctx, user, controller); err != nil { + return errors.E(op, err) } - // Ensure the new cloud could not mask the name of a known public cloud. - reservedNames := j.ReservedCloudNames - if len(reservedNames) == 0 { - reservedNames = DefaultReservedCloudNames - } - for _, n := range reservedNames { - if tag.Id() == n { - return errors.E(op, errors.CodeAlreadyExists, fmt.Sprintf("cloud %q already exists", tag.Id())) - } + if err := checkReservedCloudNames(tag, j.ReservedCloudNames); err != nil { + return errors.E(op, err) } - if cloud.HostCloudRegion != "" { - parts := strings.SplitN(cloud.HostCloudRegion, "/", 2) - if len(parts) != 2 || parts[0] == "" { - return errors.E(op, errors.CodeIncompatibleClouds, fmt.Sprintf("cloud host region %q has invalid cloud/region format", cloud.HostCloudRegion)) - } - region, err := j.Database.FindRegion(ctx, parts[0], parts[1]) - if err != nil { - if errors.ErrorCode(err) == errors.CodeNotFound { - return errors.E(op, err, errors.CodeIncompatibleClouds, fmt.Sprintf("unable to find cloud/region %q", cloud.HostCloudRegion)) - } - return errors.E(op, err) - } - allowedAddModel, err := user.IsAllowedAddModel(ctx, region.Cloud.ResourceTag()) - if err != nil { - return errors.E(op, err) - } - - if !allowedAddModel { - return errors.E(op, errors.CodeUnauthorized, fmt.Sprintf("missing access to %q", cloud.HostCloudRegion)) - } - - if region.Cloud.HostCloudRegion != "" { - // Do not support creating a new cloud on an already hosted - // cloud. - return errors.E(op, errors.CodeIncompatibleClouds, fmt.Sprintf("cloud already hosted %q", cloud.HostCloudRegion)) - } - - found := false - for _, rc := range region.Controllers { - if rc.Controller.Name == controllerName { - found = true - break - } - } - if !found { - return errors.E(op, errors.CodeNotFound, "controller not found") - } + if err := validateCloudRegion(ctx, &j.Database, user, cloud, controllerName); err != nil { + return errors.E(op, err) } - var dbCloud dbmodel.Cloud - dbCloud.FromJujuCloud(cloud) - dbCloud.Name = tag.Id() - - ccloud, err := j.addControllerCloud(ctx, &controller, user.ResourceTag(), tag, cloud, force) + dbCloud, err := j.addCloudToDatabase(ctx, controller, user, tag, cloud, force) if err != nil { return errors.E(op, err) } - dbCloud.FromJujuCloud(*ccloud) - for i := range dbCloud.Regions { - dbCloud.Regions[i].Controllers = []dbmodel.CloudRegionControllerPriority{{ - ControllerID: controller.ID, - Priority: dbmodel.CloudRegionControllerPrioritySupported, - }} - } - if err := j.Database.AddCloud(ctx, &dbCloud); err != nil { + // TODO(ale8k): We've added the cloud to the db, but the access failed. + // This call needs to be idempotent. + if err := j.addCloudControllerRelation(ctx, dbCloud, controller); err != nil { return errors.E(op, err) } - - err = j.OpenFGAClient.AddCloudController(ctx, dbCloud.ResourceTag(), controller.ResourceTag()) - if err != nil { - zapctx.Error( - ctx, - "failed to add controller relation between controller and cloud", - zap.String("controller", controller.ResourceTag().Id()), - zap.String("cloud", dbCloud.ResourceTag().Id()), - zap.Error(err), - ) - } - - // TODO(Kian) CSS-6081 Give user access to the cloud here and potentially everyone. - return nil } @@ -814,3 +741,105 @@ func (j *JIMM) RemoveCloudFromController(ctx context.Context, user *openfga.User return nil } + +// addCloudControllerRelation adds a controller relation between a cloud and controller. +func (j *JIMM) addCloudControllerRelation(ctx context.Context, cloud dbmodel.Cloud, ctl *dbmodel.Controller) error { + err := j.OpenFGAClient.AddCloudController(ctx, cloud.ResourceTag(), ctl.ResourceTag()) + if err != nil { + zapctx.Error( + ctx, + "failed to add controller relation between controller and cloud", + zap.String("controller", ctl.ResourceTag().Id()), + zap.String("cloud", cloud.ResourceTag().Id()), + zap.Error(err), + ) + } + return err +} + +// validateCloudRegion validates that the cloud region: +// +// - Exists +// - The user can add models using this cloud +// - The host cloud region is set +// - The controller we wish to add a cloud to is in the region +func validateCloudRegion(ctx context.Context, db *db.Database, user *openfga.User, cloud jujuparams.Cloud, controllerName string) error { + if cloud.HostCloudRegion == "" { + return nil + } + + parts := strings.SplitN(cloud.HostCloudRegion, "/", 2) + if len(parts) != 2 || parts[0] == "" { + return errors.E(errors.CodeIncompatibleClouds, fmt.Sprintf("cloud host region %q has invalid cloud/region format", cloud.HostCloudRegion)) + } + + region, err := db.FindRegion(ctx, parts[0], parts[1]) + if err != nil { + if errors.ErrorCode(err) == errors.CodeNotFound { + return errors.E(errors.CodeIncompatibleClouds, fmt.Sprintf("unable to find cloud/region %q", cloud.HostCloudRegion)) + } + return err + } + + allowedAddModel, err := user.IsAllowedAddModel(ctx, region.Cloud.ResourceTag()) + if err != nil { + return err + } + if !allowedAddModel { + return errors.E(errors.CodeUnauthorized, fmt.Sprintf("missing access to %q", cloud.HostCloudRegion)) + } + + if region.Cloud.HostCloudRegion != "" { + return errors.E(errors.CodeIncompatibleClouds, fmt.Sprintf("cloud already hosted %q", cloud.HostCloudRegion)) + } + + for _, rc := range region.Controllers { + if rc.Controller.Name == controllerName { + return nil + } + } + return errors.E(errors.CodeNotFound, "controller not found") +} + +// checkReservedCloudNames checks if the tag intended to be added to JIMM +// is a reserved name. +func checkReservedCloudNames(tag names.CloudTag, reservedCloudNames []string) error { + reservedNames := reservedCloudNames + if len(reservedNames) == 0 { + reservedNames = DefaultReservedCloudNames + } + for _, n := range reservedNames { + if tag.Id() == n { + return errors.E(errors.CodeAlreadyExists, fmt.Sprintf("cloud %q already exists", tag.Id())) + } + } + return nil +} + +// addCloudToDatabase adds the cloud to the database for this controller. +// Additionally, it sets the cloud to controller access relation. +func (j *JIMM) addCloudToDatabase(ctx context.Context, controller *dbmodel.Controller, user *openfga.User, tag names.CloudTag, cloud jujuparams.Cloud, force bool) (dbmodel.Cloud, error) { + const op = errors.Op("jimm.addCloudToDatabase") + + var dbCloud dbmodel.Cloud + dbCloud.FromJujuCloud(cloud) + dbCloud.Name = tag.Id() + + ccloud, err := j.addControllerCloud(ctx, controller, user.ResourceTag(), tag, cloud, force) + if err != nil { + return dbCloud, errors.E(op, err) + } + + dbCloud.FromJujuCloud(*ccloud) + for i := range dbCloud.Regions { + dbCloud.Regions[i].Controllers = []dbmodel.CloudRegionControllerPriority{{ + ControllerID: controller.ID, + Priority: dbmodel.CloudRegionControllerPrioritySupported, + }} + } + if err := j.Database.AddCloud(ctx, &dbCloud); err != nil { + return dbCloud, errors.E(op, err) + } + + return dbCloud, nil +} diff --git a/internal/jimm/utils.go b/internal/jimm/utils.go new file mode 100644 index 000000000..d8dfffd43 --- /dev/null +++ b/internal/jimm/utils.go @@ -0,0 +1,62 @@ +// Copyright 2024 Canonical. +package jimm + +import ( + "context" + + "github.com/juju/names/v5" + "github.com/juju/zaputil" + "github.com/juju/zaputil/zapctx" + + "github.com/canonical/jimm/v3/internal/dbmodel" + "github.com/canonical/jimm/v3/internal/errors" + "github.com/canonical/jimm/v3/internal/openfga" +) + +/** +* Authorisation utilities +**/ + +// checkJimmAdmin checks if the user is a JIMM admin. +func (j *JIMM) checkJimmAdmin(user *openfga.User) error { + if !user.JimmAdmin { + return errors.E(errors.CodeUnauthorized, "unauthorized") + } + return nil +} + +// checkAdminAccess checks if the user is an admin of the controller. +func (j *JIMM) checkControllerAdminAccess(ctx context.Context, user *openfga.User, controller *dbmodel.Controller) error { + isAdministrator, err := openfga.IsAdministrator(ctx, user, controller.ResourceTag()) + if err != nil { + return err + } + if !isAdministrator { + return errors.E(errors.CodeUnauthorized, "unauthorized") + } + return nil +} + +/** +* General utility +**/ + +// getController gets the controller from the database by name. +func (j *JIMM) getControllerByName(ctx context.Context, controllerName string) (*dbmodel.Controller, error) { + controller := dbmodel.Controller{Name: controllerName} + err := j.Database.GetController(ctx, &controller) + if err != nil { + return nil, errors.E(errors.CodeNotFound, "controller not found") + } + return &controller, nil +} + +// dialController dials a controller. +func (j *JIMM) dialController(ctx context.Context, ctl *dbmodel.Controller) (API, error) { + api, err := j.dial(ctx, ctl, names.ModelTag{}) + if err != nil { + zapctx.Error(ctx, "failed to dial the controller", zaputil.Error(err)) + return nil, err + } + return api, nil +} From 7a3ae6349ce6ec5d37971f379ceb3d128501391b Mon Sep 17 00:00:00 2001 From: Alexander <42068202+ale8k@users.noreply.github.com> Date: Fri, 16 Aug 2024 16:13:04 +0100 Subject: [PATCH 12/30] Refactor add controller (#1313) * Refactors add-cloud-to-controller The function was confusing and now attempts to be readable by function names. * update * refactors add controller * pr comments * pr comment * attempt to fix tests * fix custom tls hostname error * Reset refactor * wip * fix custom tls * wip * wip * wip * prevent refactor of tests * remove extra get * revert access control test and update godoc * update comment * add controller transactor * Update for ales' comments Co-authored-by: Ales Stimec --------- Co-authored-by: Ales Stimec --- internal/cmdtest/jimmsuite.go | 7 - internal/jimm/access.go | 30 +-- internal/jimm/cloud.go | 4 +- internal/jimm/controller.go | 272 +++++++++++++++++------- internal/jujuapi/access_control_test.go | 15 +- internal/jujuapi/jimm.go | 22 +- 6 files changed, 235 insertions(+), 115 deletions(-) diff --git a/internal/cmdtest/jimmsuite.go b/internal/cmdtest/jimmsuite.go index fa9dfc8f7..b703a7fd8 100644 --- a/internal/cmdtest/jimmsuite.go +++ b/internal/cmdtest/jimmsuite.go @@ -108,13 +108,6 @@ func (s *JimmCmdSuite) SetUpTest(c *gc.C) { s.AdminUser = i s.AdminUser.LastLogin = db.Now() - err = s.JIMM.Database.GetIdentity(ctx, s.AdminUser) - c.Assert(err, gc.Equals, nil) - - alice := openfga.NewUser(s.AdminUser, ofgaClient) - err = alice.SetControllerAccess(context.Background(), s.JIMM.ResourceTag(), ofganames.AdministratorRelation) - c.Assert(err, gc.Equals, nil) - s.AddAdminUser(c, "alice@canonical.com") w := new(bytes.Buffer) diff --git a/internal/jimm/access.go b/internal/jimm/access.go index 6a296ea48..626e4e820 100644 --- a/internal/jimm/access.go +++ b/internal/jimm/access.go @@ -533,6 +533,12 @@ func (t *tagResolver) userTag(ctx context.Context) (*ofga.Entity, error) { "Resolving JIMM tags to Juju tags for tag kind: user", zap.String("user-name", t.trailer), ) + + valid := names.IsValidUser(t.trailer) + if !valid { + // TODO(ale8k): Return custom error for validation check at JujuAPI + return nil, errors.E("invalid user") + } return ofganames.ConvertTagWithRelation(names.NewUserTag(t.trailer), t.relation), nil } @@ -548,11 +554,13 @@ func (t *tagResolver) groupTag(ctx context.Context, db *db.Database) (*ofga.Enti } else if t.trailer != "" { entry.Name = t.trailer } + err := db.GetGroup(ctx, &entry) if err != nil { return nil, errors.E(fmt.Sprintf("group %s not found", t.trailer)) } - return ofganames.ConvertTagWithRelation(jimmnames.NewGroupTag(entry.UUID), t.relation), nil + + return ofganames.ConvertTagWithRelation(entry.ResourceTag(), t.relation), nil } func (t *tagResolver) controllerTag(ctx context.Context, jimmUUID string, db *db.Database) (*ofga.Entity, error) { @@ -579,7 +587,7 @@ func (t *tagResolver) controllerTag(ctx context.Context, jimmUUID string, db *db if err != nil { return nil, errors.E("controller not found") } - return ofganames.ConvertTagWithRelation(names.NewControllerTag(controller.UUID), t.relation), nil + return ofganames.ConvertTagWithRelation(controller.ResourceTag(), t.relation), nil } func (t *tagResolver) modelTag(ctx context.Context, db *db.Database) (*ofga.Entity, error) { @@ -625,16 +633,7 @@ func (t *tagResolver) applicationOfferTag(ctx context.Context, db *db.Database) zapctx.Debug( ctx, "failed to parse application offer url", - zap.String( - "url", - fmt.Sprintf( - "%s:%s/%s.%s", - t.controllerName, - t.userName, - t.modelName, - t.offerName, - ), - ), + zap.String("url", fmt.Sprintf("%s:%s/%s.%s", t.controllerName, t.userName, t.modelName, t.offerName)), zaputil.Error(err), ) return nil, errors.E("failed to parse offer url", err) @@ -647,7 +646,7 @@ func (t *tagResolver) applicationOfferTag(ctx context.Context, db *db.Database) return nil, errors.E("application offer not found") } - return ofganames.ConvertTagWithRelation(names.NewApplicationOfferTag(offer.UUID), t.relation), nil + return ofganames.ConvertTagWithRelation(offer.ResourceTag(), t.relation), nil } func (t *tagResolver) serviceAccountTag(ctx context.Context) (*ofga.Entity, error) { zapctx.Debug( @@ -655,6 +654,11 @@ func (t *tagResolver) serviceAccountTag(ctx context.Context) (*ofga.Entity, erro "Resolving JIMM tags to Juju tags for tag kind: serviceaccount", zap.String("serviceaccount-name", t.trailer), ) + if !jimmnames.IsValidServiceAccountId(t.trailer) { + // TODO(ale8k): Return custom error for validation check at JujuAPI + return nil, errors.E("invalid service account id") + } + return ofganames.ConvertTagWithRelation(jimmnames.NewServiceAccountTag(t.trailer), t.relation), nil } diff --git a/internal/jimm/cloud.go b/internal/jimm/cloud.go index 9488424d5..ba0259747 100644 --- a/internal/jimm/cloud.go +++ b/internal/jimm/cloud.go @@ -208,7 +208,7 @@ func (j *JIMM) AddCloudToController(ctx context.Context, user *openfga.User, con // TODO(ale8k): We've added the cloud to the db, but the access failed. // This call needs to be idempotent. - if err := j.addCloudControllerRelation(ctx, dbCloud, controller); err != nil { + if err := j.addCloudControllerRelation(ctx, dbCloud, *controller); err != nil { return errors.E(op, err) } return nil @@ -743,7 +743,7 @@ func (j *JIMM) RemoveCloudFromController(ctx context.Context, user *openfga.User } // addCloudControllerRelation adds a controller relation between a cloud and controller. -func (j *JIMM) addCloudControllerRelation(ctx context.Context, cloud dbmodel.Cloud, ctl *dbmodel.Controller) error { +func (j *JIMM) addCloudControllerRelation(ctx context.Context, cloud dbmodel.Cloud, ctl dbmodel.Controller) error { err := j.OpenFGAClient.AddCloudController(ctx, cloud.ResourceTag(), ctl.ResourceTag()) if err != nil { zapctx.Error( diff --git a/internal/jimm/controller.go b/internal/jimm/controller.go index 26d6b3e73..22264928a 100644 --- a/internal/jimm/controller.go +++ b/internal/jimm/controller.go @@ -41,6 +41,174 @@ var ( } ) +// convertJujuCloudsToDbClouds converts all of the incoming Juju clouds (from a map) into +// a slice of dbmodel Clouds. +func convertJujuCloudsToDbClouds(clouds map[names.CloudTag]jujuparams.Cloud) []dbmodel.Cloud { + var dbClouds []dbmodel.Cloud + for tag, cld := range clouds { + var cloud dbmodel.Cloud + cloud.FromJujuCloud(cld) + cloud.Name = tag.Id() + dbClouds = append(dbClouds, cloud) + } + return dbClouds +} + +// getControllerModelSummary returns the controllers model summary. +func getControllerModelSummary(ctx context.Context, api API) (jujuparams.ModelSummary, error) { + var ms jujuparams.ModelSummary + if err := api.ControllerModelSummary(ctx, &ms); err != nil { + zapctx.Error(ctx, "failed to get model summary", zaputil.Error(err)) + return ms, err + } + return ms, nil +} + +// getCloudNameFromModelSummary returns the cloud name for a model summary. +func getCloudNameFromModelSummary(modelSummary jujuparams.ModelSummary) (string, error) { + cloudTag, err := names.ParseCloudTag(modelSummary.CloudTag) + if err != nil { + return "", err + } + return cloudTag.Id(), nil +} + +// addControllerTransactor adds a controller to the database ensuring it's clouds, regions +// and region priorities have also been persisted. Additionally, it ensures the region +// priorities are set too. +type addControllerTransactor struct { + jimm *JIMM + jujuClouds []dbmodel.Cloud + controller *dbmodel.Controller + tx *db.Database +} + +// newAddControllerTransactor creates a new addControllerTransactor. +func newAddControllerTransactor(j *JIMM, jujuClouds []dbmodel.Cloud, ctl *dbmodel.Controller, tx *db.Database) *addControllerTransactor { + return &addControllerTransactor{ + jimm: j, + jujuClouds: jujuClouds, + controller: ctl, + tx: tx, + } +} + +// addCloud adds a cloud from a juju API call within a transaction. +// +// After the cloud has been added, it is returned. +func (act *addControllerTransactor) addCloud(ctx context.Context, jujuCloud dbmodel.Cloud) (dbmodel.Cloud, error) { + cloud := jujuCloud + if err := act.tx.GetCloud(ctx, &cloud); err != nil { + if errors.ErrorCode(err) != errors.CodeNotFound { + zapctx.Error(ctx, "failed to fetch the cloud", zaputil.Error(err), zap.String("cloud-name", jujuCloud.Name)) + return cloud, err + } + err := act.tx.AddCloud(ctx, &cloud) + if err != nil && errors.ErrorCode(err) != errors.CodeAlreadyExists { + zapctx.Error(ctx, "failed to add cloud", zaputil.Error(err)) + return cloud, err + } + } + return cloud, nil +} + +// addCloudRegions iterates over the regions for the passed cloud, adding them to the database in the +// existing transaction. +// +// Additionally, it appends the added cloud region (to the database) to the passed +// cloud dbmodel.Cloud. This prevents the need to get the cloud from the database again. +func (act *addControllerTransactor) addCloudRegions(ctx context.Context, cloud dbmodel.Cloud, regions []dbmodel.CloudRegion) (dbmodel.Cloud, error) { + for _, reg := range regions { + if cloud.Region(reg.Name).ID != 0 { + continue + } + reg.CloudName = cloud.Name + if err := act.tx.AddCloudRegion(ctx, ®); err != nil { + zapctx.Error(ctx, "failed to add cloud region", zaputil.Error(err)) + return cloud, err + } + } + return cloud, nil +} + +// Sets controller cloud region priorities for this dbmodel.Controller, +// these priorities are set based on the following. +// +// Regions are defined on two fields, the cloud name and the region name. +// +// We have two priorities and they are set based on whether +// the incoming region matches the controllers model region. +// +// 1. Priority supported: +// If the region is NOT the same as the controllers region, +// it holds this priority. +// 2. Priority deployed: +// If the region is the same as the controller model, +// it holds this priority. +// +// It is expected that the cloud passed has already been loaded with the previously added +// regions. These regions will be appended to the controller's cloud region priorities. +// in preparation for adding the controller. +func (act *addControllerTransactor) setCloudRegionControllerPriorities(cloud dbmodel.Cloud, regions []dbmodel.CloudRegion) { + for _, cr := range regions { + reg := cloud.Region(cr.Name) + + priority := dbmodel.CloudRegionControllerPrioritySupported + + if cloud.Name == act.controller.CloudName && cr.Name == act.controller.CloudRegion { + priority = dbmodel.CloudRegionControllerPriorityDeployed + } + + act.controller.CloudRegions = append(act.controller.CloudRegions, dbmodel.CloudRegionControllerPriority{ + CloudRegion: reg, + Priority: uint(priority), + }) + } +} + +// Run runs the transactor to add a controller to JIMM. +func (act *addControllerTransactor) Run(ctx context.Context) error { + // Add clouds and their regions to db and sets the controllers + // cloud region priorities + for i := range act.jujuClouds { + incomingJujuCloud := act.jujuClouds[i] + + // Add the cloud + addedCloud, err := act.addCloud(ctx, incomingJujuCloud) + if err != nil { + return err + } + + // Add the clouds regions + _, err = act.addCloudRegions(ctx, addedCloud, incomingJujuCloud.Regions) + if err != nil { + return err + } + // Get the cloud again to populate it's regions (regions are preloaded) + // and now they can be used for updating the controller's region priorities. + if err := act.tx.GetCloud(ctx, &addedCloud); err != nil { + return err + } + + // Update controller dbmodel's region priotiries + act.setCloudRegionControllerPriorities(addedCloud, act.jujuClouds[i].Regions) + } + + // Finally, add the controller with all clouds and their regions set + if err := act.tx.AddController(ctx, act.controller); err != nil { + return err + } + return nil +} + +// addControllerTx stores the clouds, regions, cloud region priorities and the controller itself in the database determined +// from the incoming Juju API.Clouds() call. +func addControllerTx(ctx context.Context, j *JIMM, jujuClouds []dbmodel.Cloud, ctl *dbmodel.Controller) error { + return j.Database.Transaction(func(tx *db.Database) error { + return newAddControllerTransactor(j, jujuClouds, ctl, tx).Run(ctx) + }) +} + // AddController adds the specified controller to JIMM. Only // controller-admin level users may add new controllers. If the user adding // the controller is not authorized then an error with a code of @@ -52,28 +220,28 @@ var ( func (j *JIMM) AddController(ctx context.Context, user *openfga.User, ctl *dbmodel.Controller) error { const op = errors.Op("jimm.AddController") - if !user.JimmAdmin { - return errors.E(op, errors.CodeUnauthorized, "unauthorized") + if err := j.checkJimmAdmin(user); err != nil { + return err } - api, err := j.dial(ctx, ctl, names.ModelTag{}) + api, err := j.dialController(ctx, ctl) if err != nil { - zapctx.Error(ctx, "failed to dial the controller", zaputil.Error(err)) - return errors.E(op, err, "failed to dial the controller") + return errors.E(op, "failed to dial the controller", err) } defer api.Close() - var ms jujuparams.ModelSummary - if err := api.ControllerModelSummary(ctx, &ms); err != nil { - zapctx.Error(ctx, "failed to get model summary", zaputil.Error(err)) + modelSummary, err := getControllerModelSummary(ctx, api) + if err != nil { return errors.E(op, err, "failed to get model summary") } - ct, err := names.ParseCloudTag(ms.CloudTag) + + cloudName, err := getCloudNameFromModelSummary(modelSummary) if err != nil { return errors.E(op, err, "failed to parse the cloud tag") } - ctl.CloudName = ct.Id() - ctl.CloudRegion = ms.CloudRegion + + ctl.CloudName = cloudName + ctl.CloudRegion = modelSummary.CloudRegion // TODO(mhilton) add the controller model? clouds, err := api.Clouds(ctx) @@ -81,91 +249,35 @@ func (j *JIMM) AddController(ctx context.Context, user *openfga.User, ctl *dbmod return errors.E(op, err, "failed to fetch controller clouds") } - var dbClouds []dbmodel.Cloud - for tag, cld := range clouds { - var cloud dbmodel.Cloud - cloud.FromJujuCloud(cld) - cloud.Name = tag.Id() - dbClouds = append(dbClouds, cloud) - } + dbClouds := convertJujuCloudsToDbClouds(clouds) - credentialsStored := false + // TODO(ale8k): This shouldn't be necessary to check, but tests need updating + // to set insecure credential store explicitly. if j.CredentialStore != nil { err := j.CredentialStore.PutControllerCredentials(ctx, ctl.Name, ctl.AdminIdentityName, ctl.AdminPassword) if err != nil { return errors.E(op, err, "failed to store controller credentials") } - credentialsStored = true } - err = j.Database.Transaction(func(tx *db.Database) error { - for i := range dbClouds { - cloud := dbmodel.Cloud{ - Name: dbClouds[i].Name, - } - if err := tx.GetCloud(ctx, &cloud); err != nil { - if errors.ErrorCode(err) != errors.CodeNotFound { - zapctx.Error(ctx, "failed to fetch the cloud", zaputil.Error(err), zap.String("cloud-name", dbClouds[i].Name)) - return err - } - err := tx.AddCloud(ctx, &dbClouds[i]) - if err != nil && errors.ErrorCode(err) != errors.CodeAlreadyExists { - zapctx.Error(ctx, "failed to add cloud", zaputil.Error(err)) - return err - } - if err := tx.GetCloud(ctx, &cloud); err != nil { - zapctx.Error(ctx, "failed to fetch the cloud", zaputil.Error(err), zap.String("cloud-name", dbClouds[i].Name)) - return err - } - } - for _, reg := range dbClouds[i].Regions { - if cloud.Region(reg.Name).ID != 0 { - continue - } - reg.CloudName = cloud.Name - if err := tx.AddCloudRegion(ctx, ®); err != nil { - zapctx.Error(ctx, "failed to add cloud region", zaputil.Error(err)) - return err - } - cloud.Regions = append(cloud.Regions, reg) - } - for _, cr := range dbClouds[i].Regions { - reg := cloud.Region(cr.Name) - priority := dbmodel.CloudRegionControllerPrioritySupported - if cloud.Name == ctl.CloudName && cr.Name == ctl.CloudRegion { - priority = dbmodel.CloudRegionControllerPriorityDeployed - } - ctl.CloudRegions = append(ctl.CloudRegions, dbmodel.CloudRegionControllerPriority{ - CloudRegion: reg, - Priority: uint(priority), - }) - } - } - // if we already stored controller credentials in CredentialStore - // we should not store them plain text in JIMM's DB. - if credentialsStored { - ctl.AdminIdentityName = "" - ctl.AdminPassword = "" - } - if err := tx.AddController(ctx, ctl); err != nil { - if errors.ErrorCode(err) == errors.CodeAlreadyExists { - zapctx.Error(ctx, "failed to add controller", zaputil.Error(err)) - return errors.E(op, err, fmt.Sprintf("controller %q already exists", ctl.Name)) - } - zapctx.Error(ctx, "failed to add controller", zaputil.Error(err)) - return err + // Credential store will always be set either to vault or explicitly insecure, + // no need to be persist in db. + ctl.AdminIdentityName = "" + ctl.AdminPassword = "" + + if err := addControllerTx(ctx, j, dbClouds, ctl); err != nil { + zapctx.Error(ctx, "failed to add controller", zaputil.Error(err)) + if errors.ErrorCode(err) == errors.CodeAlreadyExists { + return errors.E(op, err, fmt.Sprintf("controller %q already exists", ctl.Name)) } - return nil - }) - if err != nil { return errors.E(op, err) } for _, cloud := range dbClouds { // If this cloud is the one used by the controller model then // it is available to all users. Other clouds require `juju grant-cloud` to add permissions. - if cloud.ResourceTag().String() == ms.CloudTag { + if cloud.ResourceTag().String() == modelSummary.CloudTag { everyoneTag := names.NewUserTag(ofganames.EveryoneUser) everyoneIdentity, err := dbmodel.NewIdentity(everyoneTag.Id()) if err != nil { diff --git a/internal/jujuapi/access_control_test.go b/internal/jujuapi/access_control_test.go index efa8482c2..a5284718e 100644 --- a/internal/jujuapi/access_control_test.go +++ b/internal/jujuapi/access_control_test.go @@ -1010,9 +1010,18 @@ func (s *accessControlSuite) TestListRelationshipTuplesAfterDeletingGroup(c *gc. response, err := client.ListRelationshipTuples(&apiparams.ListRelationshipTuplesRequest{ResolveUUIDs: true}) c.Assert(err, jc.ErrorIsNil) // Create a new slice of tuples excluding the ones we expect to be deleted. - newTuples := []apiparams.RelationshipTuple{tuples[1], tuples[3]} - // first three tuples created during setup test - c.Assert(response.Tuples[12:], jc.DeepEquals, newTuples) + responseTuples := response.Tuples[12:] + c.Assert(responseTuples, gc.HasLen, 2) + + expectedUserToGroupTuple := tuples[1] + expectedGroupToOfferTuple := tuples[3] + + // Update the target to the group name + expectedUserToGroupTuple.TargetObject = "group-orange" + c.Assert(responseTuples[0], gc.DeepEquals, expectedUserToGroupTuple) + expectedGroupToOfferTuple.Object = "group-orange#member" + c.Assert(responseTuples[1], gc.DeepEquals, expectedGroupToOfferTuple) + c.Assert(len(response.Errors), gc.Equals, 0) } diff --git a/internal/jujuapi/jimm.go b/internal/jujuapi/jimm.go index e24d485f3..ce029c6ba 100644 --- a/internal/jujuapi/jimm.go +++ b/internal/jujuapi/jimm.go @@ -175,15 +175,6 @@ func (r *controllerRoot) AddController(ctx context.Context, req apiparams.AddCon } } - ctl := dbmodel.Controller{ - UUID: req.UUID, - Name: req.Name, - PublicAddress: req.PublicAddress, - CACertificate: req.CACertificate, - AdminIdentityName: req.Username, - AdminPassword: req.Password, - TLSHostname: req.TLSHostname, - } nphps, err := network.ParseProviderHostPorts(req.APIAddresses...) if err != nil { return apiparams.ControllerInfo{}, errors.E(op, errors.CodeBadRequest, err) @@ -194,7 +185,18 @@ func (r *controllerRoot) AddController(ctx context.Context, req apiparams.AddCon nphps[i].Scope = network.ScopePublic } } - ctl.Addresses = dbmodel.HostPorts{jujuparams.FromProviderHostPorts(nphps)} + + // TODO(ale8k): Don't build dbmodel here, do it as params to AddController. + ctl := dbmodel.Controller{ + UUID: req.UUID, + Name: req.Name, + PublicAddress: req.PublicAddress, + CACertificate: req.CACertificate, + AdminIdentityName: req.Username, + AdminPassword: req.Password, + TLSHostname: req.TLSHostname, + Addresses: dbmodel.HostPorts{jujuparams.FromProviderHostPorts(nphps)}, + } if err := r.jimm.AddController(ctx, r.user, &ctl); err != nil { zapctx.Error(ctx, "failed to add controller", zaputil.Error(err)) return apiparams.ControllerInfo{}, errors.E(op, err) From 7473eaab194e1acf425779b6e0b7d899e6981b76 Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Mon, 19 Aug 2024 10:44:34 +0200 Subject: [PATCH 13/30] Add a login service to the jimm layer (#1314) * add a login service to the jimm layer * fix test due to changed error message * add tests and move UserLogin method The GetUser and UpdateUserLastLogin have been unexported in favour of UserLogin. The tests in admin_test.go use a mock authenticator but primarily verify that the new methods in jimm/admin.go perform the expected validation and have the baseline desired behaviour. * remove redundant mock function * fix test timeout * add comment about service account IDs * fix test and reduce duplication - fixed a test I broke with the recent change. - reduced the cognitive complexity of `handleAdminFacade` by reducing code duplication. * update godoc for LoginWithSessionCookie --- internal/jimm/admin.go | 80 +++++++++++---- internal/jimm/admin_test.go | 137 +++++++++++++++++++++++++ internal/jimm/export_test.go | 9 ++ internal/jimm/jimm.go | 5 - internal/jimm/user.go | 21 +++- internal/jimmjwx/jwks.go | 26 +---- internal/jimmjwx/utils_test.go | 3 + internal/jimmtest/auth.go | 30 ++++-- internal/jimmtest/jimm_mock.go | 19 ++-- internal/jimmtest/mocks/login.go | 54 ++++++++++ internal/jujuapi/admin.go | 87 +++++----------- internal/jujuapi/admin_test.go | 6 +- internal/jujuapi/controllerroot.go | 7 +- internal/jujuapi/websocket.go | 2 +- internal/rpc/client_test.go | 29 ++++-- internal/rpc/proxy.go | 120 +++++++--------------- internal/rpc/proxy_test.go | 158 ++++++++++------------------- 17 files changed, 444 insertions(+), 349 deletions(-) create mode 100644 internal/jimm/admin_test.go create mode 100644 internal/jimmtest/mocks/login.go diff --git a/internal/jimm/admin.go b/internal/jimm/admin.go index bd2f79e96..844761918 100644 --- a/internal/jimm/admin.go +++ b/internal/jimm/admin.go @@ -8,55 +8,91 @@ import ( "golang.org/x/oauth2" "github.com/canonical/jimm/v3/internal/errors" - "github.com/canonical/jimm/v3/internal/jimm/credentials" + "github.com/canonical/jimm/v3/internal/openfga" + "github.com/canonical/jimm/v3/pkg/names" ) // LoginDevice starts the device login flow. -func LoginDevice(ctx context.Context, authenticator OAuthAuthenticator) (*oauth2.DeviceAuthResponse, error) { - const op = errors.Op("jujuapi.LoginDevice") - - deviceResponse, err := authenticator.Device(ctx) +func (j *JIMM) LoginDevice(ctx context.Context) (*oauth2.DeviceAuthResponse, error) { + const op = errors.Op("jimm.LoginDevice") + resp, err := j.OAuthAuthenticator.Device(ctx) if err != nil { return nil, errors.E(op, err) } - - return deviceResponse, nil + return resp, nil } -func GetDeviceSessionToken(ctx context.Context, authenticator OAuthAuthenticator, credentialStore credentials.CredentialStore, deviceOAuthResponse *oauth2.DeviceAuthResponse) (string, error) { - const op = errors.Op("jujuapi.GetDeviceSessionToken") - - if authenticator == nil { - return "", errors.E("nil authenticator") - } +// GetDeviceSessionToken polls an OIDC server while a user logs in and returns a session token scoped to the user's identity. +func (j *JIMM) GetDeviceSessionToken(ctx context.Context, deviceOAuthResponse *oauth2.DeviceAuthResponse) (string, error) { + const op = errors.Op("jimm.GetDeviceSessionToken") - if credentialStore == nil { - return "", errors.E("nil credential store") - } - - token, err := authenticator.DeviceAccessToken(ctx, deviceOAuthResponse) + token, err := j.OAuthAuthenticator.DeviceAccessToken(ctx, deviceOAuthResponse) if err != nil { return "", errors.E(op, err) } - idToken, err := authenticator.ExtractAndVerifyIDToken(ctx, token) + idToken, err := j.OAuthAuthenticator.ExtractAndVerifyIDToken(ctx, token) if err != nil { return "", errors.E(op, err) } - email, err := authenticator.Email(idToken) + email, err := j.OAuthAuthenticator.Email(idToken) if err != nil { return "", errors.E(op, err) } - if err := authenticator.UpdateIdentity(ctx, email, token); err != nil { + if err := j.OAuthAuthenticator.UpdateIdentity(ctx, email, token); err != nil { return "", errors.E(op, err) } - encToken, err := authenticator.MintSessionToken(email) + encToken, err := j.OAuthAuthenticator.MintSessionToken(email) if err != nil { return "", errors.E(op, err) } return string(encToken), nil } + +// LoginClientCredentials verifies a user's client ID and secret before the user is logged in. +func (j *JIMM) LoginClientCredentials(ctx context.Context, clientID string, clientSecret string) (*openfga.User, error) { + const op = errors.Op("jimm.LoginClientCredentials") + // We expect the client to send the service account ID "as-is" and because we know that this is a clientCredentials login, + // we can append the @serviceaccount domain to the clientID (if not already present). + clientIdWithDomain, err := names.EnsureValidServiceAccountId(clientID) + if err != nil { + return nil, errors.E(op, err) + } + + err = j.OAuthAuthenticator.VerifyClientCredentials(ctx, clientID, clientSecret) + if err != nil { + return nil, errors.E(op, err) + } + + return j.UserLogin(ctx, clientIdWithDomain) +} + +// LoginWithSessionToken verifies a user's session token before the user is logged in. +func (j *JIMM) LoginWithSessionToken(ctx context.Context, sessionToken string) (*openfga.User, error) { + const op = errors.Op("jimm.LoginWithSessionToken") + jwtToken, err := j.OAuthAuthenticator.VerifySessionToken(sessionToken) + if err != nil { + return nil, errors.E(op, err) + } + + email := jwtToken.Subject() + return j.UserLogin(ctx, email) +} + +// LoginWithSessionCookie uses the identity ID expected to have come from a session cookie, to log the user in. +// +// The work to parse and store the user's identity from the session cookie takes place in internal/jimmhttp/websocket.go +// [WSHandler.ServerHTTP] during the upgrade from an HTTP connection to a websocket. The user's identity is stored +// and passed to this function with the assumption that the cookie contained a valid session. This function is far from +// the session cookie logic due to the separation between the HTTP layer and Juju's RPC mechanism. +func (j *JIMM) LoginWithSessionCookie(ctx context.Context, identityID string) (*openfga.User, error) { + const op = errors.Op("jimm.LoginWithSessionCookie") + if identityID == "" { + return nil, errors.E(op, "missing cookie identity") + } + return j.UserLogin(ctx, identityID) +} diff --git a/internal/jimm/admin_test.go b/internal/jimm/admin_test.go new file mode 100644 index 000000000..13a8b8b4c --- /dev/null +++ b/internal/jimm/admin_test.go @@ -0,0 +1,137 @@ +// Copyright 2024 Canonical. + +package jimm_test + +import ( + "context" + "encoding/base64" + "testing" + "time" + + qt "github.com/frankban/quicktest" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/lestrrat-go/jwx/v2/jwt" + "golang.org/x/oauth2" + + "github.com/canonical/jimm/v3/internal/db" + "github.com/canonical/jimm/v3/internal/jimm" + "github.com/canonical/jimm/v3/internal/jimmtest" +) + +func TestLoginDevice(t *testing.T) { + c := qt.New(t) + mockAuthenticator := jimmtest.NewMockOAuthAuthenticator(c, nil) + jimm := jimm.JIMM{ + OAuthAuthenticator: &mockAuthenticator, + } + resp, err := jimm.LoginDevice(context.Background()) + c.Assert(err, qt.IsNil) + c.Assert(*resp, qt.CmpEquals(cmpopts.IgnoreTypes(time.Time{})), oauth2.DeviceAuthResponse{ + DeviceCode: "test-device-code", + UserCode: "test-user-code", + VerificationURI: "http://no-such-uri.canonical.com", + VerificationURIComplete: "http://no-such-uri.canonical.com", + Interval: int64(time.Minute.Seconds()), + }) +} + +func TestGetDeviceSessionToken(t *testing.T) { + c := qt.New(t) + pollingChan := make(chan string, 1) + mockAuthenticator := jimmtest.NewMockOAuthAuthenticator(c, pollingChan) + jimm := jimm.JIMM{ + OAuthAuthenticator: &mockAuthenticator, + } + pollingChan <- "user-foo" + token, err := jimm.GetDeviceSessionToken(context.Background(), nil) + c.Assert(err, qt.IsNil) + c.Assert(token, qt.Not(qt.Equals), "") + decodedToken, err := base64.StdEncoding.DecodeString(token) + c.Assert(err, qt.IsNil) + parsedToken, err := jwt.ParseInsecure([]byte(decodedToken)) + c.Assert(err, qt.IsNil) + c.Assert(parsedToken.Subject(), qt.Equals, "user-foo@canonical.com") +} + +func TestLoginClientCredentials(t *testing.T) { + c := qt.New(t) + mockAuthenticator := jimmtest.NewMockOAuthAuthenticator(c, nil) + client, _, _, err := jimmtest.SetupTestOFGAClient(c.Name(), t.Name()) + c.Assert(err, qt.IsNil) + jimm := jimm.JIMM{ + UUID: "foo", + Database: db.Database{ + DB: jimmtest.PostgresDB(c, func() time.Time { return now }), + }, + OAuthAuthenticator: &mockAuthenticator, + OpenFGAClient: client, + } + ctx := context.Background() + err = jimm.Database.Migrate(ctx, false) + c.Assert(err, qt.IsNil) + invalidClientID := "123@123@" + _, err = jimm.LoginClientCredentials(ctx, invalidClientID, "foo-secret") + c.Assert(err, qt.ErrorMatches, "invalid client ID") + + validClientID := "my-svc-acc" + user, err := jimm.LoginClientCredentials(ctx, validClientID, "foo-secret") + c.Assert(err, qt.IsNil) + c.Assert(user.Name, qt.Equals, "my-svc-acc@serviceaccount") +} + +func TestLoginWithSessionToken(t *testing.T) { + c := qt.New(t) + mockAuthenticator := jimmtest.NewMockOAuthAuthenticator(c, nil) + client, _, _, err := jimmtest.SetupTestOFGAClient(c.Name(), t.Name()) + c.Assert(err, qt.IsNil) + jimm := jimm.JIMM{ + UUID: "foo", + Database: db.Database{ + DB: jimmtest.PostgresDB(c, func() time.Time { return now }), + }, + OAuthAuthenticator: &mockAuthenticator, + OpenFGAClient: client, + } + ctx := context.Background() + err = jimm.Database.Migrate(ctx, false) + c.Assert(err, qt.IsNil) + + token, err := jwt.NewBuilder(). + Subject("alice@canonical.com"). + Build() + serialisedToken, err := jwt.NewSerializer().Serialize(token) + c.Assert(err, qt.IsNil) + b64Token := base64.StdEncoding.EncodeToString(serialisedToken) + + _, err = jimm.LoginWithSessionToken(ctx, "invalid-token") + c.Assert(err, qt.ErrorMatches, "failed to decode token") + + user, err := jimm.LoginWithSessionToken(ctx, b64Token) + c.Assert(err, qt.IsNil) + c.Assert(user.Name, qt.Equals, "alice@canonical.com") +} + +func TestLoginWithSessionCookie(t *testing.T) { + c := qt.New(t) + mockAuthenticator := jimmtest.NewMockOAuthAuthenticator(c, nil) + client, _, _, err := jimmtest.SetupTestOFGAClient(c.Name(), t.Name()) + c.Assert(err, qt.IsNil) + jimm := jimm.JIMM{ + UUID: "foo", + Database: db.Database{ + DB: jimmtest.PostgresDB(c, func() time.Time { return now }), + }, + OAuthAuthenticator: &mockAuthenticator, + OpenFGAClient: client, + } + ctx := context.Background() + err = jimm.Database.Migrate(ctx, false) + c.Assert(err, qt.IsNil) + + _, err = jimm.LoginWithSessionCookie(ctx, "") + c.Assert(err, qt.ErrorMatches, "missing cookie identity") + + user, err := jimm.LoginWithSessionCookie(ctx, "alice@canonical.com") + c.Assert(err, qt.IsNil) + c.Assert(user.Name, qt.Equals, "alice@canonical.com") +} diff --git a/internal/jimm/export_test.go b/internal/jimm/export_test.go index 411c81f6e..671e8e35c 100644 --- a/internal/jimm/export_test.go +++ b/internal/jimm/export_test.go @@ -10,6 +10,7 @@ import ( "github.com/canonical/jimm/v3/internal/db" "github.com/canonical/jimm/v3/internal/dbmodel" + "github.com/canonical/jimm/v3/internal/openfga" ) var ( @@ -47,3 +48,11 @@ func NewWatcherWithDeltaProcessedChannel(db db.Database, dialer Dialer, pubsub P func (j *JIMM) ListApplicationOfferUsers(ctx context.Context, offer names.ApplicationOfferTag, user *dbmodel.Identity, accessLevel string) ([]jujuparams.OfferUserDetails, error) { return j.listApplicationOfferUsers(ctx, offer, user, accessLevel) } + +func (j *JIMM) GetUser(ctx context.Context, identifier string) (*openfga.User, error) { + return j.getUser(ctx, identifier) +} + +func (j *JIMM) UpdateUserLastLogin(ctx context.Context, identifier string) error { + return j.updateUserLastLogin(ctx, identifier) +} diff --git a/internal/jimm/jimm.go b/internal/jimm/jimm.go index b9ec53e14..bcde7e929 100644 --- a/internal/jimm/jimm.go +++ b/internal/jimm/jimm.go @@ -87,11 +87,6 @@ type JIMM struct { OAuthAuthenticator OAuthAuthenticator } -// OAuthAuthenticationService returns the JIMM's authentication service. -func (j *JIMM) OAuthAuthenticationService() OAuthAuthenticator { - return j.OAuthAuthenticator -} - // ResourceTag returns JIMM's controller tag stating its UUID. func (j *JIMM) ResourceTag() names.ControllerTag { return names.NewControllerTag(j.UUID) diff --git a/internal/jimm/user.go b/internal/jimm/user.go index ba9e8165e..202be4278 100644 --- a/internal/jimm/user.go +++ b/internal/jimm/user.go @@ -12,9 +12,23 @@ import ( "github.com/canonical/jimm/v3/internal/openfga" ) -// GetUser fetches the user specified by the user's email or the service accounts ID +// UserLogin fetches a user based on their identityName and updates their last login time. +func (j *JIMM) UserLogin(ctx context.Context, identityName string) (*openfga.User, error) { + const op = errors.Op("jimm.UserLogin") + user, err := j.getUser(ctx, identityName) + if err != nil { + return nil, errors.E(op, err, errors.CodeUnauthorized) + } + err = j.updateUserLastLogin(ctx, identityName) + if err != nil { + return nil, errors.E(op, err, errors.CodeUnauthorized) + } + return user, nil +} + +// getUser fetches the user specified by the user's email or the service accounts ID // and returns an openfga User that can be used to verify user's permissions. -func (j *JIMM) GetUser(ctx context.Context, identifier string) (*openfga.User, error) { +func (j *JIMM) getUser(ctx context.Context, identifier string) (*openfga.User, error) { const op = errors.Op("jimm.GetUser") user, err := dbmodel.NewIdentity(identifier) @@ -36,7 +50,8 @@ func (j *JIMM) GetUser(ctx context.Context, identifier string) (*openfga.User, e return u, nil } -func (j *JIMM) UpdateUserLastLogin(ctx context.Context, identifier string) error { +// updateUserLastLogin updates the user's last login time in the database. +func (j *JIMM) updateUserLastLogin(ctx context.Context, identifier string) error { const op = errors.Op("jimm.UpdateUserLastLogin") user, err := dbmodel.NewIdentity(identifier) if err != nil { diff --git a/internal/jimmjwx/jwks.go b/internal/jimmjwx/jwks.go index 2657d66aa..0343b80be 100644 --- a/internal/jimmjwx/jwks.go +++ b/internal/jimmjwx/jwks.go @@ -106,10 +106,6 @@ func (jwks *JWKSService) StartJWKSRotator(ctx context.Context, checkRotateRequir credStore := jwks.credentialStore - // For logging and monitoring purposes, we have the rotator spit errors into - // this buffered channel ((size * amount) * 2 of errors we are currently aware of and doubling it to prevent blocks) - errorChan := make(chan error, 8) - if err := rotateJWKS(ctx, credStore, initialRotateRequiredTime); err != nil { zapctx.Error(ctx, "Rotate JWKS error", zap.Error(err)) return errors.E(op, err) @@ -121,14 +117,12 @@ func (jwks *JWKSService) StartJWKSRotator(ctx context.Context, checkRotateRequir // // In this case we generate a new set, which should expire in 3 months. go func() { - defer close(errorChan) for { select { case <-checkRotateRequired: if err := rotateJWKS(ctx, credStore, initialRotateRequiredTime); err != nil { - errorChan <- err + zapctx.Error(ctx, "security failure", zap.Any("op", op), zap.NamedError("jwks-error", err)) } - case <-ctx.Done(): zapctx.Debug(ctx, "Shutdown for JWKS rotator complete.") return @@ -136,24 +130,6 @@ func (jwks *JWKSService) StartJWKSRotator(ctx context.Context, checkRotateRequir } }() - // If for any reason the rotator has an error, we simply receive the error - // in another routine dedicated to logging said errors. - go func(errChan <-chan error) { - for err := range errChan { - zapctx.Error( - ctx, - "security failure", - zap.Any("op", op), - zap.NamedError("jwks-error", err), - ) - select { - case <-ctx.Done(): - return - default: - } - } - }(errorChan) - return nil } diff --git a/internal/jimmjwx/utils_test.go b/internal/jimmjwx/utils_test.go index f8111f5ea..91f315523 100644 --- a/internal/jimmjwx/utils_test.go +++ b/internal/jimmjwx/utils_test.go @@ -66,6 +66,9 @@ func startAndTestRotator(c *qt.C, ctx context.Context, store credentials.Credent for i := 0; i < 60; i++ { if ks == nil { ks, err = store.GetJWKS(ctx) + if err != nil { + c.Logf("failed to get JWKS: %s", err) + } time.Sleep(500 * time.Millisecond) continue } diff --git a/internal/jimmtest/auth.go b/internal/jimmtest/auth.go index 266399455..2eb24d476 100644 --- a/internal/jimmtest/auth.go +++ b/internal/jimmtest/auth.go @@ -67,7 +67,7 @@ func (a Authenticator) Authenticate(_ context.Context, _ *jujuparams.LoginReques return a.User, a.Err } -type MockOAuthAuthenticator struct { +type mockOAuthAuthenticator struct { jimm.OAuthAuthenticator c SimpleTester // PollingChan is used to simulate polling an OIDC server during the device flow. @@ -77,12 +77,15 @@ type MockOAuthAuthenticator struct { mockAccessToken string } -func NewMockOAuthAuthenticator(c SimpleTester, testChan <-chan string) MockOAuthAuthenticator { - return MockOAuthAuthenticator{c: c, PollingChan: testChan} +// NewMockOAuthAuthenticator creates a mock authenticator for tests. An channel can be passed in +// when testing the device flow to simulate polling an OIDC server. Provide a nil channel +// if the device flow will not be used in the test. +func NewMockOAuthAuthenticator(c SimpleTester, testChan <-chan string) mockOAuthAuthenticator { + return mockOAuthAuthenticator{c: c, PollingChan: testChan} } // Device is a mock implementation for the start of the device flow, returning dummy polling data. -func (m *MockOAuthAuthenticator) Device(ctx context.Context) (*oauth2.DeviceAuthResponse, error) { +func (m *mockOAuthAuthenticator) Device(ctx context.Context) (*oauth2.DeviceAuthResponse, error) { return &oauth2.DeviceAuthResponse{ DeviceCode: "test-device-code", UserCode: "test-user-code", @@ -95,7 +98,7 @@ func (m *MockOAuthAuthenticator) Device(ctx context.Context) (*oauth2.DeviceAuth // DeviceAccessToken is a mock implementation of the second step in the device flow where JIMM // polls an OIDC server for the device code. -func (m *MockOAuthAuthenticator) DeviceAccessToken(ctx context.Context, res *oauth2.DeviceAuthResponse) (*oauth2.Token, error) { +func (m *mockOAuthAuthenticator) DeviceAccessToken(ctx context.Context, res *oauth2.DeviceAuthResponse) (*oauth2.Token, error) { select { case username := <-m.PollingChan: m.polledUsername = username @@ -113,7 +116,7 @@ func (m *MockOAuthAuthenticator) DeviceAccessToken(ctx context.Context, res *oau // VerifySessionToken provides the mock implementation for verifying session tokens. // Allowing JIMM tests to create their own session tokens that will always be accepted. // Notice the use of jwt.ParseInsecure to skip JWT signature verification. -func (m *MockOAuthAuthenticator) VerifySessionToken(token string) (jwt.Token, error) { +func (m *mockOAuthAuthenticator) VerifySessionToken(token string) (jwt.Token, error) { errorFn := func(err error) error { return jimmerrors.E(err, jimmerrors.CodeUnauthorized) } @@ -136,7 +139,7 @@ func (m *MockOAuthAuthenticator) VerifySessionToken(token string) (jwt.Token, er // ExtractAndVerifyIDToken returns an ID token where the subject is equal to the username obtained during the device flow. // The auth token must match the one returned during the device flow. // If the polled username is empty it indicates an error that the device flow was not run prior to calling this function. -func (m *MockOAuthAuthenticator) ExtractAndVerifyIDToken(ctx context.Context, oauth2Token *oauth2.Token) (*oidc.IDToken, error) { +func (m *mockOAuthAuthenticator) ExtractAndVerifyIDToken(ctx context.Context, oauth2Token *oauth2.Token) (*oidc.IDToken, error) { if m.polledUsername == "" { return &oidc.IDToken{}, errors.New("unknown user for mock auth login") } @@ -147,25 +150,30 @@ func (m *MockOAuthAuthenticator) ExtractAndVerifyIDToken(ctx context.Context, oa } // Email returns the subject from an ID token. -func (m *MockOAuthAuthenticator) Email(idToken *oidc.IDToken) (string, error) { +func (m *mockOAuthAuthenticator) Email(idToken *oidc.IDToken) (string, error) { return idToken.Subject, nil } // UpdateIdentity is a no-op mock. -func (m *MockOAuthAuthenticator) UpdateIdentity(ctx context.Context, email string, token *oauth2.Token) error { +func (m *mockOAuthAuthenticator) UpdateIdentity(ctx context.Context, email string, token *oauth2.Token) error { return nil } // MintSessionToken creates an unsigned session token with the email provided. -func (m *MockOAuthAuthenticator) MintSessionToken(email string) (string, error) { +func (m *mockOAuthAuthenticator) MintSessionToken(email string) (string, error) { return newSessionToken(m.c, email, ""), nil } // AuthenticateBrowserSession always returns an error. -func (m *MockOAuthAuthenticator) AuthenticateBrowserSession(ctx context.Context, w http.ResponseWriter, req *http.Request) (context.Context, error) { +func (m *mockOAuthAuthenticator) AuthenticateBrowserSession(ctx context.Context, w http.ResponseWriter, req *http.Request) (context.Context, error) { return ctx, errors.New("authentication failed") } +// VerifyClientCredentials always returns a nil error. +func (m *mockOAuthAuthenticator) VerifyClientCredentials(ctx context.Context, clientID string, clientSecret string) error { + return nil +} + // newSessionToken returns a serialised JWT that can be used in tests. // Tests using a mock authenticator can provide an empty signatureSecret // while integration tests must provide the same secret used when verifying JWTs. diff --git a/internal/jimmtest/jimm_mock.go b/internal/jimmtest/jimm_mock.go index 04991f69a..5eff89a5e 100644 --- a/internal/jimmtest/jimm_mock.go +++ b/internal/jimmtest/jimm_mock.go @@ -17,6 +17,7 @@ import ( "github.com/canonical/jimm/v3/internal/errors" "github.com/canonical/jimm/v3/internal/jimm" jimmcreds "github.com/canonical/jimm/v3/internal/jimm/credentials" + "github.com/canonical/jimm/v3/internal/jimmtest/mocks" "github.com/canonical/jimm/v3/internal/openfga" ofganames "github.com/canonical/jimm/v3/internal/openfga/names" "github.com/canonical/jimm/v3/internal/pubsub" @@ -29,6 +30,7 @@ import ( // will delegate to the requested funcion or if the funcion is nil return // a NotImplemented error. type JIMM struct { + mocks.LoginService 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 @@ -63,7 +65,6 @@ type JIMM struct { GetControllerConfig_ func(ctx context.Context, u *dbmodel.Identity) (*dbmodel.ControllerConfig, error) GetCredentialStore_ func() jimmcreds.CredentialStore GetJimmControllerAccess_ func(ctx context.Context, user *openfga.User, tag names.UserTag) (string, error) - GetUser_ func(ctx context.Context, username string) (*openfga.User, error) GetUserCloudAccess_ func(ctx context.Context, user *openfga.User, cloud names.CloudTag) (string, error) GetUserControllerAccess_ func(ctx context.Context, user *openfga.User, controller names.ControllerTag) (string, error) GetUserModelAccess_ func(ctx context.Context, user *openfga.User, model names.ModelTag) (string, error) @@ -109,7 +110,7 @@ type JIMM struct { 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 - UpdateUserLastLogin_ func(ctx context.Context, identifier 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) } @@ -321,12 +322,6 @@ func (j *JIMM) GetJimmControllerAccess(ctx context.Context, user *openfga.User, } return j.GetJimmControllerAccess_(ctx, user, tag) } -func (j *JIMM) GetUser(ctx context.Context, username string) (*openfga.User, error) { - if j.GetUser_ == nil { - return nil, errors.E(errors.CodeNotImplemented) - } - return j.GetUser_(ctx, username) -} func (j *JIMM) GetUserCloudAccess(ctx context.Context, user *openfga.User, cloud names.CloudTag) (string, error) { if j.GetUserCloudAccess_ == nil { return "", errors.E(errors.CodeNotImplemented) @@ -593,11 +588,11 @@ func (j *JIMM) UpdateMigratedModel(ctx context.Context, user *openfga.User, mode } return j.UpdateMigratedModel_(ctx, user, modelTag, targetControllerName) } -func (j *JIMM) UpdateUserLastLogin(ctx context.Context, identifier string) error { - if j.UpdateUserLastLogin_ == nil { - return errors.E(errors.CodeNotImplemented) +func (j *JIMM) UserLogin(ctx context.Context, identityName string) (*openfga.User, error) { + if j.UserLogin_ == nil { + return nil, errors.E(errors.CodeNotImplemented) } - return j.UpdateUserLastLogin(ctx, identifier) + return j.UserLogin_(ctx, identityName) } func (j *JIMM) IdentityModelDefaults(ctx context.Context, user *dbmodel.Identity) (map[string]interface{}, error) { if j.IdentityModelDefaults_ == nil { diff --git a/internal/jimmtest/mocks/login.go b/internal/jimmtest/mocks/login.go new file mode 100644 index 000000000..0fa6fdf88 --- /dev/null +++ b/internal/jimmtest/mocks/login.go @@ -0,0 +1,54 @@ +// Copyright 2024 Canonical. +package mocks + +import ( + "context" + + "golang.org/x/oauth2" + + "github.com/canonical/jimm/v3/internal/errors" + "github.com/canonical/jimm/v3/internal/openfga" +) + +type LoginService struct { + LoginDevice_ func(ctx context.Context) (*oauth2.DeviceAuthResponse, error) + GetDeviceSessionToken_ func(ctx context.Context, deviceOAuthResponse *oauth2.DeviceAuthResponse) (string, error) + LoginClientCredentials_ func(ctx context.Context, clientID string, clientSecret string) (*openfga.User, error) + LoginWithSessionToken_ func(ctx context.Context, sessionToken string) (*openfga.User, error) + LoginWithSessionCookie_ func(ctx context.Context, identityID string) (*openfga.User, error) +} + +func (j *LoginService) LoginDevice(ctx context.Context) (*oauth2.DeviceAuthResponse, error) { + if j.LoginDevice_ == nil { + return nil, errors.E(errors.CodeNotImplemented) + } + return j.LoginDevice_(ctx) +} + +func (j *LoginService) GetDeviceSessionToken(ctx context.Context, deviceOAuthResponse *oauth2.DeviceAuthResponse) (string, error) { + if j.GetDeviceSessionToken_ == nil { + return "", errors.E(errors.CodeNotImplemented) + } + return j.GetDeviceSessionToken_(ctx, deviceOAuthResponse) +} + +func (j *LoginService) LoginClientCredentials(ctx context.Context, clientID string, clientSecret string) (*openfga.User, error) { + if j.LoginClientCredentials_ == nil { + return nil, errors.E(errors.CodeNotImplemented) + } + return j.LoginClientCredentials_(ctx, clientID, clientSecret) +} + +func (j *LoginService) LoginWithSessionToken(ctx context.Context, sessionToken string) (*openfga.User, error) { + if j.LoginWithSessionToken_ == nil { + return nil, errors.E(errors.CodeNotImplemented) + } + return j.LoginWithSessionToken_(ctx, sessionToken) +} + +func (j *LoginService) LoginWithSessionCookie(ctx context.Context, identityID string) (*openfga.User, error) { + if j.LoginWithSessionCookie_ == nil { + return nil, errors.E(errors.CodeNotImplemented) + } + return j.LoginWithSessionCookie_(ctx, identityID) +} diff --git a/internal/jujuapi/admin.go b/internal/jujuapi/admin.go index ed1b5ade8..246feb3bb 100644 --- a/internal/jujuapi/admin.go +++ b/internal/jujuapi/admin.go @@ -4,21 +4,32 @@ package jujuapi import ( "context" - stderrors "errors" "sort" "github.com/juju/juju/rpc" jujuparams "github.com/juju/juju/rpc/params" "github.com/juju/names/v5" + "golang.org/x/oauth2" - "github.com/canonical/jimm/v3/internal/auth" "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" - jimmnames "github.com/canonical/jimm/v3/pkg/names" ) +// LoginService defines the set of methods used for login to JIMM. +type LoginService interface { + // LoginDevice is step 1 in the device flow and returns the OIDC server that the client should use for login. + LoginDevice(ctx context.Context) (*oauth2.DeviceAuthResponse, error) + // GetDeviceSessionToken polls the OIDC server waiting for the client to login and return a user scoped session token. + GetDeviceSessionToken(ctx context.Context, deviceOAuthResponse *oauth2.DeviceAuthResponse) (string, error) + // LoginWithClientCredentials verifies a user by their client credentials. + LoginClientCredentials(ctx context.Context, clientID string, clientSecret string) (*openfga.User, error) + // LoginWithSessionToken verifies a user based on their session token. + LoginWithSessionToken(ctx context.Context, sessionToken string) (*openfga.User, error) + // LoginWithSessionCookie verifies a user based on an identity from a cookie obtained during websocket upgrade. + LoginWithSessionCookie(ctx context.Context, identityID string) (*openfga.User, error) +} + // unsupportedLogin returns an appropriate error for login attempts using // old version of the Admin facade. func unsupportedLogin() error { @@ -39,9 +50,9 @@ func (r *controllerRoot) LoginDevice(ctx context.Context) (params.LoginDeviceRes const op = errors.Op("jujuapi.LoginDevice") response := params.LoginDeviceResponse{} - deviceResponse, err := jimm.LoginDevice(ctx, r.jimm.OAuthAuthenticationService()) + deviceResponse, err := r.jimm.LoginDevice(ctx) if err != nil { - return response, errors.E(op, err) + return response, errors.E(op, err, errors.CodeUnauthorized) } // NOTE: As this is on the controller root struct, and a new controller root // is created per WS, it is EXPECTED that the subsequent call to GetDeviceSessionToken @@ -63,9 +74,9 @@ func (r *controllerRoot) GetDeviceSessionToken(ctx context.Context) (params.GetD const op = errors.Op("jujuapi.GetDeviceSessionToken") response := params.GetDeviceSessionTokenResponse{} - token, err := jimm.GetDeviceSessionToken(ctx, r.jimm.OAuthAuthenticationService(), r.jimm.GetCredentialStore(), r.deviceOAuthResponse) + token, err := r.jimm.GetDeviceSessionToken(ctx, r.deviceOAuthResponse) if err != nil { - return response, errors.E(op, err) + return response, errors.E(op, err, errors.CodeUnauthorized) } response.SessionToken = token @@ -81,19 +92,9 @@ func (r *controllerRoot) GetDeviceSessionToken(ctx context.Context) (params.GetD func (r *controllerRoot) LoginWithSessionCookie(ctx context.Context) (jujuparams.LoginResult, error) { const op = errors.Op("jujuapi.LoginWithSessionCookie") - // If no identity ID has come through, then no cookie was present - // and as such authentication has failed. - if r.identityId == "" { - return jujuparams.LoginResult{}, errors.E(op, &auth.AuthenticationError{}) - } - - user, err := r.jimm.GetUser(ctx, r.identityId) - if err != nil { - return jujuparams.LoginResult{}, errors.E(op, err) - } - err = r.jimm.UpdateUserLastLogin(ctx, r.identityId) + user, err := r.jimm.LoginWithSessionCookie(ctx, r.identityId) if err != nil { - return jujuparams.LoginResult{}, errors.E(op, err) + return jujuparams.LoginResult{}, errors.E(op, err, errors.CodeUnauthorized) } r.mu.Lock() @@ -122,36 +123,12 @@ func (r *controllerRoot) LoginWithSessionCookie(ctx context.Context) (jujuparams // such that subsequent facade method calls can access the authenticated user. func (r *controllerRoot) LoginWithSessionToken(ctx context.Context, req params.LoginWithSessionTokenRequest) (jujuparams.LoginResult, error) { const op = errors.Op("jujuapi.LoginWithSessionToken") - authenticationSvc := r.jimm.OAuthAuthenticationService() - // Verify the session token - jwtToken, err := authenticationSvc.VerifySessionToken(req.SessionToken) + user, err := r.jimm.LoginWithSessionToken(ctx, req.SessionToken) if err != nil { - var aerr *auth.AuthenticationError - if stderrors.As(err, &aerr) { - return aerr.LoginResult, nil - } return jujuparams.LoginResult{}, errors.E(op, err, errors.CodeUnauthorized) } - // Get an OpenFGA user to place on the controllerRoot for this WS - // such that: - // - // - Subsequent calls are aware of the user - // - Authorisation checks are done against the openfga.User - email := jwtToken.Subject() - - // At this point, we know the user exists, so simply just get - // the user to create the session token. - user, err := r.jimm.GetUser(ctx, email) - if err != nil { - return jujuparams.LoginResult{}, errors.E(op, err) - } - err = r.jimm.UpdateUserLastLogin(ctx, email) - if err != nil { - return jujuparams.LoginResult{}, errors.E(op, err) - } - // TODO(ale8k): This isn't needed I don't think as controller roots are unique // per WS, but if anyone knows different please let me know. r.mu.Lock() @@ -178,29 +155,11 @@ func (r *controllerRoot) LoginWithSessionToken(ctx context.Context, req params.L func (r *controllerRoot) LoginWithClientCredentials(ctx context.Context, req params.LoginWithClientCredentialsRequest) (jujuparams.LoginResult, error) { const op = errors.Op("jujuapi.LoginWithClientCredentials") - clientIdWithDomain, err := jimmnames.EnsureValidServiceAccountId(req.ClientID) - if err != nil { - return jujuparams.LoginResult{}, errors.E("invalid client ID") - } - - authenticationSvc := r.jimm.OAuthAuthenticationService() - if authenticationSvc == nil { - return jujuparams.LoginResult{}, errors.E("authentication service not specified") - } - err = authenticationSvc.VerifyClientCredentials(ctx, req.ClientID, req.ClientSecret) + user, err := r.jimm.LoginClientCredentials(ctx, req.ClientID, req.ClientSecret) if err != nil { return jujuparams.LoginResult{}, errors.E(err, errors.CodeUnauthorized) } - user, err := r.jimm.GetUser(ctx, clientIdWithDomain) - if err != nil { - return jujuparams.LoginResult{}, errors.E(op, err) - } - err = r.jimm.UpdateUserLastLogin(ctx, clientIdWithDomain) - if err != nil { - return jujuparams.LoginResult{}, errors.E(op, err) - } - r.mu.Lock() r.user = user r.mu.Unlock() diff --git a/internal/jujuapi/admin_test.go b/internal/jujuapi/admin_test.go index dbaa8e3e1..94e2db196 100644 --- a/internal/jujuapi/admin_test.go +++ b/internal/jujuapi/admin_test.go @@ -175,11 +175,7 @@ func (s *adminSuite) TestBrowserLoginNoCookie(c *gc.C) { lr := &jujuparams.LoginResult{} err := conn.APICall("Admin", 4, "", "LoginWithSessionCookie", nil, lr) - c.Assert( - err, - gc.ErrorMatches, - "authentication failed", - ) + c.Assert(err, gc.ErrorMatches, `missing cookie identity \(unauthorized access\)`) } // TestDeviceLogin takes a test user through the flow of logging into jimm diff --git a/internal/jujuapi/controllerroot.go b/internal/jujuapi/controllerroot.go index ed6ddd033..f0e620d1e 100644 --- a/internal/jujuapi/controllerroot.go +++ b/internal/jujuapi/controllerroot.go @@ -29,6 +29,7 @@ import ( ) type JIMM interface { + LoginService 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 @@ -36,7 +37,6 @@ type JIMM interface { 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 - OAuthAuthenticationService() jimm.OAuthAuthenticator 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) @@ -62,7 +62,6 @@ type JIMM interface { GetControllerConfig(ctx context.Context, u *dbmodel.Identity) (*dbmodel.ControllerConfig, error) GetCredentialStore() credentials.CredentialStore GetJimmControllerAccess(ctx context.Context, user *openfga.User, tag names.UserTag) (string, error) - GetUser(ctx context.Context, username string) (*openfga.User, error) GetUserCloudAccess(ctx context.Context, user *openfga.User, cloud names.CloudTag) (string, error) GetUserControllerAccess(ctx context.Context, user *openfga.User, controller names.ControllerTag) (string, error) GetUserModelAccess(ctx context.Context, user *openfga.User, model names.ModelTag) (string, error) @@ -105,7 +104,7 @@ type JIMM interface { 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 - UpdateUserLastLogin(ctx context.Context, identifier 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) } @@ -181,7 +180,7 @@ func (r *controllerRoot) masquerade(ctx context.Context, userTag string) (*openf if !r.user.JimmAdmin { return nil, errors.E(errors.CodeUnauthorized, "unauthorized") } - user, err := r.jimm.GetUser(ctx, ut.Id()) + user, err := r.jimm.UserLogin(ctx, ut.Id()) if err != nil { return nil, err } diff --git a/internal/jujuapi/websocket.go b/internal/jujuapi/websocket.go index 6bfd1fea1..df7c73318 100644 --- a/internal/jujuapi/websocket.go +++ b/internal/jujuapi/websocket.go @@ -151,7 +151,7 @@ func (s modelProxyServer) ServeWS(ctx context.Context, clientConn *websocket.Con TokenGen: &jwtGenerator, ConnectController: connectionFunc, AuditLog: auditLogger, - JIMM: s.jimm, + LoginService: s.jimm, AuthenticatedIdentityID: auth.SessionIdentityFromContext(ctx), } if err := jimmRPC.ProxySockets(ctx, proxyHelpers); err != nil { diff --git a/internal/rpc/client_test.go b/internal/rpc/client_test.go index 8fd7b26aa..092d3120b 100644 --- a/internal/rpc/client_test.go +++ b/internal/rpc/client_test.go @@ -251,7 +251,7 @@ func TestProxySockets(t *testing.T) { testTokenGen := testTokenGenerator{} f := func(context.Context) (rpc.WebsocketConnectionWithMetadata, error) { connController, err := srvController.dialer.DialWebsocket(ctx, srvController.URL) - c.Assert(err, qt.IsNil) + c.Check(err, qt.IsNil) return rpc.WebsocketConnectionWithMetadata{ Conn: connController, ModelName: "TestName", @@ -263,8 +263,10 @@ func TestProxySockets(t *testing.T) { TokenGen: &testTokenGen, ConnectController: f, AuditLog: auditLogger, + LoginService: &mockLoginService{}, } err := rpc.ProxySockets(ctx, proxyHelpers) + c.Check(err, qt.ErrorMatches, ".*use of closed network connection") errChan <- err return err }) @@ -280,8 +282,17 @@ func TestProxySockets(t *testing.T) { err = ws.WriteJSON(&msg) c.Assert(err, qt.IsNil) resp := rpc.Message{} - err = ws.ReadJSON(&resp) - c.Assert(err, qt.IsNil) + receiveChan := make(chan error) + go func() { + receiveChan <- ws.ReadJSON(&resp) + }() + select { + case err := <-receiveChan: + c.Assert(err, qt.IsNil) + case <-time.After(5 * time.Second): + c.Logf("took too long to read response") + c.FailNow() + } c.Assert(resp.Response, qt.DeepEquals, msg.Params) ws.Close() <-errChan // Ensure go routines are cleaned up @@ -299,7 +310,7 @@ func TestCancelProxySockets(t *testing.T) { testTokenGen := testTokenGenerator{} f := func(context.Context) (rpc.WebsocketConnectionWithMetadata, error) { connController, err := srvController.dialer.DialWebsocket(ctx, srvController.URL) - c.Assert(err, qt.IsNil) + c.Check(err, qt.IsNil) return rpc.WebsocketConnectionWithMetadata{ Conn: connController, ModelName: "TestName", @@ -311,8 +322,10 @@ func TestCancelProxySockets(t *testing.T) { TokenGen: &testTokenGen, ConnectController: f, AuditLog: auditLogger, + LoginService: &mockLoginService{}, } err := rpc.ProxySockets(ctx, proxyHelpers) + c.Check(err, qt.ErrorMatches, "Context cancelled") errChan <- err return err }) @@ -323,8 +336,7 @@ func TestCancelProxySockets(t *testing.T) { c.Assert(err, qt.IsNil) defer ws.Close() cancel() - err = <-errChan - c.Assert(err.Error(), qt.Equals, "Context cancelled") + <-errChan } func TestProxySocketsAuditLogs(t *testing.T) { @@ -337,10 +349,11 @@ func TestProxySocketsAuditLogs(t *testing.T) { errChan := make(chan error) srvJIMM := newServer(func(connClient *websocket.Conn) error { + defer connClient.Close() testTokenGen := testTokenGenerator{} f := func(context.Context) (rpc.WebsocketConnectionWithMetadata, error) { connController, err := srvController.dialer.DialWebsocket(ctx, srvController.URL) - c.Assert(err, qt.IsNil) + c.Check(err, qt.IsNil) return rpc.WebsocketConnectionWithMetadata{ Conn: connController, ModelName: "TestModelName", @@ -352,8 +365,10 @@ func TestProxySocketsAuditLogs(t *testing.T) { TokenGen: &testTokenGen, ConnectController: f, AuditLog: auditLogger, + LoginService: &mockLoginService{}, } err := rpc.ProxySockets(ctx, proxyHelpers) + c.Check(err, qt.ErrorMatches, ".*use of closed network connection") errChan <- err return err }) diff --git a/internal/rpc/proxy.go b/internal/rpc/proxy.go index f781510f9..4e72ec2c2 100644 --- a/internal/rpc/proxy.go +++ b/internal/rpc/proxy.go @@ -16,13 +16,10 @@ import ( "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/jimm/credentials" "github.com/canonical/jimm/v3/internal/openfga" "github.com/canonical/jimm/v3/internal/servermon" "github.com/canonical/jimm/v3/internal/utils" apiparams "github.com/canonical/jimm/v3/pkg/api/params" - jimmnames "github.com/canonical/jimm/v3/pkg/names" ) const ( @@ -60,12 +57,14 @@ type WebsocketConnectionWithMetadata struct { ModelName string } -// JIMM represents the JIMM interface used by the proxy. -type JIMM interface { - GetUser(ctx context.Context, identifier string) (*openfga.User, error) - UpdateUserLastLogin(ctx context.Context, identifier string) error - OAuthAuthenticationService() jimm.OAuthAuthenticator - GetCredentialStore() credentials.CredentialStore +// LoginService represents the LoginService interface used by the proxy. +// Currently this is a duplicate of the [jujuapi.LoginService]. +type LoginService interface { + LoginDevice(ctx context.Context) (*oauth2.DeviceAuthResponse, error) + GetDeviceSessionToken(ctx context.Context, deviceOAuthResponse *oauth2.DeviceAuthResponse) (string, error) + LoginClientCredentials(ctx context.Context, clientID string, clientSecret string) (*openfga.User, error) + LoginWithSessionToken(ctx context.Context, sessionToken string) (*openfga.User, error) + LoginWithSessionCookie(ctx context.Context, identityID string) (*openfga.User, error) } // ProxyHelpers contains all the necessary helpers for proxying a Juju client @@ -75,7 +74,7 @@ type ProxyHelpers struct { TokenGen TokenGenerator ConnectController func(context.Context) (WebsocketConnectionWithMetadata, error) AuditLog func(*dbmodel.AuditLogEntry) - JIMM JIMM + LoginService LoginService AuthenticatedIdentityID string } @@ -92,6 +91,10 @@ func ProxySockets(ctx context.Context, helpers ProxyHelpers) error { zapctx.Error(ctx, "Missing audit log function") return errors.E(op, "Missing audit log function") } + if helpers.LoginService == nil { + zapctx.Error(ctx, "Missing login service function") + return errors.E(op, "Missing login service function") + } errChan := make(chan error, 2) msgInFlight := inflightMsgs{messages: make(map[uint64]*message)} client := writeLockConn{conn: helpers.ConnClient} @@ -104,7 +107,7 @@ func ProxySockets(ctx context.Context, helpers ProxyHelpers) error { tokenGen: helpers.TokenGen, auditLog: helpers.AuditLog, conversationId: utils.NewConversationID(), - jimm: helpers.JIMM, + loginService: helpers.LoginService, authenticatedIdentityID: helpers.AuthenticatedIdentityID, }, errChan: errChan, @@ -242,7 +245,7 @@ type modelProxy struct { msgs *inflightMsgs auditLog func(*dbmodel.AuditLogEntry) tokenGen TokenGenerator - jimm JIMM + loginService LoginService modelName string conversationId string authenticatedIdentityID string @@ -609,7 +612,18 @@ func (p *clientProxy) handleAdminFacade(ctx context.Context, msg *message) (clie errorFnc := func(err error) (*message, *message, error) { return nil, nil, err } - controllerLoginMessageFnc := func(data []byte) (*message, *message, error) { + controllerLoginMessageFnc := func(user *openfga.User) (*message, *message, error) { + jwt, err := p.tokenGen.MakeLoginToken(ctx, user) + if err != nil { + return errorFnc(err) + } + data, err := json.Marshal(params.LoginRequest{ + AuthTag: names.NewUserTag(user.Name).String(), + Token: base64.StdEncoding.EncodeToString(jwt), + }) + if err != nil { + return errorFnc(err) + } m := *msg m.Type = "Admin" m.Request = "Login" @@ -619,7 +633,7 @@ func (p *clientProxy) handleAdminFacade(ctx context.Context, msg *message) (clie } switch msg.Request { case "LoginDevice": - deviceResponse, err := jimm.LoginDevice(ctx, p.jimm.OAuthAuthenticationService()) + deviceResponse, err := p.loginService.LoginDevice(ctx) if err != nil { return errorFnc(err) } @@ -635,7 +649,7 @@ func (p *clientProxy) handleAdminFacade(ctx context.Context, msg *message) (clie msg.Response = data return msg, nil, nil case "GetDeviceSessionToken": - sessionToken, err := jimm.GetDeviceSessionToken(ctx, p.jimm.OAuthAuthenticationService(), p.jimm.GetCredentialStore(), p.deviceOAuthResponse) + sessionToken, err := p.loginService.GetDeviceSessionToken(ctx, p.deviceOAuthResponse) if err != nil { return errorFnc(err) } @@ -654,95 +668,31 @@ func (p *clientProxy) handleAdminFacade(ctx context.Context, msg *message) (clie return errorFnc(err) } - // Verify the session token - token, err := p.jimm.OAuthAuthenticationService().VerifySessionToken(request.SessionToken) + user, err := p.loginService.LoginWithSessionToken(ctx, request.SessionToken) if err != nil { return errorFnc(err) } - email := token.Subject() - user, err := p.jimm.GetUser(ctx, email) - if err != nil { - return errorFnc(err) - } - err = p.jimm.UpdateUserLastLogin(ctx, email) - if err != nil { - return errorFnc(err) - } - - jwt, err := p.tokenGen.MakeLoginToken(ctx, user) - if err != nil { - return errorFnc(err) - } - data, err := json.Marshal(params.LoginRequest{ - AuthTag: names.NewUserTag(email).String(), - Token: base64.StdEncoding.EncodeToString(jwt), - }) - if err != nil { - return errorFnc(err) - } - return controllerLoginMessageFnc(data) + return controllerLoginMessageFnc(user) case "LoginWithClientCredentials": var request apiparams.LoginWithClientCredentialsRequest err := json.Unmarshal(msg.Params, &request) if err != nil { return errorFnc(err) } - clientIdWithDomain, err := jimmnames.EnsureValidServiceAccountId(request.ClientID) - if err != nil { - return errorFnc(err) - } - err = p.jimm.OAuthAuthenticationService().VerifyClientCredentials(ctx, request.ClientID, request.ClientSecret) + user, err := p.loginService.LoginClientCredentials(ctx, request.ClientID, request.ClientSecret) if err != nil { return errorFnc(err) } - user, err := p.jimm.GetUser(ctx, clientIdWithDomain) - if err != nil { - return errorFnc(err) - } - err = p.jimm.UpdateUserLastLogin(ctx, clientIdWithDomain) - if err != nil { - return errorFnc(err) - } - - jwt, err := p.tokenGen.MakeLoginToken(ctx, user) - if err != nil { - return errorFnc(err) - } - data, err := json.Marshal(params.LoginRequest{ - AuthTag: names.NewUserTag(clientIdWithDomain).String(), - Token: base64.StdEncoding.EncodeToString(jwt), - }) - if err != nil { - return errorFnc(err) - } - return controllerLoginMessageFnc(data) + return controllerLoginMessageFnc(user) case "LoginWithSessionCookie": - if p.modelProxy.authenticatedIdentityID == "" { - return errorFnc(errors.E(errors.CodeUnauthorized)) - } - user, err := p.jimm.GetUser(ctx, p.modelProxy.authenticatedIdentityID) - if err != nil { - return errorFnc(err) - } - err = p.jimm.UpdateUserLastLogin(ctx, p.modelProxy.authenticatedIdentityID) + user, err := p.loginService.LoginWithSessionCookie(ctx, p.modelProxy.authenticatedIdentityID) if err != nil { return errorFnc(err) } - jwt, err := p.tokenGen.MakeLoginToken(ctx, user) - if err != nil { - return errorFnc(err) - } - data, err := json.Marshal(params.LoginRequest{ - AuthTag: user.ResourceTag().String(), - Token: base64.StdEncoding.EncodeToString(jwt), - }) - if err != nil { - return errorFnc(err) - } - return controllerLoginMessageFnc(data) + return controllerLoginMessageFnc(user) case "Login": return errorFnc(errors.E("JIMM does not support login from old clients", errors.CodeNotSupported)) default: diff --git a/internal/rpc/proxy_test.go b/internal/rpc/proxy_test.go index 77c9d7a41..7d7ab6238 100644 --- a/internal/rpc/proxy_test.go +++ b/internal/rpc/proxy_test.go @@ -9,22 +9,18 @@ import ( "testing" "time" - "github.com/coreos/go-oidc/v3/oidc" qt "github.com/frankban/quicktest" "github.com/google/uuid" "github.com/juju/juju/rpc/params" "github.com/juju/names/v5" - "github.com/lestrrat-go/jwx/v2/jwt" "golang.org/x/oauth2" "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/jimm/credentials" - "github.com/canonical/jimm/v3/internal/jimmtest" "github.com/canonical/jimm/v3/internal/openfga" "github.com/canonical/jimm/v3/internal/rpc" apiparams "github.com/canonical/jimm/v3/pkg/api/params" + jimmnames "github.com/canonical/jimm/v3/pkg/names" ) type message struct { @@ -238,9 +234,11 @@ func TestProxySocketsAdminFacade(t *testing.T) { for _, test := range tests { t.Run(test.about, func(t *testing.T) { ctx := context.Background() + ctx, cancelFunc := context.WithCancel(ctx) + defer cancelFunc() clientWebsocket := newMockWebsocketConnection(10) controllerWebsocket := newMockWebsocketConnection(10) - authenticator := &mockOAuthAuthenticator{ + loginSvc := &mockLoginService{ email: "alice@wonderland.io", clientID: clientID, clientSecret: clientSecret, @@ -257,59 +255,54 @@ func TestProxySocketsAdminFacade(t *testing.T) { ControllerUUID: uuid.NewString(), }, nil }, - AuditLog: func(*dbmodel.AuditLogEntry) {}, - JIMM: &mockJIMM{ - authenticator: authenticator, - }, + AuditLog: func(*dbmodel.AuditLogEntry) {}, + LoginService: loginSvc, AuthenticatedIdentityID: test.authenticateEntityID, } + var wg sync.WaitGroup + wg.Add(1) go func() { + defer wg.Done() err = rpc.ProxySockets(ctx, helpers) - c.Assert(err, qt.IsNil) + c.Assert(err, qt.ErrorMatches, "Context cancelled") }() data, err := json.Marshal(test.messageToSend) c.Assert(err, qt.IsNil) - select { - case clientWebsocket.read <- data: - default: - c.Fatal("failed to send message") - } + clientWebsocket.read <- data if test.expectedClientResponse != nil { select { case data := <-clientWebsocket.write: c.Assert(string(data), qt.JSONEquals, test.expectedClientResponse) - case <-time.Tick(10 * time.Minute): - c.Fatal("time out waiting for response") + case <-time.Tick(2 * time.Second): + c.Fatal("timed out waiting for response") } } if test.expectedControllerMessage != nil { select { case data := <-controllerWebsocket.write: c.Assert(string(data), qt.JSONEquals, test.expectedControllerMessage) - case <-time.Tick(10 * time.Minute): - c.Fatal("time out waiting for response") + case <-time.Tick(2 * time.Second): + c.Fatal("timed out waiting for response") } } + cancelFunc() + wg.Wait() + t.Logf("completed test %s", t.Name()) }) } } -type mockOAuthAuthenticator struct { - jimm.OAuthAuthenticator - - err error - +type mockLoginService struct { + err error email string clientID string clientSecret string - - updatedEmail string } -func (m *mockOAuthAuthenticator) Device(ctx context.Context) (*oauth2.DeviceAuthResponse, error) { - if m.err != nil { - return nil, m.err +func (j *mockLoginService) LoginDevice(ctx context.Context) (*oauth2.DeviceAuthResponse, error) { + if j.err != nil { + return nil, j.err } return &oauth2.DeviceAuthResponse{ DeviceCode: "test-device-code", @@ -320,94 +313,48 @@ func (m *mockOAuthAuthenticator) Device(ctx context.Context) (*oauth2.DeviceAuth Interval: int64(time.Minute.Seconds()), }, nil } - -func (m *mockOAuthAuthenticator) DeviceAccessToken(ctx context.Context, res *oauth2.DeviceAuthResponse) (*oauth2.Token, error) { - if m.err != nil { - return nil, m.err - } - return &oauth2.Token{}, nil -} - -func (m *mockOAuthAuthenticator) ExtractAndVerifyIDToken(ctx context.Context, oauth2Token *oauth2.Token) (*oidc.IDToken, error) { - if m.err != nil { - return nil, m.err - } - return &oidc.IDToken{}, nil -} - -func (m *mockOAuthAuthenticator) Email(idToken *oidc.IDToken) (string, error) { - if m.err != nil { - return "", m.err - } - if m.email != "" { - return m.email, nil +func (j *mockLoginService) GetDeviceSessionToken(ctx context.Context, deviceOAuthResponse *oauth2.DeviceAuthResponse) (string, error) { + if j.err != nil { + return "", j.err } - return "", errors.E(errors.CodeNotFound) + return "test session token", nil } - -func (m *mockOAuthAuthenticator) UpdateIdentity(ctx context.Context, email string, token *oauth2.Token) error { - if m.err != nil { - return m.err +func (j *mockLoginService) LoginClientCredentials(ctx context.Context, clientID string, clientSecret string) (*openfga.User, error) { + if j.err != nil { + return nil, j.err } - m.updatedEmail = email - return nil -} - -func (m *mockOAuthAuthenticator) VerifyClientCredentials(ctx context.Context, clientID string, clientSecret string) error { - if m.err != nil { - return m.err + if clientID != j.clientID || clientSecret != j.clientSecret { + return nil, errors.E("invalid client credentials") } - if clientID == m.clientID && clientSecret == m.clientSecret { - return nil + clientIdWithDomain, err := jimmnames.EnsureValidServiceAccountId(clientID) + if err != nil { + return nil, errors.E("invalid client credential ID") } - return errors.E(errors.CodeUnauthorized) -} - -func (m *mockOAuthAuthenticator) MintSessionToken(email string) (string, error) { - if m.err != nil { - return "", m.err + identity, err := dbmodel.NewIdentity(clientIdWithDomain) + if err != nil { + return nil, err } - return "test session token", nil + return openfga.NewUser(identity, nil), nil } - -func (m *mockOAuthAuthenticator) VerifySessionToken(token string) (jwt.Token, error) { - if m.err != nil { - return nil, m.err +func (j *mockLoginService) LoginWithSessionToken(ctx context.Context, sessionToken string) (*openfga.User, error) { + if j.err != nil { + return nil, j.err } - t := jwt.New() - - if err := t.Set(jwt.SubjectKey, m.email); err != nil { + identity, err := dbmodel.NewIdentity(j.email) + if err != nil { return nil, err } - - return t, nil -} - -type mockJIMM struct { - authenticator *mockOAuthAuthenticator + return openfga.NewUser(identity, nil), nil } - -func (j *mockJIMM) OAuthAuthenticationService() jimm.OAuthAuthenticator { - return j.authenticator -} - -func (j *mockJIMM) GetUser(ctx context.Context, email string) (*openfga.User, error) { - identity, err := dbmodel.NewIdentity(email) +func (j *mockLoginService) LoginWithSessionCookie(ctx context.Context, identityID string) (*openfga.User, error) { + if j.err != nil { + return nil, j.err + } + identity, err := dbmodel.NewIdentity(j.email) if err != nil { return nil, err } - return openfga.NewUser( - identity, - nil, - ), nil -} - -func (j *mockJIMM) UpdateUserLastLogin(ctx context.Context, identifier string) error { - return nil -} - -func (j *mockJIMM) GetCredentialStore() credentials.CredentialStore { - return jimmtest.NewInMemoryCredentialStore() + return openfga.NewUser(identity, nil), nil } func newMockWebsocketConnection(capacity int) *mockWebsocketConnection { @@ -420,6 +367,7 @@ func newMockWebsocketConnection(capacity int) *mockWebsocketConnection { type mockWebsocketConnection struct { read chan []byte write chan []byte + once sync.Once } func (w *mockWebsocketConnection) ReadJSON(v interface{}) error { @@ -439,7 +387,7 @@ func (w *mockWebsocketConnection) WriteJSON(v interface{}) error { } func (w *mockWebsocketConnection) Close() error { - close(w.read) + w.once.Do(func() { close(w.read) }) return nil } From efad7249804ee92e084c84673724be69abdffc77 Mon Sep 17 00:00:00 2001 From: Alexander <42068202+ale8k@users.noreply.github.com> Date: Mon, 19 Aug 2024 11:01:37 +0100 Subject: [PATCH 14/30] Ignore test complexity (#1316) * ignore tests gocognit * ignore watchController * ineff --- internal/jimm/admin_test.go | 1 + internal/jimm/cloud_test.go | 3 ++- internal/jimm/model_test.go | 3 ++- internal/jimm/watcher.go | 2 ++ internal/jimm/watcher_test.go | 1 + internal/jimmtest/store.go | 2 +- 6 files changed, 9 insertions(+), 3 deletions(-) diff --git a/internal/jimm/admin_test.go b/internal/jimm/admin_test.go index 13a8b8b4c..c452a451e 100644 --- a/internal/jimm/admin_test.go +++ b/internal/jimm/admin_test.go @@ -99,6 +99,7 @@ func TestLoginWithSessionToken(t *testing.T) { token, err := jwt.NewBuilder(). Subject("alice@canonical.com"). Build() + c.Assert(err, qt.IsNil) serialisedToken, err := jwt.NewSerializer().Serialize(token) c.Assert(err, qt.IsNil) b64Token := base64.StdEncoding.EncodeToString(serialisedToken) diff --git a/internal/jimm/cloud_test.go b/internal/jimm/cloud_test.go index 2233d50a7..36e1119c6 100644 --- a/internal/jimm/cloud_test.go +++ b/internal/jimm/cloud_test.go @@ -1344,6 +1344,7 @@ var revokeCloudAccessTests = []struct { expectError: `failed to recognize given access: "some-unknown-access"`, }} +//nolint:gocognit func TestRevokeCloudAccess(t *testing.T) { c := qt.New(t) @@ -1373,7 +1374,7 @@ func TestRevokeCloudAccess(t *testing.T) { c.Assert(err, qt.IsNil) env.PopulateDBAndPermissions(c, j.ResourceTag(), j.Database, client) - if tt.extraInitialTuples != nil && len(tt.extraInitialTuples) > 0 { + if len(tt.extraInitialTuples) > 0 { err = client.AddRelation(ctx, tt.extraInitialTuples...) c.Assert(err, qt.IsNil) } diff --git a/internal/jimm/model_test.go b/internal/jimm/model_test.go index 01b7be8d1..57b7cbcc0 100644 --- a/internal/jimm/model_test.go +++ b/internal/jimm/model_test.go @@ -2694,6 +2694,7 @@ var revokeModelAccessTests = []struct { expectError: `failed to recognize given access: "some-unknown-access"`, }} +//nolint:gocognit func TestRevokeModelAccess(t *testing.T) { c := qt.New(t) @@ -2722,7 +2723,7 @@ func TestRevokeModelAccess(t *testing.T) { c.Assert(err, qt.IsNil) env.PopulateDBAndPermissions(c, j.ResourceTag(), j.Database, client) - if tt.extraInitialTuples != nil && len(tt.extraInitialTuples) > 0 { + if len(tt.extraInitialTuples) > 0 { err = client.AddRelation(ctx, tt.extraInitialTuples...) c.Assert(err, qt.IsNil) } diff --git a/internal/jimm/watcher.go b/internal/jimm/watcher.go index 006817a0a..72e8ba25c 100644 --- a/internal/jimm/watcher.go +++ b/internal/jimm/watcher.go @@ -223,6 +223,8 @@ func (w *Watcher) deltaProcessedNotification() { // watchController connects to the given controller and watches for model // changes on the controller. +// +// nolint:gocognit // We ignore watch as watchers are removed in Juju 4.0. func (w *Watcher) watchController(ctx context.Context, ctl *dbmodel.Controller) error { const op = errors.Op("jimm.watchController") diff --git a/internal/jimm/watcher_test.go b/internal/jimm/watcher_test.go index 31f9a1bc3..9157313ab 100644 --- a/internal/jimm/watcher_test.go +++ b/internal/jimm/watcher_test.go @@ -510,6 +510,7 @@ var watcherTests = []struct { }, }} +//nolint:gocognit func TestWatcher(t *testing.T) { c := qt.New(t) diff --git a/internal/jimmtest/store.go b/internal/jimmtest/store.go index 61e245ef7..5ba28781c 100644 --- a/internal/jimmtest/store.go +++ b/internal/jimmtest/store.go @@ -137,7 +137,7 @@ func (s *InMemoryCredentialStore) GetJWKSPrivateKey(ctx context.Context) ([]byte s.mu.RLock() defer s.mu.RUnlock() - if s.privateKey == nil || len(s.privateKey) == 0 { + if len(s.privateKey) == 0 { return nil, errors.E(errors.CodeNotFound) } From 58b8e61a39716dbed95798cfd4a6f43d0d4821ef Mon Sep 17 00:00:00 2001 From: Alexander <42068202+ale8k@users.noreply.github.com> Date: Mon, 19 Aug 2024 13:28:40 +0100 Subject: [PATCH 15/30] Refactor import model (#1317) * break watcher logic away At the end of ImportModel we perform a retrieval of deltas. Having this in it's own composed function reduces the cognitive complexity. * add dial model * update godoc * move to jimm * rename --- internal/jimm/controller.go | 41 +++++++++++++++++-------------------- internal/jimm/utils.go | 10 +++++++++ 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/internal/jimm/controller.go b/internal/jimm/controller.go index 22264928a..de57638e2 100644 --- a/internal/jimm/controller.go +++ b/internal/jimm/controller.go @@ -402,25 +402,22 @@ func (j *JIMM) GetUserControllerAccess(ctx context.Context, user *openfga.User, return ToControllerAccessString(accessLevel), nil } -// ImportModel imports model with the specified uuid from the controller. +// ImportModel imports model with the specified UUID from the controller. func (j *JIMM) ImportModel(ctx context.Context, user *openfga.User, controllerName string, modelTag names.ModelTag, newOwner string) error { const op = errors.Op("jimm.ImportModel") - if !user.JimmAdmin { - return errors.E(op, errors.CodeUnauthorized, "unauthorized") + if err := j.checkJimmAdmin(user); err != nil { + return err } - controller := dbmodel.Controller{ - Name: controllerName, - } - err := j.Database.GetController(ctx, &controller) + controller, err := j.getControllerByName(ctx, controllerName) if err != nil { return errors.E(op, err) } - api, err := j.dial(ctx, &controller, names.ModelTag{}) + api, err := j.dialController(ctx, controller) if err != nil { - return errors.E(op, err) + return errors.E(op, "failed to dial the controller", err) } defer api.Close() @@ -438,7 +435,7 @@ func (j *JIMM) ImportModel(ctx context.Context, user *openfga.User, controllerNa return errors.E(op, err) } model.ControllerID = controller.ID - model.Controller = controller + model.Controller = *controller var ownerTag names.UserTag if newOwner != "" { @@ -513,19 +510,14 @@ func (j *JIMM) ImportModel(ctx context.Context, user *openfga.User, controllerNa return errors.E(op, err) } - regionFound := false - for _, cr := range cloud.Regions { - if cr.Name == modelInfo.CloudRegion { - regionFound = true - model.CloudRegion = cr - model.CloudRegionID = cr.ID - break - } - } - if !regionFound { + cr := cloud.Region(modelInfo.CloudRegion) + if cr.Name != modelInfo.CloudRegion { return errors.E(op, "cloud region not found") } + model.CloudRegionID = cr.ID + model.CloudRegion = cr + err = j.Database.AddModel(ctx, &model) if err != nil { if errors.ErrorCode(err) == errors.CodeAlreadyExists { @@ -534,7 +526,13 @@ func (j *JIMM) ImportModel(ctx context.Context, user *openfga.User, controllerNa return errors.E(op, err) } - modelAPI, err := j.dial(ctx, &controller, modelTag) + return j.handleModelDeltas(ctx, controller, modelTag, model) +} + +func (j *JIMM) handleModelDeltas(ctx context.Context, controller *dbmodel.Controller, modelTag names.ModelTag, model dbmodel.Model) error { + const op = errors.Op("jimm.getModelDeltas") + + modelAPI, err := j.dialModel(ctx, controller, modelTag) if err != nil { return errors.E(op, err) } @@ -574,7 +572,6 @@ func (j *JIMM) ImportModel(ctx context.Context, user *openfga.User, controllerNa return errors.E(op, err) } } - return nil } diff --git a/internal/jimm/utils.go b/internal/jimm/utils.go index d8dfffd43..ce012bc69 100644 --- a/internal/jimm/utils.go +++ b/internal/jimm/utils.go @@ -60,3 +60,13 @@ func (j *JIMM) dialController(ctx context.Context, ctl *dbmodel.Controller) (API } return api, nil } + +// dialModel dials a model. +func (j *JIMM) dialModel(ctx context.Context, ctl *dbmodel.Controller, mt names.ModelTag) (API, error) { + api, err := j.dial(ctx, ctl, mt) + if err != nil { + zapctx.Error(ctx, "failed to dial the controller", zaputil.Error(err)) + return nil, err + } + return api, nil +} From 970ec0ca43a006c2c0de78e5e691713d7a0e0e18 Mon Sep 17 00:00:00 2001 From: Alexander <42068202+ale8k@users.noreply.github.com> Date: Wed, 21 Aug 2024 13:20:22 +0100 Subject: [PATCH 16/30] golangci-lint (#1319) * golangci-lint * test linter with err * will this work? * perhaps it is working? * try a 5 min timeout with verbose... * 10m no critic * try a bump * wip * reference field directly? * tmate * bump jimm? * approle * . * int overflows. * rename * extra newline * add linting to make * make target --- .github/workflows/golangci-lint.yaml | 31 +++++++++++++++++++ ...ulncheck.yaml => vulnerability-check.yaml} | 4 +-- .golangci.yaml | 8 ++--- Makefile | 6 +++- doc/golangci-lint.md | 2 +- internal/jimm/watcher.go | 1 + internal/jimmtest/env.go | 1 + internal/openfga/openfga.go | 1 + 8 files changed, 46 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/golangci-lint.yaml rename .github/workflows/{vulncheck.yaml => vulnerability-check.yaml} (89%) diff --git a/.github/workflows/golangci-lint.yaml b/.github/workflows/golangci-lint.yaml new file mode 100644 index 000000000..271f9c84e --- /dev/null +++ b/.github/workflows/golangci-lint.yaml @@ -0,0 +1,31 @@ +name: golangci-lint +on: + pull_request: + +permissions: + contents: read + checks: write # Optional: allow write access to checks to allow the action to annotate code in the PR. + +jobs: + golangci: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: stable + cache: false + + - name: Touch approle + run: touch ./local/vault/approle.json + + - name: Run Golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + args: --timeout 30m --verbose + version: v1.60 + diff --git a/.github/workflows/vulncheck.yaml b/.github/workflows/vulnerability-check.yaml similarity index 89% rename from .github/workflows/vulncheck.yaml rename to .github/workflows/vulnerability-check.yaml index 14c1a7d41..9deae64f4 100644 --- a/.github/workflows/vulncheck.yaml +++ b/.github/workflows/vulnerability-check.yaml @@ -1,4 +1,4 @@ -name: Security Check +name: Vulnerability Check on: schedule: @@ -16,5 +16,5 @@ jobs: uses: actions/setup-go@v5 with: go-version-file: go.mod - - name: Security checks + - name: Security scan uses: canonical/comsys-build-tools/.github/actions/security-scan@main diff --git a/.golangci.yaml b/.golangci.yaml index 74557b359..bd973feb4 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -9,7 +9,7 @@ run: tests: true allow-parallel-runners: false allow-serial-runners: false - # go: "1.17" # Do not set a go limit + # go: "1.23" issues: exclude-use-default: true @@ -53,8 +53,8 @@ linters: # Style based linters - promlinter - - gocritic - - gocognit # To be fixed + - gocritic + - gocognit - goheader - importas - gci @@ -87,4 +87,4 @@ linters-settings: sections: - standard - default - - localmodule \ No newline at end of file + - localmodule diff --git a/Makefile b/Makefile index 38cb491bc..f923ee7d8 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,10 @@ build: version/commit.txt version/version.txt build/server: version/commit.txt version/version.txt go build -tags version ./cmd/jimmsrv -check: version/commit.txt version/version.txt +lint: + golangci-lint run --timeout 5m + +check: version/commit.txt version/version.txt lint go test -timeout 30m $(PROJECT)/... -cover clean: @@ -117,6 +120,7 @@ endef APT_BASED := $(shell command -v apt-get >/dev/null; echo $$?) sys-deps: ifeq ($(APT_BASED),0) + @$(call check_dep,golangci-lint,Missing Golangci-lint - install from https://golangci-lint.run/welcome/install/) @$(call check_dep,go,Missing Go - install from https://go.dev/doc/install or 'sudo snap install go --classic') @$(call check_dep,git,Missing Git - install with 'sudo apt install git') @$(call check_dep,gcc,Missing gcc - install with 'sudo apt install build-essential') diff --git a/doc/golangci-lint.md b/doc/golangci-lint.md index 743ca68ce..f72fc0591 100644 --- a/doc/golangci-lint.md +++ b/doc/golangci-lint.md @@ -7,4 +7,4 @@ In the .vscode folder of this repository you will find it is defined as the lint To install, please install the golangci-lint binary or install it via "go install". The version this was tested with is: -```go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.59.1``` \ No newline at end of file +```go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.60.1``` \ No newline at end of file diff --git a/internal/jimm/watcher.go b/internal/jimm/watcher.go index 72e8ba25c..393dc74f3 100644 --- a/internal/jimm/watcher.go +++ b/internal/jimm/watcher.go @@ -472,6 +472,7 @@ func (w *Watcher) handleDelta(ctx context.Context, modelIDf func(string) *modelS var cores int64 machine := d.Entity.(*jujuparams.MachineInfo) if machine.HardwareCharacteristics != nil && machine.HardwareCharacteristics.CpuCores != nil { + //nolint:gosec // We expect cpu cores to fit into int64. cores = int64(*machine.HardwareCharacteristics.CpuCores) } sCores, ok := state.machines[eid.Id] diff --git a/internal/jimmtest/env.go b/internal/jimmtest/env.go index 863f94794..229ac7552 100644 --- a/internal/jimmtest/env.go +++ b/internal/jimmtest/env.go @@ -459,6 +459,7 @@ func (m *Model) DBObject(c *qt.C, db db.Database) dbmodel.Model { m.env.Controller(m.Controller) migrationControllerID := sql.NullInt32{} if m.MigrationController != "" { + //nolint:gosec // Database IDs for tests will fit into int32. migrationControllerID.Int32 = int32(m.env.Controller(m.MigrationController).dbo.ID) migrationControllerID.Valid = true } diff --git a/internal/openfga/openfga.go b/internal/openfga/openfga.go index 23cc77208..103b8ee44 100644 --- a/internal/openfga/openfga.go +++ b/internal/openfga/openfga.go @@ -206,6 +206,7 @@ func (o *OFGAClient) removeTuples(ctx context.Context, tuple Tuple) (err error) for { // Since we're deleting the returned tuples, it's best to avoid pagination, // and fresh query for the relations. + //nolint:gosec // The page size will not exceed int32. tuples, ct, err := o.ReadRelatedObjects(ctx, tuple, int32(pageSize), "") if err != nil { return err From 8480f903ebc784f09ffeb0c7378a584739e62a2e Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Thu, 22 Aug 2024 09:11:36 +0200 Subject: [PATCH 17/30] use more descriptive error messages in model proxy (#1320) --- internal/rpc/client_test.go | 4 ++-- internal/rpc/proxy.go | 11 ++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/internal/rpc/client_test.go b/internal/rpc/client_test.go index 092d3120b..79cfebed0 100644 --- a/internal/rpc/client_test.go +++ b/internal/rpc/client_test.go @@ -266,7 +266,7 @@ func TestProxySockets(t *testing.T) { LoginService: &mockLoginService{}, } err := rpc.ProxySockets(ctx, proxyHelpers) - c.Check(err, qt.ErrorMatches, ".*use of closed network connection") + c.Check(err, qt.ErrorMatches, "error reading from (client|controller).*") errChan <- err return err }) @@ -368,7 +368,7 @@ func TestProxySocketsAuditLogs(t *testing.T) { LoginService: &mockLoginService{}, } err := rpc.ProxySockets(ctx, proxyHelpers) - c.Check(err, qt.ErrorMatches, ".*use of closed network connection") + c.Check(err, qt.ErrorMatches, `error reading from (client|controller).*`) errChan <- err return err }) diff --git a/internal/rpc/proxy.go b/internal/rpc/proxy.go index 4e72ec2c2..142623a07 100644 --- a/internal/rpc/proxy.go +++ b/internal/rpc/proxy.go @@ -5,6 +5,7 @@ import ( "context" "encoding/base64" "encoding/json" + "fmt" "sync" "time" @@ -339,14 +340,14 @@ func (p *clientProxy) start(ctx context.Context) error { msg := new(message) if err := p.src.readJson(&msg); err != nil { // Error reading on the socket implies it is closed, simply return. - return err + return fmt.Errorf("error reading from client: %w", err) } zapctx.Debug(ctx, "Read message from client", zap.Any("message", msg)) err := p.makeControllerConnection(ctx) if err != nil { zapctx.Error(ctx, "error connecting to controller", zap.Error(err)) p.sendError(p.src, msg, err) - return err + return fmt.Errorf("failed to connect to controller: %w", err) } if err := p.auditLogMessage(msg, false); err != nil { zapctx.Error(ctx, "failed to audit log message", zap.Error(err)) @@ -438,7 +439,7 @@ func (p *controllerProxy) start(ctx context.Context) error { msg := new(message) if err := p.src.readJson(msg); err != nil { // Error reading on the socket implies it is closed, simply return. - return err + return fmt.Errorf("error reading from controller: %w", err) } zapctx.Debug(ctx, "Received message from controller", zap.Any("Message", msg)) permissionsRequired, err := checkPermissionsRequired(ctx, msg) @@ -467,7 +468,7 @@ func (p *controllerProxy) start(ctx context.Context) error { zapctx.Error(ctx, "Failed to modify message", zap.Error(err)) p.handleError(msg, err) // An error when modifying the message is a show stopper. - return err + return fmt.Errorf("error modifying controller response: %w", err) } } p.msgs.removeMessage(msg.RequestID) @@ -477,7 +478,7 @@ func (p *controllerProxy) start(ctx context.Context) error { zapctx.Debug(ctx, "Writing modified message to client", zap.Any("Message", msg)) if err := p.dst.writeJson(msg); err != nil { zapctx.Error(ctx, "controllerProxy error writing to dst", zap.Error(err)) - return err + return fmt.Errorf("error writing message to client: %w", err) } } } From db18dbf4b850c881654839d8f20b11c86241219f Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Fri, 23 Aug 2024 15:51:45 +0200 Subject: [PATCH 18/30] Add composite action for starting a JIMM environment (#1321) * Add composite action for starting a JIMM environment - Use the composite action as part of a basic integration test * Run integration test on PR - Adding a trigger to run the new integration test on PRs to verify it works. * fix naming * allow action to start dev env * rewording * remove need for env file * add default string * use extends in compose - Use `extends` to shift common config to a separate file --- .air.toml | 1 - .github/actions/test-server/README.md | 28 +++++++ .github/actions/test-server/action.yaml | 99 +++++++++++++++++++++++++ .github/workflows/ci.yaml | 18 ----- .github/workflows/integration-test.yaml | 47 ++++++++++++ Makefile | 17 ++++- compose-common.yaml | 62 ++++++++++++++++ docker-compose.yaml | 89 +++++++--------------- local/jimm/add-controller.sh | 25 +++++-- local/jimm/setup-cli-auth.sh | 10 +++ local/jimm/setup-controller.sh | 16 ++-- scripts/lxd-build-test.sh | 64 ---------------- scripts/lxd-charm-build.sh | 57 -------------- scripts/lxd-k8s-charm-build.sh | 56 -------------- scripts/lxd-release-build.sh | 53 ------------- scripts/lxd-snap-build.sh | 53 ------------- 16 files changed, 314 insertions(+), 381 deletions(-) create mode 100644 .github/actions/test-server/README.md create mode 100644 .github/actions/test-server/action.yaml create mode 100644 .github/workflows/integration-test.yaml create mode 100644 compose-common.yaml create mode 100755 local/jimm/setup-cli-auth.sh delete mode 100755 scripts/lxd-build-test.sh delete mode 100755 scripts/lxd-charm-build.sh delete mode 100755 scripts/lxd-k8s-charm-build.sh delete mode 100644 scripts/lxd-release-build.sh delete mode 100755 scripts/lxd-snap-build.sh diff --git a/.air.toml b/.air.toml index bff0eb5de..f5e8b5767 100644 --- a/.air.toml +++ b/.air.toml @@ -4,7 +4,6 @@ tmp_dir = "tmp" [build] args_bin = [] - bin = "env $(cat /vault/vault.env | xargs) ./tmp/jimm" cmd = "go build -gcflags='all=-N -l' -buildvcs=false -o ./tmp/jimm ./cmd/jimmsrv" delay = 1000 exclude_dir = [".vscode", "assets", "tmp", "vendor", "testdata"] diff --git a/.github/actions/test-server/README.md b/.github/actions/test-server/README.md new file mode 100644 index 000000000..2926efc38 --- /dev/null +++ b/.github/actions/test-server/README.md @@ -0,0 +1,28 @@ +# test-server +An action to create a JIMM server with real dependencies for integration test purposes. + +This action requires Docker to be installed to start JIMM and its related services. + +The action performs the following steps: +- Starts JIMM's docker compose test environment. +- Uses https://github.com/charmed-kubernetes/actions-operator action to start a Juju controller and connects it to JIMM. +- Ensures the local Juju CLI is setup to communicate with JIMM authenticating as a test user. + +Use the action by adding the following to a Github workflow: + +```yaml + integration-test: + runs-on: ubuntu-latest + name: Integration testing with JIMM + steps: + - name: Setup JIMM environment + uses: canonical/jimm@v3.1.7 + with: + jimm-version: "v3.1.7" + juju-channel: "3/stable" + ghcr-pat: ${{ secrets.GHCR_PAT }} +``` + +Note that it's recommended to pin the action version to the same version as `jimm-version` to ensure the action works as expected for that specific version of JIMM. + +For full details on the inputs see `action.yaml`. diff --git a/.github/actions/test-server/action.yaml b/.github/actions/test-server/action.yaml new file mode 100644 index 000000000..7fd42a836 --- /dev/null +++ b/.github/actions/test-server/action.yaml @@ -0,0 +1,99 @@ +name: JIMM Server Setup +description: "Create a JIMM environment" + +inputs: + jimm-version: + description: > + JIMM version tag to use. This will decide the version of JIMM to start e.g. v3.1.7 + A special tag of "dev" can be provided to use the current development version of JIMM. + required: true + juju-channel: + description: 'Juju snap channel to pass to charmed-kubernetes/actions-operator' + required: false + ghcr-pat: + description: > + PAT Token that has package:read access to canonical/JIMM + The PAT token can be left empty when building the development version of JIMM. + required: true + +output: + url: + description: 'URL where JIMM can be reached.' + value: "https://jimm.localhost" + client-id: + description: 'Test client ID to login to JIMM with a service account.' + value: "test-client-id" + client-secret: + description: 'Test client Secret to login to JIMM with a service account.' + value: "2M2blFbO4GX4zfggQpivQSxwWX1XGgNf" + +runs: + using: "composite" + steps: + - name: Login to GitHub Container Registry + if: ${{ inputs.jimm-version != 'dev' }} + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ inputs.ghcr-pat }} + + - name: Start server based on released version + if: ${{ inputs.jimm-version != 'dev' }} + run: make integration-test-env + shell: bash + env: + JIMM_VERSION: ${{ inputs.jimm-version }} + + - name: Start server based on development version + if: ${{ inputs.jimm-version == 'dev' }} + run: make dev-env + shell: bash + + - name: Initialise LXD + run: | + sudo lxd waitready && \ + sudo lxd init --auto && \ + sudo chmod a+wr /var/snap/lxd/common/lxd/unix.socket && \ + lxc network set lxdbr0 ipv6.address none && \ + sudo usermod -a -G lxd $USER + shell: bash + + - name: Setup cloud-init script for bootstraping Juju controllers + run: ./local/jimm/setup-controller.sh + shell: bash + env: + SKIP_BOOTSTRAP: true + CLOUDINIT_FILE: "cloudinit.temp.yaml" + + - name: Setup Juju Controller + uses: charmed-kubernetes/actions-operator@main + with: + provider: "lxd" + channel: "5.19/stable" + juju-channel: ${{ inputs.juju-channel }} + bootstrap-options: "--config cloudinit.temp.yaml --config login-token-refresh-url=https://jimm.localhost/.well-known/jwks.json" + + # As described in https://github.com/charmed-kubernetes/actions-operator grab the newly setup controller name + - name: Save LXD controller name + id: lxd-controller + 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 + shell: bash + + - name: Authenticate Juju CLI + run: chmod -R 666 ~/.local/share/juju/*.yaml && ./local/jimm/setup-cli-auth.sh + shell: bash + # Below is a hardcoded JWT using the same test-secret used in JIMM's docker compose and allows the CLI to authenticate as the jimm-test@canonical.com user. + env: + JWT: ZXlKMGVYQWlPaUpLVjFRaUxDSmhiR2NpT2lKSVV6STFOaUo5LmV5SnBjM01pT2lKUGJteHBibVVnU2xkVUlFSjFhV3hrWlhJaUxDSnBZWFFpT2pFM01qUXlNamcyTmpBc0ltVjRjQ0k2TXprMk5EYzFNelEyTUN3aVlYVmtJam9pYW1sdGJTSXNJbk4xWWlJNkltcHBiVzB0ZEdWemRFQmpZVzV2Ym1sallXd3VZMjl0SW4wLkpTWVhXcGF6T0FnX1VFZ2hkbjlOZkVQdWxhWWlJQVdaX3BuSmRDbnJvWEk= + + - name: Add LXD Juju controller to JIMM + run: ./local/jimm/add-controller.sh + shell: bash + env: + JIMM_CONTROLLER_NAME: "jimm" + CONTROLLER_NAME: ${{ steps.lxd-controller.outputs.name }} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e4ffac20e..6d43b9dce 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -54,21 +54,3 @@ jobs: PGSSLMODE: disable PGUSER: jimm PGPORT: 5432 - - smoke_test: - name: Smoke Test - runs-on: ubuntu-22.04 - # The docker compose has a healthcheck on the JIMM container. - # So if the compose returns with exit code 0 then the JIMM server successfully started. - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Add volume files - run: | - touch ./local/vault/approle.json - touch ./local/vault/roleid.txt - touch ./local/vault/vault.env - - - name: Run Smoke Test - run: docker compose --profile dev up -d --wait --timestamps diff --git a/.github/workflows/integration-test.yaml b/.github/workflows/integration-test.yaml new file mode 100644 index 000000000..9dd905e3b --- /dev/null +++ b/.github/workflows/integration-test.yaml @@ -0,0 +1,47 @@ +name: Integration Test + +on: + workflow_dispatch: + inputs: + jimm-version: + description: > + JIMM version tag to use. This will decide the version of JIMM to start e.g. v3.1.7. + View all available versions at https://github.com/canonical/jimm/pkgs/container/jimm. + required: true + pull_request: + +jobs: + startjimm: + name: Test JIMM with Juju controller + runs-on: ubuntu-22.04 + steps: + - name: Checkout JIMM repo + uses: actions/checkout@v4 + - name: Setup Go + if: ${{ github.event_name == 'pull_request' }} + uses: actions/setup-go@v4 + with: + go-version-file: 'go.mod' + - name: Go vendor to speed up docker build + if: ${{ github.event_name == 'pull_request' }} + run: go mod vendor + - name: Start JIMM (pull request) + if: ${{ github.event_name == 'pull_request' }} + uses: ./.github/actions/test-server + with: + jimm-version: dev + juju-channel: "3/stable" + ghcr-pat: ${{ secrets.GITHUB_TOKEN }} + - name: Start JIMM (manual run) + if: ${{ github.event_name == 'workflow_dispatch' }} + uses: ./.github/actions/test-server + with: + jimm-version: ${{ inputs.jimm-version }} + juju-channel: "3/stable" + ghcr-pat: ${{ secrets.GITHUB_TOKEN }} + - name: Create a model, deploy an application and run juju status + run: | + juju add-model foo && \ + juju deploy haproxy && \ + sleep 5 && \ + juju status diff --git a/Makefile b/Makefile index f923ee7d8..3b6b63ba9 100644 --- a/Makefile +++ b/Makefile @@ -47,11 +47,14 @@ dev-env-setup: sys-deps certs @make version/commit.txt && make version/version.txt dev-env: dev-env-setup - @docker compose --profile dev up --force-recreate + @docker compose --profile dev up -d --force-recreate --wait dev-env-cleanup: @docker compose --profile dev down -v --remove-orphans +integration-test-env: dev-env-setup + @JIMM_VERSION=$(JIMM_VERSION) docker compose --profile test up -d --force-recreate --wait + # Reformat all source files. format: gofmt -w -l . @@ -116,17 +119,25 @@ define check_dep fi endef -# Install packages required to develop JIMM and run tests. +# Install packages required to develop JIMM and/or run tests. APT_BASED := $(shell command -v apt-get >/dev/null; echo $$?) sys-deps: ifeq ($(APT_BASED),0) +# golangci-lint is necessary for linting. @$(call check_dep,golangci-lint,Missing Golangci-lint - install from https://golangci-lint.run/welcome/install/) +# Go acts as the test runner. @$(call check_dep,go,Missing Go - install from https://go.dev/doc/install or 'sudo snap install go --classic') +# Git is useful to have. @$(call check_dep,git,Missing Git - install with 'sudo apt install git') +# GCC is required for the compilation process. @$(call check_dep,gcc,Missing gcc - install with 'sudo apt install build-essential') +# yq is necessary for some scripts that process controller-info yaml files. @$(call check_dep,yq,Missing yq - install with 'sudo snap install yq') - @$(call check_dep,gcc,Missing microk8s - install latest none-classic from snapstore') +# Microk8s is required if you want to start a Juju controller on Microk8s. + @$(call check_dep,microk8s,Missing microk8s - install with 'sudo snap install microk8s') +# Docker is required to start the test dependencies in containers. @$(call check_dep,docker,Missing Docker - install from https://docs.docker.com/engine/install/) +# juju-db is required for tests that use Juju's test fixture, requiring MongoDB. @$(call check_dep,juju-db.mongo,Missing juju-db - install with 'sudo snap install juju-db --channel=4.4/stable') else @echo sys-deps runs only on systems with apt-get diff --git a/compose-common.yaml b/compose-common.yaml new file mode 100644 index 000000000..3b12226e1 --- /dev/null +++ b/compose-common.yaml @@ -0,0 +1,62 @@ +# This file contains a collection of common configurations used in JIMM's Docker compose file. + +services: + jimm-base: + environment: + JIMM_LOG_LEVEL: "debug" + JIMM_UUID: "3217dbc9-8ea9-4381-9e97-01eab0b3f6bb" + JIMM_DSN: "postgresql://jimm:jimm@db/jimm" + # Not needed for local test (yet). + # BAKERY_AGENT_FILE: "" + JIMM_ADMINS: "jimm-test@canonical.com" + # Note: You can comment out the Vault ENV vars below and instead use INSECURE_SECRET_STORAGE to place secrets in Postgres. + VAULT_ADDR: "http://vault:8200" + VAULT_PATH: "/jimm-kv/" + # Note: By default we should use Vault as that is the primary means of secret storage. + # INSECURE_SECRET_STORAGE: "enabled" + # JIMM_DASHBOARD_LOCATION: "" + JIMM_DNS_NAME: "jimm.localhost" + JIMM_LISTEN_ADDR: "0.0.0.0:80" + JIMM_TEST_PGXDSN: "postgresql://jimm:jimm@db/jimm" + JIMM_JWT_EXPIRY: 30s + JIMM_AUDIT_LOG_RETENTION_PERIOD_IN_DAYS: "1" + TEST_LOGGING_CONFIG: "" + BAKERY_PUBLIC_KEY: "izcYsQy3TePp6bLjqOo3IRPFvkQd2IKtyODGqC6SdFk=" + BAKERY_PRIVATE_KEY: "ly/dzsI9Nt/4JxUILQeAX79qZ4mygDiuYGqc2ZEiDEc=" + OPENFGA_SCHEME: "http" + OPENFGA_HOST: "openfga" + OPENFGA_PORT: 8080 + OPENFGA_STORE: "01GP1254CHWJC1MNGVB0WDG1T0" + OPENFGA_AUTH_MODEL: "01GP1EC038KHGB6JJ2XXXXCXKB" + OPENFGA_TOKEN: "jimm" + JIMM_IS_LEADER: true + JIMM_OAUTH_ISSUER_URL: "http://keycloak.localhost:8082/realms/jimm" # Scheme required + JIMM_OAUTH_CLIENT_ID: "jimm-device" + 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_SECURE_SESSION_COOKIES: false + JIMM_SESSION_COOKIE_MAX_AGE: 86400 + JIMM_SESSION_SECRET_KEY: Xz2RkR9g87M75xfoumhEs5OmGziIX8D88Rk5YW8FSvkBPSgeK9t5AS9IvPDJ3NnB + healthcheck: + test: [ "CMD", "curl", "http://jimm.localhost:80" ] + interval: 5s + timeout: 5s + retries: 50 # Should fail after approximately (interval*retry) seconds + depends_on: + db: + condition: service_healthy + openfga: + condition: service_healthy + traefik: + condition: service_healthy + insert-hardcoded-auth-model: + condition: service_completed_successfully + keycloak: + condition: service_healthy + labels: + traefik.enable: true + traefik.http.routers.jimm.rule: Host(`jimm.localhost`) + traefik.http.routers.jimm.entrypoints: websecure + traefik.http.routers.jimm.tls: true diff --git a/docker-compose.yaml b/docker-compose.yaml index 51fbaee36..e6a34cf77 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -2,7 +2,7 @@ services: traefik: image: "traefik:2.9" container_name: traefik - profiles: ["dev"] + profiles: ["dev", "test"] ports: - "80:80" - "443:443" @@ -19,8 +19,31 @@ services: interval: 10s timeout: 5s retries: 3 + + # An instance of JIMM used in integration tests, pulled from a tag. + jimm-test: + extends: + file: compose-common.yaml + service: jimm-base + image: ghcr.io/canonical/jimm:${JIMM_VERSION:-latest} + profiles: ["test"] + container_name: jimm-test + ports: + - 17070:80 + entrypoint: + - bash + - -c + - >- + apt update && apt install curl -y + && set -a && . /vault/vault.env && set +a && /usr/local/bin/jimmsrv + volumes: + - ./local/vault/vault.env:/vault/vault.env:rw - jimm: + # An instance of JIMM used for dev, built from source with hot-reloading. + jimm-dev: + extends: + file: compose-common.yaml + service: jimm-base image: cosmtrek/air:latest profiles: ["dev"] # working_dir value has to be the same of mapped volume @@ -36,69 +59,9 @@ services: ports: - 17070:80 - 2345:2345 - environment: - JIMM_LOG_LEVEL: "debug" - JIMM_UUID: "3217dbc9-8ea9-4381-9e97-01eab0b3f6bb" - JIMM_DSN: "postgresql://jimm:jimm@db/jimm" - # Not needed for local test (yet). - # BAKERY_AGENT_FILE: "" - JIMM_ADMINS: "jimm-test@canonical.com" - # Note: You can comment out the Vault ENV vars below and instead use INSECURE_SECRET_STORAGE to place secrets in Postgres. - VAULT_ADDR: "http://vault:8200" - VAULT_PATH: "/jimm-kv/" - # Note: By default we should use Vault as that is the primary means of secret storage. - # INSECURE_SECRET_STORAGE: "enabled" - # JIMM_DASHBOARD_LOCATION: "" - JIMM_DNS_NAME: "jimm.localhost" - JIMM_LISTEN_ADDR: "0.0.0.0:80" - JIMM_TEST_PGXDSN: "postgresql://jimm:jimm@db/jimm" - JIMM_JWT_EXPIRY: 30s - JIMM_AUDIT_LOG_RETENTION_PERIOD_IN_DAYS: "1" - TEST_LOGGING_CONFIG: "" - BAKERY_PUBLIC_KEY: "izcYsQy3TePp6bLjqOo3IRPFvkQd2IKtyODGqC6SdFk=" - BAKERY_PRIVATE_KEY: "ly/dzsI9Nt/4JxUILQeAX79qZ4mygDiuYGqc2ZEiDEc=" - OPENFGA_SCHEME: "http" - OPENFGA_HOST: "openfga" - OPENFGA_PORT: 8080 - OPENFGA_STORE: "01GP1254CHWJC1MNGVB0WDG1T0" - OPENFGA_AUTH_MODEL: "01GP1EC038KHGB6JJ2XXXXCXKB" - OPENFGA_TOKEN: "jimm" - JIMM_IS_LEADER: true - JIMM_OAUTH_ISSUER_URL: "http://keycloak.localhost:8082/realms/jimm" # Scheme required - JIMM_OAUTH_CLIENT_ID: "jimm-device" - 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_SECURE_SESSION_COOKIES: false - JIMM_SESSION_COOKIE_MAX_AGE: 86400 - JIMM_SESSION_SECRET_KEY: Xz2RkR9g87M75xfoumhEs5OmGziIX8D88Rk5YW8FSvkBPSgeK9t5AS9IvPDJ3NnB volumes: - ./:/jimm/ - - ./local/vault/approle.json:/vault/approle.json:rw - - ./local/vault/roleid.txt:/vault/roleid.txt:rw - ./local/vault/vault.env:/vault/vault.env:rw - healthcheck: - test: [ "CMD", "curl", "http://jimm.localhost:80" ] - interval: 5s - timeout: 5s - retries: 50 # Should fail after approximately (interval*retry) seconds - depends_on: - db: - condition: service_healthy - openfga: - condition: service_healthy - traefik: - condition: service_healthy - insert-hardcoded-auth-model: - condition: service_completed_successfully - keycloak: - condition: service_healthy - labels: - traefik.enable: true - traefik.http.routers.jimm.rule: Host(`jimm.localhost`) - traefik.http.routers.jimm.entrypoints: websecure - traefik.http.routers.jimm.tls: true db: image: postgres @@ -193,7 +156,7 @@ services: # Adds the auth model and updates its authorisation model id to be the expected hard-coded id such that our local JIMM can utilise it for queries. # The auth model json is retrieved from file via volume mount. insert-hardcoded-auth-model: - profiles: ["dev"] + profiles: ["dev", "test"] image: governmentpaas/psql container_name: insert-hardcoded-auth-model volumes: diff --git a/local/jimm/add-controller.sh b/local/jimm/add-controller.sh index 23f89aca3..0600c0ac8 100755 --- a/local/jimm/add-controller.sh +++ b/local/jimm/add-controller.sh @@ -20,16 +20,27 @@ echo "JIMM controller name is: $JIMM_CONTROLLER_NAME" echo "Target controller name is: $CONTROLLER_NAME" echo "Target controller path is: $CONTROLLER_YAML_PATH" echo -echo "Building jimmctl..." -# Build jimmctl so we may add a controller. -go build ./cmd/jimmctl -echo "Built." -echo +which jimmctl +jimmctlAvailable=$? +if [ $jimmctlAvailable -ne 0 ]; then + echo "Building jimmctl..." + # Build jimmctl so we may add a controller. + go build ./cmd/jimmctl + echo "Built jimmctl." + echo +else + echo "jimmctl available, skipping build" +fi +if which jimmctl | grep -q 'snap'; then + CONTROLLER_YAML_PATH="$HOME/snap/jimmctl/common/$CONTROLLER_YAML_PATH" + echo "jimmctl is installed as a snap" + echo "placing controller info file at $CONTROLLER_YAML_PATH" +fi echo "Switching juju controller to $JIMM_CONTROLLER_NAME" juju switch "$JIMM_CONTROLLER_NAME" echo echo "Retrieving controller info for $CONTROLLER_NAME" -./jimmctl controller-info --local "$CONTROLLER_NAME" "$CONTROLLER_YAML_PATH" --tls-hostname juju-apiserver +jimmctl controller-info --local "$CONTROLLER_NAME" "$CONTROLLER_YAML_PATH" --tls-hostname juju-apiserver if [[ -f "$CONTROLLER_YAML_PATH" ]]; then echo "Controller info retrieved." else @@ -38,7 +49,7 @@ else fi echo echo "Adding controller from path: $CONTROLLER_YAML_PATH" -./jimmctl add-controller "$CONTROLLER_YAML_PATH" +jimmctl add-controller "$CONTROLLER_YAML_PATH" echo echo "Updating cloud credentials for: $JIMM_CONTROLLER_NAME, from client credential: $CLIENT_CREDENTIAL_NAME" juju update-credentials "$CLIENT_CREDENTIAL_NAME" --controller "$JIMM_CONTROLLER_NAME" diff --git a/local/jimm/setup-cli-auth.sh b/local/jimm/setup-cli-auth.sh new file mode 100755 index 000000000..57464c3d7 --- /dev/null +++ b/local/jimm/setup-cli-auth.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# This script is used to setup a Juju CLI to be authenticated with JIMM without going through login. +# This is particularly useful in headless environments like CI/CD. + +set -eux + +# Note that we are working around the fact that yq is a snap and doesn't have permission to hidden folders due to snap confinement. +cat ~/.local/share/juju/accounts.yaml | yq '.controllers += {"jimm":{"type": "oauth2-device", "user": "jimm-test@canonical.com", "access-token": strenv(JWT)}}' | cat > temp-accounts.yaml && mv temp-accounts.yaml ~/.local/share/juju/accounts.yaml +cat ~/.local/share/juju/controllers.yaml | yq '.controllers += {"jimm":{"uuid": "3217dbc9-8ea9-4381-9e97-01eab0b3f6bb", "api-endpoints": ["jimm.localhost:443"]}}' | cat > temp-controllers.yaml && mv temp-controllers.yaml ~/.local/share/juju/controllers.yaml diff --git a/local/jimm/setup-controller.sh b/local/jimm/setup-controller.sh index 4c1a7a181..dca63e6c7 100755 --- a/local/jimm/setup-controller.sh +++ b/local/jimm/setup-controller.sh @@ -4,12 +4,7 @@ # It will bootstrap a Juju controller and configure the necessary config to enable the controller # to communicate with the docker compose -CLOUDINIT_FILE="cloudinit.temp.yaml" -function finish { - rm "$CLOUDINIT_FILE" -} -trap finish EXIT - +CLOUDINIT_FILE=${CLOUDINIT_FILE:-"cloudinit.temp.yaml"} CONTROLLER_NAME="${CONTROLLER_NAME:-qa-lxd}" CLOUDINIT_TEMPLATE=$'cloudinit-userdata: | preruncmd: @@ -18,7 +13,16 @@ CLOUDINIT_TEMPLATE=$'cloudinit-userdata: | trusted: - |\n%s' +# shellcheck disable=SC2059 +# We are using the variable as the printf template printf "$CLOUDINIT_TEMPLATE" "$(lxc network get lxdbr0 ipv4.address | cut -f1 -d/)" "$(cat local/traefik/certs/ca.crt | sed -e 's/^/ /')" > "${CLOUDINIT_FILE}" +echo "created cloud-init file" + +if [ "${SKIP_BOOTSTRAP:-false}" == true ]; then + echo "skipping controller bootstrap" + exit 0 +fi echo "Bootstrapping controller" juju bootstrap lxd "${CONTROLLER_NAME}" --config "${CLOUDINIT_FILE}" --config login-token-refresh-url=https://jimm.localhost/.well-known/jwks.json +rm "$CLOUDINIT_FILE" diff --git a/scripts/lxd-build-test.sh b/scripts/lxd-build-test.sh deleted file mode 100755 index 11a1d9003..000000000 --- a/scripts/lxd-build-test.sh +++ /dev/null @@ -1,64 +0,0 @@ -#!/bin/sh -# lxd-build-test.sh - run JIMM tests in a clean LXD environment - -set -eu - -image=${image:-ubuntu:18.04} -container=${container:-jimm-test-`uuidgen`} -packages="build-essential bzr git make" - -lxc launch -e $image $container -trap "lxc delete --force $container" EXIT - -lxc exec $container -- sh -c 'while [ ! -f /var/lib/cloud/instance/boot-finished ]; do sleep 0.1; done' - -lxc exec --env http_proxy=${http_proxy:-} --env no_proxy=${no_proxy:-} $container -- apt-get update -y -lxc exec --env http_proxy=${http_proxy:-} --env no_proxy=${no_proxy:-} $container -- apt-get install -y $packages -lxc exec $container -- snap set system proxy.http=${http_proxy:-} -lxc exec $container -- snap set system proxy.https=${https_proxy:-${http_proxy:-}} -lxc exec $container -- snap install go --classic -lxc exec $container -- snap install vault -lxc exec $container -- snap install juju-db --devmode -if [ -n "${http_proxy:-}" ]; then - lxc exec \ - --env HOME=/home/ubuntu \ - --cwd /home/ubuntu/ \ - --user 1000 \ - --group 1000 \ - $container -- git config --global http.proxy ${http_proxy:-} -fi - -lxc file push --uid 1000 --gid 1000 --mode 600 ${NETRC:-$HOME/.netrc} $container/home/ubuntu/.netrc -lxc exec --cwd /home/ubuntu/ --user 1000 --group 1000 $container -- mkdir -p /home/ubuntu/src -tar c . | lxc exec --cwd /home/ubuntu/src/ --user 1000 --group 1000 $container -- tar x -lxc exec \ - --env HOME=/home/ubuntu \ - --env http_proxy=${http_proxy:-} \ - --env https_proxy=${https_proxy:-${http_proxy:-}} \ - --env no_proxy=${no_proxy:-} \ - --cwd /home/ubuntu/src/ \ - --user 1000 \ - --group 1000 \ - $container -- go mod download - -if [ -n "${juju_version:-}" ]; then - lxc exec \ - --env HOME=/home/ubuntu \ - --env http_proxy=${http_proxy:-} \ - --env https_proxy=${https_proxy:-${http_proxy:-}} \ - --env no_proxy=${no_proxy:-} \ - --cwd /home/ubuntu/src/ \ - --user 1000 \ - --group 1000 \ - $container -- go get github.com/juju/juju@$juju_version -fi - -lxc exec \ - --env HOME=/home/ubuntu \ - --env http_proxy=${http_proxy:-} \ - --env https_proxy=${https_proxy:-${http_proxy:-}} \ - --env no_proxy=${no_proxy:-} \ - --cwd /home/ubuntu/src/ \ - --user 1000 \ - --group 1000 \ - $container -- make check diff --git a/scripts/lxd-charm-build.sh b/scripts/lxd-charm-build.sh deleted file mode 100755 index 48694127b..000000000 --- a/scripts/lxd-charm-build.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/sh -# lxd-snap-build.sh - build JIMM charm in a clean LXD environment - -set -eu - -charm_name=juju-jimm -image=${image:-ubuntu:20.04} -container=${container:-${charm_name}-charm-`uuidgen`} - -lxd_exec() { - lxc exec \ - --env http_proxy=${http_proxy:-} \ - --env https_proxy=${https_proxy:-${http_proxy:-}} \ - --env no_proxy=${no_proxy:-} \ - $container -- "$@" -} - -lxd_exec_ubuntu() { - lxc exec \ - --env HOME=/home/ubuntu \ - --env http_proxy=${http_proxy:-} \ - --env https_proxy=${https_proxy:-${http_proxy:-}} \ - --env no_proxy=${no_proxy:-} \ - --user 1000 \ - --group 1000 \ - --cwd=${cwd:-/home/ubuntu} \ - $container -- "$@" -} - -lxc launch -e ${image} $container -trap "lxc stop $container" EXIT - -lxd_exec sh -c 'while [ ! -f /var/lib/cloud/instance/boot-finished ]; do sleep 0.1; done' - -lxd_exec apt-get update -q -y -lxd_exec apt-get upgrade -q -y -lxd_exec apt-get install -y build-essential autoconf python-dev-is-python3 -if [ -n "${http_proxy:-}" ]; then - lxd_exec snap set system proxy.http=${http_proxy:-} - lxd_exec snap set system proxy.https=${https_proxy:-${http_proxy:-}} - lxd_exec_ubuntu git config --global http.proxy ${http_proxy:-} -fi -lxd_exec snap install charmcraft --classic -echo "Push .netrc" -lxc file push --uid 1000 --gid 1000 --mode 600 ${NETRC:-$HOME/.netrc} $container/home/ubuntu/.netrc -echo "Create src" -lxd_exec_ubuntu mkdir -p /home/ubuntu/src -echo "Transfer data" -tar c -C `dirname $0`/.. . | cwd=/home/ubuntu/src lxd_exec_ubuntu tar x - - -echo "Charmcraft build" -cwd=/home/ubuntu/src/charms/jimm lxd_exec_ubuntu sudo -E charmcraft pack --verbose --destructive-mode -echo "Find file" -charmfile=`lxd_exec_ubuntu find /home/ubuntu/src/charms/jimm -name "${charm_name}_*.charm"| head -1` -echo "Pull file" -lxc file pull $container$charmfile . diff --git a/scripts/lxd-k8s-charm-build.sh b/scripts/lxd-k8s-charm-build.sh deleted file mode 100755 index a04edeac2..000000000 --- a/scripts/lxd-k8s-charm-build.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/bin/sh -# lxd-snap-build.sh - build JIMM charm in a clean LXD environment - -set -eu - -charm_name=juju-jimm-k8s -image=${image:-ubuntu:20.04} -container=${container:-${charm_name}-charm-`uuidgen`} - -lxd_exec() { - lxc exec \ - --env http_proxy=${http_proxy:-} \ - --env https_proxy=${https_proxy:-${http_proxy:-}} \ - --env no_proxy=${no_proxy:-} \ - $container -- "$@" -} - -lxd_exec_ubuntu() { - lxc exec \ - --env HOME=/home/ubuntu \ - --env http_proxy=${http_proxy:-} \ - --env https_proxy=${https_proxy:-${http_proxy:-}} \ - --env no_proxy=${no_proxy:-} \ - --user 1000 \ - --group 1000 \ - --cwd=${cwd:-/home/ubuntu} \ - $container -- "$@" -} - -lxc launch -e ${image} $container -trap "lxc stop $container" EXIT - -lxd_exec sh -c 'while [ ! -f /var/lib/cloud/instance/boot-finished ]; do sleep 0.1; done' - -lxd_exec apt-get update -q -y -lxd_exec apt-get upgrade -q -y -lxd_exec apt-get install -y build-essential autoconf python-dev -if [ -n "${http_proxy:-}" ]; then - lxd_exec snap set system proxy.http=${http_proxy:-} - lxd_exec snap set system proxy.https=${https_proxy:-${http_proxy:-}} - lxd_exec_ubuntu git config --global http.proxy ${http_proxy:-} -fi -lxd_exec snap install charmcraft --classic -echo "Push .netrc" -lxc file push --uid 1000 --gid 1000 --mode 600 ${NETRC:-$HOME/.netrc} $container/home/ubuntu/.netrc -echo "Create src" -lxd_exec_ubuntu mkdir -p /home/ubuntu/src -echo "Transfer data" -tar c -C `dirname $0`/.. . | cwd=/home/ubuntu/src lxd_exec_ubuntu tar x - -echo "Charmcraft build" -cwd=/home/ubuntu/src/charms/jimm-k8s lxd_exec_ubuntu sudo -E charmcraft pack --verbose --destructive-mode -echo "Find file" -charmfile=`lxd_exec_ubuntu find /home/ubuntu/src/charms/jimm-k8s -name "${charm_name}_*.charm"| head -1` -echo "Pull file" -lxc file pull $container$charmfile . diff --git a/scripts/lxd-release-build.sh b/scripts/lxd-release-build.sh deleted file mode 100644 index ae4ebb066..000000000 --- a/scripts/lxd-release-build.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/bin/sh -# lxd-release-build.sh - build JIMM releases in a clean LXD environment - -set -eu - -image=${image:-ubuntu:20.04} -container=${container:-jimm-release-`uuidgen`} - -lxd_exec() { - lxc exec \ - --env http_proxy=${http_proxy:-} \ - --env https_proxy=${https_proxy:-${http_proxy:-}} \ - --env no_proxy=${no_proxy:-} \ - $container -- "$@" -} - -lxd_exec_ubuntu() { - lxc exec \ - --env HOME=/home/ubuntu \ - --env http_proxy=${http_proxy:-} \ - --env https_proxy=${https_proxy:-${http_proxy:-}} \ - --env no_proxy=${no_proxy:-} \ - --user 1000 \ - --group 1000 \ - --cwd=${cwd:-/home/ubuntu} \ - $container -- "$@" -} - -lxc launch -e ${image} $container -trap "lxc delete --force $container" EXIT - -lxd_exec sh -c 'while [ ! -f /var/lib/cloud/instance/boot-finished ]; do sleep 0.1; done' - -lxd_exec apt-get update -y -lxd_exec apt-get install -y build-essential bzr git make mongodb -if [ -n "${http_proxy:-}" ]; then - lxd_exec snap set system proxy.http=${http_proxy:-} - lxd_exec snap set system proxy.https=${https_proxy:-${http_proxy:-}} - lxd_exec_ubuntu git config --global http.proxy ${http_proxy:-} -fi -lxd_exec snap install go --classic -lxd_exec snap install vault - -lxc file push --uid 1000 --gid 1000 --mode 600 ${NETRC:-$HOME/.netrc} $container/home/ubuntu/.netrc -lxd_exec_ubuntu mkdir -p /home/ubuntu/src -tar c . | cwd=/home/ubuntu/src lxd_exec_ubuntu tar x -cwd=/home/ubuntu/src lxd_exec_ubuntu go mod download - -cwd=/home/ubuntu/src lxd_exec_ubuntu make check -cwd=/home/ubuntu/src lxd_exec_ubuntu make release - -tarfile=`lxd_exec_ubuntu find /home/ubuntu/src -name "jimm-*.tar.xz"| head -1` -lxc file pull $container$tarfile . diff --git a/scripts/lxd-snap-build.sh b/scripts/lxd-snap-build.sh deleted file mode 100755 index 438e3db07..000000000 --- a/scripts/lxd-snap-build.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/bin/sh -# lxd-snap-build.sh - build JIMM snaps in a clean LXD environment - -set -eu - -snap_name=${snap_name:-jimm} -image=${image:-ubuntu:20.04} -container=${container:-${snap_name}-snap} - -lxd_exec() { - lxc exec \ - --env http_proxy=${http_proxy:-} \ - --env https_proxy=${https_proxy:-${http_proxy:-}} \ - --env no_proxy=${no_proxy:-} \ - $container -- "$@" -} - -lxd_exec_ubuntu() { - lxc exec \ - --env HOME=/home/ubuntu \ - --env http_proxy=${http_proxy:-} \ - --env https_proxy=${https_proxy:-${http_proxy:-}} \ - --env no_proxy=${no_proxy:-} \ - --user 1000 \ - --group 1000 \ - --cwd=${cwd:-/home/ubuntu} \ - $container -- "$@" -} - -lxc launch -e ${image} $container -trap "lxc stop $container" EXIT - -lxd_exec sh -c 'while [ ! -f /var/lib/cloud/instance/boot-finished ]; do sleep 0.1; done' - -lxd_exec apt-get update -q -y -lxd_exec apt-get upgrade -q -y -lxd_exec apt-get install build-essential -q -y -if [ -n "${http_proxy:-}" ]; then - lxd_exec snap set system proxy.http=${http_proxy:-} - lxd_exec snap set system proxy.https=${https_proxy:-${http_proxy:-}} - lxd_exec_ubuntu git config --global http.proxy ${http_proxy:-} -fi -lxd_exec snap install snapcraft --classic -lxc file push --uid 1000 --gid 1000 --mode 600 ${NETRC:-$HOME/.netrc} $container/home/ubuntu/.netrc -lxd_exec_ubuntu mkdir -p /home/ubuntu/src -tar c -C `dirname $0`/.. . | cwd=/home/ubuntu/src lxd_exec_ubuntu tar x -target= -if [ -n "${target_arch:-}" ]; then - target="--target-arch ${target_arch}" -fi -cwd=/home/ubuntu/src/snaps/$snap_name lxd_exec_ubuntu snapcraft --destructive-mode $target -snapfile=`lxd_exec_ubuntu find /home/ubuntu/src -name "${snap_name}_*.snap"| head -1` -lxc file pull $container$snapfile . From adcb70b7fc96a19f07875936323d1006d109f4c7 Mon Sep 17 00:00:00 2001 From: Ales Stimec Date: Mon, 26 Aug 2024 11:37:58 +0200 Subject: [PATCH 19/30] Allow adding a model with implicit cloud. If the user does not specify which cloud to add the model to and only one cloud is known to JIMM, we will select that cloud and continue instead of returning an error. --- internal/jimm/model.go | 59 ++++++-- internal/jimm/model_test.go | 203 +++++++++++++++++++++++++- internal/jujuapi/modelmanager_test.go | 12 +- 3 files changed, 246 insertions(+), 28 deletions(-) diff --git a/internal/jimm/model.go b/internal/jimm/model.go index 1d2ee823a..0b60539b3 100644 --- a/internal/jimm/model.go +++ b/internal/jimm/model.go @@ -55,14 +55,13 @@ func (a *ModelCreateArgs) FromJujuModelCreateArgs(args *jujuparams.ModelCreateAr a.Name = args.Name a.Config = args.Config a.CloudRegion = args.CloudRegion - if args.CloudTag == "" { - return errors.E("no cloud specified for model; please specify one") - } - ct, err := names.ParseCloudTag(args.CloudTag) - if err != nil { - return errors.E(err, errors.CodeBadRequest) + if args.CloudTag != "" { + ct, err := names.ParseCloudTag(args.CloudTag) + if err != nil { + return errors.E(err, errors.CodeBadRequest) + } + a.Cloud = ct } - a.Cloud = ct if args.OwnerTag == "" { return errors.E("owner tag not specified") @@ -175,10 +174,17 @@ func (b *modelBuilder) WithConfig(cfg map[string]interface{}) *modelBuilder { } // WithCloud returns a builder with the specified cloud. -func (b *modelBuilder) WithCloud(cloud names.CloudTag) *modelBuilder { +func (b *modelBuilder) WithCloud(user *openfga.User, cloud names.CloudTag) *modelBuilder { if b.err != nil { return b } + + // if cloud was not specified then we try to determine if + // JIMM knows of only one cloud and use that one + if cloud.Id() == "" { + return b.withImplicitCloud(user) + } + c := dbmodel.Cloud{ Name: cloud.Id(), } @@ -192,6 +198,34 @@ func (b *modelBuilder) WithCloud(cloud names.CloudTag) *modelBuilder { return b } +// withImplicitCloud returns a builder with the only cloud known to JIMM. Should JIMM +// know of multiple clouds an error will be raised. +func (b *modelBuilder) withImplicitCloud(user *openfga.User) *modelBuilder { + if b.err != nil { + return b + } + var clouds []*dbmodel.Cloud + err := b.jimm.ForEachUserCloud(b.ctx, user, func(c *dbmodel.Cloud) error { + clouds = append(clouds, c) + return nil + }) + if err != nil { + b.err = err + return b + } + if len(clouds) == 0 { + b.err = fmt.Errorf("no available clouds") + return b + } + if len(clouds) != 1 { + b.err = fmt.Errorf("no cloud specified for model; please specify one") + return b + } + b.cloud = clouds[0] + + return b +} + // WithCloudRegion returns a builder with the specified cloud region. func (b *modelBuilder) WithCloudRegion(region string) *modelBuilder { if b.err != nil { @@ -563,12 +597,11 @@ func (j *JIMM) AddModel(ctx context.Context, user *openfga.User, args *ModelCrea builder = builder.WithConfig(cloudDefaults.Defaults) } - if args.Cloud != (names.CloudTag{}) { - builder = builder.WithCloud(args.Cloud) - if err := builder.Error(); err != nil { - return nil, errors.E(op, err) - } + builder = builder.WithCloud(user, args.Cloud) + if err := builder.Error(); err != nil { + return nil, errors.E(op, err) } + builder = builder.WithCloudRegion(args.CloudRegion) if err := builder.Error(); err != nil { return nil, errors.E(op, err) diff --git a/internal/jimm/model_test.go b/internal/jimm/model_test.go index 57b7cbcc0..f7e256803 100644 --- a/internal/jimm/model_test.go +++ b/internal/jimm/model_test.go @@ -104,14 +104,6 @@ func TestModelCreateArgs(t *testing.T) { CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice/test-credential-1").String(), }, expectedError: "owner tag not specified", - }, { - about: "cloud tag not specified", - args: jujuparams.ModelCreateArgs{ - Name: "test-model", - OwnerTag: names.NewUserTag("alice@canonical.com").String(), - CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice/test-credential-1").String(), - }, - expectedError: "no cloud specified for model; please specify one", }} opts := []cmp.Option{ @@ -886,6 +878,198 @@ users: CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential-1").String(), }, expectError: "unauthorized", +}, { + name: "CreateModelWithImplicitCloud", + env: ` +clouds: +- name: test-cloud + type: test-provider + regions: + - name: test-region-1 + users: + - user: alice@canonical.com + access: add-model +user-defaults: +- user: alice@canonical.com + defaults: + key4: value4 +cloud-defaults: +- user: alice@canonical.com + cloud: test-cloud + region: test-region-1 + defaults: + key1: value1 + key2: value2 +- user: alice@canonical.com + cloud: test-cloud + defaults: + key3: value3 +cloud-credentials: +- name: test-credential-1 + owner: alice@canonical.com + cloud: test-cloud + auth-type: empty +controllers: +- name: controller-1 + uuid: 00000000-0000-0000-0000-0000-0000000000001 + cloud: test-cloud + region: test-region-1 + cloud-regions: + - cloud: test-cloud + region: test-region-1 + priority: 0 +- name: controller-2 + uuid: 00000000-0000-0000-0000-0000-0000000000002 + cloud: test-cloud + region: test-region-1 + cloud-regions: + - cloud: test-cloud + region: test-region-1 + priority: 2 +`[1:], + updateCredential: func(_ context.Context, _ jujuparams.TaggedCredential) ([]jujuparams.UpdateCredentialModelResult, error) { + return nil, nil + }, + grantJIMMModelAdmin: func(_ context.Context, _ names.ModelTag) error { + return nil + }, + createModel: assertConfig(map[string]interface{}{ + "key4": "value4", + }, createModel(` +uuid: 00000001-0000-0000-0000-0000-000000000001 +status: + status: started + info: running a test +life: alive +users: +- user: alice@canonical.com + access: admin +- user: bob + access: read +`[1:])), + username: "alice@canonical.com", + jimmAdmin: true, + args: jujuparams.ModelCreateArgs{ + Name: "test-model", + OwnerTag: names.NewUserTag("alice@canonical.com").String(), + CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential-1").String(), + }, + expectModel: dbmodel.Model{ + Name: "test-model", + UUID: sql.NullString{ + String: "00000001-0000-0000-0000-0000-000000000001", + Valid: true, + }, + Owner: dbmodel.Identity{ + Name: "alice@canonical.com", + }, + Controller: dbmodel.Controller{ + Name: "controller-2", + UUID: "00000000-0000-0000-0000-0000-0000000000002", + CloudName: "test-cloud", + CloudRegion: "test-region-1", + }, + CloudRegion: dbmodel.CloudRegion{ + Cloud: dbmodel.Cloud{ + Name: "test-cloud", + Type: "test-provider", + }, + Name: "test-region-1", + }, + CloudCredential: dbmodel.CloudCredential{ + Name: "test-credential-1", + AuthType: "empty", + }, + Life: state.Alive.String(), + Status: dbmodel.Status{ + Status: "started", + Info: "running a test", + }, + }, +}, { + name: "CreateModelWithImplicitCloudAndMultipleClouds", + env: ` +clouds: +- name: test-cloud + type: test-provider + regions: + - name: test-region-1 + users: + - user: alice@canonical.com + access: add-model +- name: test-cloud-2 + type: test-provider-2 + regions: + - name: test-region-2 + users: + - user: alice@canonical.com + access: add-model +user-defaults: +- user: alice@canonical.com + defaults: + key4: value4 +cloud-defaults: +- user: alice@canonical.com + cloud: test-cloud + region: test-region-1 + defaults: + key1: value1 + key2: value2 +- user: alice@canonical.com + cloud: test-cloud + defaults: + key3: value3 +cloud-credentials: +- name: test-credential-1 + owner: alice@canonical.com + cloud: test-cloud + auth-type: empty +controllers: +- name: controller-1 + uuid: 00000000-0000-0000-0000-0000-0000000000001 + cloud: test-cloud + region: test-region-1 + cloud-regions: + - cloud: test-cloud + region: test-region-1 + priority: 0 +- name: controller-2 + uuid: 00000000-0000-0000-0000-0000-0000000000002 + cloud: test-cloud + region: test-region-1 + cloud-regions: + - cloud: test-cloud + region: test-region-1 + priority: 2 +`[1:], + updateCredential: func(_ context.Context, _ jujuparams.TaggedCredential) ([]jujuparams.UpdateCredentialModelResult, error) { + return nil, nil + }, + grantJIMMModelAdmin: func(_ context.Context, _ names.ModelTag) error { + return nil + }, + createModel: assertConfig(map[string]interface{}{ + "key4": "value4", + }, createModel(` +uuid: 00000001-0000-0000-0000-0000-000000000001 +status: + status: started + info: running a test +life: alive +users: +- user: alice@canonical.com + access: admin +- user: bob + access: read +`[1:])), + username: "alice@canonical.com", + jimmAdmin: true, + args: jujuparams.ModelCreateArgs{ + Name: "test-model", + OwnerTag: names.NewUserTag("alice@canonical.com").String(), + CloudCredentialTag: names.NewCloudCredentialTag("test-cloud/alice@canonical.com/test-credential-1").String(), + }, + expectError: "no cloud specified for model; please specify one", }} func TestAddModel(t *testing.T) { @@ -982,6 +1166,9 @@ func createModel(template string) func(context.Context, *jujuparams.ModelCreateA func assertConfig(config map[string]interface{}, fnc func(context.Context, *jujuparams.ModelCreateArgs, *jujuparams.ModelInfo) error) func(context.Context, *jujuparams.ModelCreateArgs, *jujuparams.ModelInfo) error { return func(ctx context.Context, args *jujuparams.ModelCreateArgs, mi *jujuparams.ModelInfo) error { + if args.CloudTag == "" { + return errors.E("cloud not specified") + } if len(config) != len(args.Config) { return errors.E(fmt.Sprintf("expected %d config settings, got %d", len(config), len(args.Config))) } diff --git a/internal/jujuapi/modelmanager_test.go b/internal/jujuapi/modelmanager_test.go index 4e2c915dd..402489578 100644 --- a/internal/jujuapi/modelmanager_test.go +++ b/internal/jujuapi/modelmanager_test.go @@ -860,19 +860,17 @@ var createModelTests = []struct { cloudTag: "not-a-cloud-tag", credentialTag: "cloudcred-" + jimmtest.TestCloudName + "_bob@canonical.com_cred1", expectError: `"not-a-cloud-tag" is not a valid tag \(bad request\)`, -}, { - about: "no cloud tag", - name: "model-8", - ownerTag: names.NewUserTag("bob@canonical.com").String(), - cloudTag: "", - credentialTag: "cloudcred-" + jimmtest.TestCloudName + "_bob@canonical.com_cred1", - expectError: `no cloud specified for model; please specify one`, }, { about: "no credential tag selects unambigous creds", name: "model-8", ownerTag: names.NewUserTag("bob@canonical.com").String(), cloudTag: names.NewCloudTag(jimmtest.TestCloudName).String(), region: jimmtest.TestCloudRegionName, +}, { + about: "success - without a cloud tag", + name: "model-9", + ownerTag: names.NewUserTag("bob@canonical.com").String(), + credentialTag: "cloudcred-" + jimmtest.TestCloudName + "_bob@canonical.com_cred", }} func (s *modelManagerSuite) TestCreateModel(c *gc.C) { From 565186306585c6233a69ecd1f9726ab5a2f8604a Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Tue, 27 Aug 2024 10:28:27 +0200 Subject: [PATCH 20/30] Vault test setup tweaks (#1322) * remove need for approle.json * remove references to vault env and approle files * remove sql init volume * Add new lines --- .air.toml | 2 +- .github/workflows/cache.yaml | 6 --- .github/workflows/ci.yaml | 6 --- .github/workflows/golangci-lint.yaml | 3 -- .github/workflows/integration-test.yaml | 5 +++ .gitignore | 4 -- Makefile | 2 - compose-common.yaml | 2 + docker-compose.yaml | 27 +++-------- internal/jimmtest/vault.go | 33 +++----------- local/init.sql | 21 --------- local/vault/Dockerfile | 20 +++++++++ local/vault/approle.go | 11 ----- local/vault/entrypoint.sh | 48 ++++++++++++++++++++ local/vault/init.sh | 60 ------------------------- local/vault/vault.hcl | 8 ---- 16 files changed, 86 insertions(+), 172 deletions(-) delete mode 100644 local/init.sql create mode 100644 local/vault/Dockerfile delete mode 100644 local/vault/approle.go create mode 100755 local/vault/entrypoint.sh delete mode 100755 local/vault/init.sh delete mode 100644 local/vault/vault.hcl diff --git a/.air.toml b/.air.toml index f5e8b5767..5a6fb2c75 100644 --- a/.air.toml +++ b/.air.toml @@ -11,7 +11,7 @@ tmp_dir = "tmp" exclude_regex = ["_test.go"] exclude_unchanged = false follow_symlink = false - full_bin = "env $(cat /vault/vault.env | xargs) dlv exec --accept-multiclient --log --headless --continue --listen :2345 --api-version 2 ./tmp/jimm" + full_bin = "dlv exec --accept-multiclient --log --headless --continue --listen :2345 --api-version 2 ./tmp/jimm" include_dir = [] include_ext = ["go", "tpl", "tmpl", "html"] kill_delay = "0s" diff --git a/.github/workflows/cache.yaml b/.github/workflows/cache.yaml index 623fe65a5..3f8ab5ddb 100644 --- a/.github/workflows/cache.yaml +++ b/.github/workflows/cache.yaml @@ -22,11 +22,5 @@ jobs: with: go-version-file: 'go.mod' - - name: Add volume files - run: | - touch ./local/vault/approle.json - touch ./local/vault/roleid.txt - touch ./local/vault/vault.env - - name: Build run: go build ./... diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6d43b9dce..578a71618 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -27,12 +27,6 @@ jobs: - name: Install juju-db run: sudo snap install juju-db --channel 4.4/stable - - name: Add volume files - run: | - touch ./local/vault/approle.json - touch ./local/vault/roleid.txt - touch ./local/vault/vault.env - - name: Create test certs run: make certs diff --git a/.github/workflows/golangci-lint.yaml b/.github/workflows/golangci-lint.yaml index 271f9c84e..fc5a7e625 100644 --- a/.github/workflows/golangci-lint.yaml +++ b/.github/workflows/golangci-lint.yaml @@ -20,9 +20,6 @@ jobs: go-version: stable cache: false - - name: Touch approle - run: touch ./local/vault/approle.json - - name: Run Golangci-lint uses: golangci/golangci-lint-action@v6 with: diff --git a/.github/workflows/integration-test.yaml b/.github/workflows/integration-test.yaml index 9dd905e3b..0e1e2a3e4 100644 --- a/.github/workflows/integration-test.yaml +++ b/.github/workflows/integration-test.yaml @@ -17,14 +17,17 @@ jobs: steps: - name: Checkout JIMM repo uses: actions/checkout@v4 + - name: Setup Go if: ${{ github.event_name == 'pull_request' }} uses: actions/setup-go@v4 with: go-version-file: 'go.mod' + - name: Go vendor to speed up docker build if: ${{ github.event_name == 'pull_request' }} run: go mod vendor + - name: Start JIMM (pull request) if: ${{ github.event_name == 'pull_request' }} uses: ./.github/actions/test-server @@ -32,6 +35,7 @@ jobs: jimm-version: dev juju-channel: "3/stable" ghcr-pat: ${{ secrets.GITHUB_TOKEN }} + - name: Start JIMM (manual run) if: ${{ github.event_name == 'workflow_dispatch' }} uses: ./.github/actions/test-server @@ -39,6 +43,7 @@ jobs: jimm-version: ${{ inputs.jimm-version }} juju-channel: "3/stable" ghcr-pat: ${{ secrets.GITHUB_TOKEN }} + - name: Create a model, deploy an application and run juju status run: | juju add-model foo && \ diff --git a/.gitignore b/.gitignore index 18d156d4a..84d8a8902 100644 --- a/.gitignore +++ b/.gitignore @@ -11,10 +11,6 @@ /version/commit.txt /version/version.txt /tmp -/local/vault/approle.json -local/vault/approle.json -local/vault/roleid.txt -local/vault/vault.env *.crt *.key diff --git a/Makefile b/Makefile index 3b6b63ba9..cb95caf0e 100644 --- a/Makefile +++ b/Makefile @@ -36,14 +36,12 @@ certs: @cd local/traefik/certs; ./certs.sh; cd - test-env: sys-deps certs - @touch ./local/vault/approle.json && touch ./local/vault/roleid.txt && touch ./local/vault/vault.env @docker compose up --force-recreate -d --wait test-env-cleanup: @docker compose down -v --remove-orphans dev-env-setup: sys-deps certs - @touch ./local/vault/approle.json && touch ./local/vault/roleid.txt && touch ./local/vault/vault.env @make version/commit.txt && make version/version.txt dev-env: dev-env-setup diff --git a/compose-common.yaml b/compose-common.yaml index 3b12226e1..c467cda94 100644 --- a/compose-common.yaml +++ b/compose-common.yaml @@ -12,6 +12,8 @@ services: # Note: You can comment out the Vault ENV vars below and instead use INSECURE_SECRET_STORAGE to place secrets in Postgres. VAULT_ADDR: "http://vault:8200" VAULT_PATH: "/jimm-kv/" + VAULT_ROLE_ID: test-role-id + VAULT_ROLE_SECRET_ID: test-secret-id # Note: By default we should use Vault as that is the primary means of secret storage. # INSECURE_SECRET_STORAGE: "enabled" # JIMM_DASHBOARD_LOCATION: "" diff --git a/docker-compose.yaml b/docker-compose.yaml index e6a34cf77..89bda35e9 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -35,9 +35,7 @@ services: - -c - >- apt update && apt install curl -y - && set -a && . /vault/vault.env && set +a && /usr/local/bin/jimmsrv - volumes: - - ./local/vault/vault.env:/vault/vault.env:rw + && /usr/local/bin/jimmsrv # An instance of JIMM used for dev, built from source with hot-reloading. jimm-dev: @@ -61,7 +59,6 @@ services: - 2345:2345 volumes: - ./:/jimm/ - - ./local/vault/vault.env:/vault/vault.env:rw db: image: postgres @@ -69,8 +66,6 @@ services: restart: always ports: - 5432:5432 - volumes: - - ./local/init.sql:/docker-entrypoint-initdb.d/init.sql environment: POSTGRES_DB: jimm POSTGRES_USER: jimm @@ -85,30 +80,18 @@ services: retries: 5 vault: - image: hashicorp/vault:latest + build: + context: ./local/vault/ + dockerfile: Dockerfile container_name: vault ports: - 8200:8200 environment: - # For CLI VAULT_ADDR: "http://localhost:8200" - # Dev Flag VAULT_DEV_LISTEN_ADDRESS: "0.0.0.0:8200" - # Dev Flag - VAULT_DEV_ROOT_TOKEN_ID: "token" + VAULT_DEV_ROOT_TOKEN_ID: "root" cap_add: - IPC_LOCK - volumes: - - ./local/vault/vault.hcl:/vault/config/vault.hcl - - ./local/vault/init.sh:/vault/init.sh - - ./local/vault/policy.hcl:/vault/policy.hcl - - ./local/vault/approle.json:/vault/approle.json - - ./local/vault/roleid.txt:/vault/roleid.txt:rw - - ./local/vault/vault.env:/vault/vault.env:rw - command: /vault/init.sh - depends_on: - db: - condition: service_healthy migrateopenfga: image: openfga/openfga:v1.2.0 diff --git a/internal/jimmtest/vault.go b/internal/jimmtest/vault.go index 3a4b76578..884761afe 100644 --- a/internal/jimmtest/vault.go +++ b/internal/jimmtest/vault.go @@ -3,11 +3,12 @@ package jimmtest import ( - "encoding/json" - "github.com/hashicorp/vault/api" +) - vault_test "github.com/canonical/jimm/v3/local/vault" +const ( + testRoleID = "test-role-id" + testSecretID = "test-secret-id" ) type fatalF interface { @@ -20,29 +21,5 @@ func VaultClient(tb fatalF) (*api.Client, string, string, string, bool) { cfg := api.DefaultConfig() cfg.Address = "http://localhost:8200" vaultClient, _ := api.NewClient(cfg) - - appRole := vault_test.AppRole - var vaultAPISecret api.Secret - err := json.Unmarshal(appRole, &vaultAPISecret) - if err != nil { - panic("cannot unmarshal vault secret") - } - - roleID, ok := vaultAPISecret.Data["role_id"] - if !ok { - panic("role ID not found") - } - roleSecretID, ok := vaultAPISecret.Data["secret_id"] - if !ok { - panic("role secret ID not found") - } - roleIDString, ok := roleID.(string) - if !ok { - panic("failed to convert role ID to string") - } - roleSecretIDString, ok := roleSecretID.(string) - if !ok { - panic("failed to convert role secret ID to string") - } - return vaultClient, "jimm-kv", roleIDString, roleSecretIDString, true + return vaultClient, "jimm-kv", testRoleID, testSecretID, true } diff --git a/local/init.sql b/local/init.sql deleted file mode 100644 index 978210f4a..000000000 --- a/local/init.sql +++ /dev/null @@ -1,21 +0,0 @@ - -/* Setup kv store path for postgres backend */ -CREATE TABLE vault_kv_store ( - parent_path TEXT COLLATE "C" NOT NULL, - path TEXT COLLATE "C", - key TEXT COLLATE "C", - value BYTEA, - CONSTRAINT pkey PRIMARY KEY (path, key) -); - -/* Set index for kv parent */ -CREATE INDEX parent_path_idx ON vault_kv_store (parent_path); - -/* Setup HA locks, so we can emulate a production environment locally */ -CREATE TABLE vault_ha_locks ( - ha_key TEXT COLLATE "C" NOT NULL, - ha_identity TEXT COLLATE "C" NOT NULL, - ha_value TEXT COLLATE "C", - valid_until TIMESTAMP WITH TIME ZONE NOT NULL, - CONSTRAINT ha_key PRIMARY KEY (ha_key) -); diff --git a/local/vault/Dockerfile b/local/vault/Dockerfile new file mode 100644 index 000000000..90cb372c1 --- /dev/null +++ b/local/vault/Dockerfile @@ -0,0 +1,20 @@ +FROM hashicorp/vault:latest + +# Add jq to make scripting the calls a bit easier +# ref: https://stedolan.github.io/jq/ +RUN apk add --no-cache bash jq + +# Add our policy and entrypoint +COPY policy.hcl /vault/policy.hcl +COPY entrypoint.sh /vault/entrypoint.sh + +EXPOSE 8200 + +ENTRYPOINT [ "/vault/entrypoint.sh" ] + +HEALTHCHECK \ + --start-period=5s \ + --interval=1s \ + --timeout=1s \ + --retries=30 \ + CMD [ "/bin/sh", "-c", "[ -f /tmp/healthy ]" ] diff --git a/local/vault/approle.go b/local/vault/approle.go deleted file mode 100644 index 0649aa4ba..000000000 --- a/local/vault/approle.go +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2024 Canonical. - -// This package exists to hold files used to authenticate with Vault during tests. -package vault - -import ( - _ "embed" -) - -//go:embed approle.json -var AppRole []byte diff --git a/local/vault/entrypoint.sh b/local/vault/entrypoint.sh new file mode 100755 index 000000000..5de2a6bfe --- /dev/null +++ b/local/vault/entrypoint.sh @@ -0,0 +1,48 @@ +#!/bin/sh + +# Much of the below was lifted from the sample Vault application setup +# in https://github.com/hashicorp/hello-vault-go/tree/main/sample-app + +set -e + +export VAULT_ADDR='http://localhost:8200' +export VAULT_FORMAT='json' + +# Dev mode defaults some addresses, but also enables us +# to have a custom root key & automatically unsealed vault. +vault server -dev & +sleep 5s + +# Authenticate container's local Vault CLI +# ref: https://www.vaultproject.io/docs/commands/login +vault login -no-print "${VAULT_DEV_ROOT_TOKEN_ID}" + +# AppRole auth is what we use in JIMM, an awesome tutorial +# on how this is setup can be found below. +# HOW-TO: https://developer.hashicorp.com/vault/docs/auth/approle +# AND: +# https://developer.hashicorp.com/vault/tutorials/auth-methods/approle + +echo "Enabling AppRole auth" +vault auth enable approle + +echo "Creating access policy to JIMM stores" +vault policy write jimm-app /vault/policy.hcl + +echo "Creating jimm-app AppRole" +vault write auth/approle/role/jimm-app policies=jimm-app + +# Set fixed role ID and secret ID to simplify testing +vault write auth/approle/role/jimm-app/role-id role_id="test-role-id" +vault write auth/approle/role/jimm-app/custom-secret-id secret_id="test-secret-id" + +# Enable the KV at the defined policy path +echo "Enabling KV at policy path /jimm-kv" +echo "/jimm-kv accessible by policy jimm-app" +vault secrets enable -version=2 -path /jimm-kv kv + +# This container is now healthy +touch /tmp/healthy + +# Keep container alive +tail -f /dev/null & trap 'kill %1' TERM ; wait diff --git a/local/vault/init.sh b/local/vault/init.sh deleted file mode 100755 index 986e5d6d2..000000000 --- a/local/vault/init.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/bin/sh - -# Grab JQ for ease of use. -ARCH=$(arch | sed s/aarch64/arm64/ | sed s/x86_64/amd64/) -wget -O jq https://github.com/jqlang/jq/releases/download/jq-1.7/jq-linux-$ARCH -chmod +x ./jq -cp jq /usr/bin - -# Dev mode defaults some addresses, but also enables us -# to have a custom root key & automatically unsealed vault. -vault server -dev -config=/vault/config/vault.hcl & -sleep 2 - -# Login. -echo "token" | vault login - - -# Set address for local client. -export VAULT_ADDR="http://localhost:8200" -# Makes reading easier. -export VAULT_FORMAT=json - -# AppRole auth is what we use in JIMM, an awesome tutorial -# on how this is setup can be found below. -# HOW-TO: https://developer.hashicorp.com/vault/docs/auth/approle -# AND: -# https://developer.hashicorp.com/vault/tutorials/auth-methods/approle -echo "Enabling AppRole auth" -vault auth enable approle - -echo "Creating access policy to JIMM stores" -vault policy write jimm-app /vault/policy.hcl - -echo "Creating jimm-app AppRole" -vault write auth/approle/role/jimm-app \ - policies=jimm-app - - -# We mimic the normal flow just for reference. Ultimately we passed to secret itself. -# This is because our flow looks at a raw unwrapped secret, rather than carefully -# extracting the role id & secret id from the unwrapped token in cubbyhole. -JIMM_ROLE_ID=$(vault read auth/approle/role/jimm-app/role-id | jq -r '.data.role_id') -echo "AppRole created, role ID is: $JIMM_ROLE_ID" -JIMM_SECRET_WRAPPED=$(vault write -wrap-ttl=10h -force auth/approle/role/jimm-app/secret-id | jq -r '.wrap_info.token') -echo "SecretID applied & wrapped in cubbyhole for 10h, token is: $JIMM_SECRET_WRAPPED" - -# Enable the KV at the defined policy path -echo "Enabling KV at policy path /jimm-kv" -echo "/jimm-kv accessible by policy jimm-app" -vault secrets enable -version=2 -path /jimm-kv kv -echo "Creating approle auth file." -VAULT_TOKEN=$JIMM_SECRET_WRAPPED vault unwrap > /vault/approle_tmp.yaml -echo "$JIMM_ROLE_ID" > /vault/roleid.txt - -jq ".data.role_id = \"$JIMM_ROLE_ID\"" /vault/approle_tmp.yaml > /vault/approle.json -role_id=$(cat /vault/approle.json | jq -r ".data.role_id") -role_secret_id=$(cat /vault/approle.json | jq -r ".data.secret_id") -echo "VAULT_ROLE_ID=$role_id" > /vault/vault.env -echo "VAULT_ROLE_SECRET_ID=$role_secret_id" >> /vault/vault.env -wait - diff --git a/local/vault/vault.hcl b/local/vault/vault.hcl deleted file mode 100644 index be54919f2..000000000 --- a/local/vault/vault.hcl +++ /dev/null @@ -1,8 +0,0 @@ - -storage "postgresql" { - connection_url = "postgres://jimm:jimm@db:5432/jimm" -} - -# Reachable here: http://localhost:8200/ui/ -ui = true - From 73357b936ac88ad13f92b910ae6b2cc0329c58bf Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Tue, 27 Aug 2024 15:09:56 +0200 Subject: [PATCH 21/30] Simplify openfga setup (#1323) * simplify Docker compose and OpenFGA setup * entrypoint tweaks --- compose-common.yaml | 2 - docker-compose.yaml | 54 +++----------------------- local/openfga/Dockerfile | 20 +++++++++- local/openfga/authorisation_model.json | 1 - local/openfga/entrypoint.sh | 24 ++++++++++++ local/vault/entrypoint.sh | 4 +- 6 files changed, 50 insertions(+), 55 deletions(-) delete mode 120000 local/openfga/authorisation_model.json create mode 100755 local/openfga/entrypoint.sh diff --git a/compose-common.yaml b/compose-common.yaml index c467cda94..3e905dab0 100644 --- a/compose-common.yaml +++ b/compose-common.yaml @@ -53,8 +53,6 @@ services: condition: service_healthy traefik: condition: service_healthy - insert-hardcoded-auth-model: - condition: service_completed_successfully keycloak: condition: service_healthy labels: diff --git a/docker-compose.yaml b/docker-compose.yaml index 89bda35e9..c2c352393 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -63,7 +63,7 @@ services: db: image: postgres container_name: postgres - restart: always + restart: on-failure ports: - 5432:5432 environment: @@ -93,65 +93,23 @@ services: cap_add: - IPC_LOCK - migrateopenfga: - image: openfga/openfga:v1.2.0 - container_name: migrateopenfga - command: migrate --datastore-engine postgres --datastore-uri 'postgresql://jimm:jimm@db/jimm?sslmode=disable' - depends_on: - db: - condition: service_healthy - - insert-hardcoded-store: - image: governmentpaas/psql - container_name: insert-hardcoded-store - command: psql -Atx postgresql://jimm:jimm@db/jimm?sslmode=disable -c "INSERT INTO store (id,name,created_at,updated_at) VALUES ('01GP1254CHWJC1MNGVB0WDG1T0','jimm',NOW(),NOW());" - depends_on: - migrateopenfga: - condition: service_completed_successfully - openfga: - # We use our 'image' to mimic juju standard. - # image: openfga/openfga:latest build: - context: . - dockerfile: ./local/openfga/Dockerfile + context: ./local/openfga/ + dockerfile: Dockerfile container_name: openfga environment: OPENFGA_AUTHN_METHOD: "preshared" OPENFGA_AUTHN_PRESHARED_KEYS: "jimm" OPENFGA_DATASTORE_ENGINE: "postgres" OPENFGA_DATASTORE_URI: "postgresql://jimm:jimm@db/jimm?sslmode=disable" - command: run + volumes: + - ./openfga/authorisation_model.json:/app/authorisation_model.json ports: - 8080:8080 - 3000:3000 depends_on: - migrateopenfga: - condition: service_completed_successfully - insert-hardcoded-store: - condition: service_completed_successfully - healthcheck: - test: [ "CMD", "curl", "http://0.0.0.0:8080/healthz" ] - interval: 5s - timeout: 5s - retries: 10 - - # Adds the auth model and updates its authorisation model id to be the expected hard-coded id such that our local JIMM can utilise it for queries. - # The auth model json is retrieved from file via volume mount. - insert-hardcoded-auth-model: - profiles: ["dev", "test"] - image: governmentpaas/psql - container_name: insert-hardcoded-auth-model - volumes: - - ./local/openfga/authorisation_model.json:/authorisation_model.json - command: - - /bin/sh - - -c - - | - wget -q -O - --header 'Content-Type: application/json' --header 'Authorization: Bearer jimm' --post-file authorisation_model.json openfga:8080/stores/01GP1254CHWJC1MNGVB0WDG1T0/authorization-models - psql -Atx postgresql://jimm:jimm@db/jimm?sslmode=disable -c "UPDATE authorization_model SET authorization_model_id = '01GP1EC038KHGB6JJ2XXXXCXKB' WHERE store = '01GP1254CHWJC1MNGVB0WDG1T0';" - depends_on: - openfga: + db: condition: service_healthy keycloak: diff --git a/local/openfga/Dockerfile b/local/openfga/Dockerfile index e2c9b06a7..7bc5c9b72 100644 --- a/local/openfga/Dockerfile +++ b/local/openfga/Dockerfile @@ -1,9 +1,25 @@ # syntax=docker/dockerfile:1.3.1 FROM ubuntu:20.04 AS build -RUN apt-get -qq update && apt-get -qq install -y ca-certificates curl + +# Install some tools necessary for health checks and setup. +RUN apt-get -qq update && apt-get -qq install -y ca-certificates curl wget postgresql-client + EXPOSE 8081 EXPOSE 8080 + WORKDIR /app + +# Copy OpenFGA binaries from upstream image COPY --from=openfga/openfga:v1.2.0 /openfga /app/openfga COPY --from=openfga/openfga:v1.2.0 /assets /app/assets -ENTRYPOINT ["/app/openfga"] + +COPY entrypoint.sh /app/entrypoint.sh + +ENTRYPOINT [ "/app/entrypoint.sh" ] + +HEALTHCHECK \ + --start-period=5s \ + --interval=1s \ + --timeout=5s \ + --retries=10 \ + CMD [ "curl", "http://0.0.0.0:8080/healthz" ] diff --git a/local/openfga/authorisation_model.json b/local/openfga/authorisation_model.json deleted file mode 120000 index 97998ba1b..000000000 --- a/local/openfga/authorisation_model.json +++ /dev/null @@ -1 +0,0 @@ -../../openfga/authorisation_model.json \ No newline at end of file diff --git a/local/openfga/entrypoint.sh b/local/openfga/entrypoint.sh new file mode 100755 index 000000000..b27400c80 --- /dev/null +++ b/local/openfga/entrypoint.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +# This script starts the OpenFGA server, migrates the associated database and applies JIMM's auth model. +# It also manually edits the authorization_model_id to a hardcoded value for easier testing. +# Note that this script expects an authorisation_model.json file to be present. We provide that file +# by mounting the file from the host rather than putting it into the Docker container to avoid duplication. + +set -e + +# Migrate the database +./openfga migrate --datastore-engine postgres --datastore-uri "$OPENFGA_DATASTORE_URI" + +./openfga run & +sleep 3 + +# Cleanup old auth model from previous starts +psql -Atx "$OPENFGA_DATASTORE_URI" -c "DELETE FROM authorization_model;" +# Adds the auth model and updates its authorisation model id to be the expected hard-coded id such that our local JIMM can utilise it for queries. +wget -q -O - --header 'Content-Type: application/json' --header 'Authorization: Bearer jimm' --post-file authorisation_model.json localhost:8080/stores/01GP1254CHWJC1MNGVB0WDG1T0/authorization-models +psql -Atx "$OPENFGA_DATASTORE_URI" -c "INSERT INTO store (id,name,created_at,updated_at) VALUES ('01GP1254CHWJC1MNGVB0WDG1T0','jimm',NOW(),NOW()) ON CONFLICT DO NOTHING;" +psql -Atx "$OPENFGA_DATASTORE_URI" -c "UPDATE authorization_model SET authorization_model_id = '01GP1EC038KHGB6JJ2XXXXCXKB' WHERE store = '01GP1254CHWJC1MNGVB0WDG1T0';" + +# Handle exit signals +trap 'kill %1' TERM ; wait diff --git a/local/vault/entrypoint.sh b/local/vault/entrypoint.sh index 5de2a6bfe..a6534793e 100755 --- a/local/vault/entrypoint.sh +++ b/local/vault/entrypoint.sh @@ -44,5 +44,5 @@ vault secrets enable -version=2 -path /jimm-kv kv # This container is now healthy touch /tmp/healthy -# Keep container alive -tail -f /dev/null & trap 'kill %1' TERM ; wait +# Handle exit signals +trap 'kill %1' TERM ; wait From 0364bebcdf946d7a8a831d6885c65079693100d1 Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Wed, 28 Aug 2024 14:38:48 +0200 Subject: [PATCH 22/30] Create model manager interface (#1328) * create model manager interface * add godocs --- internal/jimmtest/jimm_mock.go | 144 ++----------------------- internal/jimmtest/mocks/model.go | 167 +++++++++++++++++++++++++++++ internal/jujuapi/controllerroot.go | 21 +--- internal/jujuapi/modelmanager.go | 26 +++++ 4 files changed, 204 insertions(+), 154 deletions(-) create mode 100644 internal/jimmtest/mocks/model.go 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. From d809f1354a06ba5ff597005aab7550bf9a439e31 Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Wed, 28 Aug 2024 15:55:15 +0200 Subject: [PATCH 23/30] tweaks to improve integration testing (#1329) --- .github/actions/test-server/action.yaml | 11 +++++++++-- compose-common.yaml | 2 +- internal/jimm/cache.go | 6 +++--- internal/jujuclient/applicationoffers.go | 6 ++++-- internal/rpc/proxy.go | 4 +++- local/jimm/setup-service-account.sh | 13 +++++++++++++ 6 files changed, 33 insertions(+), 9 deletions(-) create mode 100755 local/jimm/setup-service-account.sh 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/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" From 98c95bfcaf5329d374c2cc6714df36602b92bd83 Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Wed, 28 Aug 2024 16:36:30 +0200 Subject: [PATCH 24/30] Eliminate the need for TLS certs in tests (#1330) * eliminate the need for TLS certs in tests * remove InsecureSkipVerify config --- .github/workflows/ci.yaml | 14 --- Makefile | 2 +- cmd/jaas/cmd/export_test.go | 30 ++---- cmd/jimmctl/cmd/export_test.go | 168 ++++++++++----------------------- internal/cmdtest/cmdsetup.go | 37 ++++++++ internal/cmdtest/jimmsuite.go | 56 +---------- 6 files changed, 100 insertions(+), 207 deletions(-) create mode 100644 internal/cmdtest/cmdsetup.go diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 578a71618..0dc2ea76d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,9 +12,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - with: - fetch-tags: true - fetch-depth: 0 - name: Setup Go uses: actions/setup-go@v4 @@ -27,9 +24,6 @@ jobs: - name: Install juju-db run: sudo snap install juju-db --channel 4.4/stable - - name: Create test certs - run: make certs - - name: Start test environment run: docker compose up -d --wait @@ -40,11 +34,3 @@ jobs: - name: Build and Test run: go test -mod readonly ./... -timeout 1h -cover - env: - JIMM_DSN: postgresql://jimm:jimm@localhost:5432/jimm - JIMM_TEST_PGXDSN: postgresql://jimm:jimm@localhost:5432/jimm - PGHOST: localhost - PGPASSWORD: jimm - PGSSLMODE: disable - PGUSER: jimm - PGPORT: 5432 diff --git a/Makefile b/Makefile index cb95caf0e..9682e87b1 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,7 @@ clean: certs: @cd local/traefik/certs; ./certs.sh; cd - -test-env: sys-deps certs +test-env: sys-deps @docker compose up --force-recreate -d --wait test-env-cleanup: diff --git a/cmd/jaas/cmd/export_test.go b/cmd/jaas/cmd/export_test.go index 955d46936..76bee863c 100644 --- a/cmd/jaas/cmd/export_test.go +++ b/cmd/jaas/cmd/export_test.go @@ -7,15 +7,14 @@ import ( jujuapi "github.com/juju/juju/api" "github.com/juju/juju/cmd/modelcmd" "github.com/juju/juju/jujuclient" + + "github.com/canonical/jimm/v3/internal/cmdtest" ) func NewAddServiceAccountCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &addServiceAccountCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: cmdtest.TestDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -23,11 +22,8 @@ func NewAddServiceAccountCommandForTesting(store jujuclient.ClientStore, lp juju func NewListServiceAccountCredentialsCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &listServiceAccountCredentialsCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: cmdtest.TestDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -35,11 +31,8 @@ func NewListServiceAccountCredentialsCommandForTesting(store jujuclient.ClientSt func NewUpdateCredentialsCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &updateCredentialCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: cmdtest.TestDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -47,11 +40,8 @@ func NewUpdateCredentialsCommandForTesting(store jujuclient.ClientStore, lp juju func NewGrantCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &grantCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: cmdtest.TestDialOpts(lp), } return modelcmd.WrapBase(cmd) diff --git a/cmd/jimmctl/cmd/export_test.go b/cmd/jimmctl/cmd/export_test.go index eb2569252..3f06e1802 100644 --- a/cmd/jimmctl/cmd/export_test.go +++ b/cmd/jimmctl/cmd/export_test.go @@ -8,6 +8,8 @@ import ( "github.com/juju/juju/cloud" "github.com/juju/juju/cmd/modelcmd" "github.com/juju/juju/jujuclient" + + "github.com/canonical/jimm/v3/internal/cmdtest" ) var ( @@ -22,11 +24,8 @@ type AccessResult = accessResult func NewListControllersCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &listControllersCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: cmdtest.TestDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -34,11 +33,8 @@ func NewListControllersCommandForTesting(store jujuclient.ClientStore, lp jujuap func NewModelStatusCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &modelStatusCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: cmdtest.TestDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -46,11 +42,8 @@ func NewModelStatusCommandForTesting(store jujuclient.ClientStore, lp jujuapi.Lo func NewGrantAuditLogAccessCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &grantAuditLogAccessCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: cmdtest.TestDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -58,11 +51,8 @@ func NewGrantAuditLogAccessCommandForTesting(store jujuclient.ClientStore, lp ju func NewRevokeAuditLogAccessCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &revokeAuditLogAccessCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: cmdtest.TestDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -70,11 +60,8 @@ func NewRevokeAuditLogAccessCommandForTesting(store jujuclient.ClientStore, lp j func NewListAuditEventsCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &listAuditEventsCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: cmdtest.TestDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -84,10 +71,7 @@ func NewAddCloudToControllerCommandForTesting(store jujuclient.ClientStore, lp j cmd := &addCloudToControllerCommand{ store: store, cloudByNameFunc: cloudByNameFunc, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + dialOpts: cmdtest.TestDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -97,11 +81,8 @@ type RemoveCloudFromControllerAPI = removeCloudFromControllerAPI func NewRemoveCloudFromControllerCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider, removeCloudFromControllerAPIFunc func() (RemoveCloudFromControllerAPI, error)) cmd.Command { cmd := &removeCloudFromControllerCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: cmdtest.TestDialOpts(lp), removeCloudFromControllerAPIFunc: removeCloudFromControllerAPIFunc, } if removeCloudFromControllerAPIFunc == nil { @@ -113,11 +94,8 @@ func NewRemoveCloudFromControllerCommandForTesting(store jujuclient.ClientStore, func NewAddControllerCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &addControllerCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: cmdtest.TestDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -125,11 +103,8 @@ func NewAddControllerCommandForTesting(store jujuclient.ClientStore, lp jujuapi. func NewRemoveControllerCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &removeControllerCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: cmdtest.TestDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -145,11 +120,8 @@ func NewControllerInfoCommandForTesting(store jujuclient.ClientStore) cmd.Comman func NewSetControllerDeprecatedCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &setControllerDeprecatedCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: cmdtest.TestDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -157,11 +129,8 @@ func NewSetControllerDeprecatedCommandForTesting(store jujuclient.ClientStore, l func NewImportModelCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &importModelCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: cmdtest.TestDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -169,11 +138,8 @@ func NewImportModelCommandForTesting(store jujuclient.ClientStore, lp jujuapi.Lo func NewUpdateMigratedModelCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &updateMigratedModelCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: cmdtest.TestDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -181,11 +147,8 @@ func NewUpdateMigratedModelCommandForTesting(store jujuclient.ClientStore, lp ju func NewImportCloudCredentialsCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &importCloudCredentialsCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: cmdtest.TestDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -193,11 +156,8 @@ func NewImportCloudCredentialsCommandForTesting(store jujuclient.ClientStore, lp func NewAddGroupCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &addGroupCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: cmdtest.TestDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -205,11 +165,8 @@ func NewAddGroupCommandForTesting(store jujuclient.ClientStore, lp jujuapi.Login func NewRenameGroupCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &renameGroupCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: cmdtest.TestDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -217,11 +174,8 @@ func NewRenameGroupCommandForTesting(store jujuclient.ClientStore, lp jujuapi.Lo func NewRemoveGroupCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &removeGroupCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: cmdtest.TestDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -229,11 +183,8 @@ func NewRemoveGroupCommandForTesting(store jujuclient.ClientStore, lp jujuapi.Lo func NewListGroupsCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &listGroupsCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: cmdtest.TestDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -241,11 +192,8 @@ func NewListGroupsCommandForTesting(store jujuclient.ClientStore, lp jujuapi.Log func NewAddRelationCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &addRelationCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: cmdtest.TestDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -253,11 +201,8 @@ func NewAddRelationCommandForTesting(store jujuclient.ClientStore, lp jujuapi.Lo func NewRemoveRelationCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &removeRelationCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: cmdtest.TestDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -265,11 +210,8 @@ func NewRemoveRelationCommandForTesting(store jujuclient.ClientStore, lp jujuapi func NewListRelationsCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &listRelationsCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: cmdtest.TestDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -277,11 +219,8 @@ func NewListRelationsCommandForTesting(store jujuclient.ClientStore, lp jujuapi. func NewCheckRelationCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &checkRelationCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: cmdtest.TestDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -289,11 +228,8 @@ func NewCheckRelationCommandForTesting(store jujuclient.ClientStore, lp jujuapi. func NewCrossModelQueryCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &crossModelQueryCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: cmdtest.TestDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -301,11 +237,8 @@ func NewCrossModelQueryCommandForTesting(store jujuclient.ClientStore, lp jujuap func NewPurgeLogsCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &purgeLogsCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: cmdtest.TestDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -313,11 +246,8 @@ func NewPurgeLogsCommandForTesting(store jujuclient.ClientStore, lp jujuapi.Logi func NewMigrateModelCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &migrateModelCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: cmdtest.TestDialOpts(lp), } return modelcmd.WrapBase(cmd) diff --git a/internal/cmdtest/cmdsetup.go b/internal/cmdtest/cmdsetup.go new file mode 100644 index 000000000..fb3ad2cce --- /dev/null +++ b/internal/cmdtest/cmdsetup.go @@ -0,0 +1,37 @@ +// Copyright 2024 Canonical. +package cmdtest + +import ( + "context" + "crypto/tls" + "strings" + + "github.com/gorilla/websocket" + jujuapi "github.com/juju/juju/api" + "github.com/juju/juju/rpc/jsoncodec" +) + +func TestDialOpts(lp jujuapi.LoginProvider) *jujuapi.DialOpts { + return &jujuapi.DialOpts{ + LoginProvider: lp, + DialWebsocket: getDialWebsocketWithInsecureUrl(), + } +} + +// getDialWebsocketWithInsecureUrl forces the URL used for dialing to use insecure websockets +// so that tests don't need to start an HTTPS server and manage certs. +func getDialWebsocketWithInsecureUrl() func(ctx context.Context, urlStr string, tlsConfig *tls.Config, ipAddr string) (jsoncodec.JSONConn, error) { + // Modified from github.com/juju/juju@v0.0.0-20240304110523-55fb5d03683b/api/apiclient.go gorillaDialWebsocket + + dialWebsocket := func(ctx context.Context, urlStr string, tlsConfig *tls.Config, ipAddr string) (jsoncodec.JSONConn, error) { + urlStr = strings.Replace(urlStr, "wss", "ws", 1) + dialer := &websocket.Dialer{} + c, resp, err := dialer.Dial(urlStr, nil) + defer resp.Body.Close() + if err != nil { + return nil, err + } + return jsoncodec.NewWebsocketConn(c), nil + } + return dialWebsocket +} diff --git a/internal/cmdtest/jimmsuite.go b/internal/cmdtest/jimmsuite.go index b703a7fd8..295125189 100644 --- a/internal/cmdtest/jimmsuite.go +++ b/internal/cmdtest/jimmsuite.go @@ -5,15 +5,11 @@ package cmdtest import ( - "bytes" "context" - "crypto/tls" - "encoding/pem" "net/http" "net/http/httptest" "net/url" "os" - "path/filepath" "strings" "time" @@ -58,8 +54,7 @@ func (s *JimmCmdSuite) SetUpTest(c *gc.C) { s.cancel = cancel s.HTTP = httptest.NewUnstartedServer(nil) - s.HTTP.TLS = setupTLS(c) - u, err := url.Parse("https://" + s.HTTP.Listener.Addr().String()) + u, err := url.Parse("http://" + s.HTTP.Listener.Addr().String()) c.Assert(err, gc.Equals, nil) ofgaClient, cofgaClient, cofgaParams, err := jimmtest.SetupTestOFGAClient(c.TestName()) @@ -95,9 +90,9 @@ func (s *JimmCmdSuite) SetUpTest(c *gc.C) { err = s.Service.StartJWKSRotator(ctx, time.NewTicker(time.Hour).C, time.Now().UTC().AddDate(0, 3, 0)) c.Assert(err, gc.Equals, nil) - s.HTTP.StartTLS() + s.HTTP.Start() - // NOW we can set up the juju conn suites + // Now we can set up the juju conn suites s.ControllerConfigAttrs = map[string]interface{}{ "login-token-refresh-url": u.String() + "/.well-known/jwks.json", } @@ -110,13 +105,6 @@ func (s *JimmCmdSuite) SetUpTest(c *gc.C) { s.AddAdminUser(c, "alice@canonical.com") - w := new(bytes.Buffer) - err = pem.Encode(w, &pem.Block{ - Type: "CERTIFICATE", - Bytes: s.HTTP.TLS.Certificates[0].Certificate[0], - }) - c.Assert(err, gc.Equals, nil) - s.ClientStore = func() *jjclient.MemStore { store := jjclient.NewMemStore() store.CurrentControllerName = "JIMM" @@ -124,7 +112,6 @@ func (s *JimmCmdSuite) SetUpTest(c *gc.C) { ControllerUUID: "914487b5-60e7-42bb-bd63-1adc3fd3a388", APIEndpoints: []string{u.Host}, PublicDNSName: s.HTTP.URL, - CACert: w.String(), } return store } @@ -153,43 +140,6 @@ func (s *JimmCmdSuite) TearDownTest(c *gc.C) { s.JujuConnSuite.TearDownTest(c) } -func getRootJimmPath(c *gc.C) string { - path, err := os.Getwd() - c.Assert(err, gc.IsNil) - dirs := strings.Split(path, "/") - c.Assert(len(dirs), gc.Not(gc.Equals), 1) - dirs = dirs[1:] - jimmIndex := -1 - // Range over dirs from the end to ensure no top-level jimm - // folders interfere with our search. - for i := len(dirs) - 1; i >= 0; i-- { - if dirs[i] == "jimm" { - jimmIndex = i + 1 - break - } - } - c.Assert(jimmIndex, gc.Not(gc.Equals), -1) - return "/" + filepath.Join(dirs[:jimmIndex]...) -} - -func setupTLS(c *gc.C) *tls.Config { - jimmPath := getRootJimmPath(c) - pathToCert := filepath.Join(jimmPath, "local/traefik/certs/server.crt") - localhostCert, err := os.ReadFile(pathToCert) - c.Assert(err, gc.IsNil, gc.Commentf("Unable to find cert at %s. Run make cert in root directory.", pathToCert)) - - pathToKey := filepath.Join(jimmPath, "local/traefik/certs/server.key") - localhostKey, err := os.ReadFile(pathToKey) - c.Assert(err, gc.IsNil, gc.Commentf("Unable to find key at %s. Run make cert in root directory.", pathToKey)) - - cert, err := tls.X509KeyPair(localhostCert, localhostKey) - c.Assert(err, gc.IsNil, gc.Commentf("Failed to generate certificate key pair.")) - - tlsConfig := new(tls.Config) - tlsConfig.Certificates = []tls.Certificate{cert} - return tlsConfig -} - func (s *JimmCmdSuite) AddAdminUser(c *gc.C, email string) { identity, err := dbmodel.NewIdentity(email) c.Assert(err, gc.IsNil) From 6c1d24d1b16b580efc5ee9f5a72c88cb523cdffb Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Wed, 28 Aug 2024 17:03:43 +0200 Subject: [PATCH 25/30] Avoid duplicate everyone check (#1327) * avoid duplicate everyone checks * make method not exported --- internal/jimm/applicationoffer.go | 11 +-------- internal/jimm/cloud.go | 37 ------------------------------- internal/jimm/cloud_test.go | 20 +++-------------- internal/jimm/controller.go | 12 +--------- internal/jimm/export_test.go | 4 ++++ internal/jimm/utils.go | 8 +++++++ 6 files changed, 17 insertions(+), 75 deletions(-) diff --git a/internal/jimm/applicationoffer.go b/internal/jimm/applicationoffer.go index 4b8ce6e8e..004f7aaf2 100644 --- a/internal/jimm/applicationoffer.go +++ b/internal/jimm/applicationoffer.go @@ -166,16 +166,7 @@ func (j *JIMM) Offer(ctx context.Context, user *openfga.User, offer AddApplicati zap.String("application-offer", doc.UUID)) } - everyoneIdentity, err := dbmodel.NewIdentity(ofganames.EveryoneUser) - if err != nil { - return errors.E(op, err) - } - - everyone := openfga.NewUser( - everyoneIdentity, - j.OpenFGAClient, - ) - if err := everyone.SetApplicationOfferAccess(ctx, doc.ResourceTag(), ofganames.ReaderRelation); err != nil { + if err := j.everyoneUser().SetApplicationOfferAccess(ctx, doc.ResourceTag(), ofganames.ReaderRelation); err != nil { zapctx.Error( ctx, "failed relation between user and application offer", diff --git a/internal/jimm/cloud.go b/internal/jimm/cloud.go index ba0259747..f94e0d842 100644 --- a/internal/jimm/cloud.go +++ b/internal/jimm/cloud.go @@ -23,18 +23,6 @@ import ( // GetUserCloudAccess returns users access level for the specified cloud. func (j *JIMM) GetUserCloudAccess(ctx context.Context, user *openfga.User, cloud names.CloudTag) (string, error) { accessLevel := user.GetCloudAccess(ctx, cloud) - if accessLevel == ofganames.NoRelation { - everyoneTag := names.NewUserTag(ofganames.EveryoneUser) - everyoneIdentity, err := dbmodel.NewIdentity(everyoneTag.Id()) - if err != nil { - return "", err - } - everyone := openfga.NewUser( - everyoneIdentity, - j.OpenFGAClient, - ) - accessLevel = everyone.GetCloudAccess(ctx, cloud) - } return ToCloudAccessString(accessLevel), nil } @@ -84,7 +72,6 @@ func (j *JIMM) ForEachUserCloud(ctx context.Context, user *openfga.User, f func( if err != nil { return errors.E(op, err, "cannot load clouds") } - seen := make(map[string]bool, len(clouds)) for _, cloud := range clouds { userAccess := ToCloudAccessString(user.GetCloudAccess(ctx, cloud.ResourceTag())) if userAccess == "" { @@ -95,30 +82,6 @@ func (j *JIMM) ForEachUserCloud(ctx context.Context, user *openfga.User, f func( if err := f(&cloud); err != nil { return err } - seen[cloud.Name] = true - } - - // Also include "public" clouds - everyoneDB, err := dbmodel.NewIdentity(ofganames.EveryoneUser) - if err != nil { - return errors.E(op, err) - } - - everyone := openfga.NewUser(everyoneDB, j.OpenFGAClient) - - for _, cloud := range clouds { - if seen[cloud.Name] { - continue - } - userAccess := ToCloudAccessString(everyone.GetCloudAccess(ctx, cloud.ResourceTag())) - if userAccess == "" { - // if user does not have access to the cloud, - // we skip this cloud - continue - } - if err := f(&cloud); err != nil { - return err - } } return nil diff --git a/internal/jimm/cloud_test.go b/internal/jimm/cloud_test.go index 36e1119c6..7b21b7086 100644 --- a/internal/jimm/cloud_test.go +++ b/internal/jimm/cloud_test.go @@ -75,13 +75,6 @@ func TestGetCloud(t *testing.T) { ) c.Assert(err, qt.IsNil) - everyoneIdentity, err := dbmodel.NewIdentity(ofganames.EveryoneUser) - c.Assert(err, qt.IsNil) - everyone := openfga.NewUser( - everyoneIdentity, - client, - ) - cloud := &dbmodel.Cloud{ Name: "test-cloud-1", } @@ -106,7 +99,7 @@ func TestGetCloud(t *testing.T) { err = client.AddCloudController(context.Background(), cloud2.ResourceTag(), j.ResourceTag()) c.Assert(err, qt.IsNil) - err = everyone.SetCloudAccess(context.Background(), cloud2.ResourceTag(), ofganames.CanAddModelRelation) + err = j.EveryoneUser().SetCloudAccess(context.Background(), cloud2.ResourceTag(), ofganames.CanAddModelRelation) c.Assert(err, qt.IsNil) _, err = j.GetCloud(ctx, alice, names.NewCloudTag("test-cloud-0")) @@ -204,13 +197,6 @@ func TestForEachCloud(t *testing.T) { ) daphne.JimmAdmin = true - everyoneIdentity, err := dbmodel.NewIdentity(ofganames.EveryoneUser) - c.Assert(err, qt.IsNil) - everyone := openfga.NewUser( - everyoneIdentity, - client, - ) - cloud := &dbmodel.Cloud{ Name: "test-cloud-1", } @@ -230,7 +216,7 @@ func TestForEachCloud(t *testing.T) { err = bob.SetCloudAccess(ctx, cloud2.ResourceTag(), ofganames.CanAddModelRelation) c.Assert(err, qt.IsNil) - err = everyone.SetCloudAccess(ctx, cloud2.ResourceTag(), ofganames.CanAddModelRelation) + err = j.EveryoneUser().SetCloudAccess(ctx, cloud2.ResourceTag(), ofganames.CanAddModelRelation) c.Assert(err, qt.IsNil) cloud3 := &dbmodel.Cloud{ @@ -239,7 +225,7 @@ func TestForEachCloud(t *testing.T) { err = j.Database.AddCloud(ctx, cloud3) c.Assert(err, qt.IsNil) - err = everyone.SetCloudAccess(ctx, cloud3.ResourceTag(), ofganames.CanAddModelRelation) + err = j.EveryoneUser().SetCloudAccess(ctx, cloud3.ResourceTag(), ofganames.CanAddModelRelation) c.Assert(err, qt.IsNil) var clds []dbmodel.Cloud diff --git a/internal/jimm/controller.go b/internal/jimm/controller.go index de57638e2..32c44c7ac 100644 --- a/internal/jimm/controller.go +++ b/internal/jimm/controller.go @@ -278,17 +278,7 @@ func (j *JIMM) AddController(ctx context.Context, user *openfga.User, ctl *dbmod // If this cloud is the one used by the controller model then // it is available to all users. Other clouds require `juju grant-cloud` to add permissions. if cloud.ResourceTag().String() == modelSummary.CloudTag { - everyoneTag := names.NewUserTag(ofganames.EveryoneUser) - everyoneIdentity, err := dbmodel.NewIdentity(everyoneTag.Id()) - if err != nil { - zapctx.Error(ctx, "failed to create identity model", zap.Error(err)) - return errors.E(op, err) - } - everyone := openfga.NewUser( - everyoneIdentity, - j.OpenFGAClient, - ) - if err := everyone.SetCloudAccess(ctx, cloud.ResourceTag(), ofganames.CanAddModelRelation); err != nil { + if err := j.everyoneUser().SetCloudAccess(ctx, cloud.ResourceTag(), ofganames.CanAddModelRelation); err != nil { zapctx.Error(ctx, "failed to grant everyone add-model access", zap.Error(err)) } } diff --git a/internal/jimm/export_test.go b/internal/jimm/export_test.go index 671e8e35c..9fd272ff7 100644 --- a/internal/jimm/export_test.go +++ b/internal/jimm/export_test.go @@ -56,3 +56,7 @@ func (j *JIMM) GetUser(ctx context.Context, identifier string) (*openfga.User, e func (j *JIMM) UpdateUserLastLogin(ctx context.Context, identifier string) error { return j.updateUserLastLogin(ctx, identifier) } + +func (j *JIMM) EveryoneUser() *openfga.User { + return j.everyoneUser() +} diff --git a/internal/jimm/utils.go b/internal/jimm/utils.go index ce012bc69..c97cdf225 100644 --- a/internal/jimm/utils.go +++ b/internal/jimm/utils.go @@ -11,12 +11,20 @@ import ( "github.com/canonical/jimm/v3/internal/dbmodel" "github.com/canonical/jimm/v3/internal/errors" "github.com/canonical/jimm/v3/internal/openfga" + ofganames "github.com/canonical/jimm/v3/internal/openfga/names" ) /** * Authorisation utilities **/ +// everyoneUser is a convenience method to retrieve the "everyone" user +// whose permissions will translate into granting all users with access. +func (j *JIMM) everyoneUser() *openfga.User { + everyoneIdentity := &dbmodel.Identity{Name: ofganames.EveryoneUser} + return openfga.NewUser(everyoneIdentity, j.OpenFGAClient) +} + // checkJimmAdmin checks if the user is a JIMM admin. func (j *JIMM) checkJimmAdmin(user *openfga.User) error { if !user.JimmAdmin { From b5fcf087a52a1b0a81a72039e90bd2a1c900630f Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Fri, 30 Aug 2024 08:54:05 +0200 Subject: [PATCH 26/30] Add JIMM's CA cert as an output variable (#1334) --- .github/actions/test-server/action.yaml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/actions/test-server/action.yaml b/.github/actions/test-server/action.yaml index a1d2133f1..2d2933b36 100644 --- a/.github/actions/test-server/action.yaml +++ b/.github/actions/test-server/action.yaml @@ -16,7 +16,7 @@ inputs: The PAT token can be left empty when building the development version of JIMM. required: true -output: +outputs: url: description: 'URL where JIMM can be reached.' value: "https://jimm.localhost" @@ -26,6 +26,9 @@ output: client-secret: description: 'Test client Secret to login to JIMM with a service account.' value: "2M2blFbO4GX4zfggQpivQSxwWX1XGgNf" + ca-cert: + description: 'The CA certificate used to genereate the JIMM server cert.' + value: ${{ steps.fetch-cert.outputs.jimm-ca }} runs: using: "composite" @@ -50,6 +53,14 @@ runs: run: make dev-env shell: bash + - name: Retrieve server CA cert. + id: fetch-cert + run: | + echo 'jimm-ca<> $GITHUB_OUTPUT + cat ./local/traefik/certs/ca.crt >> $GITHUB_OUTPUT + echo 'EOF' >> $GITHUB_OUTPUT + shell: bash + - name: Initialise LXD run: | sudo lxd waitready && \ From f8a7174919bfe974ebd53727a3c64f632a62a016 Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Fri, 30 Aug 2024 10:44:26 +0200 Subject: [PATCH 27/30] Return cloud-credentials with empty attribute (#1333) * return cloud-credentials with empty attribute * set empty map if attributes not found * change application logic to not return error on empty attributes * add app layer test * return empty map rather than nil * fix test --- internal/jimm/cloudcredential.go | 6 +++--- internal/jimm/cloudcredential_test.go | 23 +++++++++++++++++++++-- internal/jujuapi/cloud_test.go | 27 +++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/internal/jimm/cloudcredential.go b/internal/jimm/cloudcredential.go index 9e710a982..5691a2c06 100644 --- a/internal/jimm/cloudcredential.go +++ b/internal/jimm/cloudcredential.go @@ -342,6 +342,9 @@ func (j *JIMM) GetCloudCredentialAttributes(ctx context.Context, user *openfga.U err = errors.E(op, err) return } + if len(attrs) == 0 { + return map[string]string{}, nil, nil + } if hidden { return @@ -377,8 +380,5 @@ func (j *JIMM) getCloudCredentialAttributes(ctx context.Context, cred *dbmodel.C if err != nil { return nil, errors.E(op, err) } - if len(attr) == 0 && cred.AuthType != "empty" { - return nil, errors.E(op, errors.CodeNotFound, "cloud-credential attributes not found") - } return attr, nil } diff --git a/internal/jimm/cloudcredential_test.go b/internal/jimm/cloudcredential_test.go index 775b48d23..4e4151e4d 100644 --- a/internal/jimm/cloudcredential_test.go +++ b/internal/jimm/cloudcredential_test.go @@ -5,6 +5,7 @@ package jimm_test import ( "context" "database/sql" + "fmt" "sync" "testing" "time" @@ -1538,6 +1539,10 @@ cloud-credentials: client-id: 1234 private-key: super-secret project-id: 5678 +- name: cred-2 + cloud: test-cloud + owner: bob@canonical.com + auth-type: certificate users: - username: alice@canonical.com controller-access: superuser @@ -1549,6 +1554,7 @@ var getCloudCredentialAttributesTests = []struct { username string hidden bool jimmAdmin bool + cred string expectAttributes map[string]string expectRedacted []string expectError string @@ -1557,16 +1563,25 @@ var getCloudCredentialAttributesTests = []struct { name: "OwnerNoHidden", username: "bob@canonical.com", jimmAdmin: true, + cred: "cred-1", expectAttributes: map[string]string{ "client-email": "bob@example.com", "client-id": "1234", "project-id": "5678", }, expectRedacted: []string{"private-key"}, +}, { + name: "OwnerNoAttributes", + username: "bob@canonical.com", + jimmAdmin: true, + cred: "cred-2", + expectAttributes: map[string]string{}, + expectRedacted: nil, }, { name: "OwnerWithHidden", username: "bob@canonical.com", hidden: true, + cred: "cred-1", expectAttributes: map[string]string{ "client-email": "bob@example.com", "client-id": "1234", @@ -1577,6 +1592,7 @@ var getCloudCredentialAttributesTests = []struct { name: "SuperUserNoHidden", username: "alice@canonical.com", jimmAdmin: true, + cred: "cred-1", expectAttributes: map[string]string{ "client-email": "bob@example.com", "client-id": "1234", @@ -1588,11 +1604,13 @@ var getCloudCredentialAttributesTests = []struct { username: "alice@canonical.com", hidden: true, jimmAdmin: true, + cred: "cred-1", expectError: `unauthorized`, expectErrorCode: errors.CodeUnauthorized, }, { name: "OtherUserUnauthorized", username: "charlie@canonical.com", + cred: "cred-1", expectError: `unauthorized`, expectErrorCode: errors.CodeUnauthorized, }} @@ -1623,7 +1641,8 @@ func TestGetCloudCredentialAttributes(t *testing.T) { env.PopulateDBAndPermissions(c, j.ResourceTag(), j.Database, client) u := env.User("bob@canonical.com").DBObject(c, j.Database) userBob := openfga.NewUser(&u, client) - cred, err := j.GetCloudCredential(ctx, userBob, names.NewCloudCredentialTag("test-cloud/bob@canonical.com/cred-1")) + credTag := fmt.Sprintf("test-cloud/bob@canonical.com/%s", test.cred) + cred, err := j.GetCloudCredential(ctx, userBob, names.NewCloudCredentialTag(credTag)) c.Assert(err, qt.IsNil) u = env.User(test.username).DBObject(c, j.Database) @@ -1714,7 +1733,7 @@ func TestCloudCredentialAttributeStore(t *testing.T) { // Update to an "empty" credential args.Credential.AuthType = "empty" - args.Credential.Attributes = nil + args.Credential.Attributes = map[string]string{} _, err = j.UpdateCloudCredential(ctx, user, args) c.Assert(err, qt.IsNil) diff --git a/internal/jujuapi/cloud_test.go b/internal/jujuapi/cloud_test.go index 58d4144e3..e2fa8a3b8 100644 --- a/internal/jujuapi/cloud_test.go +++ b/internal/jujuapi/cloud_test.go @@ -729,6 +729,33 @@ func (s *cloudSuite) TestCredentialContents(c *gc.C) { }}) } +func (s *cloudSuite) TestCredentialContentsWithEmptyAttributes(c *gc.C) { + conn := s.open(c, nil, "test") + defer conn.Close() + client := cloudapi.NewClient(conn) + credentialTag := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/test@canonical.com/cred3") + err := client.AddCredential( + credentialTag.String(), + cloud.NewCredential( + "certificate", + nil, + ), + ) + c.Assert(err, gc.Equals, nil) + creds, err := client.CredentialContents(jimmtest.TestCloudName, "cred3", false) + c.Assert(err, gc.Equals, nil) + c.Assert(creds, jc.DeepEquals, []jujuparams.CredentialContentResult{{ + Result: &jujuparams.ControllerCredentialInfo{ + Content: jujuparams.CredentialContent{ + Name: "cred3", + Cloud: jimmtest.TestCloudName, + AuthType: "certificate", + Attributes: nil, + }, + }, + }}) +} + func (s *cloudSuite) TestRemoveCloud(c *gc.C) { conn := s.open(c, nil, "test") defer conn.Close() From cbcf694e64a9a350df766d30497f5f1b66f616d8 Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Fri, 30 Aug 2024 12:43:00 +0200 Subject: [PATCH 28/30] CSS-10401 cleanup repo (#1331) * improve readme and supporting docs * PR tweaks * Add a line explaining building/publishing section * put jimm history in a separate doc * add project links --- .vscode/extensions.json | 6 ++ CONTRIBUTING.md | 104 +++++++++++++++++++++++ Makefile | 1 - README.md | 140 +++++++++++-------------------- doc/versioning.md | 21 +++++ internal/jujuapi/package_test.go | 1 + local/README.md | 30 +++---- 7 files changed, 193 insertions(+), 110 deletions(-) create mode 100644 .vscode/extensions.json create mode 100644 CONTRIBUTING.md create mode 100644 doc/versioning.md diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..78b2385ac --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "golang.go", + "babakks.vscode-go-test-suite" + ] +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..7c6a6c333 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,104 @@ +## Filing Bugs +File bugs at https://github.com/canonical/jimm/issues. + +## Testing +Many tests in JIMM require real services to be reachable i.e. Postgres, Vault, OpenFGA +and an IdP (Identity Provider). + +JIMM's docker compose file provides a convenient way of starting these services. + +### TLDR +Run: +``` +$ make test-env +$ go test ./... +``` + +### Pre-requisite +To check if your system has all the prequisites installed simply run `make sys-deps`. +This will check for all test prequisites and inform you how to install them if not installed. +You will need to install `make` first with `sudo apt install make` + +### Understanding the test suite +In order to enable testing with Juju's internal suites, it is required to have juju-db +(mongod) service installed. +This can be installed via: `sudo snap install juju-db --channel=4.4/stable`. + +Tests inside of `cmd/` and `internal/jujuapi/` are integration based, spinning up JIMM and +a Juju controller for testing. To spin up a Juju controller we use the `JujuConnSuite` which +in turn uses the [gocheck](http://labix.org/gocheck) test library. + +Because of the `JujuConnSuite` and its use in JIMM's test suites, there are 2 test libraries in JIMM: +- GoCheck based tests, identified in the function signature with `func Test(c *gc.C)`. + - These tests normally interact with a Juju controller. + - GoCheck should only be used when using the suites in `internal/jimmtest`. +- Stdlib `testing.T` tests, identified in the function signature with `func Test(t *testing.T)`. + - These tests vary in their scope but do not require a Juju controller. + - To provide assertions, the project uses [quicktest](https://github.com/frankban/quicktest), + a lean testing library. + +Because many tests rely on PostgreSQL, OpenFGA and Vault which are dockerised +you may simply run `make test-env` to be integration test ready. + +The above command won't start a dockerised instance of JIMM as tests are normally run locally. +Instead, to start a dockerised JIMM that will auto-reload on code changes, follow the instructions +in `local/README.md`. + +### Manual commands +If using VSCode, we recommend installing the +[go-test-suite](https://marketplace.visualstudio.com/items?itemName=babakks.vscode-go-test-suite) +extension to enable running these tests from the GUI as you would with normal Go tests and the Go +VSCode extension. + +Because [gocheck](http://labix.org/gocheck) does not parse the `go test -run` flags, the examples +below show how to run individual tests in a suite: +```bash +$ go test -check.f dialSuite.TestDialWithCredentialsStoredInVault` +$ go test -check.f MyTestSuite +$ go test -check.f "Test.*Works" +$ go test -check.f "MyTestSuite.Test.*Works" +``` + +For more verbose output, add `check.v` and `check.vv`. + +**Note:** The `check.f` command only applies to Go Check tests, any package with both Go Check tests +and normal `testing.T` tests will result in both sets of tests running. To avoid this look for where +Go Check registers its test suite into the Go test runner, normally in a file called `package_test.go` +and only run that test function. +E.g. in `internal/jujuapi` an example command to only run a single suite test would be: +``` +$ go test ./internal/jujuapi -check.f modelManagerSuite.TestListModelSummaries -run TestPackage ./internal/jujuapi +``` + +## Building/Publishing +Below are instructions on building the various binaries that are part of the project as well as +some information on how they are published. + +### jimmsrv +To build the JIMM server run `go build ./cmd/jimmsrv` + +The JIMM server is published as an OCI image using +[Rockcraft](https://documentation.ubuntu.com/rockcraft/en/latest/) +(a tool to create OCI images based on Ubuntu). + +Run `make rock` to pack the rock. The images are published to the Github repo's container registry +for later use by the JIMM-k8s charm. + +The JIMM server is also available as a snap and can be built with `make jimm-snap`. This snap is +not published to the snap store as it is intended to be used as part of a machine charm deployment. + +### jimmctl +To build jimmctl run `go build ./cmd/jimmctl` + +The jimmctl tool is published as a [Snap](https://snapcraft.io/jimmctl). + +Run `make jimmctl-snap` to build the snap. The snaps are published to the Snap Store +from where they can be conveniently installed. + +### jaas plugin +To build the jaas plugin run `go build ./cmd/jaas` + +The jaas plugin is published as a [Snap](https://snapcraft.io/jaas). + +Run `make jaas-snap` to build the snap. The snaps are published to the Snap Store +from where they can be conveniently installed. diff --git a/Makefile b/Makefile index 9682e87b1..4dd4e6646 100644 --- a/Makefile +++ b/Makefile @@ -152,7 +152,6 @@ help: @echo 'make sys-deps - Install the development environment system packages.' @echo 'make format - Format the source files.' @echo 'make simplify - Format and simplify the source files.' - @echo 'make get-local-auth - Get local auth to the API WSS endpoint locally.' @echo 'make rock - Build the JIMM rock.' @echo 'make load-rock - Load the most recently built rock into your local docker daemon.' diff --git a/README.md b/README.md index 789da1a7f..5c13a93eb 100644 --- a/README.md +++ b/README.md @@ -1,111 +1,73 @@ -# Juju Intelligent Model Manager +# JIMM - Juju Intelligent Model Manager -This service provides the ability to manage multiple juju models. It is -considered a work in progress. +[comment]: <> (Update the chat link below with a JIMM specific room) +

+ Chat | + Docs | + Charm +

-## Installation +JIMM is a Go based webserver used to provide extra functionality on top of Juju controllers. +If you are unfamiliar with Juju, we suggest visiting the [Juju docs](https://juju.is/) - +the open source orchestration engine for software operators. -To start using JIMM, first ensure you have a valid Go environment, -then run the following: +JIMM provides the ability to manage multiple Juju controllers from a single location with +enhanced enterprise functionality. - go get github.com/canonical/jimm +JIMM is the central component of JAAS (Juju As A Service), where JAAS is a set of services +acting together to enable storing state, storing secrets and auth. -## Go dependencies +## Features -The project uses Go modules (https://golang.org/cmd/go/#hdr-Module_maintenance) to manage Go -dependencies. **Note: Go 1.11 or greater needed.** +JIMM/JAAS provides enterprise level functionality layered on top of your Juju controller like: +- Federated login via an external identity provider using OAuth2.0 and OIDC. +- Fine grained access control with the ability to create user groups. +- A single gateway into your Juju estate. +- The ability to query for information across all your Juju controllers. -## JIMM versioning - -JIMM v0 and v1 follow a different versioning strategy than future releases. JIMM v0 was the initial release and used MongoDB to store state. -JIMM v1 was an upgrade that switched to using PostgreSQL for storing state but still retained similar functionality to v0. -These versions worked with Juju v2 and v3. - -Since a refresh of the project, there was an addition of delegated authorization in JIMM. This means that users are authenticated and authorized in JIMM before requests are forwarded to Juju. This work encompassed a breaking change and required changes in Juju (requiring a Juju controller of at least version 3.3). To better align JIMM with Juju it was decided to switch our versioning strategy to align with Juju. As a result of this, there is no JIMM v2 and instead from JIMM v3, the versioning strategy we follow is to match JIMM's to the Juju major versions we support. As an example, JIMM v3 can speak to Juju v3 controllers AND the last minor version of the previous major (Juju v2.9) for migration purposes. - -## Development environment +For a full overview of the capabilties, check out +[the docs](https://canonical-jaas-documentation.readthedocs-hosted.com/en/latest/explanation/jaas_overview/). -### Local: -A couple of system packages are required in order to set up a development -environment. To install them, run the following: -`make sysdeps` +## Dependencies -At this point, from the root of this branch, run the command: -`make install` +The project uses [Go modules](https://golang.org/cmd/go/#hdr-Module_maintenance) to manage +Go dependencies. **Note: Go 1.11 or greater needed.** -The command above builds and installs the JIMM binaries, and places -them in `$GOPATH/bin`. This is the list of the installed commands: - -- jemd: start the JIMM server; -- jaas-admin: perform admin commands on JIMM; - -### Docker compose: -See [here](./local/README.md) on how to get started. - -## Testing +A brief explanation of the various services that JIMM depends on is below: +- [Vault](https://www.vaultproject.io/): User cloud-credentials and private keys are stored in Vault. Cloud-credentials are API keys that +enable Juju to communicate with a cloud's API. +- [PostgreSQL](https://www.postgresql.org/): All non-sensitive state is stored in Postgres. +- [OpenFGA](https://openfga.dev/): A distributed authorisation server where authorisation rules are stored and queried +using relation based access control. +- IdP: An identity provider which supports OAuth2.0 and OIDC. -## TLDR -Run: -``` -$ make test-env -$ go test ./... -``` -### Pre-requisite -To check if your system has all the prequisites installed simply run `make sysdeps`. -This will check for all test prequisites and inform you how to install them if not installed. -You will need to install `make` first with `sudo apt install make` - -### Understanding the test suite -As the juju controller internal suites start their our mongod instances, it is required to have juju-db (mongod). -This can be installed via: `sudo snap install juju-db`. -The latest JIMM has an upgraded dependency on Juju which requires in turn requires juju-db from channel `4.4/stable`, - this can be installed with `sudo snap install juju-db --channel=4.4/stable` - -Tests inside of `cmd/` create a JIMM server and test the jimmctl and jaas CLI packages. The Juju CLI requires that it connects to -an HTTPS server, but these tests also start a Juju controller which expects to be able to fetch a JWKS and macaroon publickey -from JIMM (which is running as an HTTPS server). This would normally result in a TLS certificate error, however JIMM will -attempt to use a custom self-signed cert from the certificate generated in `local/traefik/certs`. The make command `make certs` will generate these certs and place the CA in your system's cert pool which will be picked up by the Go HTTP client. - -The rest of the suite relies on PostgreSQL, OpenFGA and Hashicorp Vault which are dockerised -and as such you may simple run `make test-env` to be integration test ready. -The above command won't start a dockerised instance of JIMM as tests are normally run locally. Instead, to start a -dockerised JIMM that will auto-reload on code changes, follow the instructions in `local/README.md`. - -### Manual commands -The tests utilise [go.check](http://labix.org/gocheck) for suites and you may run tests individually like so: -```bash -$ go test -check.f dialSuite.TestDialWithCredentialsStoredInVault` -$ go test -check.f MyTestSuite -$ go test -check.f "Test.*Works" -$ go test -check.f "MyTestSuite.Test.*Works" -``` - -For more verbose output, use `-check.v` and `-check.vv` +## JIMM versioning +The versioning strategy we follow is to match JIMM's major version to the corresponding +Juju major version we support. -### Make -Run `make check` to test the application. -Run `make help` to display help about all the available make targets. +Additionally JIMM will also support Juju's last minor version of the previous major to +support model migrations. -## Local QA +E.g. JIMM v3 supports Juju v3 controllers AND the last minor version +of the previous major, v2.9. -To start a local server for QA purposes do the following: +For more information on JIMM's history and previous version strategy see [here](./doc/versioning.md). - sudo cp tools/jimmqa.crt /usr/local/share/ca-certificates - sudo update-ca-certificates - make server +## Binaries -This will start JIMM server running on localhost:8082 which is configured -to use https://api.staging.jujucharms.com/identity as its identity -provider. +This repository contains 3 binaries: +- jimmsrv: The JIMM server. +- jimmctl: A CLI tool for administrators of JIMM to view audit logs, manage permissions, etc. +Available as a snap. +- jaas: A plugin for the Juju CLI, extend the base set of command with extra functionality when +communicating with a JAAS environment. -To add the new JIMM to your juju environment use the command: +## Development environment - juju login localhost:8082 -c local-jaas +See [here](./local/README.md) on how to get started. -To bootstrap a new controller and add it to the local JIMM use the -following commands: +## Testing - juju bootstrap --config identity-url=https://api.staging.jujucharms.com/identity --config allow-model-access=true / - jaas-admin --jimm-url https://localhost:8082 add-controller / +See [here](./CONTRIBUTING.md) on how to get started. diff --git a/doc/versioning.md b/doc/versioning.md new file mode 100644 index 000000000..1db6973aa --- /dev/null +++ b/doc/versioning.md @@ -0,0 +1,21 @@ +## Version History + +JIMM v0 and v1 follow a different versioning strategy than future releases. JIMM v0 was the initial +release and used MongoDB to store state. JIMM v1 was an upgrade that switched to using PostgreSQL +for storing state but still retained similar functionality to v0. +These versions worked with Juju v2 and v3. + +Subsequently JIMM introduced a large shift in how the service worked: +- JIMM now acts as a proxy between all client and Juju controller interactions. Previously +users were redirected to a Juju controller. +- Juju controllers support JWT login where secure tokens are issued by JIMM. +- JIMM acts as an authorisation gateway creating trusted short-lived JWT tokens to authorize +user actions against Juju controllers. + +The above work encompassed a breaking change and required changes in Juju (requiring a +Juju controller of at least version 3.3). + +Further, to better align the two projects, JIMM's versioning now aligns with Juju. + +As a result of this, there is no JIMM v2 and instead from JIMM v3, the versioning strategy +we follow is to match JIMM's major version to the corresponding Juju major version we support. diff --git a/internal/jujuapi/package_test.go b/internal/jujuapi/package_test.go index a8920ec4d..afbc7e686 100644 --- a/internal/jujuapi/package_test.go +++ b/internal/jujuapi/package_test.go @@ -8,6 +8,7 @@ import ( jujutesting "github.com/juju/juju/testing" ) +// Registers Go Check tests into the Go test runner. func TestPackage(t *testing.T) { jujutesting.MgoTestPackage(t) } diff --git a/local/README.md b/local/README.md index 37f77aad6..d14463a1e 100644 --- a/local/README.md +++ b/local/README.md @@ -13,30 +13,20 @@ used for integration testing within the JIMM test suite. The service is started using Docker Compose, the following services should be started: - JIMM (only started in the dev profile) +- Traefik (only started in the dev profile) - Vault - Postgres - OpenFGA -- Traefik -> Any changes made inside the repo will automatically restart the JIMM server via a volume mount. So there's no need -to re-run the compose continuously, but note, if you do bring the compose down, remove the volumes otherwise -vault will not behave correctly, this can be done via `docker compose down -v` - -Now please checkout the [Authentication Steps](#authentication-steps) to authenticate postman for local testing & Q/A. - -# Q/A Using Postman -#### Setup -1. Run `make get-local-auth` -2. Head to postman and follow the instructions given by get-local-auth. -#### Facades in Postman -You will see JIMM's controller WS API broken up into separate WS requests. -This is intentional. -Inside of each WS request will be a set of `saved messages` (on the right-hand side), these are the calls to facades for the given API under that request. - -The `request name` represents the literal WS endpoint, i.e., `API = /api`. - -> Remember to run the `Login` message when spinning up a new WS connection, otherwise you will not be able to send subsequent calls to this WS. +Some notes on the setup: +- Local images are created in the repo's `/local/` folder where any init scripts are defined for each service using the service's upstream docker image. +- The docker compose has a base at `compose-common.yaml` for common elements to reduce duplication. +- The compose has 2 additional profiles (dev and test). + - Starting the compose with no profile will spin up the necessary components for testing. + - The dev profile will start JIMM in a container using [air](https://github.com/air-verse/air), a tool for auto-reloading Go code when the source changes. + - The test profile will start JIMM by pulling a version of the JIMM image from a container registry, useful in integration tests. +> Any changes made inside the repo will automatically restart the JIMM server via a volume mount + air. So there's no need to re-run the compose continuously. # Q/A Using jimmctl @@ -47,7 +37,7 @@ The `request name` represents the literal WS endpoint, i.e., `API = /api`. 1. The following commands might need to be run to work around an [LXC networking issue](https://github.com/docker/for-linux/issues/103#issuecomment-383607773): `sudo iptables -F FORWARD && sudo iptables -P FORWARD ACCEPT`. -2. Install Juju: `sudo snap install juju --channel=3.5/stable` (minimum Juju version is `3.5`). +2. Install Juju: `sudo snap install juju --channel=3.5/stable` (minimum required Juju version is `3.5`). 3. Install JQ: `sudo snap install jq`. ## All-In-One scripts From 4663c9315bc938a1082af1be46814e2266a21891 Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Fri, 30 Aug 2024 14:48:31 +0200 Subject: [PATCH 29/30] use cache on lint action (#1332) --- .github/workflows/cache.yaml | 8 +++++++- .github/workflows/golangci-lint.yaml | 1 - 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cache.yaml b/.github/workflows/cache.yaml index 3f8ab5ddb..253df3cd7 100644 --- a/.github/workflows/cache.yaml +++ b/.github/workflows/cache.yaml @@ -7,7 +7,7 @@ on: jobs: go_cache: - name: Install And Cache Go Dependencies and Build Artifacts + name: Cache Go Dependencies and Build/Lint Artifacts runs-on: ubuntu-22.04 timeout-minutes: 15 steps: @@ -24,3 +24,9 @@ jobs: - name: Build run: go build ./... + + - name: Run Golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + args: --timeout 30m --verbose + version: v1.60 diff --git a/.github/workflows/golangci-lint.yaml b/.github/workflows/golangci-lint.yaml index fc5a7e625..3053f4ec0 100644 --- a/.github/workflows/golangci-lint.yaml +++ b/.github/workflows/golangci-lint.yaml @@ -18,7 +18,6 @@ jobs: uses: actions/setup-go@v5 with: go-version: stable - cache: false - name: Run Golangci-lint uses: golangci/golangci-lint-action@v6 From 9cc7983836b1f81580ccd729584eff4e4eb2742a Mon Sep 17 00:00:00 2001 From: Kian Parvin <46668016+kian99@users.noreply.github.com> Date: Mon, 2 Sep 2024 15:24:08 +0200 Subject: [PATCH 30/30] Tuple queries with UUID (#1335) * tweak how tags are resolved - allow resolving tags without hitting the db if a UUID is specified * simplify matcher and tests * fix tests - Additionally cleaned up duplicated logic in ToJAASTag() * improve error message --- cmd/jimmctl/cmd/relation_test.go | 6 +- internal/errors/errors.go | 2 +- internal/jimm/access.go | 208 ++++++++++------------ internal/jimm/access_test.go | 223 +++++++++--------------- internal/jujuapi/access_control.go | 7 +- internal/jujuapi/access_control_test.go | 63 ++++--- 6 files changed, 212 insertions(+), 297 deletions(-) diff --git a/cmd/jimmctl/cmd/relation_test.go b/cmd/jimmctl/cmd/relation_test.go index 1c9b7688b..a1e4d32fb 100644 --- a/cmd/jimmctl/cmd/relation_test.go +++ b/cmd/jimmctl/cmd/relation_test.go @@ -360,11 +360,11 @@ func (s *relationSuite) TestListRelations(c *gc.C) { }, { Object: "group-group-1#member", Relation: "administrator", - TargetObject: "model-" + env.controllers[0].Name + ":" + env.models[0].OwnerIdentityName + "/" + env.models[0].Name, + TargetObject: "model-" + env.models[0].OwnerIdentityName + "/" + env.models[0].Name, }, { Object: "user-" + env.users[1].Name, Relation: "administrator", - TargetObject: "applicationoffer-" + env.controllers[0].Name + ":" + env.applicationOffers[0].Model.OwnerIdentityName + "/" + env.applicationOffers[0].Model.Name + "." + env.applicationOffers[0].Name, + TargetObject: "applicationoffer-" + env.applicationOffers[0].URL, }, { Object: "user-" + env.users[0].Name, Relation: "administrator", @@ -573,7 +573,7 @@ func (s *relationSuite) TestCheckRelationViaSuperuser(c *gc.C) { // Test reader is OK userToCheck := "user-" + u.Name - modelToCheck := "model-" + controller.Name + ":" + u.Name + "/" + model.Name + modelToCheck := "model-" + u.Name + "/" + model.Name cmdCtx, err := cmdtesting.RunCommand( c, cmd.NewCheckRelationCommandForTesting(s.ClientStore(), bClient), diff --git a/internal/errors/errors.go b/internal/errors/errors.go index 08fbc7695..26f4d57bc 100644 --- a/internal/errors/errors.go +++ b/internal/errors/errors.go @@ -122,7 +122,7 @@ const ( CodeStillAlive Code = apiparams.CodeStillAlive CodeUnauthorized Code = jujuparams.CodeUnauthorized CodeUpgradeInProgress Code = jujuparams.CodeUpgradeInProgress - CodeFailedToParseTupleKey Code = "failed to parse tuple object key" + CodeFailedToParseTupleKey Code = "failed to parse tuple" CodeFailedToResolveTupleResource Code = "failed resolve resource" CodeOpenFGARequestFailed Code = "failed request to OpenFGA" CodeJWKSRetrievalFailed Code = "jwks retrieval failure" diff --git a/internal/jimm/access.go b/internal/jimm/access.go index 626e4e820..fb2978350 100644 --- a/internal/jimm/access.go +++ b/internal/jimm/access.go @@ -12,10 +12,8 @@ import ( "github.com/canonical/ofga" "github.com/google/uuid" - "github.com/juju/juju/core/crossmodel" jujuparams "github.com/juju/juju/rpc/params" "github.com/juju/names/v5" - "github.com/juju/zaputil" "github.com/juju/zaputil/zapctx" "go.uber.org/zap" @@ -34,28 +32,23 @@ const ( var ( // Matches juju uris, jimm user/group tags and UUIDs - // Performs a single match and breaks the juju URI into 10 groups, each successive group is XORD to ensure we can run - // this just once. - // The groups are as so: + // Performs a single match and breaks the juju URI into 4 groups. + // The groups are: // [0] - Entire match // [1] - tag - // [2] - A single "-", ignored - // [3] - Controller name OR user name OR group name - // [4] - A single ":", ignored - // [5] - Controller user / model owner - // [6] - A single "/", ignored - // [7] - Model name - // [8] - A single ".", ignored - // [9] - Application offer name - // [10] - Relation specifier (i.e., #member) + // [2] - trailer (i.e. resource identifier) + // [3] - Relation specifier (i.e., #member) // A complete matcher example would look like so with square-brackets denoting groups and paranthsis denoting index: - // (1)[controller](2)[-](3)[controller-1](4)[:](5)[alice@canonical.com-place](6)[/](7)[model-1](8)[.](9)[offer-1](10)[#relation-specifier]" - // In the case of something like: user-alice@wonderland or group-alices-wonderland#member, it would look like so: - // (1)[user](2)[-](3)[alices@wonderland] - // (1)[group](2)[-](3)[alices-wonderland](10)[#member] - // So if a group, user, UUID, controller name comes in, it will always be index 3 for them - // and if a relation specifier is present, it will always be index 10 - jujuURIMatcher = regexp.MustCompile(`([a-zA-Z0-9]*)(\-|\z)([a-zA-Z0-9-@.]*)(\:|)([a-zA-Z0-9-@.]*)(\/|)([a-zA-Z0-9-]*)(\.|)([a-zA-Z0-9-]*)([a-zA-Z#]*|\z)\z`) + // (1)[controller][-](2)[myFavoriteController][#](3)[relation-specifier]" + // An example without a relation: `user-alice@wonderland`: + // (1)[user][-](2)[alice@wonderland] + // An example with a relaton `group-alices-wonderland#member`: + // (1)[group][-](2)[alices-wonderland][#](3)[member] + jujuURIMatcher = regexp.MustCompile(`([a-zA-Z0-9]*)(?:-)([^#]+)(?:#([a-zA-Z]+)|\z)`) + + // modelOwnerAndNameMatcher matches a string based on the + // the expected form / + modelOwnerAndNameMatcher = regexp.MustCompile(`(.+)/(.+)`) ) // ToOfferAccessString maps relation to an application offer access string. @@ -400,9 +393,17 @@ func (j *JIMM) ToJAASTag(ctx context.Context, tag *ofganames.Tag, resolveUUIDs b return res, nil } + tagToString := func(kind, id string) string { + res := kind + "-" + id + if tag.Relation.String() != "" { + res += "#" + tag.Relation.String() + } + return res + } + switch tag.Kind { case names.UserTagKind: - return names.UserTagKind + "-" + tag.ID, nil + return tagToString(names.UserTagKind, tag.ID), nil case jimmnames.ServiceAccountTagKind: return jimmnames.ServiceAccountTagKind + "-" + tag.ID, nil case names.ControllerTagKind: @@ -416,11 +417,7 @@ func (j *JIMM) ToJAASTag(ctx context.Context, tag *ofganames.Tag, resolveUUIDs b if err != nil { return "", errors.E(err, fmt.Sprintf("failed to fetch controller information: %s", controller.UUID)) } - controllerString := names.ControllerTagKind + "-" + controller.Name - if tag.Relation.String() != "" { - controllerString = controllerString + "#" + tag.Relation.String() - } - return controllerString, nil + return tagToString(names.ControllerTagKind, controller.Name), nil case names.ModelTagKind: model := dbmodel.Model{ UUID: sql.NullString{ @@ -432,11 +429,8 @@ func (j *JIMM) ToJAASTag(ctx context.Context, tag *ofganames.Tag, resolveUUIDs b if err != nil { return "", errors.E(err, fmt.Sprintf("failed to fetch model information: %s", model.UUID.String)) } - modelString := names.ModelTagKind + "-" + model.Controller.Name + ":" + model.OwnerIdentityName + "/" + model.Name - if tag.Relation.String() != "" { - modelString = modelString + "#" + tag.Relation.String() - } - return modelString, nil + modelUserID := model.OwnerIdentityName + "/" + model.Name + return tagToString(names.ModelTagKind, modelUserID), nil case names.ApplicationOfferTagKind: ao := dbmodel.ApplicationOffer{ UUID: tag.ID, @@ -445,11 +439,7 @@ func (j *JIMM) ToJAASTag(ctx context.Context, tag *ofganames.Tag, resolveUUIDs b if err != nil { return "", errors.E(err, fmt.Sprintf("failed to fetch application offer information: %s", ao.UUID)) } - aoString := names.ApplicationOfferTagKind + "-" + ao.Model.Controller.Name + ":" + ao.Model.OwnerIdentityName + "/" + ao.Model.Name + "." + ao.Name - if tag.Relation.String() != "" { - aoString = aoString + "#" + tag.Relation.String() - } - return aoString, nil + return tagToString(names.ApplicationOfferTagKind, ao.URL), nil case jimmnames.GroupTagKind: group := dbmodel.GroupEntry{ UUID: tag.ID, @@ -458,11 +448,7 @@ func (j *JIMM) ToJAASTag(ctx context.Context, tag *ofganames.Tag, resolveUUIDs b if err != nil { return "", errors.E(err, fmt.Sprintf("failed to fetch group information: %s", group.UUID)) } - groupString := jimmnames.GroupTagKind + "-" + group.Name - if tag.Relation.String() != "" { - groupString = groupString + "#" + tag.Relation.String() - } - return groupString, nil + return tagToString(jimmnames.GroupTagKind, group.Name), nil case names.CloudTagKind: cloud := dbmodel.Cloud{ Name: tag.ID, @@ -471,59 +457,43 @@ func (j *JIMM) ToJAASTag(ctx context.Context, tag *ofganames.Tag, resolveUUIDs b if err != nil { return "", errors.E(err, fmt.Sprintf("failed to fetch cloud information: %s", cloud.Name)) } - cloudString := names.CloudTagKind + "-" + cloud.Name - if tag.Relation.String() != "" { - cloudString = cloudString + "#" + tag.Relation.String() - } - return cloudString, nil + return tagToString(names.CloudTagKind, cloud.Name), nil default: return "", errors.E(fmt.Sprintf("unexpected tag kind: %v", tag.Kind)) } } type tagResolver struct { - resourceUUID string - trailer string - controllerName string - userName string - modelName string - offerName string - relation ofga.Relation + resourceUUID string + trailer string + relation ofga.Relation } func newTagResolver(tag string) (*tagResolver, string, error) { matches := jujuURIMatcher.FindStringSubmatch(tag) + if len(matches) != 4 { + return nil, "", errors.E("tag is not properly formatted", errors.CodeBadRequest) + } tagKind := matches[1] resourceUUID := "" trailer := "" - // We first attempt to see if group3 is a uuid - if _, err := uuid.Parse(matches[3]); err == nil { + // We first attempt to see if group2 is a uuid + if _, err := uuid.Parse(matches[2]); err == nil { // We know it's a UUID - resourceUUID = matches[3] + resourceUUID = matches[2] } else { - // We presume it's a user or a group - trailer = matches[3] - } - - // Matchers along the way to determine segments of the string, they'll be empty - // if the match has failed - controllerName := matches[3] - userName := matches[5] - modelName := matches[7] - offerName := matches[9] - relationString := strings.TrimLeft(matches[10], "#") - relation, err := ofganames.ParseRelation(relationString) + // We presume the information the matcher needs is in the trailer + trailer = matches[2] + } + + relation, err := ofganames.ParseRelation(matches[3]) if err != nil { return nil, "", errors.E("failed to parse relation", errors.CodeBadRequest) } return &tagResolver{ - resourceUUID: resourceUUID, - trailer: trailer, - controllerName: controllerName, - userName: userName, - modelName: modelName, - offerName: offerName, - relation: relation, + resourceUUID: resourceUUID, + trailer: trailer, + relation: relation, }, tagKind, nil } @@ -548,12 +518,10 @@ func (t *tagResolver) groupTag(ctx context.Context, db *db.Database) (*ofga.Enti "Resolving JIMM tags to Juju tags for tag kind: group", zap.String("group-name", t.trailer), ) - var entry dbmodel.GroupEntry if t.resourceUUID != "" { - entry.UUID = t.resourceUUID - } else if t.trailer != "" { - entry.Name = t.trailer + return ofganames.ConvertTagWithRelation(jimmnames.NewGroupTag(t.resourceUUID), t.relation), nil } + entry := dbmodel.GroupEntry{Name: t.trailer} err := db.GetGroup(ctx, &entry) if err != nil { @@ -568,20 +536,14 @@ func (t *tagResolver) controllerTag(ctx context.Context, jimmUUID string, db *db ctx, "Resolving JIMM tags to Juju tags for tag kind: controller", ) - controller := dbmodel.Controller{} if t.resourceUUID != "" { - controller.UUID = t.resourceUUID - } else if t.controllerName != "" { - if t.controllerName == jimmControllerName { - return ofganames.ConvertTagWithRelation(names.NewControllerTag(jimmUUID), t.relation), nil - } - controller.Name = t.controllerName + return ofganames.ConvertTagWithRelation(names.NewControllerTag(t.resourceUUID), t.relation), nil } - - // NOTE (alesstimec) Do we need to special-case the - // controller-jimm case - jimm controller does not exist - // in the database, but has a clearly defined UUID? + if t.trailer == jimmControllerName { + return ofganames.ConvertTagWithRelation(names.NewControllerTag(jimmUUID), t.relation), nil + } + controller := dbmodel.Controller{Name: t.trailer} err := db.GetController(ctx, &controller) if err != nil { @@ -595,27 +557,25 @@ func (t *tagResolver) modelTag(ctx context.Context, db *db.Database) (*ofga.Enti ctx, "Resolving JIMM tags to Juju tags for tag kind: model", ) - model := dbmodel.Model{} if t.resourceUUID != "" { - model.UUID = sql.NullString{String: t.resourceUUID, Valid: true} - } else if t.controllerName != "" && t.userName != "" && t.modelName != "" { - controller := dbmodel.Controller{Name: t.controllerName} - err := db.GetController(ctx, &controller) - if err != nil { - return nil, errors.E("controller not found") - } - model.ControllerID = controller.ID - model.OwnerIdentityName = t.userName - model.Name = t.modelName + return ofganames.ConvertTagWithRelation(names.NewModelTag(t.resourceUUID), t.relation), nil + } + + model := dbmodel.Model{} + matches := modelOwnerAndNameMatcher.FindStringSubmatch(t.trailer) + if len(matches) != 3 { + return nil, errors.E("model name format incorrect, expected /") } + model.OwnerIdentityName = matches[1] + model.Name = matches[2] err := db.GetModel(ctx, &model) if err != nil { return nil, errors.E("model not found") } - return ofganames.ConvertTagWithRelation(names.NewModelTag(model.UUID.String), t.relation), nil + return ofganames.ConvertTagWithRelation(model.ResourceTag(), t.relation), nil } func (t *tagResolver) applicationOfferTag(ctx context.Context, db *db.Database) (*ofga.Entity, error) { @@ -623,23 +583,11 @@ func (t *tagResolver) applicationOfferTag(ctx context.Context, db *db.Database) ctx, "Resolving JIMM tags to Juju tags for tag kind: applicationoffer", ) - offer := dbmodel.ApplicationOffer{} if t.resourceUUID != "" { - offer.UUID = t.resourceUUID - } else if t.controllerName != "" && t.userName != "" && t.modelName != "" && t.offerName != "" { - offerURL, err := crossmodel.ParseOfferURL(fmt.Sprintf("%s:%s/%s.%s", t.controllerName, t.userName, t.modelName, t.offerName)) - if err != nil { - zapctx.Debug( - ctx, - "failed to parse application offer url", - zap.String("url", fmt.Sprintf("%s:%s/%s.%s", t.controllerName, t.userName, t.modelName, t.offerName)), - zaputil.Error(err), - ) - return nil, errors.E("failed to parse offer url", err) - } - offer.URL = offerURL.String() + return ofganames.ConvertTagWithRelation(names.NewApplicationOfferTag(t.resourceUUID), t.relation), nil } + offer := dbmodel.ApplicationOffer{URL: t.trailer} err := db.GetApplicationOffer(ctx, &offer) if err != nil { @@ -648,6 +596,26 @@ func (t *tagResolver) applicationOfferTag(ctx context.Context, db *db.Database) return ofganames.ConvertTagWithRelation(offer.ResourceTag(), t.relation), nil } + +func (t *tagResolver) cloudTag(ctx context.Context, db *db.Database) (*ofga.Entity, error) { + zapctx.Debug( + ctx, + "Resolving JIMM tags to Juju tags for tag kind: cloud", + ) + + if t.resourceUUID != "" { + return ofganames.ConvertTagWithRelation(names.NewCloudTag(t.resourceUUID), t.relation), nil + } + cloud := dbmodel.Cloud{Name: t.trailer} + + err := db.GetCloud(ctx, &cloud) + if err != nil { + return nil, errors.E("application offer not found") + } + + return ofganames.ConvertTagWithRelation(cloud.ResourceTag(), t.relation), nil +} + func (t *tagResolver) serviceAccountTag(ctx context.Context) (*ofga.Entity, error) { zapctx.Debug( ctx, @@ -672,7 +640,7 @@ func resolveTag(jimmUUID string, db *db.Database, tag string) (*ofganames.Tag, e ctx := context.Background() resolver, tagKind, err := newTagResolver(tag) if err != nil { - return nil, errors.E("failed to setup tag resolver", err) + return nil, errors.E(fmt.Errorf("failed to setup tag resolver: %w", err)) } switch tagKind { @@ -686,10 +654,12 @@ func resolveTag(jimmUUID string, db *db.Database, tag string) (*ofganames.Tag, e return resolver.modelTag(ctx, db) case names.ApplicationOfferTagKind: return resolver.applicationOfferTag(ctx, db) + case names.CloudTagKind: + return resolver.cloudTag(ctx, db) case jimmnames.ServiceAccountTagKind: return resolver.serviceAccountTag(ctx) } - return nil, errors.E("failed to map tag " + tagKind) + return nil, errors.E(errors.CodeBadRequest, fmt.Sprintf("failed to map tag, unknown kind: %s", tagKind)) } // ParseTag attempts to parse the provided key into a tag whilst additionally diff --git a/internal/jimm/access_test.go b/internal/jimm/access_test.go index 41d26bdf1..58292071e 100644 --- a/internal/jimm/access_test.go +++ b/internal/jimm/access_test.go @@ -5,6 +5,7 @@ package jimm_test import ( "context" "database/sql" + "fmt" "sort" "testing" "time" @@ -446,9 +447,9 @@ func TestParseTag(t *testing.T) { err = j.Database.Migrate(ctx, false) c.Assert(err, qt.IsNil) - user, _, controller, model, _, _, _ := createTestControllerEnvironment(ctx, c, j.Database) + user, _, _, model, _, _, _ := createTestControllerEnvironment(ctx, c, j.Database) - jimmTag := "model-" + controller.Name + ":" + user.Name + "/" + model.Name + "#administrator" + jimmTag := "model-" + user.Name + "/" + model.Name + "#administrator" // JIMM tag syntax for models tag, err := j.ParseTag(ctx, jimmTag) @@ -467,7 +468,7 @@ func TestParseTag(t *testing.T) { c.Assert(tag.Relation.String(), qt.Equals, "administrator") } -func TestResolveJIMM(t *testing.T) { +func TestResolveTags(t *testing.T) { c := qt.New(t) ctx := context.Background() @@ -482,145 +483,69 @@ func TestResolveJIMM(t *testing.T) { err := j.Database.Migrate(ctx, false) c.Assert(err, qt.IsNil) - jimmTag := "controller-jimm" + identity, group, controller, model, offer, cloud, _ := createTestControllerEnvironment(ctx, c, j.Database) - jujuTag, err := jimm.ResolveTag(j.UUID, &j.Database, jimmTag) - c.Assert(err, qt.IsNil) - c.Assert(jujuTag, qt.DeepEquals, ofganames.ConvertTag(names.NewControllerTag(j.UUID))) -} - -func TestResolveTupleObjectMapsApplicationOffersUUIDs(t *testing.T) { - c := qt.New(t) - ctx := context.Background() - - now := time.Now().UTC().Round(time.Millisecond) - j := &jimm.JIMM{ - UUID: uuid.NewString(), - Database: db.Database{ - DB: jimmtest.PostgresDB(c, func() time.Time { return now }), - }, - } - - err := j.Database.Migrate(ctx, false) - c.Assert(err, qt.IsNil) - - user, _, controller, model, offer, _, _ := createTestControllerEnvironment(ctx, c, j.Database) - - jimmTag := "applicationoffer-" + controller.Name + ":" + user.Name + "/" + model.Name + "." + offer.Name + "#administrator" - - jujuTag, err := jimm.ResolveTag(j.UUID, &j.Database, jimmTag) - c.Assert(err, qt.IsNil) - c.Assert(jujuTag, qt.DeepEquals, ofganames.ConvertTagWithRelation(names.NewApplicationOfferTag(offer.UUID), ofganames.AdministratorRelation)) -} - -func TestResolveTupleObjectMapsModelUUIDs(t *testing.T) { - c := qt.New(t) - ctx := context.Background() - - now := time.Now().UTC().Round(time.Millisecond) - j := &jimm.JIMM{ - UUID: uuid.NewString(), - Database: db.Database{ - DB: jimmtest.PostgresDB(c, func() time.Time { return now }), - }, - } - - err := j.Database.Migrate(ctx, false) - c.Assert(err, qt.IsNil) - - user, _, controller, model, _, _, _ := createTestControllerEnvironment(ctx, c, j.Database) - - jimmTag := "model-" + controller.Name + ":" + user.Name + "/" + model.Name + "#administrator" - - tag, err := jimm.ResolveTag(j.UUID, &j.Database, jimmTag) - c.Assert(err, qt.IsNil) - c.Assert(tag, qt.DeepEquals, ofganames.ConvertTagWithRelation(names.NewModelTag(model.UUID.String), ofganames.AdministratorRelation)) -} - -func TestResolveTupleObjectMapsControllerUUIDs(t *testing.T) { - c := qt.New(t) - ctx := context.Background() - - now := time.Now().UTC().Round(time.Millisecond) - j := &jimm.JIMM{ - UUID: uuid.NewString(), - Database: db.Database{ - DB: jimmtest.PostgresDB(c, func() time.Time { return now }), - }, - } - - err := j.Database.Migrate(ctx, false) - c.Assert(err, qt.IsNil) - - cloud := dbmodel.Cloud{ - Name: "test-cloud", - } - err = j.Database.AddCloud(context.Background(), &cloud) - c.Assert(err, qt.IsNil) - - uuid, _ := uuid.NewRandom() - controller := dbmodel.Controller{ - Name: "mycontroller", - UUID: uuid.String(), - CloudName: "test-cloud", - } - err = j.Database.AddController(ctx, &controller) - c.Assert(err, qt.IsNil) - - tag, err := jimm.ResolveTag(j.UUID, &j.Database, "controller-mycontroller#administrator") - c.Assert(err, qt.IsNil) - c.Assert(tag, qt.DeepEquals, ofganames.ConvertTagWithRelation(names.NewControllerTag(uuid.String()), ofganames.AdministratorRelation)) -} - -func TestResolveTupleObjectMapsGroups(t *testing.T) { - c := qt.New(t) - ctx := context.Background() - - now := time.Now().UTC().Round(time.Millisecond) - j := &jimm.JIMM{ - UUID: uuid.NewString(), - Database: db.Database{ - DB: jimmtest.PostgresDB(c, func() time.Time { return now }), - }, - } - - err := j.Database.Migrate(ctx, false) - c.Assert(err, qt.IsNil) - - _, err = j.Database.AddGroup(ctx, "myhandsomegroupofdigletts") - c.Assert(err, qt.IsNil) - group := &dbmodel.GroupEntry{ - Name: "myhandsomegroupofdigletts", - } - err = j.Database.GetGroup(ctx, group) - c.Assert(err, qt.IsNil) - // Test resolution via name and via UUID. - tag, err := jimm.ResolveTag(j.UUID, &j.Database, "group-"+group.Name+"#member") - c.Assert(err, qt.IsNil) - c.Assert(tag, qt.DeepEquals, ofganames.ConvertTagWithRelation(jimmnames.NewGroupTag(group.UUID), ofganames.MemberRelation)) - tag, err = jimm.ResolveTag(j.UUID, &j.Database, "group-"+group.UUID+"#member") - c.Assert(err, qt.IsNil) - c.Assert(tag, qt.DeepEquals, ofganames.ConvertTagWithRelation(jimmnames.NewGroupTag(group.UUID), ofganames.MemberRelation)) -} - -func TestResolveTagObjectMapsUsers(t *testing.T) { - c := qt.New(t) - ctx := context.Background() + testCases := []struct { + desc string + input string + expected *ofga.Entity + }{{ + desc: "map identity name with relation", + input: "user-" + identity.Name + "#member", + expected: ofganames.ConvertTagWithRelation(names.NewUserTag(identity.Name), ofganames.MemberRelation), + }, { + desc: "map group name with relation", + input: "group-" + group.Name + "#member", + expected: ofganames.ConvertTagWithRelation(jimmnames.NewGroupTag(group.UUID), ofganames.MemberRelation), + }, { + desc: "map group UUID", + input: "group-" + group.UUID, + expected: ofganames.ConvertTag(jimmnames.NewGroupTag(group.UUID)), + }, { + desc: "map group UUID with relation", + input: "group-" + group.UUID + "#member", + expected: ofganames.ConvertTagWithRelation(jimmnames.NewGroupTag(group.UUID), ofganames.MemberRelation), + }, { + desc: "map jimm controller", + input: "controller-" + "jimm", + expected: ofganames.ConvertTag(names.NewControllerTag(j.UUID)), + }, { + desc: "map controller", + input: "controller-" + controller.Name + "#administrator", + expected: ofganames.ConvertTagWithRelation(names.NewControllerTag(model.UUID.String), ofganames.AdministratorRelation), + }, { + desc: "map controller UUID", + input: "controller-" + controller.UUID, + expected: ofganames.ConvertTag(names.NewControllerTag(model.UUID.String)), + }, { + desc: "map model", + input: "model-" + model.OwnerIdentityName + "/" + model.Name + "#administrator", + expected: ofganames.ConvertTagWithRelation(names.NewModelTag(model.UUID.String), ofganames.AdministratorRelation), + }, { + desc: "map model UUID", + input: "model-" + model.UUID.String, + expected: ofganames.ConvertTag(names.NewModelTag(model.UUID.String)), + }, { + desc: "map offer", + input: "applicationoffer-" + offer.URL + "#administrator", + expected: ofganames.ConvertTagWithRelation(names.NewApplicationOfferTag(offer.UUID), ofganames.AdministratorRelation), + }, { + desc: "map offer UUID", + input: "applicationoffer-" + offer.UUID, + expected: ofganames.ConvertTag(names.NewApplicationOfferTag(offer.UUID)), + }, { + desc: "map cloud", + input: "cloud-" + cloud.Name + "#administrator", + expected: ofganames.ConvertTagWithRelation(names.NewCloudTag(cloud.Name), ofganames.AdministratorRelation), + }} - now := time.Now().UTC().Round(time.Millisecond) - j := &jimm.JIMM{ - UUID: uuid.NewString(), - Database: db.Database{ - DB: jimmtest.PostgresDB(c, func() time.Time { return now }), - }, + for _, tC := range testCases { + t.Run(tC.desc, func(t *testing.T) { + jujuTag, err := jimm.ResolveTag(j.UUID, &j.Database, tC.input) + c.Assert(err, qt.IsNil) + c.Assert(jujuTag, qt.DeepEquals, tC.expected) + }) } - - err := j.Database.Migrate(ctx, false) - c.Assert(err, qt.IsNil) - - tag, err := jimm.ResolveTag(j.UUID, &j.Database, "user-alex@canonical.com-werly#member") - c.Assert(err, qt.IsNil) - c.Assert(tag, qt.DeepEquals, ofganames.ConvertTagWithRelation(names.NewUserTag("alex@canonical.com-werly"), ofganames.MemberRelation)) } func TestResolveTupleObjectHandlesErrors(t *testing.T) { @@ -649,7 +574,7 @@ func TestResolveTupleObjectHandlesErrors(t *testing.T) { // Resolves bad tuple objects in general { input: "unknowntag-blabla", - want: "failed to map tag unknowntag", + want: "failed to map tag, unknown kind: unknowntag", }, // Resolves bad groups where they do not exist { @@ -669,17 +594,27 @@ func TestResolveTupleObjectHandlesErrors(t *testing.T) { // Resolves bad models where it cannot be found on the specified controller { input: "model-" + controller.Name + ":alex/", - want: "model not found", + want: "model name format incorrect, expected /", }, // Resolves bad applicationoffers where it cannot be found on the specified controller/model combo { input: "applicationoffer-" + controller.Name + ":alex/" + model.Name + "." + offer.Name + "fluff", want: "application offer not found", }, + { + input: "abc", + want: "failed to setup tag resolver: tag is not properly formatted", + }, + { + input: "model-test-unknowncontroller-1:alice@canonical.com/test-model-1", + want: "model not found", + }, } - for _, tc := range tests { - _, err := jimm.ResolveTag(j.UUID, &j.Database, tc.input) - c.Assert(err, qt.ErrorMatches, tc.want) + for i, tc := range tests { + t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) { + _, err := jimm.ResolveTag(j.UUID, &j.Database, tc.input) + c.Assert(err, qt.ErrorMatches, tc.want) + }) } } diff --git a/internal/jujuapi/access_control.go b/internal/jujuapi/access_control.go index 5f8faf2d0..403359cc4 100644 --- a/internal/jujuapi/access_control.go +++ b/internal/jujuapi/access_control.go @@ -4,6 +4,7 @@ package jujuapi import ( "context" + "fmt" "strconv" "time" @@ -198,7 +199,7 @@ func (r *controllerRoot) parseTuple(ctx context.Context, tuple apiparams.Relatio // to be specific to the erroneous offender. parseTagError := func(msg string, key string, err error) error { zapctx.Debug(ctx, msg, zap.String("key", key), zap.Error(err)) - return errors.E(op, errors.CodeFailedToParseTupleKey, err, msg+" "+key) + return errors.E(op, errors.CodeFailedToParseTupleKey, fmt.Errorf("%s, key %s: %w", msg, key, err)) } if tuple.TargetObject == "" { @@ -207,14 +208,14 @@ func (r *controllerRoot) parseTuple(ctx context.Context, tuple apiparams.Relatio if tuple.TargetObject != "" { targetTag, err := r.jimm.ParseTag(ctx, tuple.TargetObject) if err != nil { - return nil, parseTagError("failed to parse tuple target object key", tuple.TargetObject, err) + return nil, parseTagError("failed to parse tuple target", tuple.TargetObject, err) } t.Target = targetTag } if tuple.Object != "" { objectTag, err := r.jimm.ParseTag(ctx, tuple.Object) if err != nil { - return nil, parseTagError("failed to parse tuple object key", tuple.Object, err) + return nil, parseTagError("failed to parse tuple object", tuple.Object, err) } t.Object = objectTag } diff --git a/internal/jujuapi/access_control_test.go b/internal/jujuapi/access_control_test.go index a5284718e..533d89dcf 100644 --- a/internal/jujuapi/access_control_test.go +++ b/internal/jujuapi/access_control_test.go @@ -337,7 +337,7 @@ func (s *accessControlSuite) TestAddRelation(c *gc.C) { }, // Test user -> model by name { - input: tuple{"user-" + user.Name, "writer", "model-" + controller.Name + ":" + user.Name + "/" + model.Name}, + input: tuple{"user-" + user.Name, "writer", "model-" + user.Name + "/" + model.Name}, want: createTuple( "user:"+user.Name, "writer", @@ -359,7 +359,7 @@ func (s *accessControlSuite) TestAddRelation(c *gc.C) { }, // Test user -> applicationoffer by name { - input: tuple{"user-" + user.Name, "consumer", "applicationoffer-" + controller.Name + ":" + user.Name + "/" + model.Name + "." + offer.Name}, + input: tuple{"user-" + user.Name, "consumer", "applicationoffer-" + offer.URL}, want: createTuple( "user:"+user.Name, "consumer", @@ -403,7 +403,7 @@ func (s *accessControlSuite) TestAddRelation(c *gc.C) { }, // Test group -> model by name { - input: tuple{"group-" + group.Name + "#member", "writer", "model-" + controller.Name + ":" + user.Name + "/" + model.Name}, + input: tuple{"group-" + group.Name + "#member", "writer", "model-" + user.Name + "/" + model.Name}, want: createTuple( "group:"+group.UUID+"#member", "writer", @@ -425,7 +425,7 @@ func (s *accessControlSuite) TestAddRelation(c *gc.C) { }, // Test group -> applicationoffer by name { - input: tuple{"group-" + group.Name + "#member", "consumer", "applicationoffer-" + controller.Name + ":" + user.Name + "/" + model.Name + "." + offer.Name}, + input: tuple{"group-" + group.Name + "#member", "consumer", "applicationoffer-" + offer.URL}, want: createTuple( "group:"+group.UUID+"#member", "consumer", @@ -599,7 +599,7 @@ func (s *accessControlSuite) TestRemoveRelation(c *gc.C) { Relation: "writer", Target: ofganames.ConvertTag(model.ResourceTag()), }, - toRemove: tuple{"user-" + user.Name, "writer", "model-" + controller.Name + ":" + user.Name + "/" + model.Name}, + toRemove: tuple{"user-" + user.Name, "writer", "model-" + user.Name + "/" + model.Name}, want: createTuple( "user:"+user.Name, "writer", @@ -631,7 +631,7 @@ func (s *accessControlSuite) TestRemoveRelation(c *gc.C) { Relation: "consumer", Target: ofganames.ConvertTag(offer.ResourceTag()), }, - toRemove: tuple{"user-" + user.Name, "consumer", "applicationoffer-" + controller.Name + ":" + user.Name + "/" + model.Name + "." + offer.Name}, + toRemove: tuple{"user-" + user.Name, "consumer", "applicationoffer-" + offer.URL}, want: createTuple( "user:"+user.Name, "consumer", @@ -695,7 +695,7 @@ func (s *accessControlSuite) TestRemoveRelation(c *gc.C) { Relation: "writer", Target: ofganames.ConvertTag(model.ResourceTag()), }, - toRemove: tuple{"group-" + group.Name + "#member", "writer", "model-" + controller.Name + ":" + user.Name + "/" + model.Name}, + toRemove: tuple{"group-" + group.Name + "#member", "writer", "model-" + user.Name + "/" + model.Name}, want: createTuple( "group:"+group.UUID+"#member", "writer", @@ -727,7 +727,7 @@ func (s *accessControlSuite) TestRemoveRelation(c *gc.C) { Relation: "consumer", Target: ofganames.ConvertTag(offer.ResourceTag()), }, - toRemove: tuple{"group-" + group.Name + "#member", "consumer", "applicationoffer-" + controller.Name + ":" + user.Name + "/" + model.Name + "." + offer.Name}, + toRemove: tuple{"group-" + group.Name + "#member", "consumer", "applicationoffer-" + offer.URL}, want: createTuple( "group:"+group.UUID+"#member", "consumer", @@ -819,10 +819,10 @@ func (s *accessControlSuite) TestJAASTag(c *gc.C) { expectedJAASTag: "controller-" + controller.Name, }, { tag: ofganames.ConvertTag(model.ResourceTag()), - expectedJAASTag: "model-" + controller.Name + ":" + user.Name + "/" + model.Name, + expectedJAASTag: "model-" + user.Name + "/" + model.Name, }, { tag: ofganames.ConvertTag(applicationOffer.ResourceTag()), - expectedJAASTag: "applicationoffer-" + controller.Name + ":" + user.Name + "/" + model.Name + "." + applicationOffer.Name, + expectedJAASTag: "applicationoffer-" + applicationOffer.URL, }, { tag: &ofganames.Tag{}, expectedError: "unexpected tag kind: ", @@ -891,7 +891,7 @@ func (s *accessControlSuite) TestJAASTagNoUUIDResolution(c *gc.C) { func (s *accessControlSuite) TestListRelationshipTuples(c *gc.C) { ctx := context.Background() - user, _, controller, model, applicationOffer, _, _, client, closeClient := createTestControllerEnvironment(ctx, c, s) + user, _, controller, _, applicationOffer, _, _, client, closeClient := createTestControllerEnvironment(ctx, c, s) defer closeClient() _, err := client.AddGroup(&apiparams.AddGroupRequest{Name: "yellow"}) @@ -914,7 +914,7 @@ func (s *accessControlSuite) TestListRelationshipTuples(c *gc.C) { }, { Object: "group-orange#member", Relation: "administrator", - TargetObject: "applicationoffer-" + controller.Name + ":" + user.Name + "/" + model.Name + "." + applicationOffer.Name, + TargetObject: "applicationoffer-" + applicationOffer.URL, }} err = client.AddRelation(&apiparams.AddRelationRequest{Tuples: tuples}) @@ -928,18 +928,27 @@ func (s *accessControlSuite) TestListRelationshipTuples(c *gc.C) { response, err = client.ListRelationshipTuples(&apiparams.ListRelationshipTuplesRequest{ Tuple: apiparams.RelationshipTuple{ - TargetObject: "applicationoffer-" + controller.Name + ":" + user.Name + "/" + model.Name + "." + applicationOffer.Name, + TargetObject: "applicationoffer-" + applicationOffer.URL, }, ResolveUUIDs: true, }) c.Assert(err, jc.ErrorIsNil) c.Assert(response.Tuples, jc.DeepEquals, []apiparams.RelationshipTuple{tuples[3]}) c.Assert(len(response.Errors), gc.Equals, 0) + + // Test error message when a resource is not found + _, err = client.ListRelationshipTuples(&apiparams.ListRelationshipTuplesRequest{ + Tuple: apiparams.RelationshipTuple{ + TargetObject: "applicationoffer-" + "fake-offer", + }, + ResolveUUIDs: true, + }) + c.Assert(err, gc.ErrorMatches, "failed to parse tuple target, key applicationoffer-fake-offer: application offer not found.*") } func (s *accessControlSuite) TestListRelationshipTuplesNoUUIDResolution(c *gc.C) { ctx := context.Background() - user, _, controller, model, applicationOffer, _, _, client, closeClient := createTestControllerEnvironment(ctx, c, s) + _, _, _, _, applicationOffer, _, _, client, closeClient := createTestControllerEnvironment(ctx, c, s) defer closeClient() _, err := client.AddGroup(&apiparams.AddGroupRequest{Name: "orange"}) @@ -964,7 +973,7 @@ func (s *accessControlSuite) TestListRelationshipTuplesNoUUIDResolution(c *gc.C) }} response, err := client.ListRelationshipTuples(&apiparams.ListRelationshipTuplesRequest{ Tuple: apiparams.RelationshipTuple{ - TargetObject: "applicationoffer-" + controller.Name + ":" + user.Name + "/" + model.Name + "." + applicationOffer.Name, + TargetObject: "applicationoffer-" + applicationOffer.URL, }, ResolveUUIDs: false, }) @@ -975,7 +984,7 @@ func (s *accessControlSuite) TestListRelationshipTuplesNoUUIDResolution(c *gc.C) func (s *accessControlSuite) TestListRelationshipTuplesAfterDeletingGroup(c *gc.C) { ctx := context.Background() - user, _, controller, model, applicationOffer, _, _, client, closeClient := createTestControllerEnvironment(ctx, c, s) + user, _, controller, _, applicationOffer, _, _, client, closeClient := createTestControllerEnvironment(ctx, c, s) defer closeClient() _, err := client.AddGroup(&apiparams.AddGroupRequest{Name: "yellow"}) @@ -998,7 +1007,7 @@ func (s *accessControlSuite) TestListRelationshipTuplesAfterDeletingGroup(c *gc. }, { Object: "group-orange#member", Relation: "administrator", - TargetObject: "applicationoffer-" + controller.Name + ":" + user.Name + "/" + model.Name + "." + applicationOffer.Name, + TargetObject: "applicationoffer-" + applicationOffer.URL, }} err = client.AddRelation(&apiparams.AddRelationRequest{Tuples: tuples}) @@ -1092,7 +1101,7 @@ func (s *accessControlSuite) TestCheckRelationOfferReaderFlow(c *gc.C) { ctx := context.Background() ofgaClient := s.JIMM.OpenFGAClient - user, group, controller, model, offer, _, _, client, closeClient := createTestControllerEnvironment(ctx, c, s) + user, group, _, _, offer, _, _, client, closeClient := createTestControllerEnvironment(ctx, c, s) defer closeClient() // Some tags (tuples) to assist in the creation of tuples within OpenFGA (such that they can be tested against) @@ -1102,7 +1111,7 @@ func (s *accessControlSuite) TestCheckRelationOfferReaderFlow(c *gc.C) { // JAAS style keys, to be translated and checked against UUIDs/users/groups userJAASKey := "user-" + user.Name - offerJAASKey := "applicationoffer-" + controller.Name + ":" + user.Name + "/" + model.Name + "." + offer.Name + offerJAASKey := "applicationoffer-" + offer.URL // Test direct relation to an applicationoffer from a user of a group via "reader" relation @@ -1163,7 +1172,7 @@ func (s *accessControlSuite) TestCheckRelationOfferConsumerFlow(c *gc.C) { ctx := context.Background() ofgaClient := s.JIMM.OpenFGAClient - user, group, controller, model, offer, _, _, client, closeClient := createTestControllerEnvironment(ctx, c, s) + user, group, _, _, offer, _, _, client, closeClient := createTestControllerEnvironment(ctx, c, s) defer closeClient() // Some keys to assist in the creation of tuples within OpenFGA (such that they can be tested against) @@ -1173,7 +1182,7 @@ func (s *accessControlSuite) TestCheckRelationOfferConsumerFlow(c *gc.C) { // JAAS style keys, to be translated and checked against UUIDs/users/groups userJAASKey := "user-" + user.Name - offerJAASKey := "applicationoffer-" + controller.Name + ":" + user.Name + "/" + model.Name + "." + offer.Name + offerJAASKey := "applicationoffer-" + offer.URL // Test direct relation to an applicationoffer from a user of a group via "consumer" relation userToGroupMember := openfga.Tuple{ @@ -1232,7 +1241,7 @@ func (s *accessControlSuite) TestCheckRelationModelReaderFlow(c *gc.C) { ctx := context.Background() ofgaClient := s.JIMM.OpenFGAClient - user, group, controller, model, _, _, _, client, closeClient := createTestControllerEnvironment(ctx, c, s) + user, group, _, model, _, _, _, client, closeClient := createTestControllerEnvironment(ctx, c, s) defer closeClient() // Some keys to assist in the creation of tuples within OpenFGA (such that they can be tested against) @@ -1244,7 +1253,7 @@ func (s *accessControlSuite) TestCheckRelationModelReaderFlow(c *gc.C) { // JAAS style keys, to be translated and checked against UUIDs/users/groups userJAASKey := "user-" + user.Name - modelJAASKey := "model-" + controller.Name + ":" + user.Name + "/" + model.Name + modelJAASKey := "model-" + user.Name + "/" + model.Name // Test direct relation to a model from a user of a group via "reader" relation userToGroupMember := openfga.Tuple{ @@ -1303,7 +1312,7 @@ func (s *accessControlSuite) TestCheckRelationModelWriterFlow(c *gc.C) { ctx := context.Background() ofgaClient := s.JIMM.OpenFGAClient - user, group, controller, model, _, _, _, client, closeClient := createTestControllerEnvironment(ctx, c, s) + user, group, _, model, _, _, _, client, closeClient := createTestControllerEnvironment(ctx, c, s) defer closeClient() // Some keys to assist in the creation of tuples within OpenFGA (such that they can be tested against) @@ -1325,7 +1334,7 @@ func (s *accessControlSuite) TestCheckRelationModelWriterFlow(c *gc.C) { // JAAS style keys, to be translated and checked against UUIDs/users/groups userJAASKey := "user-" + user.Name - modelJAASKey := "model-" + controller.Name + ":" + user.Name + "/" + model.Name + modelJAASKey := "model-" + user.Name + "/" + model.Name err := ofgaClient.AddRelation( ctx, @@ -1386,8 +1395,8 @@ func (s *accessControlSuite) TestCheckRelationControllerAdministratorFlow(c *gc. userJAASKey := "user-" + user.Name groupJAASKey := "group-" + group.Name controllerJAASKey := "controller-" + controller.Name - modelJAASKey := "model-" + controller.Name + ":" + user.Name + "/" + model.Name - offerJAASKey := "applicationoffer-" + controller.Name + ":" + user.Name + "/" + model.Name + "." + offer.Name + modelJAASKey := "model-" + user.Name + "/" + model.Name + offerJAASKey := "applicationoffer-" + offer.URL // Test the administrator flow of a group user being related to a controller via administrator relation userToGroup := openfga.Tuple{