diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 26b3bfb..3272277 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,26 +11,25 @@ jobs: runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/') steps: - - - name: Checkout - uses: actions/checkout@v3 - - - name: Unshallow + - name: Checkout + uses: actions/checkout@v4.1.1 + + - name: Unshallow run: git fetch --prune --unshallow - - - name: Set up Go - uses: actions/setup-go@v3 + + - name: Set up Go + uses: actions/setup-go@v5 with: - go-version: 1.18 - - - name: Import GPG key + go-version: 1.21 + + - name: Import GPG key id: import_gpg uses: crazy-max/ghaction-import-gpg@v5.0.0 with: gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} passphrase: ${{ secrets.PASSPHRASE }} - - - name: Run GoReleaser + + - name: Run GoReleaser uses: goreleaser/goreleaser-action@v4 with: version: latest diff --git a/.jfrog-pipelines/pipeline.yml b/.jfrog-pipelines/pipeline.yml index 2198c8b..a70fcb7 100644 --- a/.jfrog-pipelines/pipeline.yml +++ b/.jfrog-pipelines/pipeline.yml @@ -25,7 +25,7 @@ pipelines: auto: language: go versions: - - "1.18" + - "1.21" requiresApproval: approvers: - alexh diff --git a/CHANGELOG.md b/CHANGELOG.md index a81b966..15cd2b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## 1.2.0 (January 10, 2023) + +IMPROVEMENTS: + +* Add `refreshable` and `include_reference_token` parameters to both `roles/` and `user_token/` paths. PR: [144](https://github.com/jfrog/vault-plugin-secrets-artifactory/pull/144) +* Bump jfrog/artifactory-jcr from 7.71.8 to 7.71.9 in /scripts PR: [143](https://github.com/jfrog/vault-plugin-secrets-artifactory/pull/143) +* Bump golang.org/x/crypto from 0.14.0 to 0.17.0 PR: [142](https://github.com/jfrog/vault-plugin-secrets-artifactory/pull/142) +* Bump github.com/hashicorp/go-hclog from 1.6.1 to 1.6.2 PR: [141](https://github.com/jfrog/vault-plugin-secrets-artifactory/pull/141) +* Bump jfrog/artifactory-jcr from 7.71.5 to 7.71.8 in /scripts PR: [140](https://github.com/jfrog/vault-plugin-secrets-artifactory/pull/140) +* Bump github.com/hashicorp/go-hclog from 1.5.0 to 1.6.1 PR: [139](https://github.com/jfrog/vault-plugin-secrets-artifactory/pull/139) +* Bump jfrog/artifactory-jcr from 7.71.4 to 7.71.5 in /scripts PR: [138](https://github.com/jfrog/vault-plugin-secrets-artifactory/pull/138) + ## 1.1.4 (November 22, 2023) BUG FIXES: diff --git a/Makefile b/Makefile index 5109392..9e08f87 100644 --- a/Makefile +++ b/Makefile @@ -71,7 +71,7 @@ clean: fmt: go fmt $$(go list ./...) -setup: disable register enable admin testrole +setup: disable register enable admin: vault write $(PLUGIN_VAULT_PATH)/config/admin url=$(JFROG_URL) access_token=$(JFROG_ACCESS_TOKEN) @@ -79,6 +79,12 @@ admin: vault write -f $(PLUGIN_VAULT_PATH)/config/rotate vault read $(PLUGIN_VAULT_PATH)/config/admin +usertoken: + vault write $(PLUGIN_VAULT_PATH)/config/admin url=$(JFROG_URL) access_token=$(JFROG_ACCESS_TOKEN) + vault write $(PLUGIN_VAULT_PATH)/config/user_token default_description="Vault Test" + vault read $(PLUGIN_VAULT_PATH)/config/user_token + vault read $(PLUGIN_VAULT_PATH)/user_token/test refreshable=true include_reference_token=true + testrole: vault write $(PLUGIN_VAULT_PATH)/roles/test scope="$(ARTIFACTORY_SCOPE)" max_ttl=3h default_ttl=2h vault read $(PLUGIN_VAULT_PATH)/roles/test diff --git a/README.md b/README.md index 0730c55..63c10e1 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # Vault Artifactory Secrets Plugin -This plugin is actively maintained by JFrog Inc. Please refer to [CONTRIBUTING.md](CONTRIBUTING.md) for contributions and create GitHub issues to ask for feature requests and support. +This plugin is actively maintained by JFrog Inc. Please refer to [CONTRIBUTING.md](CONTRIBUTING.md) for contributions and [create GitHub issues](https://github.com/jfrog/vault-plugin-secrets-artifactory/issues/new/choose) to ask for feature requests and support. Contact [JFrog Support](https://jfrog.com/support/) for urgent, time sensitive issues. ---------------------------------------------------------------- -This is a [HashiCorp Vault](https://www.vaultproject.io/) plugin which talks to JFrog Artifactory server and will +This is a [HashiCorp Vault](https://www.vaultproject.io/) secret plugin which talks to JFrog Artifactory server and will dynamically provision access tokens with specified scopes. This backend can be mounted multiple times to provide access to multiple Artifactory servers. @@ -18,6 +18,7 @@ This backend creates access tokens in Artifactory using the admin credentials pr ### Admin Token Expiration Notice +> [!IMPORTANT] > Prior to Artifactory 7.42.1, admin access token was created with the system token expiration (default to 1 year) even when `expires_in` API field is set to `0`. In 7.42.1, admin token expiration no longer constrained by system configuration and therefore can be set to non-expiring. > See section ["Generate a Non-expiry Admin Token without Changing the Configuration"](https://www.jfrog.com/confluence/display/JFROG/Artifactory+Release+Notes#ArtifactoryReleaseNotes-Artifactory7.42.1Cloud) in the release note. > @@ -131,10 +132,10 @@ vault plugin register \ secret artifactory ``` -> **Note** +> [!NOTE] > you may need to also add arguments to the registration like `-args="-ca-cert ca.pem` or something insecure like: `-args="-tls-skip-verify"` depending on your environment. (see `./path/to/plugins/artifactory -help` for all the options) -> **Note** +> [!CAUTION] > This inline checksum calculation above is provided for illustration purpose and does not validate your binary. It should **not** be used for production environment. Instead you should use the checksum provided as [part of the release](https://github.com/jfrog/vault-plugin-secrets-artifactory/releases). See [How to verify binary checksums](#how-to-verify-binary-checksums) section. You can now enable the Artifactory secrets plugin: @@ -145,7 +146,7 @@ vault secrets enable artifactory ### How to verify binary checksums -Checksums for each binary are provided in the `artifactory-secrets-plugin__checksums.txt` file. It is signed with the public key `vault-plugin-secrets-artifactory-public-key.asc` which creates the signature file `artifactory-secrets-plugin__checksums.txt.sig`. +Checksums for each binary are provided in the `artifactory-secrets-plugin__checksums.txt` file. It is signed with the public key [`vault-plugin-secrets-artifactory-public-key.asc`](vault-plugin-secrets-artifactory-public-key.asc) which creates the signature file `artifactory-secrets-plugin__checksums.txt.sig`. If the public key is not in your GPG keychain, import it: ```sh @@ -206,10 +207,10 @@ vault write artifactory/config/admin \ vault write -f artifactory/config/rotate ``` -**NOTE** some versions of artifactory (notably `7.39.10`) fail to rotate correctly. As noted above, we recommend being on `7.42.1` or higher. The token was indeed rotated, but as the error indicates, the old token could not be revoked. +> [!NOTE] +> some versions of artifactory (notably `7.39.10`) fail to rotate correctly. As noted above, we recommend being on `7.42.1` or higher. The token was indeed rotated, but as the error indicates, the old token could not be revoked. -**ALSO** If you want to change the username for the admin token (tired of it just being "admin"?) or set a "Description" on the token, those parameters are optionally -available on the `artifactory/config/rotate` endpoint. +**ALSO** If you want to change the username for the admin token (tired of it just being "admin"?) or set a "Description" on the token, those parameters are optionally available on the `artifactory/config/rotate` endpoint. ```sh vault write artifactory/config/rotate username="new-username" description="A token used by vault-secrets-engine on our vault server"` @@ -259,7 +260,8 @@ vault write artifactory/roles/jenkins \ Also supports `grant_type=[Optional, default: "client_credentials"]`, and `audience=[Optional, default: *@*]` see [JFrog documentation][artifactory-create-token]. -NOTE: By default, the username will be generated automatically using the template `v-(RoleName)-(random 8)` (i.e. `v-jenkins-x4mohTA8`). If you would prefer to have a static username (the same for every token), you can set `username=whatever-you-want`, but keep in mind that in a dynamic environment, someone or something using an old, expired token might cause a denial of service (too many failed logins) against users with the correct token. +> [!NOTE] +> By default, the username will be generated automatically using the template `v-(RoleName)-(random 8)` (i.e. `v-jenkins-x4mohTA8`). If you would prefer to have a static username (the same for every token), you can set `username=whatever-you-want`, but keep in mind that in a dynamic environment, someone or something using an old, expired token might cause a denial of service (too many failed logins) against users with the correct token.
CLICK for: Create a Role (scope for artifactory < 7.21.1) @@ -273,7 +275,7 @@ vault write artifactory/roles/jenkins \
-> **Note** +> [!NOTE] > There are some changes in the **scopes** supported in artifactory request >7.21. Please refer to the JFrog documentation for the same according to the artifactory version. ```sh @@ -283,7 +285,6 @@ vault list artifactory/roles Example Output: ```console - Keys ---- jenkins @@ -301,7 +302,7 @@ Key Value lease_id artifactory/token/jenkins/9hHxV1NlyLzPgmNIzjssRCa9 lease_duration 1h lease_renewable true -access_token eyJ2ZXIiOiIyIiw.... +access_token eyJ2ZXIiOiIyIiw... role jenkins scope applied-permissions/groups:automation token_id 06d962b2-63e2-4279-a25d-d2a9cab6507f @@ -318,25 +319,27 @@ path "artifactory/user_token/{{identity.entity.aliases.azure-ad-oidc.metadata.up } ``` -Default values for the token's description, ttl, max_ttl and audience may be configured at the `/artifactory/config/admin` endpoint. TTL rules follow Vault's [general cases](https://developer.hashicorp.com/vault/docs/concepts/tokens#the-general-case) and [token hierarchy](https://developer.hashicorp.com/vault/docs/concepts/tokens#token-hierarchies-and-orphan-tokens). The desired lease TTL will be determined by the most specific TTL value specified with the request ttl parameter being highest precedence, followed by the plugin configuration, secret mount tuning, or system default ttl. The maximum TTL value allowed is limited to the lowest value of the max_ttl setting set on the system, secret mount tuning, plugin configuration, or the specific request. +Default values for the token's `description`, `ttl`, `max_ttl`, `audience`, `refreshable`, and `include_reference_token` may be configured at the `/artifactory/config/user_token` endpoint. TTL rules follow Vault's [general cases](https://developer.hashicorp.com/vault/docs/concepts/tokens#the-general-case) and [token hierarchy](https://developer.hashicorp.com/vault/docs/concepts/tokens#token-hierarchies-and-orphan-tokens). The desired lease TTL will be determined by the most specific TTL value specified with the request ttl parameter being highest precedence, followed by the plugin configuration, secret mount tuning, or system default ttl. The maximum TTL value allowed is limited to the lowest value of the `max_ttl` setting set on the system, secret mount tuning, plugin configuration, or the specific request. Example Token Configuration: -```sh +```console vault write artifactory/config/user_token default_description="Generated by Vault" max_ttl=604800 default_ttl=86400 ``` ```console $ vault read artifactory/config/user_token -Key Value ---- ----- -audience n/a -default_description Generated by Vault -default_ttl 24h -max_ttl 168h -scope applied-permissions/admin -token_id 8df5dd21-31ae-4062-bbe5-580a607f5645 -username vault-admin +Key Value +--- ----- +audience n/a +default_description Generated by Vault +default_ttl 24h +include_reference_token true +max_ttl 168h +refreshable true +scope applied-permissions/user +token_id 8df5dd21-31ae-4062-bbe5-580a607f5645 +username vault-admin ``` Example Usage: @@ -347,8 +350,10 @@ Key Value lease_id artifactory/user_token/admin/4UhTThCwctPGX0TYXeoyoVEt lease_duration 24h lease_renewable true -access_token eyJ2Z424242424..... +access_token eyJ2Z424242424... description Dev Desktop +reference_token cmVmdGtu... +refresh_token 629299be-... scope applied-permissions/user token_id 3c6b2e63-87dc-4d26-9698-ffdfb282a6ee username admin @@ -373,7 +378,7 @@ username admin ### Testing Locally -If you're compiling this yourself and want to test locally, you will need a working docker environment. You will also need vault and golang installed, then you can follow the steps below. +If you're compiling this yourself and want to test locally, you will need a working Docker environment. You will also need Vault cli and Golang installed, then you can follow the steps below. * In first terminal, build the plugin and start the local dev server: @@ -387,7 +392,7 @@ make make artifactory ``` -* In the same terminal, setup artifactory-secrets-engine in vault with values: +* In the same terminal, setup `artifactory-secrets-engine` in vault with values: ```sh export VAULT_ADDR=http://localhost:8200 @@ -395,6 +400,12 @@ export VAULT_TOKEN=root make setup ``` +* In the same terminal, you can configure and generate an admin access token: + +```sh +make admin +``` + NOTE: Each time you rebuild (`make`), vault will restart, so you will need to run `make setup` again, since vault is in dev mode. * Once you are done testing, you can destroy the local artifactory instance: @@ -417,16 +428,12 @@ brew tap hashicorp/tap brew install hashicorp/tap/vault ``` ----------------------------------------------------------------- - #### Start Vault dev server ```sh make start ``` ----------------------------------------------------------------- - #### Export Vault url and token ```sh @@ -434,16 +441,12 @@ export VAULT_ADDR='http://127.0.0.1:8200' export VAULT_TOKEN=root ``` ----------------------------------------------------------------- - #### Build plugin binary ```sh make build ``` ----------------------------------------------------------------- - #### Upgrade plugin binary To build and upgrade the plugin without having to reconfigure it... @@ -452,8 +455,6 @@ To build and upgrade the plugin without having to reconfigure it... make upgrade ``` ----------------------------------------------------------------- - #### Create Test Artifactory ```sh @@ -468,16 +469,17 @@ Example: make artifactory ARTIFACTORY_VERSION=7.49.10 ``` -NOTE: If you get a message like: - -```console -make: Nothing to be done for `artifactory'. -``` - -This simply means that "make" thinks artifactory is already running due to the existence of the `./vault/artifactory.env` file. -If you want to run a different version, first use `make stop_artifactory`. If you stopped artifactory using other means (docker), then `rm vault/artifactory.env` manually. +> [!NOTE] +> If you get a message like: +> +>```console +>make: Nothing to be done for `artifactory'. +>``` +> +>This simply means that "make" thinks artifactory is >already running due to the existence of the `./vault/>artifactory.env` file. +> +>If you want to run a different version, first use `make >stop_artifactory`. If you stopped artifactory using other >means (docker), then `rm vault/artifactory.env` manually. ----------------------------------------------------------------- #### Register artifactory-secrets plugin with Vault server @@ -487,25 +489,20 @@ If you didn't run `make upgrade` (i.e. just `make build`), then you need to regi make register ``` ----------------------------------------------------------------- - #### Enable artifactory-secrets plugin ```sh make enable ``` ----------------------------------------------------------------- - #### Disable plugin (unmount from vault) ```sh make disable ``` -NOTE: This is a good idea before stopping artifactory, especially if you plan to change versions of artifactory. Alternatively, just exit vault (Ctrl+c), and it will go back to default state. - ----------------------------------------------------------------- +> [!NOTE] +> This is a good idea before stopping artifactory, especially if you plan to change versions of artifactory. Alternatively, just exit vault (Ctrl+c), and it will go back to default state. #### Get ADMIN Artifactory token and write it to vault @@ -525,7 +522,7 @@ For example: JFROG_URL=https://artifactory.example.org ARTIFACTORY_USERNAME=tommy ARTIFACTORY_PASSWORD='SuperSecret' make admin ``` -If you already have a JFROG_ACCESS_TOKEN, you can skip straight to that too: +If you already have a `JFROG_ACCESS_TOKEN``, you can skip straight to that too: ```sh export JFROG_URL=https://artifactory.example.com @@ -533,16 +530,12 @@ export JFROG_ACCESS_TOKEN=(PASTE YOUR JFROG ADMIN TOKEN) make admin ``` ----------------------------------------------------------------- - * Setup a "test" role, bound to the "readers" group ```sh make testrole ``` ----------------------------------------------------------------- - #### Run Acceptance Tests ```sh @@ -564,7 +557,7 @@ See the [contribution guide](./CONTRIBUTING.md). ## License -Copyright (c) 2023 JFrog. +Copyright (c) 2024 JFrog. Apache 2.0 licensed, see [LICENSE][LICENSE] file. diff --git a/artifactory.go b/artifactory.go index c84b0b7..cb775f5 100644 --- a/artifactory.go +++ b/artifactory.go @@ -25,16 +25,17 @@ const ( var ErrIncompatibleVersion = errors.New("incompatible version") +type errorResponse struct { + Code string `json:"code"` + Message string `json:"message"` + Detail string `json:"detail"` +} + func (b *backend) RevokeToken(config adminConfiguration, secret logical.Secret) error { - accessToken := secret.InternalData["access_token"].(string) tokenId := secret.InternalData["token_id"].(string) - - values := url.Values{} - values.Set("token", accessToken) - u, err := url.Parse(config.ArtifactoryURL) if err != nil { - b.Backend.Logger().Warn("could not parse artifactory url", "url", u, "err", err) + b.Logger().Error("could not parse artifactory url", "url", u, "err", err) return err } @@ -43,14 +44,17 @@ func (b *backend) RevokeToken(config adminConfiguration, secret logical.Secret) if b.useNewAccessAPI() { resp, err = b.performArtifactoryDelete(config, "/access/api/v1/tokens/"+tokenId) if err != nil { - b.Backend.Logger().Warn("error deleting access token", "tokenId", tokenId, "response", resp, "err", err) + b.Logger().Error("error deleting access token", "tokenId", tokenId, "response", resp, "err", err) return err } - } else { + accessToken := secret.InternalData["access_token"].(string) + values := url.Values{} + values.Set("token", accessToken) + resp, err = b.performArtifactoryPost(config, u.Path+"/api/security/token/revoke", values) if err != nil { - b.Backend.Logger().Warn("error deleting token", "tokenId", tokenId, "response", resp, "err", err) + b.Logger().Error("error deleting token", "tokenId", tokenId, "response", resp, "err", err) return err } } @@ -58,46 +62,47 @@ func (b *backend) RevokeToken(config adminConfiguration, secret logical.Secret) defer resp.Body.Close() if resp.StatusCode >= http.StatusBadRequest { - bodyBytes, err := io.ReadAll(resp.Body) - if err != nil { - b.Backend.Logger().Warn("revokeToken could not read bad response body", "response", resp, "err", err) + e := fmt.Errorf("could not revoke tokenID: %v - HTTP response %v", tokenId, resp.StatusCode) + + var errResp errorResponse + if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil { + b.Logger().Error("revokenToken could not parse error response body", "err", err) + return e } - b.Backend.Logger().Warn("revokeToken got bad http status code", "statusCode", resp.StatusCode, "body", string(bodyBytes)) - return fmt.Errorf("could not revoke tokenID: %v - HTTP response %v", tokenId, resp.StatusCode) + b.Logger().Error("revokeToken got bad http status code", "statusCode", resp.StatusCode, "body", errResp) + return fmt.Errorf("could not revoke tokenID: %v - %s", tokenId, errResp.Detail) } return nil } type CreateTokenRequest struct { - GrantType string `json:"grant_type,omitempty"` - Username string `json:"username,omitempty"` - Scope string `json:"scope,omitempty"` - ExpiresIn int64 `json:"expires_in"` - Refreshable bool `json:"refreshable,omitempty"` - Description string `json:"description,omitempty"` - Audience string `json:"audience,omitempty"` - ForceRevocable bool `json:"force_revocable,omitempty"` + GrantType string `json:"grant_type,omitempty"` + Username string `json:"username,omitempty"` + Scope string `json:"scope,omitempty"` + ExpiresIn int64 `json:"expires_in"` + Refreshable bool `json:"refreshable,omitempty"` + Description string `json:"description,omitempty"` + Audience string `json:"audience,omitempty"` + ForceRevocable bool `json:"force_revocable,omitempty"` + IncludeReferenceToken bool `json:"include_reference_token,omitempty"` } func (b *backend) CreateToken(config adminConfiguration, role artifactoryRole) (*createTokenResponse, error) { request := CreateTokenRequest{ - GrantType: role.GrantType, - Username: role.Username, - Scope: role.Scope, - Audience: role.Audience, - Description: role.Description, + GrantType: role.GrantType, + Username: role.Username, + Scope: role.Scope, + Audience: role.Audience, + Description: role.Description, + Refreshable: role.Refreshable, + IncludeReferenceToken: role.IncludeReferenceToken, } if len(request.Username) == 0 { return nil, fmt.Errorf("empty username not allowed, possibly a template error") } - // A refreshable access token gets replaced by a new access token, which is not - // what a consumer of tokens from this backend would be expecting; instead they'd - // likely just request a new token periodically. - request.Refreshable = false - // Artifactory will not let you revoke a token that has an expiry unless it also meets // criteria that can only be set in its configuration file. The version of Artifactory // I'm testing against will actually delete a token when you ask it to revoke by token_id, @@ -111,7 +116,7 @@ func (b *backend) CreateToken(config adminConfiguration, role artifactoryRole) ( u, err := url.Parse(config.ArtifactoryURL) if err != nil { - b.Backend.Logger().Warn("could not parse artifactory url", "url", u, "err", err) + b.Logger().Error("could not parse artifactory url", "url", u, "err", err) return nil, err } @@ -130,7 +135,7 @@ func (b *backend) CreateToken(config adminConfiguration, role artifactoryRole) ( resp, err := b.performArtifactoryPostWithJSON(config, path, jsonReq) if err != nil { - b.Backend.Logger().Warn("error making token request", "response", resp, "err", err) + b.Logger().Error("error making token request", "response", resp, "err", err) return nil, err } @@ -138,17 +143,20 @@ func (b *backend) CreateToken(config adminConfiguration, role artifactoryRole) ( defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - bodyBytes, err := io.ReadAll(resp.Body) - if err != nil { - b.Backend.Logger().Warn("createToken could not read bad response", "response", resp, "err", err) + e := fmt.Errorf("could not create access token: HTTP response %v", resp.StatusCode) + + var errResp errorResponse + if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil { + b.Logger().Error("revokenToken could not parse error response body", "err", err) + return nil, e } - b.Backend.Logger().Warn("createToken got non-200 status code", "statusCode", resp.StatusCode, "body", string(bodyBytes)) - return nil, fmt.Errorf("could not create access token: HTTP response %v", resp.StatusCode) + b.Logger().Error("createToken got non-200 status code", "statusCode", resp.StatusCode, "body", errResp) + return nil, fmt.Errorf("could not create access token: HTTP response: %s", errResp.Detail) } var createdToken createTokenResponse if err := json.NewDecoder(resp.Body).Decode(&createdToken); err != nil { - b.Backend.Logger().Warn("could not parse response", "response", resp, "err", err) + b.Logger().Error("could not parse response", "response", resp, "err", err) return nil, err } @@ -173,20 +181,20 @@ func (b *backend) useNewAccessAPI() bool { func (b *backend) getVersion(config adminConfiguration) (err error) { resp, err := b.performArtifactoryGet(config, "/artifactory/api/system/version") if err != nil { - b.Backend.Logger().Warn("error making system version request", "response", resp, "err", err) + b.Logger().Error("error making system version request", "response", resp, "err", err) return } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - b.Backend.Logger().Warn("got non-200 status code", "statusCode", resp.StatusCode) + b.Logger().Error("got non-200 status code", "statusCode", resp.StatusCode) return fmt.Errorf("could not get the system version: HTTP response %v", resp.StatusCode) } var systemVersion systemVersionResponse if err = json.NewDecoder(resp.Body).Decode(&systemVersion); err != nil { - b.Backend.Logger().Warn("could not parse system version response", "response", resp, "err", err) + b.Logger().Error("could not parse system version response", "response", resp, "err", err) return } b.version = systemVersion.Version @@ -196,16 +204,15 @@ func (b *backend) getVersion(config adminConfiguration) (err error) { // checkVersion will return a boolean and error to check compatibility before making an API call // -- This was formerly "checkSystemStatus" but that was hard-coded, that method now calls this one func (b *backend) checkVersion(ver string) (compatible bool) { - v1, err := version.NewVersion(b.version) if err != nil { - b.Backend.Logger().Warn("could not parse Artifactory system version", "ver", b.version, "err", err) + b.Logger().Error("could not parse Artifactory system version", "ver", b.version, "err", err) return } v2, err := version.NewVersion(ver) if err != nil { - b.Backend.Logger().Warn("could not parse provided version", "ver", ver, "err", err) + b.Logger().Error("could not parse provided version", "ver", ver, "err", err) return } @@ -223,7 +230,7 @@ func (b *backend) parseJWT(config adminConfiguration, token string) (jwtToken *j cert, err := b.getRootCert(config) if err != nil { if errors.Is(err, ErrIncompatibleVersion) { - b.Logger().Warn("outdated artifactory, unable to retrieve root cert, skipping token validation") + b.Logger().Error("outdated artifactory, unable to retrieve root cert, skipping token validation") validate = false } else { b.Logger().Error("error retrieving root cert", "err", err.Error()) @@ -269,7 +276,7 @@ func (b *backend) getTokenInfo(config adminConfiguration, token string) (info *T // Parse Current Token (to get tokenID/scope) jwtToken, err := b.parseJWT(config, token) if err != nil { - return nil, err + return } claims, ok := jwtToken.Claims.(jwt.MapClaims) @@ -294,7 +301,7 @@ func (b *backend) getTokenInfo(config adminConfiguration, token string) (info *T case json.Number: v, err := exp.Int64() if err != nil { - b.Backend.Logger().Warn("error parsing token exp as json.Number", "err", err) + b.Logger().Error("error parsing token exp as json.Number", "err", err) } info.Expires = v } @@ -312,21 +319,21 @@ func (b *backend) getRootCert(config adminConfiguration) (cert *x509.Certificate resp, err := b.performArtifactoryGet(config, "/access/api/v1/cert/root") if err != nil { - b.Backend.Logger().Warn("error requesting cert/root", "response", resp, "err", err) + b.Logger().Error("error requesting cert/root", "response", resp, "err", err) return } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - b.Backend.Logger().Warn("got non-200 status code", "statusCode", resp.StatusCode) + b.Logger().Error("got non-200 status code", "statusCode", resp.StatusCode) return cert, fmt.Errorf("could not get the certificate: HTTP response %v", resp.StatusCode) } body, err := io.ReadAll(resp.Body) // body, err := ioutil.ReadAll(resp.Body) Go.1.15 and earlier if err != nil { - b.Backend.Logger().Error("error reading root cert response body", "err", err) + b.Logger().Error("error reading root cert response body", "err", err) return } @@ -334,13 +341,13 @@ func (b *backend) getRootCert(config adminConfiguration) (cert *x509.Certificate binCert := make([]byte, len(body)) n, err := base64.StdEncoding.Decode(binCert, body) if err != nil { - b.Backend.Logger().Error("error decoding body", "err", err) + b.Logger().Error("error decoding body", "err", err) return } cert, err = x509.ParseCertificate(binCert[0:n]) if err != nil { - b.Backend.Logger().Error("error parsing certificate", "err", err) + b.Logger().Error("error parsing certificate", "err", err) return } return @@ -369,13 +376,13 @@ func (b *backend) sendUsage(config adminConfiguration, featureId string) { jsonReq, err := json.Marshal(usage) if err != nil { - b.Backend.Logger().Info("error marshalling call home request", "err", err) + b.Logger().Info("error marshalling call home request", "err", err) return } resp, err := b.performArtifactoryPostWithJSON(config, "artifactory/api/system/usage", jsonReq) if err != nil { - b.Backend.Logger().Info("error making call home request", "response", resp, "err", err) + b.Logger().Info("error making call home request", "response", resp, "err", err) return } diff --git a/artifactory_test.go b/artifactory_test.go index cd13a84..f1730ff 100644 --- a/artifactory_test.go +++ b/artifactory_test.go @@ -2,7 +2,7 @@ package artifactory import ( "context" - "fmt" + "net/http" "testing" "time" @@ -20,7 +20,7 @@ func TestBackend_CreateTokenSuccess(t *testing.T) { mockArtifactoryUsageVersionRequests("") httpmock.RegisterResponder( - "POST", + http.MethodPost, "http://myserver.com:80/artifactory/api/security/token", httpmock.NewStringResponder(200, canonicalAccessToken)) @@ -53,7 +53,6 @@ func TestBackend_CreateTokenSuccess(t *testing.T) { Storage: config.StorageView, }) assert.NotNil(t, resp) - fmt.Printf("resp :%v", resp) assert.NoError(t, err) // Verify response @@ -75,7 +74,7 @@ func TestBackend_CreateTokenArtifactoryUnavailable(t *testing.T) { mockArtifactoryUsageVersionRequests("") httpmock.RegisterResponder( - "POST", + http.MethodPost, "http://myserver.com:80/artifactory/api/security/token", httpmock.NewStringResponder(400, "")) @@ -120,10 +119,15 @@ func TestBackend_CreateTokenUnauthorized(t *testing.T) { mockArtifactoryUsageVersionRequests("") + errResp := errorResponse{ + Code: "Boom", + Message: "foo", + Detail: "bar", + } httpmock.RegisterResponder( - "POST", + http.MethodPost, "http://myserver.com:80/artifactory/api/security/token", - httpmock.NewStringResponder(401, "")) + httpmock.NewJsonResponderOrPanic(401, &errResp)) b, config := configuredBackend(t, map[string]interface{}{ "access_token": "test-access-token", @@ -168,10 +172,15 @@ func TestBackend_CreateTokenArtifactoryMisconfigured(t *testing.T) { mockArtifactoryUsageVersionRequests("") + errResp := errorResponse{ + Code: "Boom", + Message: "foo", + Detail: "bar", + } httpmock.RegisterResponder( - "POST", + http.MethodPost, "http://myserver.com:80/artifactory/api/security/token", - httpmock.NewStringResponder(401, `

Bad Gateway


`)) + httpmock.NewJsonResponderOrPanic(401, &errResp)) b, config := configuredBackend(t, map[string]interface{}{ "access_token": "test-access-token", @@ -215,12 +224,12 @@ func TestBackend_RevokeToken(t *testing.T) { mockArtifactoryUsageVersionRequests("") httpmock.RegisterResponder( - "POST", + http.MethodPost, "http://myserver.com:80/artifactory/api/security/token", httpmock.NewStringResponder(200, canonicalAccessToken)) httpmock.RegisterResponder( - "POST", + http.MethodPost, "http://myserver.com:80/artifactory/api/security/token/revoke", httpmock.NewStringResponder(200, "")) @@ -277,17 +286,17 @@ func TestBackend_RotateAdminToken(t *testing.T) { mockArtifactoryUsageVersionRequests(`{"version" : "7.33.8", "revision" : "73308900"}`) httpmock.RegisterResponder( - "GET", + http.MethodGet, "http://myserver.com:80/access/api/v1/cert/root", httpmock.NewStringResponder(200, rootCert)) httpmock.RegisterResponder( - "POST", + http.MethodPost, "http://myserver.com:80/access/api/v1/tokens", httpmock.NewStringResponder(200, jwtAccessToken)) httpmock.RegisterResponder( - "DELETE", + http.MethodDelete, "http://myserver.com:80/access/api/v1/tokens/84c0626b-7973-40c9-9d37-701aaf73cfb4", httpmock.NewStringResponder(200, "")) @@ -316,5 +325,4 @@ func TestBackend_RotateAdminToken(t *testing.T) { }) assert.NoError(t, err) assert.Nil(t, resp) - } diff --git a/go.mod b/go.mod index 231056a..1d25c9f 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/jfrog/vault-plugin-secrets-artifactory -go 1.18 +go 1.21 require ( github.com/golang-jwt/jwt/v4 v4.5.0 diff --git a/go.sum b/go.sum index b55c90b..3c6befc 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,5 @@ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= @@ -16,6 +17,7 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= +github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M= github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -41,6 +43,7 @@ github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYF github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/frankban/quicktest v1.14.2 h1:SPb1KFFmM+ybpEjPUhCCkZOM5xlovT5UbrMvWnXyBns= github.com/frankban/quicktest v1.14.2/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA= @@ -51,6 +54,7 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= +github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -70,6 +74,7 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= @@ -131,6 +136,7 @@ github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInw github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= +github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= @@ -158,6 +164,7 @@ github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp9 github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= +github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= @@ -172,13 +179,16 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= @@ -210,6 +220,7 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= @@ -218,6 +229,7 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -256,6 +268,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -302,6 +315,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -311,3 +325,4 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= +gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= diff --git a/path_config.go b/path_config.go index a5d47bc..c46c6eb 100644 --- a/path_config.go +++ b/path_config.go @@ -54,9 +54,10 @@ func (b *backend) pathConfig() *framework.Path { }, HelpSynopsis: `Interact with the Artifactory secrets configuration.`, HelpDescription: ` -Configure the parameters used to connect to the Artifactory server integrated with this backend. The two main -parameters are "url" which is the absolute URL to the Artifactory server. Note that "/artifactory/api" is prepended by the -individual calls, so do not include it in the URL here. +Configure the parameters used to connect to the Artifactory server integrated with this backend. + +The two main parameters are "url" which is the absolute URL to the Artifactory server. Note that "/artifactory/api" +is prepended by the individual calls, so do not include it in the URL here. The second is "access_token" which must be an access token powerful enough to generate the other access tokens you'll be using. This value is stored seal wrapped when available. Once set, the access token cannot be retrieved, but the backend diff --git a/path_config_rotate.go b/path_config_rotate.go index 2b0eeb9..afd4ae7 100644 --- a/path_config_rotate.go +++ b/path_config_rotate.go @@ -13,23 +13,21 @@ func (b *backend) pathConfigRotate() *framework.Path { Fields: map[string]*framework.FieldSchema{ "username": { Type: framework.TypeString, - Description: "Optional. Override Artifactory token username for new admin token.", + Description: "Optional. Override Artifactory token username for new access token.", }, "description": { Type: framework.TypeString, - Description: "Optional. Set Artifactory token description on new admin token.", + Description: "Optional. Set Artifactory token description on new access token.", }, }, Operations: map[logical.Operation]framework.OperationHandler{ logical.UpdateOperation: &framework.PathOperation{ Callback: b.pathConfigRotateWrite, - Summary: "Rotate the Artifactory Admin Token.", + Summary: "Rotate the Artifactory Access Token.", }, }, - HelpSynopsis: `Rotate the Artifactory Admin Token.`, - HelpDescription: ` -This will rotate the "access_token" used to access artifactory from this plugin, and revoke the old admin token. -`, + HelpSynopsis: `Rotate the Artifactory Access Token.`, + HelpDescription: `This will rotate the "access_token" used to access artifactory from this plugin. A new access token is created first then revokes the old access token.`, } } @@ -53,7 +51,7 @@ func (b *backend) pathConfigRotateWrite(ctx context.Context, req *logical.Reques // Parse Current Token (to get tokenID/scope) token, err := b.getTokenInfo(*config, oldAccessToken) if err != nil { - return logical.ErrorResponse("error parsing existing AccessToken: " + err.Error()), err + return logical.ErrorResponse("error parsing existing access token: " + err.Error()), err } // Check for submitted username @@ -75,13 +73,13 @@ func (b *backend) pathConfigRotateWrite(ctx context.Context, req *logical.Reques if val, ok := data.GetOk("description"); ok { role.Description = val.(string) } else { - role.Description = "Rotated Admin token for artifactory-secrets plugin in Vault" + role.Description = "Rotated access token for artifactory-secrets plugin in Vault" } // Create a new token resp, err := b.CreateToken(*config, *role) if err != nil { - return logical.ErrorResponse("error creating new token"), err + return logical.ErrorResponse("error creating new access token"), err } // Set new token @@ -107,7 +105,7 @@ func (b *backend) pathConfigRotateWrite(ctx context.Context, req *logical.Reques } err = b.RevokeToken(*config, oldSecret) if err != nil { - return logical.ErrorResponse("error revoking existing AccessToken"), err + return logical.ErrorResponse("error revoking existing access token %s", token.TokenID), err } return nil, nil diff --git a/path_config_rotate_test.go b/path_config_rotate_test.go index e5c3b89..9652c71 100644 --- a/path_config_rotate_test.go +++ b/path_config_rotate_test.go @@ -75,7 +75,7 @@ func (e *accTestEnv) PathConfigRotateCreateTokenErr(t *testing.T) { }) resp, err := e.update("config/rotate", testData{}) assert.NotNil(t, resp) - assert.Contains(t, resp.Data["error"], "error creating new token") + assert.Contains(t, resp.Data["error"], "error creating new access token") assert.ErrorContains(t, err, "could not create access token") e.revokeTestToken(t, e.AccessToken, tokenId) } diff --git a/path_config_user_token.go b/path_config_user_token.go index 176128e..c3cc04a 100644 --- a/path_config_user_token.go +++ b/path_config_user_token.go @@ -16,6 +16,16 @@ func (b *backend) pathConfigUserToken() *framework.Path { Type: framework.TypeString, Description: `Optional. See the JFrog Artifactory REST documentation on "Create Token" for a full and up to date description.`, }, + "refreshable": { + Type: framework.TypeBool, + Default: false, + Description: `Optional. Defaults to 'false'. A refreshable access token gets replaced by a new access token, which is not what a consumer of tokens from this backend would be expecting; instead they'd likely just request a new token periodically. Set this to 'true' only if your usage requires this. See the JFrog Artifactory documentation on "Generating Refreshable Tokens" (https://jfrog.com/help/r/jfrog-platform-administration-documentation/generating-refreshable-tokens) for a full and up to date description.`, + }, + "include_reference_token": { + Type: framework.TypeBool, + Default: false, + Description: `Optional. Defaults to 'false'. Generate a Reference Token (alias to Access Token) in addition to the full token (available from Artifactory 7.38.10). A reference token is a shorter, 64-character string, which can be used as a bearer token, a password, or with the ״X-JFrog-Art-Api״ header. Note: Using the reference token might have performance implications over a full length token.`, + }, "default_ttl": { Type: framework.TypeDurationSecond, Description: `Optional. Default TTL for issued user access tokens. If unset, uses the backend's default_ttl. Cannot exceed max_ttl.`, @@ -45,10 +55,12 @@ func (b *backend) pathConfigUserToken() *framework.Path { } type userTokenConfiguration struct { - Audience string `json:"audience,omitempty"` - DefaultTTL time.Duration `json:"default_ttl,omitempty"` - MaxTTL time.Duration `json:"max_ttl,omitempty"` - DefaultDescription string `json:"default_description,omitempty"` + Audience string `json:"audience,omitempty"` + Refreshable bool `json:"refreshable,omitempty"` + IncludeReferenceToken bool `json:"include_reference_token,omitempty"` + DefaultTTL time.Duration `json:"default_ttl,omitempty"` + MaxTTL time.Duration `json:"max_ttl,omitempty"` + DefaultDescription string `json:"default_description,omitempty"` } // fetchAdminConfiguration will return nil,nil if there's no configuration @@ -96,6 +108,14 @@ func (b *backend) pathConfigUserTokenUpdate(ctx context.Context, req *logical.Re userTokenConfig.Audience = val.(string) } + if val, ok := data.GetOk("refreshable"); ok { + userTokenConfig.Refreshable = val.(bool) + } + + if val, ok := data.GetOk("include_reference_token"); ok { + userTokenConfig.IncludeReferenceToken = val.(bool) + } + if val, ok := data.GetOk("default_ttl"); ok { userTokenConfig.DefaultTTL = time.Duration(val.(int)) * time.Second } @@ -142,10 +162,12 @@ func (b *backend) pathConfigUserTokenRead(ctx context.Context, req *logical.Requ } configMap := map[string]interface{}{ - "audience": userTokenConfig.Audience, - "default_ttl": userTokenConfig.DefaultTTL.Seconds(), - "max_ttl": userTokenConfig.MaxTTL.Seconds(), - "default_description": userTokenConfig.DefaultDescription, + "audience": userTokenConfig.Audience, + "refreshable": userTokenConfig.Refreshable, + "include_reference_token": userTokenConfig.IncludeReferenceToken, + "default_ttl": userTokenConfig.DefaultTTL.Seconds(), + "max_ttl": userTokenConfig.MaxTTL.Seconds(), + "default_description": userTokenConfig.DefaultDescription, } // Optionally include token info if it parses properly diff --git a/path_config_user_token_test.go b/path_config_user_token_test.go index d26e231..a063d7c 100644 --- a/path_config_user_token_test.go +++ b/path_config_user_token_test.go @@ -14,6 +14,8 @@ func TestAcceptanceBackend_PathConfigUserToken(t *testing.T) { t.Run("update default_description", accTestEnv.PathConfigDefaultDescriptionUpdate) t.Run("update audience", accTestEnv.PathConfigAudienceUpdate) + t.Run("update refreshable", accTestEnv.PathConfigRefreshableUpdate) + t.Run("update include_reference_token", accTestEnv.PathConfigIncludeReferenceTokenUpdate) t.Run("update default_ttl", accTestEnv.PathConfigDefaultTTLUpdate) t.Run("update max_ttl", accTestEnv.PathConfigMaxTTLUpdate) } @@ -26,6 +28,14 @@ func (e *accTestEnv) PathConfigAudienceUpdate(t *testing.T) { e.pathConfigUserTokenUpdateStringField(t, "audience") } +func (e *accTestEnv) PathConfigRefreshableUpdate(t *testing.T) { + e.pathConfigUserTokenUpdateBoolField(t, "refreshable") +} + +func (e *accTestEnv) PathConfigIncludeReferenceTokenUpdate(t *testing.T) { + e.pathConfigUserTokenUpdateBoolField(t, "include_reference_token") +} + func (e *accTestEnv) PathConfigDefaultTTLUpdate(t *testing.T) { e.pathConfigUserTokenUpdateDurationField(t, "default_ttl") } @@ -48,6 +58,20 @@ func (e *accTestEnv) pathConfigUserTokenUpdateStringField(t *testing.T, fieldNam assert.Equal(t, "test456", data[fieldName]) } +func (e *accTestEnv) pathConfigUserTokenUpdateBoolField(t *testing.T, fieldName string) { + e.UpdateConfigUserToken(t, testData{ + fieldName: true, + }) + data := e.ReadConfigUserToken(t) + assert.Equal(t, true, data[fieldName]) + + e.UpdateConfigUserToken(t, testData{ + fieldName: false, + }) + data = e.ReadConfigUserToken(t) + assert.Equal(t, false, data[fieldName]) +} + func (e *accTestEnv) pathConfigUserTokenUpdateDurationField(t *testing.T, fieldName string) { e.UpdateConfigUserToken(t, testData{ fieldName: 1.0, diff --git a/path_roles.go b/path_roles.go index aa74a60..a6244d6 100644 --- a/path_roles.go +++ b/path_roles.go @@ -40,12 +40,22 @@ func (b *backend) pathRoles() *framework.Path { "scope": { Type: framework.TypeString, Required: true, - Description: `Required. See the JFrog Artifactory REST documentation on "Create Token" for a full and up to date description.`, + Description: `Required. Space-delimited list. See the JFrog Artifactory REST documentation on "Create Token" for a full and up to date description.`, + }, + "refreshable": { + Type: framework.TypeBool, + Default: false, + Description: `Optional. Defaults to 'false'. A refreshable access token gets replaced by a new access token, which is not what a consumer of tokens from this backend would be expecting; instead they'd likely just request a new token periodically. Set this to 'true' only if your usage requires this. See the JFrog Artifactory documentation on "Generating Refreshable Tokens" (https://jfrog.com/help/r/jfrog-platform-administration-documentation/generating-refreshable-tokens) for a full and up to date description.`, }, "audience": { Type: framework.TypeString, Description: `Optional. See the JFrog Artifactory REST documentation on "Create Token" for a full and up to date description.`, }, + "include_reference_token": { + Type: framework.TypeBool, + Default: false, + Description: `Optional. Defaults to 'false'. Generate a Reference Token (alias to Access Token) in addition to the full token (available from Artifactory 7.38.10). A reference token is a shorter, 64-character string, which can be used as a bearer token, a password, or with the "X-JFrog-Art-Api" header. Note: Using the reference token might have performance implications over a full length token.`, + }, "default_ttl": { Type: framework.TypeDurationSecond, Description: `Default TTL for issued access tokens. If unset, uses the backend's default_ttl. Cannot exceed max_ttl.`, @@ -79,13 +89,15 @@ func (b *backend) pathRoles() *framework.Path { } type artifactoryRole struct { - GrantType string `json:"grant_type,omitempty"` - Username string `json:"username,omitempty"` - Scope string `json:"scope"` - Audience string `json:"audience,omitempty"` - Description string `json:"description,omitempty"` - DefaultTTL time.Duration `json:"default_ttl,omitempty"` - MaxTTL time.Duration `json:"max_ttl,omitempty"` + GrantType string `json:"grant_type,omitempty"` + Username string `json:"username,omitempty"` + Scope string `json:"scope"` + Refreshable bool `json:"refreshable"` + Audience string `json:"audience,omitempty"` + Description string `json:"description,omitempty"` + IncludeReferenceToken bool `json:"include_reference_token"` + DefaultTTL time.Duration `json:"default_ttl,omitempty"` + MaxTTL time.Duration `json:"max_ttl,omitempty"` } func (b *backend) pathRoleList(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) { @@ -149,10 +161,18 @@ func (b *backend) pathRoleWrite(ctx context.Context, req *logical.Request, data role.Scope = value.(string) } + if value, ok := data.GetOk("refreshable"); ok { + role.Refreshable = value.(bool) + } + if value, ok := data.GetOk("audience"); ok { role.Audience = value.(string) } + if value, ok := data.GetOk("include_reference_token"); ok { + role.IncludeReferenceToken = value.(bool) + } + // Looking at database/path_roles.go, it doesn't do any validation on these values during role creation. if value, ok := data.GetOk("default_ttl"); ok { role.DefaultTTL = time.Duration(value.(int)) * time.Second @@ -236,10 +256,12 @@ func (b *backend) Role(ctx context.Context, storage logical.Storage, roleName st func (b *backend) roleToMap(roleName string, role artifactoryRole) (roleMap map[string]interface{}) { roleMap = map[string]interface{}{ - "role": roleName, - "scope": role.Scope, - "default_ttl": role.DefaultTTL.Seconds(), - "max_ttl": role.MaxTTL.Seconds(), + "role": roleName, + "scope": role.Scope, + "default_ttl": role.DefaultTTL.Seconds(), + "max_ttl": role.MaxTTL.Seconds(), + "refreshable": role.Refreshable, + "include_reference_token": role.IncludeReferenceToken, } // Optional Attributes diff --git a/path_roles_test.go b/path_roles_test.go index 1ba97e5..d789e65 100644 --- a/path_roles_test.go +++ b/path_roles_test.go @@ -147,12 +147,14 @@ func TestBackend_PathRoleWriteThenRead(t *testing.T) { }) roleData := map[string]interface{}{ - "role": "test-role", - "username": "test-username", - "scope": "test-scope", - "audience": "test-audience", - "default_ttl": 30 * time.Minute, - "max_ttl": 45 * time.Minute, + "role": "test-role", + "username": "test-username", + "scope": "test-scope", + "audience": "test-audience", + "refreshable": true, + "include_reference_token": true, + "default_ttl": 30 * time.Minute, + "max_ttl": 45 * time.Minute, } _, err := b.HandleRequest(context.Background(), &logical.Request{ @@ -174,6 +176,8 @@ func TestBackend_PathRoleWriteThenRead(t *testing.T) { assert.EqualValues(t, "test-username", resp.Data["username"]) assert.EqualValues(t, "test-scope", resp.Data["scope"]) assert.EqualValues(t, "test-audience", resp.Data["audience"]) + assert.EqualValues(t, true, resp.Data["refreshable"]) + assert.EqualValues(t, true, resp.Data["include_reference_token"]) assert.EqualValues(t, 30*time.Minute.Seconds(), resp.Data["default_ttl"]) assert.EqualValues(t, 45*time.Minute.Seconds(), resp.Data["max_ttl"]) } diff --git a/path_token_create.go b/path_token_create.go index abdb9a2..e766426 100644 --- a/path_token_create.go +++ b/path_token_create.go @@ -31,6 +31,13 @@ func (b *backend) pathTokenCreate() *framework.Path { }, }, HelpSynopsis: `Create an Artifactory access token for the specified role.`, + HelpDescription: ` +Create an Artifactory access token using paramters from the specified role. + +An optional 'ttl' parameter will override the role's 'default_ttl' parameter. + +An optional 'max_ttl' parameter will override the role's 'max_ttl' parameter. +`, } } @@ -40,11 +47,13 @@ type systemVersionResponse struct { } type createTokenResponse struct { - TokenId string `json:"token_id"` - AccessToken string `json:"access_token"` - ExpiresIn int `json:"expires_in"` - Scope string `json:"scope"` - TokenType string `json:"token_type"` + TokenId string `json:"token_id"` + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + ExpiresIn int `json:"expires_in"` + Scope string `json:"scope"` + TokenType string `json:"token_type"` + ReferenceToken string `json:"reference_token"` } func (b *backend) pathTokenCreatePerform(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { @@ -68,7 +77,6 @@ func (b *backend) pathTokenCreatePerform(ctx context.Context, req *logical.Reque roleName := data.Get("role").(string) role, err := b.Role(ctx, req.Storage, roleName) - if err != nil { return nil, err } @@ -113,16 +121,20 @@ func (b *backend) pathTokenCreatePerform(ctx context.Context, req *logical.Reque } response := b.Secret(SecretArtifactoryAccessTokenType).Response(map[string]interface{}{ - "access_token": resp.AccessToken, - "role": roleName, - "scope": resp.Scope, - "token_id": resp.TokenId, - "username": role.Username, + "access_token": resp.AccessToken, + "refresh_token": resp.RefreshToken, + "role": roleName, + "scope": resp.Scope, + "token_id": resp.TokenId, + "username": role.Username, + "reference_token": resp.ReferenceToken, }, map[string]interface{}{ - "role": roleName, - "access_token": resp.AccessToken, - "token_id": resp.TokenId, - "username": role.Username, + "role": roleName, + "access_token": resp.AccessToken, + "refresh_token": resp.RefreshToken, + "token_id": resp.TokenId, + "username": role.Username, + "reference_token": resp.ReferenceToken, }) response.Secret.TTL = ttl diff --git a/path_user_token_create.go b/path_user_token_create.go index d0b550f..04adf48 100644 --- a/path_user_token_create.go +++ b/path_user_token_create.go @@ -21,6 +21,16 @@ func (b *backend) pathUserTokenCreate() *framework.Path { Type: framework.TypeString, Description: `Optional. Description for the user token.`, }, + "refreshable": { + Type: framework.TypeBool, + Default: false, + Description: `Optional. Defaults to 'false'. A refreshable access token gets replaced by a new access token, which is not what a consumer of tokens from this backend would be expecting; instead they'd likely just request a new token periodically. Set this to 'true' only if your usage requires this. See the JFrog Artifactory documentation on "Generating Refreshable Tokens" (https://jfrog.com/help/r/jfrog-platform-administration-documentation/generating-refreshable-tokens) for a full and up to date description.`, + }, + "include_reference_token": { + Type: framework.TypeBool, + Default: false, + Description: `Optional. Defaults to 'false'. Generate a Reference Token (alias to Access Token) in addition to the full token (available from Artifactory 7.38.10). A reference token is a shorter, 64-character string, which can be used as a bearer token, a password, or with the ״X-JFrog-Art-Api״ header. Note: Using the reference token might have performance implications over a full length token.`, + }, "max_ttl": { Type: framework.TypeDurationSecond, Description: `Optional. Override the maximum TTL for this access token. Cannot exceed smallest (system, mount, backend) maximum TTL.`, @@ -35,7 +45,8 @@ func (b *backend) pathUserTokenCreate() *framework.Path { Callback: b.pathUserTokenCreatePerform, }, }, - HelpSynopsis: `Create an Artifactory access token for the specified user.`, + HelpSynopsis: `Create an Artifactory access token for the specified user.`, + HelpDescription: `Provides optional paramter to override default values for the user_token/ path`, } } @@ -59,7 +70,7 @@ func (b *backend) pathUserTokenCreatePerform(ctx context.Context, req *logical.R return nil, err } - role := &artifactoryRole{ + role := artifactoryRole{ GrantType: "client_credentials", Username: data.Get("username").(string), Scope: "applied-permissions/user", @@ -91,29 +102,41 @@ func (b *backend) pathUserTokenCreatePerform(ctx context.Context, req *logical.R ttl = role.MaxTTL } + if value, ok := data.GetOk("refreshable"); ok { + role.Refreshable = value.(bool) + } + if value, ok := data.GetOk("audience"); ok { role.Audience = value.(string) } + if value, ok := data.GetOk("include_reference_token"); ok { + role.IncludeReferenceToken = value.(bool) + } + if value, ok := data.GetOk("description"); ok { role.Description = value.(string) } - resp, err := b.CreateToken(*config, *role) + resp, err := b.CreateToken(*config, role) if err != nil { return nil, err } response := b.Secret(SecretArtifactoryAccessTokenType).Response(map[string]interface{}{ - "access_token": resp.AccessToken, - "scope": resp.Scope, - "token_id": resp.TokenId, - "username": role.Username, - "description": role.Description, + "access_token": resp.AccessToken, + "refresh_token": resp.RefreshToken, + "scope": resp.Scope, + "token_id": resp.TokenId, + "username": role.Username, + "description": role.Description, + "reference_token": resp.ReferenceToken, }, map[string]interface{}{ - "access_token": resp.AccessToken, - "token_id": resp.TokenId, - "username": role.Username, + "access_token": resp.AccessToken, + "refresh_token": resp.RefreshToken, + "token_id": resp.TokenId, + "username": role.Username, + "reference_token": resp.ReferenceToken, }) response.Secret.TTL = ttl diff --git a/test_utils.go b/test_utils.go index 90a291b..7bd26c3 100644 --- a/test_utils.go +++ b/test_utils.go @@ -2,6 +2,7 @@ package artifactory import ( "context" + "net/http" "os" "testing" "time" @@ -196,12 +197,14 @@ func (e *accTestEnv) update(path string, data testData) (*logical.Response, erro func (e *accTestEnv) CreatePathRole(t *testing.T) { roleData := map[string]interface{}{ - "role": "test-role", - "username": "admin", - "scope": "applied-permissions/user", - "audience": "*@*", - "default_ttl": 30 * time.Minute, - "max_ttl": 45 * time.Minute, + "role": "test-role", + "username": "admin", + "scope": "applied-permissions/user", + "audience": "*@*", + "refreshable": true, + "include_reference_token": true, + "default_ttl": 30 * time.Minute, + "max_ttl": 45 * time.Minute, } resp, err := e.Backend.HandleRequest(context.Background(), &logical.Request{ @@ -228,6 +231,8 @@ func (e *accTestEnv) ReadPathRole(t *testing.T) { assert.EqualValues(t, "admin", resp.Data["username"]) assert.EqualValues(t, "applied-permissions/user", resp.Data["scope"]) assert.EqualValues(t, "*@*", resp.Data["audience"]) + assert.EqualValues(t, true, resp.Data["refreshable"]) + assert.EqualValues(t, true, resp.Data["include_reference_token"]) assert.EqualValues(t, 30*time.Minute.Seconds(), resp.Data["default_ttl"]) assert.EqualValues(t, 45*time.Minute.Seconds(), resp.Data["max_ttl"]) } @@ -257,6 +262,8 @@ func (e *accTestEnv) CreatePathToken(t *testing.T) { assert.Equal(t, "admin", resp.Data["username"]) assert.Equal(t, "test-role", resp.Data["role"]) assert.Equal(t, "applied-permissions/user", resp.Data["scope"]) + assert.NotEmpty(t, resp.Data["refresh_token"]) + assert.NotEmpty(t, resp.Data["reference_token"]) } func (e *accTestEnv) CreatePathUserToken(t *testing.T) { @@ -265,7 +272,9 @@ func (e *accTestEnv) CreatePathUserToken(t *testing.T) { Path: "user_token/admin", Storage: e.Storage, Data: map[string]interface{}{ - "description": "buffalo", + "description": "buffalo", + "refreshable": true, + "include_reference_token": true, }, }) @@ -276,6 +285,8 @@ func (e *accTestEnv) CreatePathUserToken(t *testing.T) { assert.Equal(t, "admin", resp.Data["username"]) assert.Equal(t, "applied-permissions/user", resp.Data["scope"]) assert.Equal(t, "buffalo", resp.Data["description"]) + assert.NotEmpty(t, resp.Data["refresh_token"]) + assert.NotEmpty(t, resp.Data["reference_token"]) } // Cleanup will delete the admin configuration and revoke the token @@ -416,11 +427,11 @@ func mockArtifactoryUsageVersionRequests(version string) { } httpmock.RegisterResponder( - "POST", + http.MethodPost, "http://myserver.com:80/artifactory/api/system/usage", httpmock.NewStringResponder(200, "")) httpmock.RegisterResponder( - "GET", + http.MethodGet, "http://myserver.com:80/artifactory/api/system/version", httpmock.NewStringResponder(200, versionString)) } diff --git a/ttl_test.go b/ttl_test.go index abf2b55..49d1da1 100644 --- a/ttl_test.go +++ b/ttl_test.go @@ -2,6 +2,7 @@ package artifactory import ( "context" + "net/http" "testing" "time" @@ -21,7 +22,7 @@ func TestBackend_NoRoleMaxTTLUsesSystemMaxTTL(t *testing.T) { mockArtifactoryUsageVersionRequests("") httpmock.RegisterResponder( - "POST", + http.MethodPost, "http://myserver.com:80/artifactory/api/security/token", httpmock.NewStringResponder(200, ` { @@ -75,7 +76,7 @@ func TestBackend_WorkingWithBothMaxTTLs(t *testing.T) { mockArtifactoryUsageVersionRequests("") httpmock.RegisterResponder( - "POST", + http.MethodPost, "http://myserver.com:80/artifactory/api/security/token", httpmock.NewStringResponder(200, canonicalAccessToken)) @@ -123,7 +124,7 @@ func TestBackend_NoUserTokensMaxTTLUsesSystemMaxTTL(t *testing.T) { mockArtifactoryUsageVersionRequests("") httpmock.RegisterResponder( - "POST", + http.MethodPost, "http://myserver.com:80/artifactory/api/security/token", httpmock.NewStringResponder(200, ` { @@ -172,7 +173,7 @@ func TestBackend_UserTokenConfigMaxTTLUseSystem(t *testing.T) { mockArtifactoryUsageVersionRequests("") httpmock.RegisterResponder( - "POST", + http.MethodPost, "http://myserver.com:80/artifactory/api/security/token", httpmock.NewStringResponder(200, canonicalAccessToken)) @@ -205,7 +206,7 @@ func TestBackend_UserTokenConfigMaxTTLUseConfigMaxTTL(t *testing.T) { mockArtifactoryUsageVersionRequests("") httpmock.RegisterResponder( - "POST", + http.MethodPost, "http://myserver.com:80/artifactory/api/security/token", httpmock.NewStringResponder(200, canonicalAccessToken)) @@ -238,7 +239,7 @@ func TestBackend_UserTokenMaxTTLUseRequestTTL(t *testing.T) { mockArtifactoryUsageVersionRequests("") httpmock.RegisterResponder( - "POST", + http.MethodPost, "http://myserver.com:80/artifactory/api/security/token", httpmock.NewStringResponder(200, canonicalAccessToken)) @@ -272,7 +273,7 @@ func TestBackend_UserTokenMaxTTLEnforced(t *testing.T) { mockArtifactoryUsageVersionRequests("") httpmock.RegisterResponder( - "POST", + http.MethodPost, "http://myserver.com:80/artifactory/api/security/token", httpmock.NewStringResponder(200, canonicalAccessToken)) @@ -307,7 +308,7 @@ func TestBackend_UserTokenTTLRequest(t *testing.T) { mockArtifactoryUsageVersionRequests("") httpmock.RegisterResponder( - "POST", + http.MethodPost, "http://myserver.com:80/artifactory/api/security/token", httpmock.NewStringResponder(200, canonicalAccessToken)) @@ -338,7 +339,7 @@ func TestBackend_UserTokenDefaultTTL(t *testing.T) { mockArtifactoryUsageVersionRequests("") httpmock.RegisterResponder( - "POST", + http.MethodPost, "http://myserver.com:80/artifactory/api/security/token", httpmock.NewStringResponder(200, canonicalAccessToken))