Skip to content

Commit

Permalink
Merge pull request #965 from jfrog/add-project-admin-token-support
Browse files Browse the repository at this point in the history
Add project admin token support
  • Loading branch information
alexhung authored May 20, 2024
2 parents dc22073 + 679c5fb commit 7c3f7c2
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 43 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 10.8.0 (May 20, 2024)

IMPROVEMENTS:

* 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.7 (May 17, 2024). Tested on Artifactory 7.84.10 with Terraform CLI v1.8.3

BUG FIXES:
Expand Down
7 changes: 6 additions & 1 deletion docs/resources/scoped_token.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:<group-name>[,<group-name>...]`
- `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: `<resource-type>:<target>[/<sub-resource>]:<actions>`
- Where:
- `<resource-type>` - one of the permission resource types, from a predefined closed list. Currently, the only resource type that is supported is the artifact resource type.
Expand All @@ -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: `<service-id>/users/<username>`. Limited to 255 characters.

### Read-Only
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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:<group-name>[,<group-name>...]" +
"* `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" +
"`<resource-type>:<target>[/<sub-resource>]:<actions>`\n" +
"Where:\n" +
Expand All @@ -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,
Expand All @@ -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:<group-name>[,<group-name>...]'"),
stringvalidator.RegexMatches(regexp.MustCompile(`^applied-permissions\/roles:.+:.+$`), "must be 'applied-permissions/roles:<project-key>:<role-name>[,<role-name>...]'"),
stringvalidator.RegexMatches(regexp.MustCompile(`^artifact:.+:([rwdamxs*]|([rwdamxs]+(,[rwdamxs]+)))$`), "must be '<resource-type>:<target>[/<sub-resource>]:<actions>'"),
),
stringvalidator.RegexMatches(regexp.MustCompile(`^applied-permissions\/groups:.+$`), "must be 'applied-permissions/groups:<group-name>[,<group-name>...]'"),
stringvalidator.RegexMatches(regexp.MustCompile(`^artifact:.+:([rwdamxs*]|([rwdamxs]+(,[rwdamxs]+)))$`), "must be '<resource-type>:<target>[/<sub-resource>]:<actions>'"),
),
),
},
},
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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}")
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -126,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"),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -285,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",
Expand All @@ -298,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
Expand All @@ -314,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 security.CheckAccessToken(id, request)
}),
ExternalProviders: map[string]resource.ExternalProvider{
"project": {
Source: "jfrog/project",
},
},
CheckDestroy: acctest.VerifyDeleted(fqrn, checkAccessToken),
Steps: []resource.TestStep{
{
Config: accessTokenConfig,
Expand Down Expand Up @@ -545,6 +553,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")

Expand Down Expand Up @@ -816,3 +895,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}")
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
`

Expand All @@ -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
}
`

Expand All @@ -379,7 +379,7 @@ func TestAccUnmanagedUser_name_change(t *testing.T) {
resource.TestCheckResourceAttr(fqrn, "name", username),
resource.TestCheckResourceAttr(fqrn, "email", fmt.Sprintf("dummy_user%[email protected]", 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"),
),
Expand Down

0 comments on commit 7c3f7c2

Please sign in to comment.