Skip to content

Fix type assertion safety issues to prevent runtime panics #845

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jun 30, 2025
Merged
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/unreleased/fixed-20250605-135507.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: fixed
body: Fixed type assertion safety issues in multiple services to prevent potential runtime panics when API responses have unexpected structure or types
time: 2025-06-05T13:55:07.511122417Z
custom:
Issue: "839"
6 changes: 5 additions & 1 deletion internal/services/connection/api_connection.go
Original file line number Diff line number Diff line change
@@ -174,7 +174,11 @@ func (client *client) ShareConnection(ctx context.Context, environmentId, connec
}

func getPrincipalString(principal map[string]any, key string) (string, error) {
value, ok := principal[key].(string)
raw, exists := principal[key]
if !exists {
return "", fmt.Errorf("principal key %s does not exist", key)
}
value, ok := raw.(string)
if !ok {
return "", fmt.Errorf("failed to convert principal %s to string", key)
}
39 changes: 30 additions & 9 deletions internal/services/data_record/api_data_record.go
Original file line number Diff line number Diff line change
@@ -118,9 +118,11 @@ func (client *client) GetDataRecordsByODataQuery(ctx context.Context, environmen
}

var totalRecords *int64
if response["@Microsoft.Dynamics.CRM.totalrecordcount"] != nil {
count := int64(response["@Microsoft.Dynamics.CRM.totalrecordcount"].(float64))
totalRecords = &count
if rawCount, exists := response["@Microsoft.Dynamics.CRM.totalrecordcount"]; exists && rawCount != nil {
if count, ok := rawCount.(float64); ok {
countInt := int64(count)
totalRecords = &countInt
}
}
var totalRecordsCountLimitExceeded *bool
if val, ok := response["@Microsoft.Dynamics.CRM.totalrecordcountlimitexceeded"].(bool); ok {
@@ -145,7 +147,20 @@ func (client *client) GetDataRecordsByODataQuery(ctx context.Context, environmen
records = append(records, response)
}

pluralName := strings.Split(response["@odata.context"].(string), "#")[1]
// Safe extraction of @odata.context
odataCtxRaw, exists := response["@odata.context"]
if !exists {
return nil, errors.New("@odata.context field missing from response")
}
odataCtx, ok := odataCtxRaw.(string)
if !ok {
return nil, errors.New("@odata.context field is not a string")
}
splitParts := strings.Split(odataCtx, "#")
if len(splitParts) < 2 {
return nil, errors.New("@odata.context string is malformed")
}
pluralName := splitParts[1]
if index := strings.IndexAny(pluralName, "(/"); index != -1 {
pluralName = pluralName[:index]
}
@@ -154,7 +169,7 @@ func (client *client) GetDataRecordsByODataQuery(ctx context.Context, environmen
Records: records,
TotalRecord: totalRecords,
TotalRecordLimitExceeded: totalRecordsCountLimitExceeded,
TableMetadataUrl: response["@odata.context"].(string),
TableMetadataUrl: odataCtx,
// url will be as example: https://org.crm4.dynamics.com/api/data/v9.2/$metadata#tablepluralname/$entity.
TablePluralName: pluralName,
}, nil
@@ -279,10 +294,16 @@ func (client *client) GetTableSingularNameFromPlural(ctx context.Context, enviro
}

var result string
if mapResponse["value"] != nil && len(mapResponse["value"].([]any)) > 0 {
if value, ok := mapResponse["value"].([]any)[0].(map[string]any); ok {
if logicalName, ok := value["LogicalName"].(string); ok {
result = logicalName
if rawVal, exists := mapResponse["value"]; exists && rawVal != nil {
valueSlice, ok := rawVal.([]any)
if !ok {
return nil, errors.New("value field is not of type []any")
}
if len(valueSlice) > 0 {
if value, ok := valueSlice[0].(map[string]any); ok {
if logicalName, ok := value["LogicalName"].(string); ok {
result = logicalName
}
}
}
} else if logicalName, ok := mapResponse["LogicalName"].(string); ok {
46 changes: 28 additions & 18 deletions internal/services/data_record/datasource_data_record.go
Original file line number Diff line number Diff line change
@@ -275,27 +275,30 @@
attributes := make(map[string]attr.Value)

for key, value := range columns {
switch value.(type) {
switch v := value.(type) {
case bool:
caseBool(columns[key].(bool), attributes, attributeTypes, key)
caseBool(v, attributes, attributeTypes, key)
case int64:
caseInt64(columns[key].(int64), attributes, attributeTypes, key)
caseInt64(v, attributes, attributeTypes, key)
case float64:
caseFloat64(columns[key].(float64), attributes, attributeTypes, key)
caseFloat64(v, attributes, attributeTypes, key)
case string:
caseString(columns[key].(string), attributes, attributeTypes, key)
caseString(v, attributes, attributeTypes, key)
case map[string]any:
typ, val, _ := d.buildObjectValueFromX(columns[key].(map[string]any))
typ, val, _ := d.buildObjectValueFromX(v)
tupleElementType := types.ObjectType{
AttrTypes: typ,
}
v, _ := types.ObjectValue(typ, val)
attributes[key] = v
objVal, _ := types.ObjectValue(typ, val)
attributes[key] = objVal
attributeTypes[key] = tupleElementType
case []any:
typeObj, valObj := d.buildExpandObject(columns[key].([]any))
typeObj, valObj := d.buildExpandObject(v)
attributeTypes[key] = typeObj
attributes[key] = valObj
default:
// Handle unexpected types gracefully by skipping them
continue
}
}

@@ -309,27 +312,34 @@
knownObjectValue := map[string]attr.Value{}

for key, value := range columns {
switch value.(type) {
switch v := value.(type) {
case bool:
caseBool(columns[key].(bool), knownObjectValue, knownObjectType, key)
caseBool(v, knownObjectValue, knownObjectType, key)
case int64:
caseInt64(columns[key].(int64), knownObjectValue, knownObjectType, key)
caseInt64(v, knownObjectValue, knownObjectType, key)
case float64:
caseFloat64(columns[key].(float64), knownObjectValue, knownObjectType, key)
caseFloat64(v, knownObjectValue, knownObjectType, key)
case string:
caseString(columns[key].(string), knownObjectValue, knownObjectType, key)
caseString(v, knownObjectValue, knownObjectType, key)
case map[string]any:
typ, val, _ := d.buildObjectValueFromX(columns[key].(map[string]any))
typ, val, _ := d.buildObjectValueFromX(v)
tupleElementType := types.ObjectType{
AttrTypes: typ,
}
v, _ := types.ObjectValue(typ, val)
knownObjectValue[key] = v
objVal, _ := types.ObjectValue(typ, val)
knownObjectValue[key] = objVal
knownObjectType[key] = tupleElementType
case []any:
typeObj, valObj := d.buildExpandObject(columns[key].([]any))
typeObj, valObj := d.buildExpandObject(v)
knownObjectValue[key] = valObj
knownObjectType[key] = typeObj
default:
// Log unexpected types for debugging purposes
tflog.Debug(context.Background(), "Skipping unhandled type in buildObjectValueFromX", map[string]interface{}{

Check failure on line 338 in internal/services/data_record/datasource_data_record.go

GitHub Actions / lint

use-any: since Go 1.18 'interface{}' can be replaced by 'any' (revive)
"key": key,
"valueType": fmt.Sprintf("%T", value),
})
continue
}
}
return knownObjectType, knownObjectValue, nil
Original file line number Diff line number Diff line change
@@ -118,7 +118,14 @@
)
return
}
d.SolutionCheckerRulesClient = newSolutionCheckerRulesClient(client.Api)
// Additional safety check for nil client
if client != nil {
d.SolutionCheckerRulesClient = newSolutionCheckerRulesClient(client.Api)
} else {
tflog.Warn(ctx, "Client is nil. Datasource will not be fully configured.", map[string]interface{}{

Check failure on line 125 in internal/services/solution_checker_rules/datasource_solution_checker_rules.go

GitHub Actions / lint

use-any: since Go 1.18 'interface{}' can be replaced by 'any' (revive)
"datasource": d.FullTypeName(),
})
}
}

func (d *DataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
9 changes: 5 additions & 4 deletions internal/services/tenant_settings/api_tenant_settings.go
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@ package tenant_settings

import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
@@ -82,12 +83,12 @@ func (client *client) UpdateTenantSettings(ctx context.Context, tenantSettings t
return &backendSettings, nil
}

func applyCorrections(ctx context.Context, planned tenantSettingsDto, actual tenantSettingsDto) *tenantSettingsDto {
func applyCorrections(ctx context.Context, planned tenantSettingsDto, actual tenantSettingsDto) (*tenantSettingsDto, error) {
correctedFilter := filterDto(ctx, planned, actual)
corrected, ok := correctedFilter.(*tenantSettingsDto)
if !ok {
tflog.Error(ctx, "Type assertion to failed in applyCorrections")
return nil
tflog.Error(ctx, "Type assertion failed in applyCorrections")
return nil, errors.New("type assertion to *tenantSettingsDto failed in applyCorrections")
}

if planned.PowerPlatform != nil && planned.PowerPlatform.Governance != nil {
@@ -101,7 +102,7 @@ func applyCorrections(ctx context.Context, planned tenantSettingsDto, actual ten
corrected.PowerPlatform.Governance.EnvironmentRoutingTargetEnvironmentGroupId = &zu
}
}
return corrected
return corrected, nil
}

// This function is used to filter out the fields that are not opted in to configuration.
24 changes: 18 additions & 6 deletions internal/services/tenant_settings/resource_tenant_settings.go
Original file line number Diff line number Diff line change
@@ -407,7 +407,11 @@ func (r *TenantSettingsResource) Create(ctx context.Context, req resource.Create
return
}

stateDto := applyCorrections(ctx, plannedSettingsDto, *tenantSettingsDto)
stateDto, err := applyCorrections(ctx, plannedSettingsDto, *tenantSettingsDto)
if err != nil {
resp.Diagnostics.AddError("Error applying corrections to tenant settings", err.Error())
return
}

state, _, err := convertFromTenantSettingsDto[TenantSettingsResourceModel](*stateDto, plan.Timeouts)
if err != nil {
@@ -453,7 +457,11 @@ func (r *TenantSettingsResource) Read(ctx context.Context, req resource.ReadRequ
resp.Diagnostics.AddError("Error converting to tenant settings DTO", err.Error())
return
}
newStateDto := applyCorrections(ctx, oldStateDto, *tenantSettings)
newStateDto, err := applyCorrections(ctx, oldStateDto, *tenantSettings)
if err != nil {
resp.Diagnostics.AddError("Error applying corrections to tenant settings", err.Error())
return
}
newState, _, err := convertFromTenantSettingsDto[TenantSettingsResourceModel](*newStateDto, state.Timeouts)
if err != nil {
resp.Diagnostics.AddError("Error converting tenant settings", err.Error())
@@ -522,7 +530,11 @@ func (r *TenantSettingsResource) Update(ctx context.Context, req resource.Update
}

// need to make corrections from what the API returns to match what terraform expects
filteredDto := applyCorrections(ctx, plannedDto, *updatedSettingsDto)
filteredDto, err := applyCorrections(ctx, plannedDto, *updatedSettingsDto)
if err != nil {
resp.Diagnostics.AddError("Error applying corrections to tenant settings", err.Error())
return
}

newState, _, err := convertFromTenantSettingsDto[TenantSettingsResourceModel](*filteredDto, plan.Timeouts)
if err != nil {
@@ -567,10 +579,10 @@ func (r *TenantSettingsResource) Delete(ctx context.Context, req resource.Delete
return
}

correctedDto := applyCorrections(ctx, stateDto, originalSettings)
if correctedDto == nil {
correctedDto, err := applyCorrections(ctx, stateDto, originalSettings)
if err != nil {
resp.Diagnostics.AddError(
"Error applying corrections", "Error applying corrections",
"Error applying corrections", fmt.Sprintf("Error applying corrections: %s", err.Error()),
)
return
}
Loading