Skip to content

Commit 187cc8b

Browse files
authored
Support approved_ips on account_user resource (#628)
* Add approved_ips to account_user resource * Add approved_ips to account_user resource * Add approved_ips to account_user resource * Add approved_ips to account_user resource * Add approved_ips to account_user resource * Add approved_ips to account_user resource * Add approved_ips to account_user resource * Add approved_ips to account_user resource
1 parent 1fb1066 commit 187cc8b

File tree

5 files changed

+183
-34
lines changed

5 files changed

+183
-34
lines changed

incapsula/client_account_user.go

Lines changed: 57 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@ const endpointUserOperationNew = "identity-management/v3/idm-users"
1515
// UserApisResponse contains the relevant user information when adding, getting or updating a user
1616
type UserApisResponse struct {
1717
Data []struct {
18-
UserID string `json:"id"`
19-
AccountID int `json:"accountId"`
20-
FirstName string `json:"firstName"`
21-
LastName string `json:"lastName"`
22-
Email string `json:"email"`
23-
Roles []struct {
18+
UserID string `json:"id"`
19+
AccountID int `json:"accountId"`
20+
FirstName string `json:"firstName"`
21+
LastName string `json:"lastName"`
22+
Email string `json:"email"`
23+
ApprovedIps []string `json:"approvedIps"`
24+
Roles []struct {
2425
RoleID int `json:"id"`
2526
RoleName string `json:"name"`
2627
} `json:"roles"`
@@ -29,39 +30,47 @@ type UserApisResponse struct {
2930

3031
type UserApisUpdateResponse struct {
3132
Data []struct {
32-
UserID string `json:"id"`
33-
AccountID int `json:"accountId"`
34-
FirstName string `json:"firstName"`
35-
LastName string `json:"lastName"`
36-
Email string `json:"email"`
37-
Roles []struct {
33+
UserID string `json:"id"`
34+
AccountID int `json:"accountId"`
35+
FirstName string `json:"firstName"`
36+
LastName string `json:"lastName"`
37+
Email string `json:"email"`
38+
ApprovedIps []string `json:"approvedIps"`
39+
Roles []struct {
3840
RoleID int `json:"id"`
3941
RoleName string `json:"name"`
4042
} `json:"roles"`
4143
} `json:"data"`
4244
}
4345

4446
type UserAddReq struct {
45-
UserEmail string `json:"email"`
46-
RoleIds []int `json:"roleIds"`
47-
FirstName string `json:"firstName"`
48-
LastName string `json:"lastName"`
47+
UserEmail string `json:"email"`
48+
RoleIds []int `json:"roleIds"`
49+
FirstName string `json:"firstName"`
50+
LastName string `json:"lastName"`
51+
ApprovedIps []string `json:"approvedIps"`
4952
}
5053

5154
type UserUpdateReq struct {
52-
RoleIds []int `json:"roleIds"`
55+
RoleIds *[]int `json:"roleIds,omitempty"`
56+
ApprovedIps *[]string `json:"approvedIps,omitempty"`
5357
}
5458

5559
// AddAccountUser adds a user to Incapsula Account
56-
func (c *Client) AddAccountUser(accountID int, email, firstName, lastName string, roleIds []interface{}) (*UserApisResponse, error) {
60+
func (c *Client) AddAccountUser(accountID int, email, firstName, lastName string, roleIds []interface{}, approvedIps []interface{}) (*UserApisResponse, error) {
5761
log.Printf("[INFO] Adding Incapsula account user for email: %s (account ID %d)\n", email, accountID)
5862

5963
listRoles := make([]int, len(roleIds))
6064
for i, v := range roleIds {
6165
listRoles[i] = v.(int)
6266
}
6367

64-
userAddReq := UserAddReq{UserEmail: email, RoleIds: listRoles, FirstName: firstName, LastName: lastName}
68+
listApprovedIps := make([]string, len(approvedIps))
69+
for i, v := range approvedIps {
70+
listApprovedIps[i] = v.(string)
71+
}
72+
73+
userAddReq := UserAddReq{UserEmail: email, RoleIds: listRoles, FirstName: firstName, LastName: lastName, ApprovedIps: listApprovedIps}
6574

6675
userJSON, err := json.Marshal(userAddReq)
6776
if err != nil {
@@ -144,16 +153,40 @@ func (c *Client) GetAccountUser(accountID int, email string) (*UserApisResponse,
144153
}
145154

146155
// UpdateAccountUser User Roles
147-
func (c *Client) UpdateAccountUser(accountID int, email string, roleIds []interface{}) (*UserApisUpdateResponse, error) {
156+
// Pass nil for roleIds or approvedIps to leave them unchanged (for PATCH semantics)
157+
func (c *Client) UpdateAccountUser(accountID int, email string, roleIds []interface{}, approvedIps []interface{}) (*UserApisUpdateResponse, error) {
148158
log.Printf("[INFO] Update Incapsula User for email: %s (account ID %d)\n", email, accountID)
149-
listRoles := make([]int, len(roleIds))
150-
for i, v := range roleIds {
151-
listRoles[i] = v.(int)
159+
log.Printf("[DEBUG] UpdateAccountUser called with roleIds=%v (nil: %v), approvedIps=%v (nil: %v)\n",
160+
roleIds, roleIds == nil, approvedIps, approvedIps == nil)
161+
162+
userUpdateReq := UserUpdateReq{}
163+
164+
// Only include roleIds if provided (not nil)
165+
if roleIds != nil {
166+
listRoles := make([]int, len(roleIds))
167+
for i, v := range roleIds {
168+
listRoles[i] = v.(int)
169+
}
170+
userUpdateReq.RoleIds = &listRoles
171+
log.Printf("[DEBUG] Including roleIds in request: %v\n", listRoles)
172+
} else {
173+
log.Printf("[DEBUG] NOT including roleIds in request (nil parameter)\n")
152174
}
153175

154-
userUpdateReq := UserUpdateReq{RoleIds: listRoles}
176+
// Only include approvedIps if provided (not nil)
177+
if approvedIps != nil {
178+
listApprovedIps := make([]string, len(approvedIps))
179+
for i, v := range approvedIps {
180+
listApprovedIps[i] = v.(string)
181+
}
182+
userUpdateReq.ApprovedIps = &listApprovedIps
183+
log.Printf("[DEBUG] Including approvedIps in request: %v\n", listApprovedIps)
184+
} else {
185+
log.Printf("[DEBUG] NOT including approvedIps in request (nil parameter)\n")
186+
}
155187

156188
userJSON, err := json.Marshal(userUpdateReq)
189+
log.Printf("[DEBUG] Final JSON payload: %s\n", string(userJSON))
157190
if err != nil {
158191
return nil, fmt.Errorf("Failed to JSON marshal IncapRule: %s", err)
159192
}

incapsula/client_account_user_test.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ func TestClientAddUserBadConnection(t *testing.T) {
2020

2121
roleIds := make([]interface{}, 1)
2222
roleIds[0] = 0
23-
UserAddResponse, err := client.AddAccountUser(0, email, "", "", roleIds)
23+
approvedIps := make([]interface{}, 0)
24+
UserAddResponse, err := client.AddAccountUser(0, email, "", "", roleIds, approvedIps)
2425
if err == nil {
2526
t.Errorf("Should have received an error")
2627
}
@@ -47,7 +48,8 @@ func TestClientAddUserBadJSON(t *testing.T) {
4748
email := "example@example.com"
4849
roleIds := make([]interface{}, 1)
4950
roleIds[0] = 10
50-
UserAddResponse, err := client.AddAccountUser(accountID, email, "f", "l", roleIds)
51+
approvedIps := make([]interface{}, 0)
52+
UserAddResponse, err := client.AddAccountUser(accountID, email, "f", "l", roleIds, approvedIps)
5153
if err == nil {
5254
t.Errorf("Should have received an error")
5355
}
@@ -156,7 +158,8 @@ func TestClientUpdateUserBadConnection(t *testing.T) {
156158
email := "example@example.com"
157159
roleIds := make([]interface{}, 1)
158160
roleIds[0] = 10
159-
updateUserResponse, err := client.UpdateAccountUser(accountID, email, roleIds)
161+
approvedIps := make([]interface{}, 0)
162+
updateUserResponse, err := client.UpdateAccountUser(accountID, email, roleIds, approvedIps)
160163
if err == nil {
161164
t.Errorf("Should have received an error")
162165
}
@@ -182,9 +185,10 @@ func TestClientUpdateUserBadJSON(t *testing.T) {
182185

183186
roleIds := make([]interface{}, 1)
184187
roleIds[0] = 10
188+
approvedIps := make([]interface{}, 0)
185189
config := &Config{APIID: "foo", APIKey: "bar", BaseURLAPI: server.URL}
186190
client := &Client{config: config, httpClient: &http.Client{}}
187-
updateUserResponse, err := client.UpdateAccountUser(accountID, email, roleIds)
191+
updateUserResponse, err := client.UpdateAccountUser(accountID, email, roleIds, approvedIps)
188192
if err == nil {
189193
t.Errorf("Should have received an error")
190194
}

incapsula/resource_account_user.go

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,16 @@ func resourceAccountUser() *schema.Resource {
7676
Type: schema.TypeInt,
7777
},
7878
Optional: true,
79+
Computed: true,
80+
},
81+
"approved_ips": {
82+
Description: "List of approved IP addresses from which the user is allowed to access the Cloud Security Console via the UI or API.",
83+
Type: schema.TypeList,
84+
Elem: &schema.Schema{
85+
Type: schema.TypeString,
86+
},
87+
Optional: true,
88+
Computed: true,
7989
},
8090

8191
// Computed Arguments
@@ -121,12 +131,14 @@ func resourceUserCreate(d *schema.ResourceData, m interface{}) error {
121131
log.Printf("[INFO] Creating Incapsula user for email: %s\n", email)
122132

123133
roleIds := d.Get("role_ids").(*schema.Set)
134+
approvedIps := d.Get("approved_ips").([]interface{})
124135
UserAddResponse, err := client.AddAccountUser(
125136
accountId,
126137
email,
127138
d.Get("first_name").(string),
128139
d.Get("last_name").(string),
129140
roleIds.List(),
141+
approvedIps,
130142
)
131143

132144
if err != nil {
@@ -167,16 +179,37 @@ func resourceUserRead(d *schema.ResourceData, m interface{}) error {
167179

168180
log.Printf("[INFO]listRoles : %v\n", userStatusResponse.Data[0].Roles)
169181

170-
listRolesIds := make([]int, len(userStatusResponse.Data[0].Roles))
171-
listRolesNames := make([]string, len(userStatusResponse.Data[0].Roles))
172-
for i, v := range userStatusResponse.Data[0].Roles {
182+
// Normalize roles: if API returns null, treat it as empty list
183+
roles := userStatusResponse.Data[0].Roles
184+
if roles == nil {
185+
log.Printf("[DEBUG] API returned nil for roles, normalizing to empty list\n")
186+
roles = make([]struct {
187+
RoleID int `json:"id"`
188+
RoleName string `json:"name"`
189+
}, 0)
190+
}
191+
192+
listRolesIds := make([]int, len(roles))
193+
listRolesNames := make([]string, len(roles))
194+
for i, v := range roles {
173195
listRolesIds[i] = v.RoleID
174196
listRolesNames[i] = v.RoleName
175197
}
198+
log.Printf("[DEBUG] Setting role_ids in state: %v\n", listRolesIds)
176199

177200
d.Set("email", userStatusResponse.Data[0].Email)
178201
d.Set("account_id", userStatusResponse.Data[0].AccountID)
179202

203+
// Normalize approved_ips: if API returns null, treat it as empty list
204+
// This prevents Terraform from showing drift when approved_ips is not set
205+
approvedIps := userStatusResponse.Data[0].ApprovedIps
206+
if approvedIps == nil {
207+
log.Printf("[DEBUG] API returned nil for approved_ips, normalizing to empty list\n")
208+
approvedIps = []string{}
209+
}
210+
log.Printf("[DEBUG] Setting approved_ips in state: %v\n", approvedIps)
211+
d.Set("approved_ips", approvedIps)
212+
180213
accountStatusResponse, err := client.AccountStatus(accountID, ReadAccount)
181214
if accountStatusResponse != nil && accountStatusResponse.AccountType == "Sub Account" {
182215
log.Printf("[DEBUG] User creation on Sub Account, setting null value to avoid forces replacement\n")
@@ -199,13 +232,39 @@ func resourceUserUpdate(d *schema.ResourceData, m interface{}) error {
199232
email := d.Get("email").(string)
200233
accountId := d.Get("account_id").(int)
201234

202-
log.Printf("[INFO] Creating Incapsula user for email: %s\n", email)
235+
log.Printf("[INFO] Updating Incapsula user for email: %s\n", email)
236+
237+
// Only send fields that have changed (PATCH semantics)
238+
var roleIds []interface{}
239+
var approvedIps []interface{}
240+
241+
roleIdsChanged := d.HasChange("role_ids")
242+
approvedIpsChanged := d.HasChange("approved_ips")
243+
244+
log.Printf("[DEBUG] role_ids changed: %v, approved_ips changed: %v\n", roleIdsChanged, approvedIpsChanged)
245+
246+
if roleIdsChanged {
247+
roleIds = d.Get("role_ids").(*schema.Set).List()
248+
log.Printf("[DEBUG] role_ids will be updated: %v\n", roleIds)
249+
} else {
250+
roleIds = nil
251+
log.Printf("[DEBUG] role_ids will NOT be updated (nil)\n")
252+
}
253+
254+
if approvedIpsChanged {
255+
approvedIps = d.Get("approved_ips").([]interface{})
256+
log.Printf("[DEBUG] approved_ips will be updated: %v (is nil: %v, length: %d)\n",
257+
approvedIps, approvedIps == nil, len(approvedIps))
258+
} else {
259+
approvedIps = nil
260+
log.Printf("[DEBUG] approved_ips will NOT be updated (nil)\n")
261+
}
203262

204-
roleIds := d.Get("role_ids").(*schema.Set)
205263
userUpdateResponse, err := client.UpdateAccountUser(
206264
accountId,
207265
email,
208-
roleIds.List(),
266+
roleIds,
267+
approvedIps,
209268
)
210269
if err != nil {
211270
log.Printf("[ERROR] Could not update user for email: %s, %s\n", email, err)

incapsula/resource_account_user_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,26 @@ func TestIncapsulaAccountUser_Update(t *testing.T) {
8282
})
8383
}
8484

85+
func TestIncapsulaAccountUser_WithApprovedIps(t *testing.T) {
86+
resource.Test(t, resource.TestCase{
87+
PreCheck: func() { testAccPreCheck(t) },
88+
Providers: testAccProviders,
89+
CheckDestroy: testCheckIncapsulaAccountUserDestroy,
90+
Steps: []resource.TestStep{
91+
{
92+
Config: testCheckIncapsulaAccountUserConfigWithApprovedIps(t, accountUserEmail),
93+
Check: resource.ComposeTestCheckFunc(
94+
testCheckIncapsulaAccountUserExists(accountResourceUserTypeName),
95+
resource.TestCheckResourceAttr(accountResourceUserTypeName, "email", accountUserEmail),
96+
resource.TestCheckResourceAttr(accountResourceUserTypeName, "approved_ips.#", "2"),
97+
resource.TestCheckResourceAttr(accountResourceUserTypeName, "approved_ips.0", "192.168.1.1"),
98+
resource.TestCheckResourceAttr(accountResourceUserTypeName, "approved_ips.1", "10.0.0.5"),
99+
),
100+
},
101+
},
102+
})
103+
}
104+
85105
func TestIncapsulaAccountUser_ImportBasic(t *testing.T) {
86106
resource.Test(t, resource.TestCase{
87107
PreCheck: func() { testAccPreCheck(t) },
@@ -240,3 +260,18 @@ func testCheckIncapsulaAccountUserConfigUpdate(t *testing.T) string {
240260
accountResourceUserType, accountResourceUserName, accountUserEmail, accountUserFirstName, accountUserLastName,
241261
)
242262
}
263+
264+
func testCheckIncapsulaAccountUserConfigWithApprovedIps(t *testing.T, email string) string {
265+
return fmt.Sprintf(`
266+
data "incapsula_account_data" "account_data" {}
267+
268+
resource "%s" "%s" {
269+
account_id = data.incapsula_account_data.account_data.current_account
270+
email = "%s"
271+
first_name = "%s"
272+
last_name = "%s"
273+
approved_ips = ["192.168.1.1", "10.0.0.5"]
274+
}`,
275+
accountResourceUserType, accountResourceUserName, email, accountUserFirstName, accountUserLastName,
276+
)
277+
}

website/docs/r/account_user.html.markdown

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,21 @@ resource "incapsula_account_user" "user_1" {
3535
3636
```
3737

38+
### Usage with Approved IPs
39+
40+
User creation with approved IP addresses for login restrictions.
41+
42+
```hcl
43+
resource "incapsula_account_user" "user_with_ips" {
44+
account_id = data.incapsula_account_data.account_data.current_account
45+
email = "example@terraform.com"
46+
first_name = "First"
47+
last_name = "Last"
48+
approved_ips = ["192.168.1.1", "10.0.0.5", "172.16.0.10"]
49+
}
50+
51+
```
52+
3853
### Role References Usage
3954

4055
Usage with role reference.
@@ -88,6 +103,7 @@ resource "incapsula_account_user" "user_3" {
88103
incapsula_account_role.role_2.id,
89104
data.incapsula_account_roles.roles.reader_role_id,
90105
]
106+
approved_ips = ["192.168.1.1", "10.0.0.0/24"]
91107
}
92108
```
93109

@@ -151,6 +167,8 @@ The following arguments are supported:
151167
* `last_name` - (Optional) The user's last name. This attribute cannot be updated.
152168
* `role_ids` - (Optional) List of role ids to be associated with the user. <p/>
153169
Default value is an empty list (user with no roles).
170+
* `approved_ips` - (Optional) List of approved IP addresses from which the user is allowed to access the Cloud Security Console via the UI or API. <p/>
171+
Supports individual IPs, IP ranges, and CIDR notation. Default value is an empty list (no IP restrictions).
154172

155173

156174
## Attributes Reference

0 commit comments

Comments
 (0)