Skip to content

Commit

Permalink
Support creating and updating users with a PIN (#65)
Browse files Browse the repository at this point in the history
* Replace User.Pin field with a Pin struct that allows setting a PIN value when creating or updating users

* Update User acceptance test to check the pin attribute

* Add new User test case

* Add separate example for a user with pin

* Ignore User.Pin.IsSet when setting an updated user PIN

* Pointer receiver in MarshalJSON function
  • Loading branch information
jm-araujo authored Aug 29, 2024
1 parent d2871ff commit 88d5538
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 15 deletions.
1 change: 1 addition & 0 deletions docs/resources/user.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ The organization resource allows managing information about a particular Pritunl
- `groups` (List of String) Enter list of groups to allow connections from. Names are case sensitive. If empty all groups will able to connect.
- `mac_addresses` (List of String) Comma separated list of MAC addresses client is allowed to connect from. The validity of the MAC address provided by the VPN client cannot be verified.
- `network_links` (List of String) Network address with cidr subnet. This will provision access to a clients local network to the attached vpn servers and other clients. Multiple networks may be separated by a comma. Router must have a static route to VPN virtual network through client.
- `pin` (String) The PIN code for the user.
- `port_forwarding` (List of Map of String) Comma seperated list of ports to forward using format source_port:dest_port/protocol or start_port-end_port/protocol. Such as 80, 80/tcp, 80:8000/tcp, 1000-2000/udp.

### Read-Only
Expand Down
10 changes: 10 additions & 0 deletions examples/provider/provider.tf
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ resource "pritunl_user" "test" {
]
}

resource "pritunl_user" "test_pin" {
name = "test-user-pin"
organization_id = pritunl_organization.developers.id
email = "[email protected]"
pin = "123456"
groups = [
"admins",
]
}

resource "pritunl_server" "test" {
name = "test"

Expand Down
38 changes: 37 additions & 1 deletion internal/pritunl/user.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package pritunl

import (
"encoding/json"
)

type User struct {
ID string `json:"id,omitempty"`
Name string `json:"name"`
Type string `json:"type,omitempty"`
AuthType string `json:"auth_type,omitempty"`
DnsServers []string `json:"dns_servers,omitempty"`
Pin bool `json:"pin,omitempty"`
DnsSuffix string `json:"dns_suffix,omitempty"`
DnsMapping string `json:"dns_mapping,omitempty"`
Disabled bool `json:"disabled,omitempty"`
Expand All @@ -26,10 +29,43 @@ type User struct {
OtpAuth bool `json:"otp_auth,omitempty"`
DeviceAuth bool `json:"device_auth,omitempty"`
Organization string `json:"organization,omitempty"`
Pin *Pin `json:"pin,omitempty"`
}

type PortForwarding struct {
Dport string `json:"dport"`
Protocol string `json:"protocol"`
Port string `json:"port"`
}

type Pin struct {
IsSet bool
Secret string
}

// MarshalJSON customizes the JSON encoding of the Pin struct.
//
// When marshaling a User JSON, the "pin" field will contain the PIN secret
// if it is set, otherwise the field is excluded. This is used when making
// a user create or update request to the Pritunl API.
func (p *Pin) MarshalJSON() ([]byte, error) {
if p.Secret != "" {
return json.Marshal(p.Secret)
}
return json.Marshal(nil)
}

// UnmarshalJSON customizes the JSON decoding of the Pin struct.
//
// When unmarshaling a User JSON, the "pin" field will contain a boolean
// indicating whether the user has a PIN set or not. This is used when
// reading a user response from the Pritunl API.
func (p *Pin) UnmarshalJSON(data []byte) error {
var b bool
err := json.Unmarshal(data, &b)
if err == nil {
p.IsSet = b
p.Secret = ""
}
return err
}
7 changes: 4 additions & 3 deletions internal/provider/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,10 @@ func importStep(name string, ignore ...string) resource.TestStep {
// pritunl_user import requires organization and user IDs
func pritunlUserImportStep(name string) resource.TestStep {
step := resource.TestStep{
ResourceName: name,
ImportState: true,
ImportStateVerify: true,
ResourceName: name,
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"pin"},
ImportStateIdFunc: func(state *terraform.State) (string, error) {
userId := state.RootModule().Resources["pritunl_user.test"].Primary.Attributes["id"]
orgId := state.RootModule().Resources["pritunl_organization.test"].Primary.Attributes["id"]
Expand Down
21 changes: 20 additions & 1 deletion internal/provider/resource_user.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ package provider
import (
"context"
"fmt"
"strings"

"github.com/disc/terraform-provider-pritunl/internal/pritunl"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"strings"
)

func resourceUser() *schema.Resource {
Expand Down Expand Up @@ -100,6 +101,12 @@ func resourceUser() *schema.Resource {
Optional: true,
Description: "Bypass secondary authentication such as the PIN and two-factor authentication. Use for server users that can't provide a two-factor code.",
},
"pin": {
Type: schema.TypeString,
Optional: true,
Sensitive: true,
Description: "The PIN for user authentication.",
},
},
CreateContext: resourceUserCreate,
ReadContext: resourceUserRead,
Expand Down Expand Up @@ -170,6 +177,12 @@ func resourceUserUpdate(ctx context.Context, d *schema.ResourceData, meta interf
return diag.FromErr(err)
}

if d.HasChange("pin") {
if v, ok := d.GetOk("pin"); ok {
user.Pin = &pritunl.Pin{Secret: v.(string)}
}
}

if v, ok := d.GetOk("name"); ok {
user.Name = v.(string)
}
Expand Down Expand Up @@ -295,6 +308,12 @@ func resourceUserCreate(_ context.Context, d *schema.ResourceData, meta interfac
Groups: groups,
}

if pin, ok := d.GetOk("pin"); ok {
userData.Pin = &pritunl.Pin{
Secret: pin.(string),
}
}

user, err := apiClient.CreateUser(userData)
if err != nil {
return diag.FromErr(err)
Expand Down
56 changes: 46 additions & 10 deletions internal/provider/resource_user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ func TestAccPritunlUser(t *testing.T) {
check := resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("pritunl_user.test", "name", username),
resource.TestCheckResourceAttr("pritunl_organization.test", "name", orgName),
resource.TestCheckNoResourceAttr("pritunl_user.test", "pin"),
)

resource.Test(t, resource.TestCase{
Expand All @@ -30,17 +31,52 @@ func TestAccPritunlUser(t *testing.T) {
},
})
})
t.Run("creates users with PIN without error", func(t *testing.T) {
username := "tfacc-user2"
orgName := "tfacc-org2"
pin := "123456"

check := resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("pritunl_user.test", "name", username),
resource.TestCheckResourceAttr("pritunl_user.test", "pin", pin),
resource.TestCheckResourceAttr("pritunl_organization.test", "name", orgName),
)

resource.Test(t, resource.TestCase{
PreCheck: func() { preCheck(t) },
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: testPritunlUserConfigWithPin(username, orgName, pin),
Check: check,
},
// import test
pritunlUserImportStep("pritunl_user.test"),
},
})
})
}

func testPritunlUserConfig(username, orgName string) string {
return fmt.Sprintf(`
resource "pritunl_organization" "test" {
name = "%[2]s"
}
resource "pritunl_user" "test" {
name = "%[1]s"
organization_id = pritunl_organization.test.id
}
`, username, orgName)
return testPritunlUserConfigWithPin(username, orgName, "")
}

func testPritunlUserConfigWithPin(username, orgName, pin string) string {
resources := fmt.Sprintf(`
resource "pritunl_organization" "test" {
name = "%[2]s"
}
resource "pritunl_user" "test" {
name = "%[1]s"
organization_id = pritunl_organization.test.id
`, username, orgName)

if pin != "" {
resources += fmt.Sprintf("pin = \"%[1]s\"\n", pin)
}

resources += "}\n"

return resources
}

0 comments on commit 88d5538

Please sign in to comment.