Skip to content

Commit

Permalink
Enchance organization_user resource
Browse files Browse the repository at this point in the history
  • Loading branch information
ottramst committed Nov 25, 2024
1 parent a7007eb commit 1dbbe58
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 28 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
## [Unreleased]

## v0.4.3

* Add `access_all` attribute to `vaultwarden_organization_user` resource
* Add proper update logic to `vaultwarden_organization_user` resource
* Update tests for `vaultwarden_organization_user` resource

## v0.4.2

* Fix bug where all auth methods were being required for the client
Expand Down
39 changes: 38 additions & 1 deletion internal/provider/resource_organization_user.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
Expand Down Expand Up @@ -38,6 +39,7 @@ type OrganizationUserModel struct {
OrganizationID types.String `tfsdk:"organization_id"`
Email types.String `tfsdk:"email"`
Type types.String `tfsdk:"type"`
AccessAll types.Bool `tfsdk:"access_all"`
Status types.String `tfsdk:"status"`
}

Expand Down Expand Up @@ -73,6 +75,12 @@ func (r *OrganizationUser) Schema(ctx context.Context, req resource.SchemaReques
stringvalidator.OneOf("Owner", "Admin", "User", "Manager"),
},
},
"access_all": schema.BoolAttribute{
MarkdownDescription: "Whether the user has access to all collections in the organization. Defaults to `false`",
Computed: true,
Optional: true,
Default: booldefault.StaticBool(false),
},
"status": schema.StringAttribute{
MarkdownDescription: "The status of the user",
Computed: true,
Expand Down Expand Up @@ -126,7 +134,8 @@ func (r *OrganizationUser) Create(ctx context.Context, req resource.CreateReques

// Call the client method to invite the user
inviteReq := vaultwarden.InviteOrganizationUserRequest{
Type: userType,
Type: userType,
AccessAll: data.AccessAll.ValueBool(),
}

if err := r.client.InviteOrganizationUser(ctx, inviteReq, data.Email.ValueString(), data.OrganizationID.ValueString()); err != nil {
Expand All @@ -150,6 +159,7 @@ func (r *OrganizationUser) Create(ctx context.Context, req resource.CreateReques
// Map response body to schema and populate Computed attribute values
data.ID = types.StringValue(userResp.ID)
data.Status = types.StringValue(userResp.Status.String())
data.AccessAll = types.BoolValue(userResp.AccessAll)
data.Type = types.StringValue(userResp.Type.String())

// Write logs using the tflog package
Expand Down Expand Up @@ -183,6 +193,7 @@ func (r *OrganizationUser) Read(ctx context.Context, req resource.ReadRequest, r
// Overwrite the model with the refreshed data
data.Email = types.StringValue(userResp.Email)
data.Status = types.StringValue(userResp.Status.String())
data.AccessAll = types.BoolValue(userResp.AccessAll)
data.Type = types.StringValue(userResp.Type.String())

// Save updated data into Terraform state
Expand All @@ -199,6 +210,31 @@ func (r *OrganizationUser) Update(ctx context.Context, req resource.UpdateReques
return
}

// Parse the type string into a UserOrgType (value will always be present due to schema default)
var userType models.UserOrgType
if err := userType.FromString(data.Type.ValueString()); err != nil {
resp.Diagnostics.AddError(
"Error parsing user type",
"Could not parse user type: "+err.Error(),
)
return
}

// Update the user if needed
user := models.OrganizationUserDetails{
Email: data.Email.ValueString(),
Type: userType,
AccessAll: data.AccessAll.ValueBool(),
}

if _, err := r.client.UpdateOrganizationUser(ctx, data.ID.ValueString(), data.OrganizationID.ValueString(), user); err != nil {
resp.Diagnostics.AddError(
"Error updating organization user",
"Could not update organization user with ID "+data.ID.ValueString()+": "+err.Error(),
)
return
}

// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
Expand Down Expand Up @@ -253,5 +289,6 @@ func (r *OrganizationUser) ImportState(ctx context.Context, req resource.ImportS
// Map response body to schema and populate the rest of the attributes
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("email"), userResp.Email)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("type"), userResp.Type.String())...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("access_all"), userResp.AccessAll)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("status"), userResp.Status.String())...)
}
109 changes: 86 additions & 23 deletions internal/provider/resource_organization_user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/terraform"
"github.com/ottramst/terraform-provider-vaultwarden/internal/vaultwarden/test"
"regexp"
"testing"
)

Expand All @@ -17,58 +18,120 @@ func TestAccOrganizationUser(t *testing.T) {
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
// Create and Read testing
// Create and Read testing with default values
{
Config: testAccOrganizationUserConfig(orgName, email),
Config: testAccOrganizationUserConfigBasic(orgName, email),
Check: resource.ComposeAggregateTestCheckFunc(
// Organization checks
resource.TestCheckResourceAttr("vaultwarden_organization.test", "name", orgName),
// User checks
// User checks - default values
resource.TestCheckResourceAttr("vaultwarden_organization_user.test", "email", email),
resource.TestCheckResourceAttr("vaultwarden_organization_user.test", "type", "User"), // Default type
resource.TestCheckResourceAttr("vaultwarden_organization_user.test", "type", "User"),
resource.TestCheckResourceAttr("vaultwarden_organization_user.test", "access_all", "false"),
resource.TestCheckResourceAttr("vaultwarden_organization_user.test", "status", "Invited"),
resource.TestCheckResourceAttrSet("vaultwarden_organization_user.test", "id"),
resource.TestCheckResourceAttrSet("vaultwarden_organization_user.test", "organization_id"),
),
},
// Update testing with Admin role and access_all true
{
Config: testAccOrganizationUserConfigCustom(orgName, email, "Admin", true),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("vaultwarden_organization_user.test", "email", email),
resource.TestCheckResourceAttr("vaultwarden_organization_user.test", "type", "Admin"),
resource.TestCheckResourceAttr("vaultwarden_organization_user.test", "access_all", "true"),
),
},
// Update to Manager role with access_all false
{
Config: testAccOrganizationUserConfigCustom(orgName, email, "Manager", false),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("vaultwarden_organization_user.test", "email", email),
resource.TestCheckResourceAttr("vaultwarden_organization_user.test", "type", "Manager"),
resource.TestCheckResourceAttr("vaultwarden_organization_user.test", "access_all", "false"),
),
},
// Import testing
{
ResourceName: "vaultwarden_organization_user.test",
ImportState: true,
ImportStateVerify: true,
ImportStateIdFunc: func(s *terraform.State) (string, error) {
rs, ok := s.RootModule().Resources["vaultwarden_organization_user.test"]
if !ok {
return "", fmt.Errorf("resource not found in state")
}
ImportStateIdFunc: testAccOrganizationUserImportStateIdFunc(),
},
},
})
}

return fmt.Sprintf("%s/%s",
rs.Primary.Attributes["organization_id"],
rs.Primary.Attributes["id"]), nil
},
func TestAccOrganizationUserInvalidType(t *testing.T) {
orgName := gofakeit.Company()
email := gofakeit.Email()

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccOrganizationUserConfigCustom(orgName, email, "InvalidType", false),
ExpectError: regexp.MustCompile(`Invalid Attribute Value Match`),
},
// Delete testing automatically occurs in TestCase
},
})
}

// Base configuration
func testAccOrganizationUserConfig(orgName, email string) string {
// Basic configuration with default values
func testAccOrganizationUserConfigBasic(orgName, email string) string {
return fmt.Sprintf(`
provider "vaultwarden" {
endpoint = %[1]q
email = %[2]q
master_password = %[3]q
admin_token = %[4]q
endpoint = %[1]q
email = %[2]q
master_password = %[3]q
admin_token = %[4]q
}
resource "vaultwarden_organization" "test" {
name = %[5]q
name = %[5]q
}
resource "vaultwarden_organization_user" "test" {
organization_id = vaultwarden_organization.test.id
email = %[6]q
organization_id = vaultwarden_organization.test.id
email = %[6]q
}
`, test.TestBaseURL, test.TestEmail, test.TestPassword, test.TestAdminToken, orgName, email)
}

// Configuration with custom type and access_all settings
func testAccOrganizationUserConfigCustom(orgName, email, userType string, accessAll bool) string {
return fmt.Sprintf(`
provider "vaultwarden" {
endpoint = %[1]q
email = %[2]q
master_password = %[3]q
admin_token = %[4]q
}
resource "vaultwarden_organization" "test" {
name = %[5]q
}
resource "vaultwarden_organization_user" "test" {
organization_id = vaultwarden_organization.test.id
email = %[6]q
type = %[7]q
access_all = %[8]t
}
`, test.TestBaseURL, test.TestEmail, test.TestPassword, test.TestAdminToken, orgName, email, userType, accessAll)
}

// Import state function
func testAccOrganizationUserImportStateIdFunc() resource.ImportStateIdFunc {
return func(s *terraform.State) (string, error) {
rs, ok := s.RootModule().Resources["vaultwarden_organization_user.test"]
if !ok {
return "", fmt.Errorf("resource not found in state")
}

return fmt.Sprintf("%s/%s",
rs.Primary.Attributes["organization_id"],
rs.Primary.Attributes["id"]), nil
}
}
9 changes: 5 additions & 4 deletions internal/vaultwarden/models/organization.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,11 @@ type OrganizationCollections struct {

// OrganizationUserDetails represents a user in an organization
type OrganizationUserDetails struct {
ID string `json:"id"`
Email string `json:"email"`
Status UserOrgStatus `json:"status"`
Type UserOrgType `json:"type"`
ID string `json:"id"`
Email string `json:"email"`
Status UserOrgStatus `json:"status"`
Type UserOrgType `json:"type"`
AccessAll bool `json:"accessAll"`
}

// OrganizationUsers represents a list of users in an organization
Expand Down
10 changes: 10 additions & 0 deletions internal/vaultwarden/organization.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,3 +223,13 @@ func (c *Client) DeleteOrganizationUser(ctx context.Context, userID, orgID strin

return nil
}

// UpdateOrganizationUser updates a user in an organization by their ID
func (c *Client) UpdateOrganizationUser(ctx context.Context, userID, orgID string, user models.OrganizationUserDetails) (*models.OrganizationUserDetails, error) {
var userResp models.OrganizationUserDetails
if _, err := c.doRequest(ctx, http.MethodPut, fmt.Sprintf("/api/organizations/%s/users/%s", orgID, userID), user, &userResp); err != nil {
return nil, fmt.Errorf("failed to update organization user: %w", err)
}

return &userResp, nil
}

0 comments on commit 1dbbe58

Please sign in to comment.