Skip to content

Commit

Permalink
Add vaultwarden_organization_user resource (#18)
Browse files Browse the repository at this point in the history
* Add vaultwarden_organization_user_invite resource

* Add vaultwarden_account_register resource (#17)

* Add vaultwarden_account_register resource

* Remove support for versions older than 1.28

* Finish
  • Loading branch information
ottramst authored Nov 21, 2024
1 parent a38651a commit 85d930c
Show file tree
Hide file tree
Showing 17 changed files with 610 additions and 37 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
- '1.8.*'
- '1.9.*'
vaultwarden_version:
- 1.32.4
- 1.32.5
- 1.31.0
- 1.30.5
- 1.29.2
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## [Unreleased]

* Add `vaultwarden_account_register` resource
* Add `vaultwarden_organization_user` resource

## v0.3.0

Expand Down
50 changes: 50 additions & 0 deletions docs/resources/organization_user.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "vaultwarden_organization_user Resource - vaultwarden"
subcategory: ""
description: |-
This resource invites a user to an organization on the Vaultwarden server.
---

# vaultwarden_organization_user (Resource)

This resource invites a user to an organization on the Vaultwarden server.

## Example Usage

```terraform
resource "vaultwarden_organization" "example" {
name = "Example"
}
resource "vaultwarden_organization_user" "example" {
organization_id = vaultwarden_organization.example.id
email = "[email protected]"
type = "User"
}
```

<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `email` (String) The email of the user to invite
- `organization_id` (String) ID of the organization to invite the user to

### Optional

- `type` (String) The role type of the user (Owner, Admin, User, Manager). Defaults to `User`

### Read-Only

- `id` (String) ID of the invited user
- `status` (String) The status of the user

## Import

Import is supported using the following syntax:

```shell
terraform import vaultwarden_organization_user.example <id>
```
10 changes: 5 additions & 5 deletions docs/resources/user_invite.md → docs/resources/user.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "vaultwarden_user_invite Resource - vaultwarden"
page_title: "vaultwarden_user Resource - vaultwarden"
subcategory: ""
description: |-
This resource invites a user to the Vaultwarden server.
---

# vaultwarden_user_invite (Resource)
# vaultwarden_user (Resource)

This resource invites a user to the Vaultwarden server.

## Example Usage

```terraform
resource "vaultwarden_user_invite" "example" {
resource "vaultwarden_user" "example" {
email = "[email protected]"
}
```
Expand All @@ -27,12 +27,12 @@ resource "vaultwarden_user_invite" "example" {

### Read-Only

- `id` (String) ID of the invited user
- `id` (String) ID of the user

## Import

Import is supported using the following syntax:

```shell
terraform import vaultwarden_user_invite.example <id>
terraform import vaultwarden_user.example <id>
```
1 change: 1 addition & 0 deletions examples/resources/vaultwarden_organization_user/import.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
terraform import vaultwarden_organization_user.example <id>
9 changes: 9 additions & 0 deletions examples/resources/vaultwarden_organization_user/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
resource "vaultwarden_organization" "example" {
name = "Example"
}

resource "vaultwarden_organization_user" "example" {
organization_id = vaultwarden_organization.example.id
email = "[email protected]"
type = "User"
}
1 change: 1 addition & 0 deletions examples/resources/vaultwarden_user/import.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
terraform import vaultwarden_user.example <id>
3 changes: 3 additions & 0 deletions examples/resources/vaultwarden_user/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
resource "vaultwarden_user" "example" {
email = "[email protected]"
}
1 change: 0 additions & 1 deletion examples/resources/vaultwarden_user_invite/import.sh

This file was deleted.

3 changes: 0 additions & 3 deletions examples/resources/vaultwarden_user_invite/resource.tf

This file was deleted.

3 changes: 2 additions & 1 deletion internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,10 +285,11 @@ func (p *VaultwardenProvider) Configure(ctx context.Context, req provider.Config

func (p *VaultwardenProvider) Resources(ctx context.Context) []func() resource.Resource {
return []func() resource.Resource{
UserInviteResource,
UserResource,
OrganizationResource,
OrganizationCollectionResource,
AccountRegisterResource,
OrganizationUserResource,
}
}

Expand Down
253 changes: 253 additions & 0 deletions internal/provider/resource_organization_user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
package provider

import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"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/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/ottramst/terraform-provider-vaultwarden/internal/vaultwarden"
"github.com/ottramst/terraform-provider-vaultwarden/internal/vaultwarden/models"
"strings"
)

// Ensure provider defined types fully satisfy framework interfaces.
var _ resource.Resource = &OrganizationUser{}
var _ resource.ResourceWithImportState = &User{}

func OrganizationUserResource() resource.Resource {
return &OrganizationUser{}
}

// OrganizationUser defines the resource implementation.
type OrganizationUser struct {
client *vaultwarden.Client
}

// OrganizationUserModel describes the resource data model.
type OrganizationUserModel struct {
ID types.String `tfsdk:"id"`
OrganizationID types.String `tfsdk:"organization_id"`
Email types.String `tfsdk:"email"`
Type types.String `tfsdk:"type"`
Status types.String `tfsdk:"status"`
}

func (r *OrganizationUser) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_organization_user"
}

func (r *OrganizationUser) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "This resource invites a user to an organization on the Vaultwarden server.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
MarkdownDescription: "ID of the invited user",
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"organization_id": schema.StringAttribute{
MarkdownDescription: "ID of the organization to invite the user to",
Required: true,
},
"email": schema.StringAttribute{
MarkdownDescription: "The email of the user to invite",
Required: true,
},
"type": schema.StringAttribute{
MarkdownDescription: "The role type of the user (Owner, Admin, User, Manager). Defaults to `User`",
Computed: true,
Optional: true,
Validators: []validator.String{
stringvalidator.OneOf("Owner", "Admin", "User", "Manager"),
},
},
"status": schema.StringAttribute{
MarkdownDescription: "The status of the user",
Computed: true,
Validators: []validator.String{
stringvalidator.OneOf("Revoked", "Invited", "Accepted", "Confirmed"),
},
},
},
}
}

func (r *OrganizationUser) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
// Prevent panic if the provider has not been configured.
if req.ProviderData == nil {
return
}

client, ok := req.ProviderData.(*vaultwarden.Client)

if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *vaultwarden.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)

return
}

r.client = client
}

func (r *OrganizationUser) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data OrganizationUserModel

// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}

// Parse the type string into a UserOrgType
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
}

// Call the client method to invite the user
inviteReq := vaultwarden.InviteOrganizationUserRequest{
Type: userType,
}

if err := r.client.InviteOrganizationUser(ctx, inviteReq, data.Email.ValueString(), data.OrganizationID.ValueString()); err != nil {
resp.Diagnostics.AddError(
"Error inviting user",
"Could not invite user, unexpected error: "+err.Error(),
)
return
}

// Fetch the invited user by email
userResp, err := r.client.GetOrganizationUserByEmail(ctx, data.Email.ValueString(), data.OrganizationID.ValueString())
if err != nil {
resp.Diagnostics.AddError(
"Error fetching registered user",
"Could not fetch registered user, unexpected error: "+err.Error(),
)
return
}

// Map response body to schema and populate Computed attribute values
data.ID = types.StringValue(userResp.ID)
data.Status = types.StringValue(userResp.Status.String())

// Write logs using the tflog package
// Documentation: https://terraform.io/plugin/log
tflog.Trace(ctx, fmt.Sprintf("created a new user_invite with ID: %s", data.ID))

// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *OrganizationUser) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data OrganizationUserModel

// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}

// Get refreshed data from the client
userResp, err := r.client.GetOrganizationUser(ctx, data.ID.ValueString(), data.OrganizationID.ValueString())
if err != nil {
resp.Diagnostics.AddError(
"Error fetching organization user",
"Could not fetch organization user, unexpected error: "+err.Error(),
)
return
}

// Overwrite the model with the refreshed data
data.Email = types.StringValue(userResp.Email)
data.Status = types.StringValue(userResp.Status.String())
data.Type = types.StringValue(userResp.Type.String())

// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *OrganizationUser) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data OrganizationUserModel

// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}

// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *OrganizationUser) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data OrganizationUserModel

// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}

// Delete the user
if err := r.client.DeleteOrganizationUser(ctx, data.ID.ValueString(), data.OrganizationID.ValueString()); err != nil {
resp.Diagnostics.AddError(
"Error deleting organization user",
"Could not delete organization user with ID "+data.ID.ValueString()+": "+err.Error(),
)
return
}
}

func (r *OrganizationUser) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
idParts := strings.Split(req.ID, "/")
if len(idParts) != 2 {
resp.Diagnostics.AddError(
"Invalid ID format",
"Expected import identifier with format: organization_id/user_id",
)
return
}

organizationID := idParts[0]
userID := idParts[1]

// Set the organization_id and id attributes
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("organization_id"), organizationID)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), userID)...)

// After setting the IDs, fetch the current state of the resource
userResp, err := r.client.GetOrganizationUser(ctx, userID, organizationID)
if err != nil {
resp.Diagnostics.AddError(
"Error fetching organization user",
"Could not fetch organization user, unexpected error: "+err.Error(),
)
return
}

// 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("status"), userResp.Status.String())...)
}
Loading

0 comments on commit 85d930c

Please sign in to comment.