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

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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
Expand Up @@ -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)
}
Expand Down
39 changes: 30 additions & 9 deletions internal/services/data_record/api_data_record.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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]
}
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
42 changes: 24 additions & 18 deletions internal/services/data_record/datasource_data_record.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,27 +275,30 @@ func (d *DataRecordDataSource) convertColumnsToState(columns map[string]any) (*b
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
}
}

Expand All @@ -309,27 +312,30 @@ func (d *DataRecordDataSource) buildObjectValueFromX(columns map[string]any) (ma
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:
// Handle unexpected types gracefully by skipping them
continue
}
}
return knownObjectType, knownObjectValue, nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,10 @@ func (d *DataSource) Configure(ctx context.Context, req datasource.ConfigureRequ
)
return
}
d.SolutionCheckerRulesClient = newSolutionCheckerRulesClient(client.Api)
// Additional safety check for nil client
if client != nil {
d.SolutionCheckerRulesClient = newSolutionCheckerRulesClient(client.Api)
}
}

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

import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
Expand Down Expand Up @@ -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 {
Expand All @@ -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.
Expand Down
24 changes: 18 additions & 6 deletions internal/services/tenant_settings/resource_tenant_settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down