Skip to content

Commit 084724a

Browse files
authored
Feature/custom user attributes (#138)
# Fix Custom Attribute Support and API Authentication Handling ## Summary This PR improves the OneLogin Terraform Provider by fixing custom attribute creation and updating authentication handling to better support both subdomain and legacy URL-based configurations. ## Changes - Fix custom attribute definition creation by properly wrapping API payloads with the required "user_field" object - Update provider to use subdomain parameter instead of region for API connections - Make authentication backward compatible with both subdomain and URL-based configurations - Reorganize test files to fix duplicate main function issues - Create utility functions for standardized API credential handling - Fix nil pointer panic in provider initialization - Remove unsupported "position" field in custom attribute schema - Update gitignore to exclude test artifacts - Add comprehensive test infrastructure for easier testing ## Issues Fixed - Fixes #67: Feature request for "set custom attributes for the user resource" - Custom attribute creation now works correctly with proper API payload structure - Fixes #122: Subdomain and region configuration problems - Provider now properly handles subdomain configuration with backward compatibility for legacy URL-based configs - Partially addresses API connectivity issues by improving credential handling and authentication patterns ## Test Results All tests have been successfully run including: - Go mod tidy - Go vet - Unit tests - Security checks - Integration tests with actual OneLogin API ## Documentation Documentation has been updated to reflect: - The ability to create custom attribute definitions - Proper usage of subdomain vs region parameters - Example usage for setting custom attribute values on users
2 parents 0c3852b + f0c6630 commit 084724a

28 files changed

+2252
-121
lines changed

.env

Lines changed: 0 additions & 3 deletions
This file was deleted.

.gitignore

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,16 @@ dist
1010
coverage.out
1111
bin
1212
*.asc
13+
14+
# Development and test files
15+
**/.claude/settings.local.json
16+
.env
17+
.terraformrc
18+
*_test.tf
19+
test_*.tf
20+
test_*.sh
21+
test-dir/
22+
test_dir/
23+
test_tool
24+
modified_*.go
25+
*.bak

GNUmakefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ GO111MODULE=on
1010

1111
PLUGINS_DIR=~/.terraform.d/plugins
1212
PLUGIN_PATH=onelogin.com/onelogin/onelogin
13-
VERSION=0.1.10
13+
VERSION=0.1.11
1414

1515
clean:
1616
rm -r ${DIST_DIR}

README.md

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,28 @@
22
[![Go Report Card](https://goreportcard.com/badge/github.com/onelogin/terraform-provider-onelogin)](https://goreportcard.com/report/github.com/onelogin/terraform-provider-onelogin)
33
<a href='https://github.com/dcaponi/gopherbadger' target='_blank'>![gopherbadger-tag-do-not-edit](https://img.shields.io/badge/Go%20Coverage-100%25-brightgreen.svg?longCache=true&style=flat)</a>
44

5+
## Latest Updates
6+
7+
### v0.1.11 - Fixed Custom User Attributes Support
8+
9+
This version fixes the custom attribute support in the OneLogin v4 API:
10+
11+
- Fixed custom attribute creation by wrapping payload with `user_field` object
12+
- **Now supports** creating, reading, updating, and deleting custom attribute definitions
13+
- Improved user management with custom attributes
14+
- Updated provider to use subdomain instead of region for API connections
15+
- See examples in `examples/onelogin_user_custom_attributes_example.tf`
16+
17+
### v0.1.10 - Custom User Attributes Support
18+
19+
This version includes support for Custom User Attributes using the OneLogin v4 API:
20+
21+
- Added new resource `onelogin_user_custom_attributes` for setting values of existing custom user attributes
22+
- Updated to OneLogin Go SDK v4.1.0
23+
- Improved user management with custom attributes
24+
525
## Prerequisites
6-
1. Install Go 1.21 or later
26+
1. Install Go 1.18 or later
727
2. Install Terraform v0.13.x or later
828
3. Install gosec (for security scanning):
929
```bash
@@ -17,7 +37,7 @@
1737
```bash
1838
export ONELOGIN_CLIENT_ID=<your client id>
1939
export ONELOGIN_CLIENT_SECRET=<your client secret>
20-
export ONELOGIN_OAPI_URL=<the api url for your region>
40+
export ONELOGIN_SUBDOMAIN=<your OneLogin subdomain>
2141
```
2242
3. Build and install the provider locally:
2343
```bash
@@ -30,7 +50,7 @@ terraform {
3050
required_providers {
3151
onelogin = {
3252
source = "onelogin.com/onelogin/onelogin"
33-
version = "0.1.10"
53+
version = "0.1.11"
3454
}
3555
}
3656
}

cmd/tests/main.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"os"
7+
8+
"github.com/onelogin/terraform-provider-onelogin/cmd/tests/standalone"
9+
"github.com/onelogin/terraform-provider-onelogin/cmd/tests/v1"
10+
"github.com/onelogin/terraform-provider-onelogin/cmd/tests/v4"
11+
)
12+
13+
func main() {
14+
// Define flags
15+
testName := flag.String("test", "", "Specify which test to run (v1_users, create_attribute, etc.)")
16+
flag.Parse()
17+
18+
// Show help if no test is specified
19+
if *testName == "" {
20+
fmt.Println("Please specify a test to run with the -test flag")
21+
fmt.Println("Available tests:")
22+
fmt.Println(" - v1_users")
23+
fmt.Println(" - create_attribute")
24+
fmt.Println(" - v4_custom_attributes")
25+
os.Exit(1)
26+
}
27+
28+
// Run the specified test
29+
switch *testName {
30+
case "v1_users":
31+
v1.TestV1Users()
32+
case "create_attribute":
33+
standalone.TestCreateAttribute()
34+
case "v4_custom_attributes":
35+
v4.TestV4CustomAttributes()
36+
default:
37+
fmt.Printf("Unknown test: %s\n", *testName)
38+
os.Exit(1)
39+
}
40+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package standalone
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"log"
7+
"net/http"
8+
"time"
9+
10+
ol "github.com/onelogin/onelogin-go-sdk/v4/pkg/onelogin"
11+
"github.com/onelogin/onelogin-go-sdk/v4/pkg/onelogin/api"
12+
"github.com/onelogin/onelogin-go-sdk/v4/pkg/onelogin/authentication"
13+
"github.com/onelogin/terraform-provider-onelogin/cmd/tests/utils"
14+
)
15+
16+
func TestCreateAttribute() {
17+
clientID, clientSecret, url, subdomain := utils.GetAPICredentials()
18+
19+
fmt.Println("Testing creating Age custom attribute with v4 SDK")
20+
fmt.Printf("URL: %s, Subdomain: %s\n", url, subdomain)
21+
22+
// Set up environment variables for the v4 SDK
23+
utils.SetupSDKEnvironment(clientID, clientSecret, subdomain, url)
24+
25+
// Initialize v4 client
26+
authenticator := authentication.NewAuthenticator(subdomain)
27+
timeoutDuration := time.Second * 60
28+
apiClient := &api.Client{
29+
HttpClient: &http.Client{Timeout: timeoutDuration},
30+
Auth: authenticator,
31+
OLdomain: url,
32+
Timeout: timeoutDuration,
33+
}
34+
35+
client := &ol.OneloginSDK{
36+
Client: apiClient,
37+
}
38+
39+
// Test creating a custom attribute
40+
fmt.Println("Creating Age custom attribute...")
41+
timestamp := time.Now().Unix()
42+
attributeName := fmt.Sprintf("Age_%d", timestamp)
43+
attributeShortname := fmt.Sprintf("age_years_%d", timestamp)
44+
45+
// Print request details for debugging
46+
fmt.Printf("Creating attribute with name: %s, shortname: %s\n", attributeName, attributeShortname)
47+
48+
// Follow the exact structure used in the resource
49+
userFieldPayload := map[string]interface{}{
50+
"name": attributeName,
51+
"shortname": attributeShortname,
52+
}
53+
54+
// Wrap in user_field object as required by API
55+
payload := map[string]interface{}{
56+
"user_field": userFieldPayload,
57+
}
58+
59+
// Convert to JSON for logging
60+
payloadJSON, _ := json.Marshal(payload)
61+
fmt.Printf("Request payload: %s\n", string(payloadJSON))
62+
63+
result, err := client.CreateCustomAttributes(payload)
64+
if err != nil {
65+
log.Fatalf("Error creating custom attribute: %v", err)
66+
}
67+
68+
fmt.Printf("Created custom attribute: %+v\n", result)
69+
70+
// Extract the custom attribute ID
71+
resultMap, ok := result.(map[string]interface{})
72+
if !ok {
73+
log.Fatalf("Failed to parse custom attribute creation response")
74+
}
75+
76+
id, ok := resultMap["id"].(float64)
77+
if !ok {
78+
log.Fatalf("Failed to extract custom attribute ID from response")
79+
}
80+
81+
attributeID := int(id)
82+
fmt.Printf("Custom attribute ID: %d\n", attributeID)
83+
84+
// Now verify that we can get the custom attribute
85+
fmt.Println("Getting all custom attributes to verify...")
86+
attributes, err := client.GetCustomAttributes()
87+
if err != nil {
88+
log.Printf("Error getting custom attributes: %v", err)
89+
} else {
90+
fmt.Printf("Custom attributes: %+v\n", attributes)
91+
92+
// Try to find our newly created attribute
93+
attrList, ok := attributes.([]interface{})
94+
if !ok {
95+
log.Fatalf("Invalid custom attributes response format")
96+
}
97+
98+
found := false
99+
for _, attr := range attrList {
100+
attrMap, ok := attr.(map[string]interface{})
101+
if !ok {
102+
continue
103+
}
104+
105+
if attrMap["shortname"] == attributeShortname {
106+
fmt.Printf("Found our attribute: %+v\n", attrMap)
107+
found = true
108+
break
109+
}
110+
}
111+
112+
if !found {
113+
fmt.Println("Could not find our newly created attribute in the list!")
114+
}
115+
}
116+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package standalone
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"log"
7+
"net/http"
8+
"os"
9+
"time"
10+
11+
ol "github.com/onelogin/onelogin-go-sdk/v4/pkg/onelogin"
12+
"github.com/onelogin/onelogin-go-sdk/v4/pkg/onelogin/api"
13+
"github.com/onelogin/onelogin-go-sdk/v4/pkg/onelogin/authentication"
14+
)
15+
16+
func TestCreateAttribute_V2() {
17+
clientID := os.Getenv("ONELOGIN_CLIENT_ID")
18+
clientSecret := os.Getenv("ONELOGIN_CLIENT_SECRET")
19+
url := os.Getenv("ONELOGIN_OAPI_URL")
20+
21+
fmt.Println("Testing creating Age custom attribute with v4 SDK - Improved Version")
22+
fmt.Printf("URL: %s\n", url)
23+
24+
// Set up environment variables for the v4 SDK
25+
os.Setenv("ONELOGIN_CLIENT_ID", clientID)
26+
os.Setenv("ONELOGIN_CLIENT_SECRET", clientSecret)
27+
28+
// Extract subdomain from URL
29+
var subdomain string
30+
if url != "" {
31+
subdomain = "api" // Use api as subdomain when custom URL is provided
32+
os.Setenv("ONELOGIN_OAPI_URL", url)
33+
} else {
34+
subdomain = "api"
35+
}
36+
os.Setenv("ONELOGIN_SUBDOMAIN", subdomain)
37+
os.Setenv("ONELOGIN_TIMEOUT", "60")
38+
39+
// Initialize v4 client
40+
authenticator := authentication.NewAuthenticator(subdomain)
41+
timeoutDuration := time.Second * 60
42+
apiClient := &api.Client{
43+
HttpClient: &http.Client{Timeout: timeoutDuration},
44+
Auth: authenticator,
45+
OLdomain: url,
46+
Timeout: timeoutDuration,
47+
}
48+
49+
client := &ol.OneloginSDK{
50+
Client: apiClient,
51+
}
52+
53+
// First, print the raw client credentials to check
54+
fmt.Println("API Client ID (length):", len(os.Getenv("ONELOGIN_CLIENT_ID")))
55+
fmt.Println("API Client Secret (length):", len(os.Getenv("ONELOGIN_CLIENT_SECRET")))
56+
57+
// For debugging, let's check our credentials by getting a list of users
58+
fmt.Println("Testing credentials by getting a list of users...")
59+
_, err := client.GetUsers(nil)
60+
if err != nil {
61+
log.Printf("Error getting users: %v", err)
62+
} else {
63+
fmt.Println("Successfully retrieved users - credentials are working!")
64+
}
65+
66+
// Get current attributes to see what exists
67+
fmt.Println("Getting current custom attributes...")
68+
currentAttrs, err := client.GetCustomAttributes()
69+
if err != nil {
70+
log.Printf("Error getting current custom attributes: %v", err)
71+
} else {
72+
// Pretty print the attributes
73+
jsonData, _ := json.MarshalIndent(currentAttrs, "", " ")
74+
fmt.Println(string(jsonData))
75+
}
76+
77+
// Test creating a custom attribute - use a timestamp to make it unique
78+
timestamp := time.Now().Unix()
79+
fmt.Println("Creating Age custom attribute...")
80+
attributeName := fmt.Sprintf("Age-%d", timestamp)
81+
attributeShortname := fmt.Sprintf("age_years_%d", timestamp)
82+
83+
payload := map[string]interface{}{
84+
"name": attributeName,
85+
"shortname": attributeShortname,
86+
}
87+
88+
// Print the payload for verification
89+
jsonPayload, _ := json.MarshalIndent(payload, "", " ")
90+
fmt.Printf("Request payload: %s\n", string(jsonPayload))
91+
92+
result, err := client.CreateCustomAttributes(payload)
93+
if err != nil {
94+
log.Printf("Error creating custom attribute: %v", err)
95+
96+
// Try with a simpler payload
97+
fmt.Println("Trying with a simpler payload...")
98+
simplePayload := map[string]string{
99+
"name": attributeName,
100+
"shortname": attributeShortname,
101+
}
102+
103+
jsonPayload, _ := json.MarshalIndent(simplePayload, "", " ")
104+
fmt.Printf("Simple payload: %s\n", string(jsonPayload))
105+
106+
result, err = client.CreateCustomAttributes(simplePayload)
107+
if err != nil {
108+
log.Fatalf("Still failed to create custom attribute: %v", err)
109+
}
110+
}
111+
112+
// If we get here, creation succeeded
113+
fmt.Printf("Created custom attribute: %+v\n", result)
114+
115+
// Pretty print the result
116+
jsonResult, _ := json.MarshalIndent(result, "", " ")
117+
fmt.Println(string(jsonResult))
118+
119+
// Extract the custom attribute ID if possible
120+
resultMap, ok := result.(map[string]interface{})
121+
if !ok {
122+
log.Printf("Warning: Couldn't parse result as map[string]interface{}")
123+
} else {
124+
if id, ok := resultMap["id"].(float64); ok {
125+
attributeID := int(id)
126+
fmt.Printf("Custom attribute ID: %d\n", attributeID)
127+
} else {
128+
fmt.Println("Could not extract ID from result")
129+
}
130+
}
131+
}

0 commit comments

Comments
 (0)