Skip to content

Commit

Permalink
Merge pull request #23 from antifuchs/user-resource
Browse files Browse the repository at this point in the history
Allow minimal management of user resources
  • Loading branch information
aviadmizrachi authored Aug 7, 2022
2 parents c8f1cf4 + ba3fec0 commit 6683408
Show file tree
Hide file tree
Showing 4 changed files with 250 additions and 9 deletions.
21 changes: 13 additions & 8 deletions internal/restclient/restclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,26 +38,26 @@ func (c *Client) Ignore404() {
}

func (c *Client) Delete(ctx context.Context, url string, out interface{}) error {
return c.Request(ctx, "DELETE", url, nil, out)
return c.RequestWithHeaders(ctx, "DELETE", url, nil, nil, out)
}

func (c *Client) Get(ctx context.Context, url string, out interface{}) error {
return c.Request(ctx, "GET", url, nil, out)
return c.RequestWithHeaders(ctx, "GET", url, nil, nil, out)
}

func (c *Client) Patch(ctx context.Context, url string, in interface{}, out interface{}) error {
return c.Request(ctx, "PATCH", url, in, out)
return c.RequestWithHeaders(ctx, "PATCH", url, nil, in, out)
}

func (c *Client) Post(ctx context.Context, url string, in interface{}, out interface{}) error {
return c.Request(ctx, "POST", url, in, out)
return c.RequestWithHeaders(ctx, "POST", url, nil, in, out)
}

func (c *Client) Put(ctx context.Context, url string, in interface{}, out interface{}) error {
return c.Request(ctx, "PUT", url, in, out)
return c.RequestWithHeaders(ctx, "PUT", url, nil, in, out)
}

func (c *Client) Request(ctx context.Context, method string, url string, in interface{}, out interface{}) error {
func (c *Client) RequestWithHeaders(ctx context.Context, method string, url string, headers http.Header, in interface{}, out interface{}) error {
conflictRetryMethod := c.conflictRetryMethod
c.conflictRetryMethod = ""
ignore404 := c.ignore404
Expand All @@ -74,6 +74,11 @@ func (c *Client) Request(ctx context.Context, method string, url string, in inte
if err != nil {
return fmt.Errorf("restclient: failed to construct request: %w", err)
}
for k, vals := range headers {
for _, v := range vals {
req.Header.Add(k, v)
}
}
req.Header.Set("Content-Type", "application/json")
if c.token != "" {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.token))
Expand All @@ -90,7 +95,7 @@ func (c *Client) Request(ctx context.Context, method string, url string, in inte
if res.StatusCode == 404 && ignore404 {
return nil
} else if res.StatusCode == 409 && conflictRetryMethod != "" {
return c.Request(ctx, conflictRetryMethod, url, in, out)
return c.RequestWithHeaders(ctx, conflictRetryMethod, url, headers, in, out)
} else if res.StatusCode < 200 || res.StatusCode >= 300 {
return fmt.Errorf(
"restclient: request failed: %s %s: %s: %v: %s",
Expand All @@ -100,7 +105,7 @@ func (c *Client) Request(ctx context.Context, method string, url string, in inte
log.Printf("[TRACE] Received response data %q", string(resBody))
if out != nil {
if err := json.Unmarshal(resBody, out); err != nil {
return fmt.Errorf("restclient: failed to decode JSON response: %w", err)
return fmt.Errorf("restclient: failed to decode JSON response %#v: %w", string(resBody), err)
}
}
return nil
Expand Down
1 change: 1 addition & 0 deletions provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ func New(version string) func() *schema.Provider {
"frontegg_webhook": resourceFronteggWebhook(),
"frontegg_workspace": resourceFronteggWorkspace(),
"frontegg_tenant": resourceFronteggTenant(),
"frontegg_user": resourceFronteggUser(),
"frontegg_redirect_uri": resourceFronteggRedirectUri(),
"frontegg_allowed_origin": resourceFronteggAllowedOrigin(),
},
Expand Down
2 changes: 1 addition & 1 deletion provider/resource_frontegg_tenant.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func resourceFronteggTenant() *schema.Resource {
"application_uri": {
Description: "The application URI for this tenant.",
Type: schema.TypeString,
Required: true,
Optional: true,
},
},
}
Expand Down
235 changes: 235 additions & 0 deletions provider/resource_frontegg_user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
package provider

import (
"context"
"fmt"
"log"
"net/http"

"github.com/frontegg/terraform-provider-frontegg/internal/restclient"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

type fronteggUserRole struct {
Id string `json:"id,omitempty"`
Key string `json:"key,omitempty"`
}

type fronteggUser struct {
Key string `json:"id,omitempty"`
Email string `json:"email,omitempty"`
Password string `json:"password,omitempty"`
CreateRoleIDs []interface{} `json:"roleIds,omitempty"`
ReadRoleIDs []fronteggUserRole `json:"roles,omitempty"`
SkipInviteEmail bool `json:"skipInviteEmail,omitempty"`
Verified bool `json:"verified,omitempty"`
}

const fronteggUserPath = "/identity/resources/users/v2"
const fronteggUserPathV1 = "/identity/resources/users/v1"

func resourceFronteggUser() *schema.Resource {
return &schema.Resource{
Description: `Configures a Frontegg user.`,

CreateContext: resourceFronteggUserCreate,
ReadContext: resourceFronteggUserRead,
DeleteContext: resourceFronteggUserDelete,
UpdateContext: resourceFronteggUserUpdate,
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},

Schema: map[string]*schema.Schema{
"email": {
Description: "The user's email address.",
Type: schema.TypeString,
Required: true,
},
"password": {
Description: "The user's login password.",
Type: schema.TypeString,
Sensitive: true,
Optional: true,
},
"skip_invite_email": {
Description: "Skip sending the invite email. If true, user is automatically verified on creation.",
Type: schema.TypeBool,
Optional: true,
},
"automatically_verify": {
Description: "Whether the user gets verified upon creation.",
Type: schema.TypeBool,
Optional: true,
},
"role_ids": {
Description: "List of the role IDs that the user has in the tenant",
Type: schema.TypeSet,
Elem: &schema.Schema{
Type: schema.TypeString,
},
MinItems: 1,
Required: true,
},
"tenant_id": {
Description: "The tenant ID for this user.",
Type: schema.TypeString,
Required: true,
},
},
}
}

func resourceFronteggUserSerialize(d *schema.ResourceData) fronteggUser {
log.Printf("role IDs: %#v", d.Get("role_ids").(*schema.Set).List())
return fronteggUser{
Email: d.Get("email").(string),
Password: d.Get("password").(string),
SkipInviteEmail: d.Get("skip_invite_email").(bool),
CreateRoleIDs: d.Get("role_ids").(*schema.Set).List(),
}
}

func resourceFronteggUserDeserialize(d *schema.ResourceData, f fronteggUser) error {
d.SetId(f.Key)
if err := d.Set("email", f.Email); err != nil {
return err
}
var roleIDs []string
for _, roleID := range f.ReadRoleIDs {
roleIDs = append(roleIDs, roleID.Id)
}
if err := d.Set("role_ids", roleIDs); err != nil {
return err
}
return nil
}

func resourceFronteggUserCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
clientHolder := meta.(*restclient.ClientHolder)
in := resourceFronteggUserSerialize(d)
var out fronteggUser
headers := http.Header{}
headers.Add("frontegg-tenant-id", d.Get("tenant_id").(string))
if err := clientHolder.ApiClient.RequestWithHeaders(ctx, "POST", fronteggUserPath, headers, in, &out); err != nil {
return diag.FromErr(err)
}
if err := resourceFronteggUserDeserialize(d, out); err != nil {
return diag.FromErr(err)
}

if !d.Get("automatically_verify").(bool) {
return nil
}
if err := clientHolder.ApiClient.Post(ctx, fmt.Sprintf("%s/%s/verify", fronteggUserPathV1, out.Key), nil, nil); err != nil {
return diag.FromErr(err)
}

return nil
}

func resourceFronteggUserRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
clientHolder := meta.(*restclient.ClientHolder)
client := clientHolder.ApiClient
client.Ignore404()
var out fronteggUser
headers := http.Header{}
headers.Add("frontegg-tenant-id", d.Get("tenant_id").(string))
if err := client.RequestWithHeaders(ctx, "GET", fmt.Sprintf("%s/%s", fronteggUserPathV1, d.Id()), headers, nil, &out); err != nil {
return diag.FromErr(err)
}
if out.Key == "" {
d.SetId("")
return nil
}

if err := resourceFronteggUserDeserialize(d, out); err != nil {
return diag.FromErr(err)
}
return nil
}

func resourceFronteggUserDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
clientHolder := meta.(*restclient.ClientHolder)
if err := clientHolder.ApiClient.Delete(ctx, fmt.Sprintf("%s/%s", fronteggUserPathV1, d.Id()), nil); err != nil {
return diag.FromErr(err)
}
return nil
}

func resourceFronteggUserUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
clientHolder := meta.(*restclient.ClientHolder)
// TODO: fields like phone number and avatar URL need https://docs.frontegg.com/reference/userscontrollerv1_updateuser

// Email address:
if d.HasChange("email") {
email := d.Get("email").(string)
if err := clientHolder.ApiClient.Put(ctx, fmt.Sprintf("%s/%s/email", fronteggUserPathV1, d.Id()), struct {
Email string `json:"email"`
}{email}, nil); err != nil {
return diag.FromErr(err)
}
d.Set("email", email)
}

// Password:
if d.HasChange("password") {
headers := http.Header{}
headers.Add("frontegg-user-id", d.Id())

oldI, newI := d.GetChange("password")
oldPw := oldI.(string)
newPw := newI.(string)

if err := clientHolder.ApiClient.RequestWithHeaders(ctx, "POST", fmt.Sprintf("%s/passwords/change", fronteggUserPathV1), headers, struct {
OldPW string `json:"password"`
NewPW string `json:"newPassword"`
}{oldPw, newPw}, nil); err != nil {
return diag.FromErr(err)
}
d.Set("password", newPw)
}

// Roles:
if d.HasChange("role_ids") {
headers := http.Header{}
headers.Add("frontegg-tenant-id", d.Get("tenant_id").(string))

oldsI, newsI := d.GetChange("role_ids")
olds := oldsI.(*schema.Set)
news := newsI.(*schema.Set)

toAddSet := news.Difference(olds)
toDelSet := olds.Difference(news)

var toAdd, toDel []string

for _, add := range toAddSet.List() {
toAdd = append(toAdd, add.(string))
}
for _, del := range toDelSet.List() {
toDel = append(toDel, del.(string))
}

if len(toAdd) > 0 {
if err := clientHolder.ApiClient.RequestWithHeaders(ctx, "POST", fmt.Sprintf("%s/%s/roles", fronteggUserPathV1, d.Id()), headers, struct {
RoleIds []string `json:"roleIds"`
}{toAdd}, nil); err != nil {
return diag.FromErr(err)
}
}
if len(toDel) > 0 {
if err := clientHolder.ApiClient.RequestWithHeaders(ctx, "DELETE", fmt.Sprintf("%s/%s/roles", fronteggUserPathV1, d.Id()), headers, struct {
RoleIds []string `json:"roleIds"`
}{toDel}, nil); err != nil {
return diag.FromErr(err)
}
}
d.Set("role_ids", news)
}

d.SetId(d.Id())
return nil

}

0 comments on commit 6683408

Please sign in to comment.