diff --git a/Makefile b/Makefile index 9e08f87..5c0f5b3 100644 --- a/Makefile +++ b/Makefile @@ -83,7 +83,7 @@ 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 + vault read $(PLUGIN_VAULT_PATH)/user_token/test refreshable=true include_reference_token=true use_expiring_tokens=true testrole: vault write $(PLUGIN_VAULT_PATH)/roles/test scope="$(ARTIFACTORY_SCOPE)" max_ttl=3h default_ttl=2h diff --git a/README.md b/README.md index 838666e..15cd294 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ Using this plugin, you can limit the accidental exposure window of Artifactory t This backend creates access tokens in Artifactory using the admin credentials provided. Note that if you provide non-administrative credentials, then the "username" must match the username of the credential owner. +Visit [JFrog Help Center](https://jfrog.com/help/r/jfrog-platform-administration-documentation/introduction-to-access-tokens) for more information on Access Tokens. + ### Admin Token Expiration Notice > [!IMPORTANT] @@ -46,8 +48,8 @@ By default, the Vault generated Artifactory tokens will not show an expiration d automatically revoke them. Vault will revoke the token when its lease expires due to logout or timeout (ttl/max_ttl). The reason for this is because of the [default Revocable/Persistency Thresholds][artifactory-token-thresholds] in Artifactory. If you would like the artifactory token itself to show an expiration, and you are using Artifactory v7.50.3 or higher, you can write -`use_expiring_tokens=true` to the `/artifactory/config/admin` endpoint. This will set the `force_revocable=true` parameter and -set `expires_in` to either max lease TTL or role max_ttl, whichever is lower, when a token is created, overriding the default +`use_expiring_tokens=true` to the `/artifactory/config/admin` path. This will set the `force_revocable=true` parameter and +set `expires_in` to either max lease TTL or role's `max_ttl`, whichever is lower, when a token is created, overriding the default thresholds mentioned above. Example: @@ -87,6 +89,7 @@ Token claims ### Artifactory Version Detection Some of the functionality of this plugin requires certain versions of Artifactory. For example, as of Artifactory 7.50.3, we can optionally set the `force_revocable` flag and set the expiration of the token to `max_ttl`. + If you have upgraded Artifactory after installing this plugin, and would like to take advantage of newer features, you can issue an empty write to the `artifactory/config/admin` endpoint to re-detect the version, or it will re-detect upon reload. Example: @@ -251,6 +254,17 @@ username vault-admin version 7.55.6 ``` +#### Use expiring tokens + +To enable creation of token that expires using TTL (system default, system max TTL, or config overrides), set `use_expiring_tokens` to `true`, e.g. + +```sh +vault write artifactory/config/admin \ + url=https://artifactory.example.org \ + access_token=$TOKEN \ + use_expiring_tokens=true +``` + ## Usage Create a role (scope for artifactory >= 7.21.1) @@ -314,7 +328,9 @@ username v-jenkins-x4mohTA8 ### User Token Path -User tokens may be obtained from the `/artifactory/user_token/` endpoint. This is useful in conjunction with [ACL Policy Path Templating](https://developer.hashicorp.com/vault/tutorials/policies/policy-templating) to allow users authenticated to Vault to obtain API tokens in Artfactory for their own account. Be careful to ensure that Vault authentication methods & policies align with user account names in Artifactory. For example the following policy allows users authenticated to the `azure-ad-oidc` authentication mount to obtain a token for Artifactory for themselves, assuming the `upn` metadata is populated in Vault during authentication. +User tokens may be obtained from the `/artifactory/user_token/` endpoint. This is useful in conjunction with [ACL Policy Path Templating](https://developer.hashicorp.com/vault/tutorials/policies/policy-templating) to allow users authenticated to Vault to obtain API tokens in Artfactory for their own account. Be careful to ensure that Vault authentication methods & policies align with user account names in Artifactory. + +For example the following policy allows users authenticated to the `azure-ad-oidc` authentication mount to obtain a token for Artifactory for themselves, assuming the `upn` metadata is populated in Vault during authentication. ``` path "artifactory/user_token/{{identity.entity.aliases.azure-ad-oidc.metadata.upn}}" { @@ -322,7 +338,11 @@ path "artifactory/user_token/{{identity.entity.aliases.azure-ad-oidc.metadata.up } ``` -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. +Default values for the token's `access_token`, `description`, `ttl`, `max_ttl`, `audience`, `refreshable`, `include_reference_token`, and `use_expiring_tokens` may be configured at the `/artifactory/config/user_token` path. + +`access_token` field allows the use of user's identity token in place of the admin access token from the `/artifactory/config/admin` path, enabling creating access token scoped to that user only. + +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: @@ -409,6 +429,12 @@ make setup make admin ``` +Generate an user token: + +```sh +make usertoken +``` + 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: diff --git a/artifactory.go b/artifactory.go index cb775f5..6ead920 100644 --- a/artifactory.go +++ b/artifactory.go @@ -52,7 +52,7 @@ func (b *backend) RevokeToken(config adminConfiguration, secret logical.Secret) values := url.Values{} values.Set("token", accessToken) - resp, err = b.performArtifactoryPost(config, u.Path+"/api/security/token/revoke", values) + resp, err = b.performArtifactoryPost(config, u.Path+"/artifactory/api/security/token/revoke", values) if err != nil { b.Logger().Error("error deleting token", "tokenId", tokenId, "response", resp, "err", err) return err @@ -62,15 +62,13 @@ func (b *backend) RevokeToken(config adminConfiguration, secret logical.Secret) defer resp.Body.Close() if resp.StatusCode >= http.StatusBadRequest { - 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 + body, err := io.ReadAll(resp.Body) + if err != nil { + b.Logger().Error("revokenToken could not read error response body", "err", err) + return fmt.Errorf("could not parse response body. Err: %v", err) } - 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) + b.Logger().Error("revokenToken got non-200 status code", "statusCode", resp.StatusCode, "body", string(body)) + return fmt.Errorf("could not revoke tokenID: %v - HTTP response %v", tokenId, body) } return nil @@ -116,7 +114,7 @@ func (b *backend) CreateToken(config adminConfiguration, role artifactoryRole) ( u, err := url.Parse(config.ArtifactoryURL) if err != nil { - b.Logger().Error("could not parse artifactory url", "url", u, "err", err) + b.Logger().Error("could not parse artifactory url", "url", config.ArtifactoryURL, "err", err) return nil, err } @@ -125,7 +123,7 @@ func (b *backend) CreateToken(config adminConfiguration, role artifactoryRole) ( if b.useNewAccessAPI() { path = "/access/api/v1/tokens" } else { - path = u.Path + "/api/security/token" + path = u.Path + "/artifactory/api/security/token" } jsonReq, err := json.Marshal(request) @@ -145,19 +143,19 @@ func (b *backend) CreateToken(config adminConfiguration, role artifactoryRole) ( if resp.StatusCode != http.StatusOK { 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 + body, err := io.ReadAll(resp.Body) + if err != nil { + b.Logger().Error("createToken could not read error response body", "err", err) + return nil, fmt.Errorf("could not parse response body. Err: %v", e) } - 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) + b.Logger().Error("createToken got non-200 status code", "statusCode", resp.StatusCode, "body", string(body)) + return nil, fmt.Errorf("could not create access token. HTTP response: %s", body) } var createdToken createTokenResponse if err := json.NewDecoder(resp.Body).Decode(&createdToken); err != nil { b.Logger().Error("could not parse response", "response", resp, "err", err) - return nil, err + return nil, fmt.Errorf("could not create access token. Err: %v", err) } return &createdToken, nil diff --git a/artifactory_test.go b/artifactory_test.go index f1730ff..27d4b3f 100644 --- a/artifactory_test.go +++ b/artifactory_test.go @@ -26,7 +26,7 @@ func TestBackend_CreateTokenSuccess(t *testing.T) { b, config := configuredBackend(t, map[string]interface{}{ "access_token": "test-access-token", - "url": "http://myserver.com:80/artifactory", + "url": "http://myserver.com:80", }) // Setup a role @@ -184,7 +184,7 @@ func TestBackend_CreateTokenArtifactoryMisconfigured(t *testing.T) { b, config := configuredBackend(t, map[string]interface{}{ "access_token": "test-access-token", - "url": "http://myserver.com:80/artifactory", + "url": "http://myserver.com:80", }) // Setup a role @@ -235,7 +235,7 @@ func TestBackend_RevokeToken(t *testing.T) { b, config := configuredBackend(t, map[string]interface{}{ "access_token": "test-access-token", - "url": "http://myserver.com:80/artifactory", + "url": "http://myserver.com:80", }) // Setup a role diff --git a/path_config.go b/path_config.go index c46c6eb..3bc7fa9 100644 --- a/path_config.go +++ b/path_config.go @@ -30,6 +30,7 @@ func (b *backend) pathConfig() *framework.Path { }, "use_expiring_tokens": { Type: framework.TypeBool, + Default: false, Description: "Optional. If Artifactory version >= 7.50.3, set expires_in to max_ttl and force_revocable.", }, "bypass_artifactory_tls_verification": { @@ -195,6 +196,7 @@ func (b *backend) pathConfigRead(ctx context.Context, req *logical.Request, _ *f "access_token_sha256": fmt.Sprintf("%x", accessTokenHash[:]), "url": config.ArtifactoryURL, "version": b.version, + "use_expiring_tokens": config.UseExpiringTokens, "bypass_artifactory_tls_verification": config.BypassArtifactoryTLSVerification, } diff --git a/path_config_user_token.go b/path_config_user_token.go index c3cc04a..9f1e4ae 100644 --- a/path_config_user_token.go +++ b/path_config_user_token.go @@ -2,6 +2,8 @@ package artifactory import ( "context" + "crypto/sha256" + "fmt" "time" "github.com/hashicorp/vault/sdk/framework" @@ -12,6 +14,10 @@ func (b *backend) pathConfigUserToken() *framework.Path { return &framework.Path{ Pattern: "config/user_token", Fields: map[string]*framework.FieldSchema{ + "access_token": { + Type: framework.TypeString, + Description: "User identity token to access Artifactory", + }, "audience": { Type: framework.TypeString, Description: `Optional. See the JFrog Artifactory REST documentation on "Create Token" for a full and up to date description.`, @@ -26,6 +32,11 @@ func (b *backend) pathConfigUserToken() *framework.Path { 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.`, }, + "use_expiring_tokens": { + Type: framework.TypeBool, + Default: false, + Description: "Optional. If Artifactory version >= 7.50.3, set expires_in to max_ttl and force_revocable.", + }, "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.`, @@ -55,9 +66,11 @@ func (b *backend) pathConfigUserToken() *framework.Path { } type userTokenConfiguration struct { + AccessToken string `json:"access_token"` Audience string `json:"audience,omitempty"` Refreshable bool `json:"refreshable,omitempty"` IncludeReferenceToken bool `json:"include_reference_token,omitempty"` + UseExpiringTokens bool `json:"use_expiring_tokens,omitempty"` DefaultTTL time.Duration `json:"default_ttl,omitempty"` MaxTTL time.Duration `json:"max_ttl,omitempty"` DefaultDescription string `json:"default_description,omitempty"` @@ -104,6 +117,10 @@ func (b *backend) pathConfigUserTokenUpdate(ctx context.Context, req *logical.Re return nil, err } + if val, ok := data.GetOk("access_token"); ok { + userTokenConfig.AccessToken = val.(string) + } + if val, ok := data.GetOk("audience"); ok { userTokenConfig.Audience = val.(string) } @@ -116,6 +133,10 @@ func (b *backend) pathConfigUserTokenUpdate(ctx context.Context, req *logical.Re userTokenConfig.IncludeReferenceToken = val.(bool) } + if val, ok := data.GetOk("use_expiring_tokens"); ok { + userTokenConfig.UseExpiringTokens = val.(bool) + } + if val, ok := data.GetOk("default_ttl"); ok { userTokenConfig.DefaultTTL = time.Duration(val.(int)) * time.Second } @@ -145,43 +166,46 @@ func (b *backend) pathConfigUserTokenRead(ctx context.Context, req *logical.Requ b.configMutex.RLock() defer b.configMutex.RUnlock() - config, err := b.fetchAdminConfiguration(ctx, req.Storage) + adminConfig, err := b.fetchAdminConfiguration(ctx, req.Storage) if err != nil { return nil, err } - if config == nil { + if adminConfig == nil { return logical.ErrorResponse("backend not configured"), nil } - go b.sendUsage(*config, "pathConfigUserTokenRead") + go b.sendUsage(*adminConfig, "pathConfigUserTokenRead") userTokenConfig, err := b.fetchUserTokenConfiguration(ctx, req.Storage) if err != nil { return nil, err } + accessTokenHash := sha256.Sum256([]byte(userTokenConfig.AccessToken)) + configMap := map[string]interface{}{ + "access_token_sha256": fmt.Sprintf("%x", accessTokenHash[:]), "audience": userTokenConfig.Audience, "refreshable": userTokenConfig.Refreshable, "include_reference_token": userTokenConfig.IncludeReferenceToken, + "use_expiring_tokens": userTokenConfig.UseExpiringTokens, "default_ttl": userTokenConfig.DefaultTTL.Seconds(), "max_ttl": userTokenConfig.MaxTTL.Seconds(), "default_description": userTokenConfig.DefaultDescription, } // Optionally include token info if it parses properly - token, err := b.getTokenInfo(*config, config.AccessToken) + token, err := b.getTokenInfo(*adminConfig, adminConfig.AccessToken) if err != nil { - b.Logger().Warn("Error parsing AccessToken: " + err.Error()) + b.Logger().Warn("Error parsing AccessToken", "err", err.Error()) } else { configMap["token_id"] = token.TokenID configMap["username"] = token.Username configMap["scope"] = token.Scope if token.Expires > 0 { configMap["exp"] = token.Expires - tm := time.Unix(token.Expires, 0) - configMap["expires"] = tm.Local() + configMap["expires"] = time.Unix(token.Expires, 0).Local() } } diff --git a/path_config_user_token_test.go b/path_config_user_token_test.go index a063d7c..76ddfae 100644 --- a/path_config_user_token_test.go +++ b/path_config_user_token_test.go @@ -12,14 +12,31 @@ func TestAcceptanceBackend_PathConfigUserToken(t *testing.T) { } accTestEnv := NewConfiguredAcceptanceTestEnv(t) + t.Run("update access_token", accTestEnv.PathConfigAccessTokenUpdate) 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 use_expiring_tokens", accTestEnv.PathConfigUseExpiringTokensUpdate) t.Run("update default_ttl", accTestEnv.PathConfigDefaultTTLUpdate) t.Run("update max_ttl", accTestEnv.PathConfigMaxTTLUpdate) } +func (e *accTestEnv) PathConfigAccessTokenUpdate(t *testing.T) { + e.UpdateConfigUserToken(t, testData{ + "access_token": "test123", + }) + data := e.ReadConfigUserToken(t) + accessTokenHash := data["access_token_sha256"] + assert.NotEmpty(t, "access_token_sha256") + + e.UpdateConfigUserToken(t, testData{ + "access_token": "test456", + }) + data = e.ReadConfigUserToken(t) + assert.NotEqual(t, data["access_token_sha256"], accessTokenHash) +} + func (e *accTestEnv) PathConfigDefaultDescriptionUpdate(t *testing.T) { e.pathConfigUserTokenUpdateStringField(t, "default_description") } @@ -36,6 +53,10 @@ func (e *accTestEnv) PathConfigIncludeReferenceTokenUpdate(t *testing.T) { e.pathConfigUserTokenUpdateBoolField(t, "include_reference_token") } +func (e *accTestEnv) PathConfigUseExpiringTokensUpdate(t *testing.T) { + e.pathConfigUserTokenUpdateBoolField(t, "use_expiring_tokens") +} + func (e *accTestEnv) PathConfigDefaultTTLUpdate(t *testing.T) { e.pathConfigUserTokenUpdateDurationField(t, "default_ttl") } diff --git a/path_token_create.go b/path_token_create.go index e766426..ae89e61 100644 --- a/path_token_create.go +++ b/path_token_create.go @@ -124,6 +124,7 @@ func (b *backend) pathTokenCreatePerform(ctx context.Context, req *logical.Reque "access_token": resp.AccessToken, "refresh_token": resp.RefreshToken, "role": roleName, + "expires_in": resp.ExpiresIn, "scope": resp.Scope, "token_id": resp.TokenId, "username": role.Username, @@ -132,6 +133,8 @@ func (b *backend) pathTokenCreatePerform(ctx context.Context, req *logical.Reque "role": roleName, "access_token": resp.AccessToken, "refresh_token": resp.RefreshToken, + "expires_in": resp.ExpiresIn, + "scope": resp.Scope, "token_id": resp.TokenId, "username": role.Username, "reference_token": resp.ReferenceToken, diff --git a/path_user_token_create.go b/path_user_token_create.go index 04adf48..14b7540 100644 --- a/path_user_token_create.go +++ b/path_user_token_create.go @@ -31,6 +31,11 @@ func (b *backend) pathUserTokenCreate() *framework.Path { 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.`, }, + "use_expiring_tokens": { + Type: framework.TypeBool, + Default: false, + Description: "Optional. If Artifactory version >= 7.50.3, set expires_in to max_ttl and force_revocable.", + }, "max_ttl": { Type: framework.TypeDurationSecond, Description: `Optional. Override the maximum TTL for this access token. Cannot exceed smallest (system, mount, backend) maximum TTL.`, @@ -54,38 +59,52 @@ func (b *backend) pathUserTokenCreatePerform(ctx context.Context, req *logical.R b.configMutex.RLock() defer b.configMutex.RUnlock() - config, err := b.fetchAdminConfiguration(ctx, req.Storage) + adminConfig, err := b.fetchAdminConfiguration(ctx, req.Storage) if err != nil { return nil, err } - if config == nil { + if adminConfig == nil { return logical.ErrorResponse("backend not configured"), nil } - go b.sendUsage(*config, "pathUserTokenCreatePerform") + go b.sendUsage(*adminConfig, "pathUserTokenCreatePerform") userTokenConfig, err := b.fetchUserTokenConfiguration(ctx, req.Storage) if err != nil { return nil, err } + if len(userTokenConfig.AccessToken) > 0 { + adminConfig.AccessToken = userTokenConfig.AccessToken + } + + adminConfig.UseExpiringTokens = userTokenConfig.UseExpiringTokens + if value, ok := data.GetOk("use_expiring_tokens"); ok { + adminConfig.UseExpiringTokens = value.(bool) + } + role := artifactoryRole{ - GrantType: "client_credentials", - Username: data.Get("username").(string), - Scope: "applied-permissions/user", - MaxTTL: b.Backend.System().MaxLeaseTTL(), - Description: userTokenConfig.DefaultDescription, + GrantType: "client_credentials", + Username: data.Get("username").(string), + Scope: "applied-permissions/user", + MaxTTL: b.Backend.System().MaxLeaseTTL(), + Audience: userTokenConfig.Audience, + Refreshable: userTokenConfig.Refreshable, + IncludeReferenceToken: userTokenConfig.IncludeReferenceToken, + Description: userTokenConfig.DefaultDescription, } + b.Logger().Debug("pathUserTokenCreatePerform", "role.Description", role.Description) + if userTokenConfig.MaxTTL != 0 && userTokenConfig.MaxTTL < role.MaxTTL { role.MaxTTL = userTokenConfig.MaxTTL } if value, ok := data.GetOk("max_ttl"); ok { - value := time.Second * time.Duration(value.(int)) - if value != 0 && value < role.MaxTTL { - role.MaxTTL = value + maxTTL := time.Second * time.Duration(value.(int)) + if maxTTL != 0 && maxTTL < role.MaxTTL { + role.MaxTTL = maxTTL } } @@ -118,7 +137,7 @@ func (b *backend) pathUserTokenCreatePerform(ctx context.Context, req *logical.R role.Description = value.(string) } - resp, err := b.CreateToken(*config, role) + resp, err := b.CreateToken(*adminConfig, role) if err != nil { return nil, err } @@ -126,6 +145,7 @@ func (b *backend) pathUserTokenCreatePerform(ctx context.Context, req *logical.R response := b.Secret(SecretArtifactoryAccessTokenType).Response(map[string]interface{}{ "access_token": resp.AccessToken, "refresh_token": resp.RefreshToken, + "expires_in": resp.ExpiresIn, "scope": resp.Scope, "token_id": resp.TokenId, "username": role.Username, @@ -134,6 +154,8 @@ func (b *backend) pathUserTokenCreatePerform(ctx context.Context, req *logical.R }, map[string]interface{}{ "access_token": resp.AccessToken, "refresh_token": resp.RefreshToken, + "expires_in": resp.ExpiresIn, + "scope": resp.Scope, "token_id": resp.TokenId, "username": role.Username, "reference_token": resp.ReferenceToken, diff --git a/path_user_token_create_test.go b/path_user_token_create_test.go index 62cd6e6..1b7c632 100644 --- a/path_user_token_create_test.go +++ b/path_user_token_create_test.go @@ -18,3 +18,18 @@ func TestAcceptanceBackend_PathUserTokenCreate(t *testing.T) { t.Run("create token for admin user", accTestEnv.CreatePathUserToken) t.Run("cleanup backend", accTestEnv.DeletePathConfig) } + +func TestAcceptanceBackend_PathUserTokenCreate_overrides(t *testing.T) { + if !runAcceptanceTests { + t.SkipNow() + } + + accTestEnv, err := newAcceptanceTestEnv() + if err != nil { + t.Fatal(err) + } + + t.Run("configure backend", accTestEnv.UpdatePathConfig) + t.Run("create token for admin user", accTestEnv.CreatePathUserToken_overrides) + t.Run("cleanup backend", accTestEnv.DeletePathConfig) +} diff --git a/test_utils.go b/test_utils.go index 7bd26c3..ac2012d 100644 --- a/test_utils.go +++ b/test_utils.go @@ -72,6 +72,8 @@ func (e *accTestEnv) createNewNonAdminTestToken(t *testing.T) (string, string) { Scope: "applied-permissions/groups:readers", } + e.Backend.(*backend).InitializeHttpClient(&config) + err := e.Backend.(*backend).getVersion(config) if err != nil { t.Fatal(err) @@ -268,6 +270,52 @@ func (e *accTestEnv) CreatePathToken(t *testing.T) { func (e *accTestEnv) CreatePathUserToken(t *testing.T) { resp, err := e.Backend.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.UpdateOperation, + Path: "config/user_token", + Storage: e.Storage, + Data: map[string]interface{}{ + "default_description": "foo", + "refreshable": true, + "include_reference_token": true, + "use_expiring_tokens": true, + }, + }) + + assert.NoError(t, err) + assert.Nil(t, resp) + + resp, err = e.Backend.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.ReadOperation, + Path: "user_token/admin", + Storage: e.Storage, + }) + + assert.NoError(t, err) + assert.NotNil(t, resp) + assert.NotEmpty(t, resp.Data["access_token"]) + assert.NotEmpty(t, resp.Data["token_id"]) + assert.Equal(t, "admin", resp.Data["username"]) + assert.Equal(t, "applied-permissions/user", resp.Data["scope"]) + assert.Equal(t, "foo", resp.Data["description"]) + assert.NotEmpty(t, resp.Data["expires_in"]) + assert.NotEmpty(t, resp.Data["refresh_token"]) + assert.NotEmpty(t, resp.Data["reference_token"]) +} + +func (e *accTestEnv) CreatePathUserToken_overrides(t *testing.T) { + resp, err := e.Backend.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.UpdateOperation, + Path: "config/user_token", + Storage: e.Storage, + Data: map[string]interface{}{ + "default_description": "foo", + }, + }) + + assert.NoError(t, err) + assert.Nil(t, resp) + + resp, err = e.Backend.HandleRequest(context.Background(), &logical.Request{ Operation: logical.ReadOperation, Path: "user_token/admin", Storage: e.Storage, @@ -275,6 +323,7 @@ func (e *accTestEnv) CreatePathUserToken(t *testing.T) { "description": "buffalo", "refreshable": true, "include_reference_token": true, + "use_expiring_tokens": true, }, }) @@ -285,6 +334,7 @@ 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["expires_in"]) assert.NotEmpty(t, resp.Data["refresh_token"]) assert.NotEmpty(t, resp.Data["reference_token"]) } @@ -302,7 +352,9 @@ func newAcceptanceTestEnv() (*accTestEnv, error) { ctx := context.Background() conf := &logical.BackendConfig{ - System: &logical.StaticSystemView{}, + System: &logical.StaticSystemView{ + MaxLeaseTTLVal: time.Duration(2592000) * time.Second, // 30 days + }, Logger: logging.NewVaultLogger(log.Debug), } backend, err := Factory(ctx, conf) @@ -408,6 +460,7 @@ func makeBackend(t *testing.T) (*backend, *logical.BackendConfig) { func configuredBackend(t *testing.T, adminConfig map[string]interface{}) (*backend, *logical.BackendConfig) { b, config := makeBackend(t) + b.InitializeHttpClient(&adminConfiguration{}) _, err := b.HandleRequest(context.Background(), &logical.Request{ Operation: logical.UpdateOperation, diff --git a/ttl_test.go b/ttl_test.go index 49d1da1..695bb3b 100644 --- a/ttl_test.go +++ b/ttl_test.go @@ -36,7 +36,7 @@ func TestBackend_NoRoleMaxTTLUsesSystemMaxTTL(t *testing.T) { b, config := configuredBackend(t, map[string]interface{}{ "access_token": "test-access-token", - "url": "http://myserver.com:80/artifactory", + "url": "http://myserver.com:80", }) // Role with no maximum TTL @@ -82,7 +82,7 @@ func TestBackend_WorkingWithBothMaxTTLs(t *testing.T) { b, config := configuredBackend(t, map[string]interface{}{ "access_token": "test-access-token", - "url": "http://myserver.com:80/artifactory", + "url": "http://myserver.com:80", "max_ttl": 10 * time.Minute, }) @@ -138,7 +138,7 @@ func TestBackend_NoUserTokensMaxTTLUsesSystemMaxTTL(t *testing.T) { b, config := configuredBackend(t, map[string]interface{}{ "access_token": "test-access-token", - "url": "http://myserver.com:80/artifactory", + "url": "http://myserver.com:80", }) resp, err := b.HandleRequest(context.Background(), &logical.Request{ @@ -179,7 +179,7 @@ func TestBackend_UserTokenConfigMaxTTLUseSystem(t *testing.T) { b, config := configuredBackend(t, map[string]interface{}{ "access_token": "test-access-token", - "url": "http://myserver.com:80/artifactory", + "url": "http://myserver.com:80", }) backend_max_ttl := b.System().MaxLeaseTTL() @@ -212,7 +212,7 @@ func TestBackend_UserTokenConfigMaxTTLUseConfigMaxTTL(t *testing.T) { b, config := configuredBackend(t, map[string]interface{}{ "access_token": "test-access-token", - "url": "http://myserver.com:80/artifactory", + "url": "http://myserver.com:80", }) backend_max_ttl := b.System().MaxLeaseTTL() @@ -245,7 +245,7 @@ func TestBackend_UserTokenMaxTTLUseRequestTTL(t *testing.T) { b, config := configuredBackend(t, map[string]interface{}{ "access_token": "test-access-token", - "url": "http://myserver.com:80/artifactory", + "url": "http://myserver.com:80", }) backend_max_ttl := b.System().MaxLeaseTTL() @@ -279,7 +279,7 @@ func TestBackend_UserTokenMaxTTLEnforced(t *testing.T) { b, config := configuredBackend(t, map[string]interface{}{ "access_token": "test-access-token", - "url": "http://myserver.com:80/artifactory", + "url": "http://myserver.com:80", }) backend_max_ttl := b.System().MaxLeaseTTL() @@ -314,7 +314,7 @@ func TestBackend_UserTokenTTLRequest(t *testing.T) { b, config := configuredBackend(t, map[string]interface{}{ "access_token": "test-access-token", - "url": "http://myserver.com:80/artifactory", + "url": "http://myserver.com:80", }) resp, err := b.HandleRequest(context.Background(), &logical.Request{ @@ -345,7 +345,7 @@ func TestBackend_UserTokenDefaultTTL(t *testing.T) { b, config := configuredBackend(t, map[string]interface{}{ "access_token": "test-access-token", - "url": "http://myserver.com:80/artifactory", + "url": "http://myserver.com:80", }) resp, err := b.HandleRequest(context.Background(), &logical.Request{