From a95feb43d50aa9afc05fc947f93cd9985a8d8daa Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Wed, 15 May 2024 13:11:25 -0700 Subject: [PATCH 1/6] Add project admin role scope validation --- docs/resources/scoped_token.md | 7 +- .../resource_artifactory_scoped_token.go | 51 ++++++------ .../resource_artifactory_scoped_token_test.go | 80 ++++++++++++++++++- 3 files changed, 108 insertions(+), 30 deletions(-) diff --git a/docs/resources/scoped_token.md b/docs/resources/scoped_token.md index a8998893f..d6dc51033 100644 --- a/docs/resources/scoped_token.md +++ b/docs/resources/scoped_token.md @@ -84,7 +84,7 @@ resource "artifactory_scoped_token" "audience" { - `applied-permissions/admin` - the scope assigned to admin users. - `applied-permissions/groups` - this scope assigns permissions to groups using the following format: `applied-permissions/groups:[,...]` - `system:metrics:r` - for getting the service metrics - - `system:livelogs:r` - for getting the service livelogs. The scope to assign to the token should be provided as a list of scope tokens, limited to 500 characters in total. + - `system:livelogs:r` - for getting the service livelogs - Resource Permissions: From Artifactory 7.38.x, resource permissions scoped tokens are also supported in the REST API. A permission can be represented as a scope token string in the following format: `:[/]:` - Where: - `` - one of the permission resource types, from a predefined closed list. Currently, the only resource type that is supported is the artifact resource type. @@ -95,6 +95,11 @@ resource "artifactory_scoped_token" "audience" { - `["applied-permissions/user", "artifact:generic-local:r"]` - `["applied-permissions/group", "artifact:generic-local/path:*"]` - `["applied-permissions/admin", "system:metrics:r", "artifact:generic-local:*"]` + - `applied-permissions/roles:project-key` - provides access to elements associated with the project based on the project role. For example, `applied-permissions/roles:project-type:developer,qa`. + + ->The scope to assign to the token should be provided as a list of scope tokens, limited to 500 characters in total. + + From Artifactory 7.84.3, [project admins](https://jfrog.com/help/r/jfrog-platform-administration-documentation/access-token-creation-by-project-admins) can create access tokens that are tied to the projects in which they hold administrative privileges. - `username` (String) The user name for which this token is created. The username is based on the authenticated user - either from the user of the authenticated token or based on the username (if basic auth was used). The username is then used to set the subject of the token: `/users/`. Limited to 255 characters. ### Read-Only diff --git a/pkg/artifactory/resource/security/resource_artifactory_scoped_token.go b/pkg/artifactory/resource/security/resource_artifactory_scoped_token.go index 0d6b3122c..605e93dee 100644 --- a/pkg/artifactory/resource/security/resource_artifactory_scoped_token.go +++ b/pkg/artifactory/resource/security/resource_artifactory_scoped_token.go @@ -8,7 +8,6 @@ import ( "strings" regex2 "github.com/dlclark/regexp2" - "github.com/go-resty/resty/v2" "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" @@ -160,10 +159,8 @@ func (r *ScopedTokenResource) Schema(ctx context.Context, req resource.SchemaReq "* `applied-permissions/admin` - the scope assigned to admin users." + "* `applied-permissions/groups` - this scope assigns permissions to groups using the following format: applied-permissions/groups:[,...]" + "* `system:metrics:r` - for getting the service metrics" + - "* `system:livelogs:r` - for getting the service livelogsr. " + - "The scope to assign to the token should be provided as a list of scope tokens, limited to 500 characters in total.\n" + - "Resource Permissions\n" + - "From Artifactory 7.38.x, resource permissions scoped tokens are also supported in the REST API. " + + "* `system:livelogs:r` - for getting the service livelogsr." + + "Resource Permissions: From Artifactory 7.38.x, resource permissions scoped tokens are also supported in the REST API. " + "A permission can be represented as a scope token string in the following format:\n" + "`:[/]:`\n" + "Where:\n" + @@ -177,7 +174,10 @@ func (r *ScopedTokenResource) Schema(ctx context.Context, req resource.SchemaReq "Examples: " + " `[\"applied-permissions/user\", \"artifact:generic-local:r\"]`\n" + " `[\"applied-permissions/group\", \"artifact:generic-local/path:*\"]`\n" + - " `[\"applied-permissions/admin\", \"system:metrics:r\", \"artifact:generic-local:*\"]`", + " `[\"applied-permissions/admin\", \"system:metrics:r\", \"artifact:generic-local:*\"]`\n" + + "* `applied-permissions/roles:project-key` - provides access to elements associated with the project based on the project role. For example, `applied-permissions/roles:project-type:developer,qa`." + + "The scope to assign to the token should be provided as a list of scope tokens, limited to 500 characters in total.\n" + + "From Artifactory 7.84.3, project admins (https://jfrog.com/help/r/jfrog-platform-administration-documentation/access-token-creation-by-project-admins) can create access tokens that are tied to the projects in which they hold administrative privileges.", Optional: true, Computed: true, ElementType: types.StringType, @@ -186,16 +186,18 @@ func (r *ScopedTokenResource) Schema(ctx context.Context, req resource.SchemaReq setplanmodifier.UseStateForUnknown(), }, Validators: []validator.Set{ - setvalidator.ValueStringsAre(stringvalidator.Any( - stringvalidator.OneOf( - "applied-permissions/user", - "applied-permissions/admin", - "system:metrics:r", - "system:livelogs:r", + setvalidator.ValueStringsAre( + stringvalidator.Any( + stringvalidator.OneOf( + "applied-permissions/user", + "applied-permissions/admin", + "system:metrics:r", + "system:livelogs:r", + ), + stringvalidator.RegexMatches(regexp.MustCompile(`^applied-permissions\/groups:.+$`), "must be 'applied-permissions/groups:[,...]'"), + stringvalidator.RegexMatches(regexp.MustCompile(`^applied-permissions\/roles:.+:.+$`), "must be 'applied-permissions/roles::[,...]'"), + stringvalidator.RegexMatches(regexp.MustCompile(`^artifact:.+:([rwdamxs*]|([rwdamxs]+(,[rwdamxs]+)))$`), "must be ':[/]:'"), ), - stringvalidator.RegexMatches(regexp.MustCompile(`^applied-permissions\/groups:.+$`), "must be 'applied-permissions/groups:[,...]'"), - stringvalidator.RegexMatches(regexp.MustCompile(`^artifact:.+:([rwdamxs*]|([rwdamxs]+(,[rwdamxs]+)))$`), "must be ':[/]:'"), - ), ), }, }, @@ -251,16 +253,17 @@ func (r *ScopedTokenResource) Schema(ctx context.Context, req resource.SchemaReq setplanmodifier.UseStateForUnknown(), }, Validators: []validator.Set{ - setvalidator.ValueStringsAre(stringvalidator.All( - stringvalidator.LengthAtLeast(1), - stringvalidator.RegexMatches(regexp.MustCompile(fmt.Sprintf(`^(%s|\*)@.+`, strings.Join(serviceTypesScopedToken, "|"))), - fmt.Sprintf( - "must either begin with %s, or *", - strings.Join(serviceTypesScopedToken, ", "), + setvalidator.ValueStringsAre( + stringvalidator.All( + stringvalidator.LengthAtLeast(1), + stringvalidator.RegexMatches(regexp.MustCompile(fmt.Sprintf(`^(%s|\*)@.+`, strings.Join(serviceTypesScopedToken, "|"))), + fmt.Sprintf( + "must either begin with %s, or *", + strings.Join(serviceTypesScopedToken, ", "), + ), ), ), ), - ), }, }, "access_token": schema.StringAttribute{ @@ -657,7 +660,3 @@ func (r *ScopedTokenResourceModel) GetResponseToState(ctx context.Context, acces r.ReferenceToken = types.StringValue("") } } - -func CheckAccessToken(id string, request *resty.Request) (*resty.Response, error) { - return request.SetPathParam("id", id).Get("access/api/v1/tokens/{id}") -} diff --git a/pkg/artifactory/resource/security/resource_artifactory_scoped_token_test.go b/pkg/artifactory/resource/security/resource_artifactory_scoped_token_test.go index 0f2003b3b..afa875c61 100644 --- a/pkg/artifactory/resource/security/resource_artifactory_scoped_token_test.go +++ b/pkg/artifactory/resource/security/resource_artifactory_scoped_token_test.go @@ -8,7 +8,6 @@ import ( "github.com/go-resty/resty/v2" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/jfrog/terraform-provider-artifactory/v10/pkg/acctest" - "github.com/jfrog/terraform-provider-artifactory/v10/pkg/artifactory/resource/security" "github.com/jfrog/terraform-provider-shared/testutil" "github.com/jfrog/terraform-provider-shared/util" ) @@ -247,7 +246,7 @@ func TestAccScopedToken_WithDefaults(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, - CheckDestroy: acctest.VerifyDeleted(fqrn, security.CheckAccessToken), + CheckDestroy: acctest.VerifyDeleted(fqrn, checkAccessToken), Steps: []resource.TestStep{ { Config: accessTokenConfig, @@ -321,7 +320,7 @@ func TestAccScopedToken_WithAttributes(t *testing.T) { ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, CheckDestroy: acctest.VerifyDeleted(fqrn, func(id string, request *resty.Request) (*resty.Response, error) { acctest.DeleteProject(t, projectKey) - return security.CheckAccessToken(id, request) + return checkAccessToken(id, request) }), Steps: []resource.TestStep{ { @@ -545,6 +544,77 @@ func TestAccScopedToken_WithInvalidResourceScopes(t *testing.T) { }) } +func TestAccScopedToken_WithRoleScope(t *testing.T) { + _, fqrn, name := testutil.MkNames("test-access-token", "artifactory_scoped_token") + _, _, projectName := testutil.MkNames("test-project", "project") + _, _, projectUserName := testutil.MkNames("test-projecuser", "project_user") + _, _, username := testutil.MkNames("test-user", "artifactory_managed_user") + + email := username + "@tempurl.org" + + accessTokenConfig := util.ExecuteTemplate( + "TestAccScopedToken", + `resource "artifactory_managed_user" "{{ .username }}" { + name = "{{ .username }}" + email = "{{ .email }}" + admin = true + disable_ui_access = false + groups = ["readers"] + password = "Passw0rd!" + } + + resource "project" "{{ .projectName }}" { + key = "{{ .projectName }}" + display_name = "{{ .projectName }}" + admin_privileges { + manage_members = true + manage_resources = true + index_resources = true + } + } + + resource "project_user" "{{ .projectUserName }}" { + name = artifactory_managed_user.{{ .username }}.name + project_key = project.{{ .projectName }}.key + roles = ["Developer"] + } + + resource "artifactory_scoped_token" "{{ .name }}" { + username = artifactory_managed_user.{{ .username }}.name + scopes = [ + "applied-permissions/roles:${project.{{ .projectName }}.key}:Developer", + ] + }`, + map[string]interface{}{ + "name": name, + "username": username, + "email": email, + "projectName": projectName, + "projectUserName": projectUserName, + }, + ) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ExternalProviders: map[string]resource.ExternalProvider{ + "project": { + Source: "jfrog/project", + }, + }, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: accessTokenConfig, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(fqrn, "username", username), + resource.TestCheckResourceAttr(fqrn, "scopes.#", "1"), + resource.TestCheckTypeSetElemAttr(fqrn, "scopes.*", fmt.Sprintf("applied-permissions/roles:%s:Developer", projectName)), + ), + }, + }, + }) +} + func TestAccScopedToken_WithInvalidScopes(t *testing.T) { _, _, name := testutil.MkNames("test-scoped-token", "artifactory_scoped_token") @@ -816,3 +886,7 @@ func TestAccScopedToken_WithExpiresInSetToZeroForNonExpiringToken(t *testing.T) }, }) } + +func checkAccessToken(id string, request *resty.Request) (*resty.Response, error) { + return request.SetPathParam("id", id).Get("access/api/v1/tokens/{id}") +} From 3e555f90ee9ce65298b462bed40f863a0b5345c3 Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Wed, 15 May 2024 13:18:16 -0700 Subject: [PATCH 2/6] Remove hard coded project creation/deletion in test --- .../resource_artifactory_scoped_token_test.go | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/pkg/artifactory/resource/security/resource_artifactory_scoped_token_test.go b/pkg/artifactory/resource/security/resource_artifactory_scoped_token_test.go index afa875c61..615c2e90f 100644 --- a/pkg/artifactory/resource/security/resource_artifactory_scoped_token_test.go +++ b/pkg/artifactory/resource/security/resource_artifactory_scoped_token_test.go @@ -284,7 +284,7 @@ func TestAccScopedToken_WithDefaults(t *testing.T) { func TestAccScopedToken_WithAttributes(t *testing.T) { _, fqrn, name := testutil.MkNames("test-access-token", "artifactory_scoped_token") - projectKey := fmt.Sprintf("test-project-%d", testutil.RandomInt()) + _, _, projectKey := testutil.MkNames("test-project", "project") accessTokenConfig := util.ExecuteTemplate( "TestAccScopedToken", @@ -297,9 +297,19 @@ func TestAccScopedToken_WithAttributes(t *testing.T) { password = "Passw0rd!" } + resource "project" "{{ .projectKey }}" { + key = "{{ .projectKey }}" + display_name = "{{ .projectKey }}" + admin_privileges { + manage_members = true + manage_resources = true + index_resources = true + } + } + resource "artifactory_scoped_token" "{{ .name }}" { username = artifactory_user.test-user.name - project_key = "{{ .projectKey }}" + project_key = project.{{ .projectKey }}.key scopes = ["applied-permissions/admin", "system:metrics:r"] description = "test description" refreshable = true @@ -313,15 +323,14 @@ func TestAccScopedToken_WithAttributes(t *testing.T) { ) resource.Test(t, resource.TestCase{ - PreCheck: func() { - acctest.PreCheck(t) - acctest.CreateProject(t, projectKey) - }, + PreCheck: func() { acctest.PreCheck(t) }, ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, - CheckDestroy: acctest.VerifyDeleted(fqrn, func(id string, request *resty.Request) (*resty.Response, error) { - acctest.DeleteProject(t, projectKey) - return checkAccessToken(id, request) - }), + ExternalProviders: map[string]resource.ExternalProvider{ + "project": { + Source: "jfrog/project", + }, + }, + CheckDestroy: acctest.VerifyDeleted(fqrn, checkAccessToken), Steps: []resource.TestStep{ { Config: accessTokenConfig, From 7018018600e3313155130e42febac087bccdce88 Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Wed, 15 May 2024 15:48:51 -0700 Subject: [PATCH 3/6] Update CHANGELOG --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7af7d44f6..1e2a42f5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 10.8.0 (May 16, 2024) + +IMPROVEMENTS: + +* resource/artifactory_scoped_token: Add support for project admin token scope. See [Access Token Creation by Project Admins](https://jfrog.com/help/r/jfrog-platform-administration-documentation/access-token-creation-by-project-admins) for more details. PR: [#965](https://github.com/jfrog/terraform-provider-artifactory/pull/965) + ## 10.7.6 (May 10, 2024) BUG FIXES: From f80982d5945e9783f9b60e2c41a8f8e66fbcc4d7 Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Wed, 15 May 2024 16:11:44 -0700 Subject: [PATCH 4/6] Update CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e2a42f5a..146d759a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ IMPROVEMENTS: -* resource/artifactory_scoped_token: Add support for project admin token scope. See [Access Token Creation by Project Admins](https://jfrog.com/help/r/jfrog-platform-administration-documentation/access-token-creation-by-project-admins) for more details. PR: [#965](https://github.com/jfrog/terraform-provider-artifactory/pull/965) +* resource/artifactory_scoped_token: Add support for project admin token scope introduced in Artifactory 7.84.3. See [Access Token Creation by Project Admins](https://jfrog.com/help/r/jfrog-platform-administration-documentation/access-token-creation-by-project-admins) for more details. PR: [#965](https://github.com/jfrog/terraform-provider-artifactory/pull/965) ## 10.7.6 (May 10, 2024) From 32cb78b7fa764d2553053d6e31914b1d3df41622 Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Thu, 16 May 2024 10:19:31 -0700 Subject: [PATCH 5/6] Fix user test --- .../user/resource_artifactory_unmanaged_user_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/artifactory/resource/user/resource_artifactory_unmanaged_user_test.go b/pkg/artifactory/resource/user/resource_artifactory_unmanaged_user_test.go index 4ba677b1e..81f27565f 100644 --- a/pkg/artifactory/resource/user/resource_artifactory_unmanaged_user_test.go +++ b/pkg/artifactory/resource/user/resource_artifactory_unmanaged_user_test.go @@ -348,7 +348,7 @@ func TestAccUnmanagedUser_name_change(t *testing.T) { password = "Password!123" profile_updatable = true disable_ui_access = false - internal_password_disabled = true + internal_password_disabled = false } ` @@ -359,7 +359,7 @@ func TestAccUnmanagedUser_name_change(t *testing.T) { password = "Password!123" profile_updatable = true disable_ui_access = false - internal_password_disabled = true + internal_password_disabled = false } ` @@ -379,7 +379,7 @@ func TestAccUnmanagedUser_name_change(t *testing.T) { resource.TestCheckResourceAttr(fqrn, "name", username), resource.TestCheckResourceAttr(fqrn, "email", fmt.Sprintf("dummy_user%d@a.com", id)), resource.TestCheckResourceAttr(fqrn, "profile_updatable", "true"), - resource.TestCheckResourceAttr(fqrn, "internal_password_disabled", "true"), + resource.TestCheckResourceAttr(fqrn, "internal_password_disabled", "false"), resource.TestCheckResourceAttr(fqrn, "password", "Password!123"), resource.TestCheckResourceAttr(fqrn, "groups.#", "0"), ), From 679c5fbd640bb2143ff65ee66498e8b63ca39133 Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Mon, 20 May 2024 09:02:40 -0700 Subject: [PATCH 6/6] Fix token test check that keeps changing value by artifactory --- .../resource/security/resource_artifactory_scoped_token_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/artifactory/resource/security/resource_artifactory_scoped_token_test.go b/pkg/artifactory/resource/security/resource_artifactory_scoped_token_test.go index 615c2e90f..5ffa60e0a 100644 --- a/pkg/artifactory/resource/security/resource_artifactory_scoped_token_test.go +++ b/pkg/artifactory/resource/security/resource_artifactory_scoped_token_test.go @@ -125,7 +125,7 @@ func TestAccScopedToken_UpgradeGH_818(t *testing.T) { Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(fqrn, "username", "testuser"), resource.TestCheckResourceAttr(fqrn, "scopes.#", "1"), - resource.TestCheckResourceAttr(fqrn, "expires_in", "32000000"), + resource.TestCheckResourceAttrSet(fqrn, "expires_in"), resource.TestCheckNoResourceAttr(fqrn, "audiences"), resource.TestCheckResourceAttrSet(fqrn, "access_token"), resource.TestCheckNoResourceAttr(fqrn, "refresh_token"),