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 all 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
60 changes: 37 additions & 23 deletions internal/services/data_record/datasource_data_record.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ func (d *DataRecordDataSource) Read(ctx context.Context, req datasource.ReadRequ

var elements = []attr.Value{}
for _, record := range queryRespnse.Records {
columns, err := d.convertColumnsToState(record)
columns, err := d.convertColumnsToState(ctx, record)
if err != nil {
resp.Diagnostics.AddError("Failed to convert columns to state", err.Error())
return
Expand All @@ -265,35 +265,42 @@ func (d *DataRecordDataSource) Read(ctx context.Context, req datasource.ReadRequ
}
}

func (d *DataRecordDataSource) convertColumnsToState(columns map[string]any) (*basetypes.DynamicValue, error) {
func (d *DataRecordDataSource) convertColumnsToState(ctx context.Context, columns map[string]any) (*basetypes.DynamicValue, error) {
if columns == nil {
return nil, nil
}
attributeTypes := make(map[string]attr.Type)
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(ctx, 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(ctx, v)
attributeTypes[key] = typeObj
attributes[key] = valObj
default:
// Handle unexpected types gracefully by skipping them
tflog.Debug(ctx, "Skipping unhandled type in convertColumnsToState", map[string]any{
"key": key,
"valueType": fmt.Sprintf("%T", value),
})
continue
}
}

Expand All @@ -302,42 +309,49 @@ func (d *DataRecordDataSource) convertColumnsToState(columns map[string]any) (*b
return &result, nil
}

func (d *DataRecordDataSource) buildObjectValueFromX(columns map[string]any) (map[string]attr.Type, map[string]attr.Value, error) {
func (d *DataRecordDataSource) buildObjectValueFromX(ctx context.Context, columns map[string]any) (map[string]attr.Type, map[string]attr.Value, error) {
knownObjectType := map[string]attr.Type{}
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(ctx, 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(ctx, v)
knownObjectValue[key] = valObj
knownObjectType[key] = typeObj
default:
// Log unexpected types for debugging purposes
tflog.Debug(ctx, "Skipping unhandled type in buildObjectValueFromX", map[string]any{
"key": key,
"valueType": fmt.Sprintf("%T", value),
})
continue
}
}
return knownObjectType, knownObjectValue, nil
}

func (d *DataRecordDataSource) buildExpandObject(items []any) (basetypes.TupleType, basetypes.TupleValue) {
func (d *DataRecordDataSource) buildExpandObject(ctx context.Context, items []any) (basetypes.TupleType, basetypes.TupleValue) {
var listTypes []attr.Type
var listValues []attr.Value
for _, item := range items {
typ, val, _ := d.buildObjectValueFromX(item.(map[string]any))
typ, val, _ := d.buildObjectValueFromX(ctx, item.(map[string]any))
tupleElementType := types.ObjectType{
AttrTypes: typ,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,14 @@ 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)
} else {
tflog.Warn(ctx, "Client is nil. Datasource will not be fully configured.", map[string]any{
"datasource": d.FullTypeName(),
})
}
}

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
25 changes: 18 additions & 7 deletions internal/services/tenant_settings/resource_tenant_settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,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 @@ -456,7 +460,11 @@ func (r *TenantSettingsResource) Read(ctx context.Context, req resource.ReadRequ
resp.Diagnostics.AddError("Unable to Convert Tenant Settings Model to DTO in Read", 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("Unable to Convert Tenant Settings DTO to Model in Read", err.Error())
Expand Down Expand Up @@ -526,7 +534,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 @@ -572,11 +584,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(
"Unable to Apply Corrections in Delete",
"Could not apply corrections to tenant settings during resource deletion",
"Error applying corrections", fmt.Sprintf("Error applying corrections: %s", err.Error()),
)
return
}
Expand Down