From e5a8007da20d9fa396d7f5b2c48769ee4ef7e923 Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Mon, 12 Feb 2024 14:28:27 -0800 Subject: [PATCH 1/6] Fix incorrect Artifactory host URL path for old APIs --- artifactory.go | 4 ++-- artifactory_test.go | 6 +++--- ttl_test.go | 18 +++++++++--------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/artifactory.go b/artifactory.go index cb775f5..b4dda65 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 @@ -125,7 +125,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) 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/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{ From 8a188b5dadb64fd5af53b09030b604a1391689cf Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Mon, 12 Feb 2024 14:29:11 -0800 Subject: [PATCH 2/6] Improve error logging in Artifactory backend --- artifactory.go | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/artifactory.go b/artifactory.go index b4dda65..6ead920 100644 --- a/artifactory.go +++ b/artifactory.go @@ -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 } @@ -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 From 1dd132d4fcc84b386f49c9269dbee07452835888 Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Mon, 12 Feb 2024 14:36:16 -0800 Subject: [PATCH 3/6] Add support for use_expiring_tokens to user token config Add same for override in user_token path toke creation --- path_config.go | 2 ++ path_config_user_token.go | 25 +++++++++++----- path_config_user_token_test.go | 5 ++++ path_token_create.go | 3 ++ path_user_token_create.go | 42 +++++++++++++++++++-------- path_user_token_create_test.go | 15 ++++++++++ test_utils.go | 53 +++++++++++++++++++++++++++++++++- 7 files changed, 125 insertions(+), 20 deletions(-) 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..815f3fb 100644 --- a/path_config_user_token.go +++ b/path_config_user_token.go @@ -2,6 +2,7 @@ package artifactory import ( "context" + "fmt" "time" "github.com/hashicorp/vault/sdk/framework" @@ -26,6 +27,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.`, @@ -58,6 +64,7 @@ type userTokenConfiguration struct { 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"` @@ -116,6 +123,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,16 +156,16 @@ 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 { @@ -165,23 +176,23 @@ func (b *backend) pathConfigUserTokenRead(ctx context.Context, req *logical.Requ "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..72e73b9 100644 --- a/path_config_user_token_test.go +++ b/path_config_user_token_test.go @@ -16,6 +16,7 @@ func TestAcceptanceBackend_PathConfigUserToken(t *testing.T) { 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) } @@ -36,6 +37,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..9852f74 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,48 @@ 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 } + 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 +133,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 +141,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 +150,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..b90e263 100644 --- a/test_utils.go +++ b/test_utils.go @@ -268,6 +268,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 +321,7 @@ func (e *accTestEnv) CreatePathUserToken(t *testing.T) { "description": "buffalo", "refreshable": true, "include_reference_token": true, + "use_expiring_tokens": true, }, }) @@ -285,6 +332,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 +350,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 +458,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, From e804203ad29632895192ff3816fef375112cda0d Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Mon, 12 Feb 2024 15:47:38 -0800 Subject: [PATCH 4/6] Add access_token override support to user_token config --- path_config_user_token.go | 13 +++++++++++++ path_config_user_token_test.go | 16 ++++++++++++++++ path_user_token_create.go | 4 ++++ test_utils.go | 2 ++ 4 files changed, 35 insertions(+) diff --git a/path_config_user_token.go b/path_config_user_token.go index 815f3fb..9f1e4ae 100644 --- a/path_config_user_token.go +++ b/path_config_user_token.go @@ -2,6 +2,7 @@ package artifactory import ( "context" + "crypto/sha256" "fmt" "time" @@ -13,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.`, @@ -61,6 +66,7 @@ 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"` @@ -111,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) } @@ -172,7 +182,10 @@ func (b *backend) pathConfigUserTokenRead(ctx context.Context, req *logical.Requ 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, diff --git a/path_config_user_token_test.go b/path_config_user_token_test.go index 72e73b9..76ddfae 100644 --- a/path_config_user_token_test.go +++ b/path_config_user_token_test.go @@ -12,6 +12,7 @@ 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) @@ -21,6 +22,21 @@ func TestAcceptanceBackend_PathConfigUserToken(t *testing.T) { 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") } diff --git a/path_user_token_create.go b/path_user_token_create.go index 9852f74..14b7540 100644 --- a/path_user_token_create.go +++ b/path_user_token_create.go @@ -75,6 +75,10 @@ func (b *backend) pathUserTokenCreatePerform(ctx context.Context, req *logical.R 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) diff --git a/test_utils.go b/test_utils.go index b90e263..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) From 4518f00b1453853ba76dde0294b53d35e252915c Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Tue, 13 Feb 2024 09:33:48 -0800 Subject: [PATCH 5/6] Update README --- Makefile | 4 +--- README.md | 34 ++++++++++++++++++++++++++++++---- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 9e08f87..42b36b1 100644 --- a/Makefile +++ b/Makefile @@ -57,12 +57,10 @@ test: acceptance: export VAULT_ACC=true && \ - export JFROG_ACCESS_TOKEN=$(JFROG_ACCESS_TOKEN) && \ go test -run TestAcceptance -cover -coverprofile=coverage.txt -v -p 1 -timeout 5m ./... alltests: export VAULT_ACC=true && \ - export JFROG_ACCESS_TOKEN=$(JFROG_ACCESS_TOKEN) && \ go test -cover -coverprofile=coverage.out -v -p 1 -timeout 5m ./... clean: @@ -83,7 +81,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 315b93e..d56e005 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: From 54aacddc56b4c045bfd4945e171c27a659413bef Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Tue, 13 Feb 2024 09:49:59 -0800 Subject: [PATCH 6/6] Restore export for JFROG_ACCESS_TOKEN for testing --- Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Makefile b/Makefile index 42b36b1..5c0f5b3 100644 --- a/Makefile +++ b/Makefile @@ -57,10 +57,12 @@ test: acceptance: export VAULT_ACC=true && \ + export JFROG_ACCESS_TOKEN=$(JFROG_ACCESS_TOKEN) && \ go test -run TestAcceptance -cover -coverprofile=coverage.txt -v -p 1 -timeout 5m ./... alltests: export VAULT_ACC=true && \ + export JFROG_ACCESS_TOKEN=$(JFROG_ACCESS_TOKEN) && \ go test -cover -coverprofile=coverage.out -v -p 1 -timeout 5m ./... clean: