Skip to content

Commit b52d6e7

Browse files
author
Felippe Ferreira
authored
Merge pull request #51 from agilize/develop
feature: import existing users and create new users with scheduled activation
2 parents 23fe741 + 1caa39f commit b52d6e7

File tree

7 files changed

+260
-22
lines changed

7 files changed

+260
-22
lines changed

docs/resources/user.md

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,51 @@ resource "jumpcloud_user" "example" {
4646
}
4747
```
4848

49+
### User with STAGED State and Scheduled Activation
50+
51+
This example shows how to create a user in STAGED state with scheduled activation:
52+
53+
```hcl
54+
resource "jumpcloud_user" "staged_user" {
55+
username = "staged.user"
56+
57+
password = "SecurePassword123!"
58+
59+
firstname = "Staged"
60+
lastname = "User"
61+
62+
# Create user in STAGED state
63+
state = "STAGED"
64+
65+
# Schedule activation for a future date
66+
activation_scheduled = true
67+
scheduled_activation_date = "2024-01-15T09:00:00Z"
68+
}
69+
```
70+
71+
### Importing Existing Users
72+
73+
You can import users that were created manually in the JumpCloud console:
74+
75+
```bash
76+
# Import using the JumpCloud user ID
77+
terraform import jumpcloud_user.existing_user 507f1f77bcf86cd799439011
78+
```
79+
80+
After importing, you can manage the user through Terraform:
81+
82+
```hcl
83+
resource "jumpcloud_user" "existing_user" {
84+
# Configuration will be populated from the imported user
85+
username = "imported.user"
86+
87+
88+
# You can now manage this user through Terraform
89+
firstname = "Imported"
90+
lastname = "User"
91+
}
92+
```
93+
4994
### User with Custom Attributes and MFA Enabled
5095

5196
```hcl
@@ -316,6 +361,9 @@ The following arguments are supported:
316361
* `samba_service_user` - (Optional) Whether the user is a Samba service user. Defaults to `false`.
317362
* `sudo` - (Optional) Whether to grant sudo access to the user. Defaults to `false`.
318363
* `suspended` - (Optional) Whether the user is suspended. Defaults to `false`.
364+
* `state` - (Optional) The state of the user. Valid values are `ACTIVATED`, `STAGED`, `DISABLED`. Defaults to `ACTIVATED`.
365+
* `activation_scheduled` - (Optional) Whether user activation is scheduled for a future date. Defaults to `false`.
366+
* `scheduled_activation_date` - (Optional) Date when user should be automatically activated (ISO 8601 format). Only used when `activation_scheduled` is `true`.
319367
* `unix_uid` - (Optional) The Unix UID for the user. Must be an integer.
320368
* `unix_guid` - (Optional) The Unix GUID for the user. Must be an integer.
321369
* `disable_device_max_login_attempts` - (Optional, Deprecated) Whether to disable maximum login attempts for the user's devices. Defaults to `false`. Use `bypass_managed_device_lockout` instead.
@@ -337,13 +385,42 @@ In addition to all arguments above, the following attributes are exported:
337385

338386
## Import
339387

340-
Users can be imported using the ID, e.g.,
388+
Users can be imported using their JumpCloud user ID. This allows you to bring existing JumpCloud users (created manually in the console or through other means) under Terraform management.
341389

342390
```bash
343391
$ terraform import jumpcloud_user.example 5f0c1b2c3d4e5f6g7h8i9j0k
344392
```
345393

346-
This allows you to bring existing JumpCloud users under Terraform management.
394+
### Finding User IDs
395+
396+
You can find the user ID in several ways:
397+
398+
1. **JumpCloud Console**: Navigate to the user's profile page - the ID is in the URL
399+
2. **JumpCloud API**: Use the `/api/systemusers` endpoint to list users and their IDs
400+
3. **CLI Tools**: Use JumpCloud's CLI tools or API clients
401+
402+
### Import Process
403+
404+
When importing a user:
405+
406+
1. The import process will read all current user attributes from JumpCloud
407+
2. All fields (including state, activation settings, custom attributes, etc.) will be populated
408+
3. After import, you can modify the Terraform configuration to manage the user going forward
409+
4. The user's current state (ACTIVATED, STAGED, etc.) will be preserved
410+
411+
### Example Import Workflow
412+
413+
```bash
414+
# 1. Import the existing user
415+
terraform import jumpcloud_user.john_doe 507f1f77bcf86cd799439011
416+
417+
# 2. Run terraform plan to see the current state
418+
terraform plan
419+
420+
# 3. Update your .tf file to match the imported state or make desired changes
421+
# 4. Apply any changes
422+
terraform apply
423+
```
347424

348425
## State Management Considerations
349426

examples/resources/jumpcloud_user/resource.tf

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,32 @@ resource "jumpcloud_user" "temporary" {
6363
password,
6464
]
6565
}
66-
}
66+
}
67+
68+
# Exemplo de usuário em estado STAGED com agendamento de ativação
69+
resource "jumpcloud_user" "staged_user" {
70+
username = "staged.user"
71+
72+
firstname = "Staged"
73+
lastname = "User"
74+
75+
password = "SecurePassword789!"
76+
77+
# Criar usuário em estado STAGED
78+
state = "STAGED"
79+
80+
# Agendar ativação para uma data futura
81+
activation_scheduled = true
82+
scheduled_activation_date = "2024-01-15T09:00:00Z"
83+
84+
# Configurações organizacionais
85+
company = "Example Inc."
86+
department = "HR"
87+
job_title = "New Hire"
88+
89+
# Atributos personalizados
90+
attributes = {
91+
onboarding_status = "pending"
92+
start_date = "2024-01-15"
93+
}
94+
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ require (
1111
github.com/ProtonMail/go-crypto v1.1.3 // indirect
1212
github.com/agext/levenshtein v1.2.2 // indirect
1313
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
14-
github.com/cloudflare/circl v1.3.7 // indirect
14+
github.com/cloudflare/circl v1.6.1 // indirect
1515
github.com/fatih/color v1.16.0 // indirect
1616
github.com/golang/protobuf v1.5.4 // indirect
1717
github.com/google/go-cmp v0.6.0 // indirect

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew
1111
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
1212
github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
1313
github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=
14-
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
15-
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
14+
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
15+
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
1616
github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo=
1717
github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
1818
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

jumpcloud/authentication/conditional_access/resource_conditional_access_rule_test.go

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,20 @@ import (
1414
func TestResourceConditionalAccessRule(t *testing.T) {
1515
r := ResourceConditionalAccessRule()
1616
// Use standard Go testing instead of assert
17-
if r == nil {
18-
t.Fatal("Expected non-nil resource")
19-
}
20-
if r.Schema["name"] == nil {
21-
t.Fatal("Expected non-nil name schema")
22-
}
23-
if r.Schema["policy_id"] == nil {
24-
t.Fatal("Expected non-nil policy_id schema")
25-
}
26-
if r.Schema["conditions"] == nil {
27-
t.Fatal("Expected non-nil conditions schema")
17+
18+
// Check resource and schema are not nil
19+
if r == nil || r.Schema == nil {
20+
t.Fatal("Expected non-nil resource and schema")
2821
}
29-
if r.Schema["action"] == nil {
30-
t.Fatal("Expected non-nil action schema")
22+
23+
// Check required schema fields
24+
requiredFields := []string{"name", "policy_id", "conditions", "action"}
25+
for _, field := range requiredFields {
26+
if r.Schema[field] == nil {
27+
t.Fatalf("Expected non-nil %s schema", field)
28+
}
3129
}
30+
// Check required context functions
3231
if r.CreateContext == nil {
3332
t.Fatal("Expected non-nil CreateContext")
3433
}

jumpcloud/users/users_directory/resource_user.go

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,8 @@ type User struct {
237237
PasswordlessSudo bool `json:"passwordless_sudo,omitempty"`
238238
SambaServiceUser bool `json:"samba_service_user,omitempty"`
239239
State string `json:"state,omitempty"`
240+
ActivationScheduled bool `json:"activation_scheduled,omitempty"`
241+
ScheduledActivationDate string `json:"scheduled_activation_date,omitempty"`
240242
Sudo bool `json:"sudo,omitempty"`
241243
Suspended bool `json:"suspended,omitempty"`
242244
SystemUsername string `json:"systemUsername,omitempty"`
@@ -280,6 +282,9 @@ func ResourceUser() *schema.Resource {
280282
ReadContext: resourceUserRead,
281283
UpdateContext: resourceUserUpdate,
282284
DeleteContext: resourceUserDelete,
285+
Importer: &schema.ResourceImporter{
286+
StateContext: resourceUserImport,
287+
},
283288
Schema: map[string]*schema.Schema{
284289
"id": {
285290
Type: schema.TypeString,
@@ -560,9 +565,21 @@ func ResourceUser() *schema.Resource {
560565
},
561566
},
562567
"state": {
563-
Type: schema.TypeString,
564-
Optional: true,
565-
Computed: true,
568+
Type: schema.TypeString,
569+
Optional: true,
570+
Computed: true,
571+
Description: "User state (ACTIVATED, STAGED, DISABLED, etc.)",
572+
},
573+
"activation_scheduled": {
574+
Type: schema.TypeBool,
575+
Optional: true,
576+
Default: false,
577+
Description: "Whether user activation is scheduled for a future date",
578+
},
579+
"scheduled_activation_date": {
580+
Type: schema.TypeString,
581+
Optional: true,
582+
Description: "Date when user should be automatically activated (ISO 8601 format)",
566583
},
567584
"totp_enabled": {
568585
Type: schema.TypeBool,
@@ -679,6 +696,10 @@ func resourceUserCreate(ctx context.Context, d *schema.ResourceData, meta any) d
679696
// Usar enable_global_admin_sudo se estiver definido, caso contrário usar sudo para compatibilidade
680697
Sudo: getFirstDefinedBool(d, []string{"enable_global_admin_sudo", "sudo"}),
681698
Suspended: d.Get("suspended").(bool),
699+
// Campos para controle de estado e agendamento de ativação
700+
State: d.Get("state").(string),
701+
ActivationScheduled: d.Get("activation_scheduled").(bool),
702+
ScheduledActivationDate: d.Get("scheduled_activation_date").(string),
682703
// Mapeamento correto: bypass_managed_device_lockout -> disableDeviceMaxLoginAttempts
683704
DisableDeviceMaxLoginAttempts: d.Get("bypass_managed_device_lockout").(bool),
684705
AllowPublicKey: d.Get("allow_public_key").(bool),
@@ -1072,6 +1093,14 @@ func resourceUserRead(ctx context.Context, d *schema.ResourceData, meta any) dia
10721093
return diag.FromErr(fmt.Errorf("error setting state: %v", err))
10731094
}
10741095

1096+
if err := d.Set("activation_scheduled", user.ActivationScheduled); err != nil {
1097+
return diag.FromErr(fmt.Errorf("error setting activation_scheduled: %v", err))
1098+
}
1099+
1100+
if err := d.Set("scheduled_activation_date", user.ScheduledActivationDate); err != nil {
1101+
return diag.FromErr(fmt.Errorf("error setting scheduled_activation_date: %v", err))
1102+
}
1103+
10751104
if err := d.Set("created", user.Created); err != nil {
10761105
return diag.FromErr(fmt.Errorf("error setting created: %v", err))
10771106
}
@@ -1398,6 +1427,10 @@ func resourceUserUpdate(ctx context.Context, d *schema.ResourceData, meta any) d
13981427
// Usar enable_global_admin_sudo se estiver definido, caso contrário usar sudo para compatibilidade
13991428
Sudo: getFirstDefinedBool(d, []string{"enable_global_admin_sudo", "sudo"}),
14001429
Suspended: d.Get("suspended").(bool),
1430+
// Campos para controle de estado e agendamento de ativação
1431+
State: d.Get("state").(string),
1432+
ActivationScheduled: d.Get("activation_scheduled").(bool),
1433+
ScheduledActivationDate: d.Get("scheduled_activation_date").(string),
14011434
// Mapeamento correto: bypass_managed_device_lockout -> disableDeviceMaxLoginAttempts
14021435
DisableDeviceMaxLoginAttempts: d.Get("bypass_managed_device_lockout").(bool),
14031436
AllowPublicKey: d.Get("allow_public_key").(bool),
@@ -1749,3 +1782,35 @@ func resourceUserDelete(ctx context.Context, d *schema.ResourceData, meta any) d
17491782

17501783
return nil
17511784
}
1785+
1786+
// resourceUserImport imports an existing user by ID
1787+
func resourceUserImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
1788+
// The ID provided during import should be the JumpCloud user ID
1789+
userID := d.Id()
1790+
1791+
// Validate that the ID is not empty
1792+
if userID == "" {
1793+
return nil, fmt.Errorf("user ID cannot be empty")
1794+
}
1795+
1796+
// Set the ID in the resource data
1797+
d.SetId(userID)
1798+
1799+
// Call the read function to populate all the fields
1800+
diags := resourceUserRead(ctx, d, meta)
1801+
if diags.HasError() {
1802+
// Convert diagnostics to error for import
1803+
var errMsgs []string
1804+
for _, diag := range diags {
1805+
errMsgs = append(errMsgs, diag.Summary)
1806+
}
1807+
return nil, fmt.Errorf("failed to read user during import: %s", strings.Join(errMsgs, "; "))
1808+
}
1809+
1810+
// Check if the resource was found (ID would be empty if not found)
1811+
if d.Id() == "" {
1812+
return nil, fmt.Errorf("user with ID %s not found", userID)
1813+
}
1814+
1815+
return []*schema.ResourceData{d}, nil
1816+
}

jumpcloud/users/users_directory/resource_user_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,3 +451,72 @@ resource "jumpcloud_user" "test_console" {
451451
}
452452
`
453453
}
454+
455+
// TestAccResourceUserImport tests the import functionality
456+
func TestAccResourceUserImport(t *testing.T) {
457+
resourceName := "jumpcloud_user.test"
458+
459+
resource.Test(t, resource.TestCase{
460+
PreCheck: func() { jctest.TestAccPreCheck(t) },
461+
ProviderFactories: jctest.GetProviderFactories(),
462+
CheckDestroy: testAccCheckJumpCloudUserDestroy,
463+
Steps: []resource.TestStep{
464+
{
465+
Config: testAccJumpCloudUserConfig_basic(),
466+
Check: resource.ComposeTestCheckFunc(
467+
testAccCheckJumpCloudUserExists(resourceName),
468+
resource.TestCheckResourceAttr(resourceName, "username", "testuser"),
469+
resource.TestCheckResourceAttr(resourceName, "email", "[email protected]"),
470+
),
471+
},
472+
{
473+
ResourceName: resourceName,
474+
ImportState: true,
475+
ImportStateVerify: true,
476+
ImportStateVerifyIgnore: []string{"password"}, // Password is not returned by API
477+
},
478+
},
479+
})
480+
}
481+
482+
// TestAccResourceUserStagedState tests creating users in STAGED state with scheduled activation
483+
func TestAccResourceUserStagedState(t *testing.T) {
484+
resourceName := "jumpcloud_user.test_staged"
485+
486+
resource.Test(t, resource.TestCase{
487+
PreCheck: func() { jctest.TestAccPreCheck(t) },
488+
ProviderFactories: jctest.GetProviderFactories(),
489+
CheckDestroy: testAccCheckJumpCloudUserDestroy,
490+
Steps: []resource.TestStep{
491+
{
492+
Config: testAccJumpCloudUserConfig_staged(),
493+
Check: resource.ComposeTestCheckFunc(
494+
testAccCheckJumpCloudUserExists(resourceName),
495+
resource.TestCheckResourceAttr(resourceName, "username", "testuser_staged"),
496+
resource.TestCheckResourceAttr(resourceName, "email", "[email protected]"),
497+
resource.TestCheckResourceAttr(resourceName, "state", "STAGED"),
498+
resource.TestCheckResourceAttr(resourceName, "activation_scheduled", "true"),
499+
resource.TestCheckResourceAttr(resourceName, "scheduled_activation_date", "2024-12-31T23:59:59Z"),
500+
),
501+
},
502+
},
503+
})
504+
}
505+
506+
// testAccJumpCloudUserConfig_staged returns a configuration for a user in STAGED state with scheduled activation
507+
func testAccJumpCloudUserConfig_staged() string {
508+
return `
509+
resource "jumpcloud_user" "test_staged" {
510+
username = "testuser_staged"
511+
512+
password = "TempPassword123!"
513+
514+
firstname = "Test"
515+
lastname = "Staged"
516+
517+
state = "STAGED"
518+
activation_scheduled = true
519+
scheduled_activation_date = "2024-12-31T23:59:59Z"
520+
}
521+
`
522+
}

0 commit comments

Comments
 (0)