Skip to content

Conversation

@domonkosadam
Copy link
Contributor

refs: CLX-3799
affects: Student
release note: none

@github-actions
Copy link

github-actions bot commented Jan 22, 2026

📊 Code Coverage Report

✅ Student

  • PR Coverage: 43.42%
  • Master Coverage: 43.42%
  • Delta: +0.00%

✅ Teacher

  • PR Coverage: 25.51%
  • Master Coverage: 25.51%
  • Delta: +0.00%

✅ Pandautils

  • PR Coverage: 22.98%
  • Master Coverage: 22.98%
  • Delta: +0.00%

📈 Overall Average

  • PR Coverage: 30.64%
  • Master Coverage: 30.64%
  • Delta: +0.00%

@github-actions
Copy link

github-actions bot commented Jan 22, 2026

🧪 Unit Test Results

✅ 📱 Student App

  • Tests: 1225 total, 0 failed, 0 skipped
  • Duration: 0.000s
  • Success Rate: 100%

✅ 🌅 Horizon

  • Tests: 531 total, 0 failed, 0 skipped
  • Duration: 36.464s
  • Success Rate: 100%

✅ 📦 Submodules

  • Tests: 2642 total, 0 failed, 0 skipped
  • Duration: 46.676s
  • Success Rate: 100%

📊 Summary

  • Total Tests: 4398
  • Failed: 0
  • Skipped: 0
  • Status: ✅ All tests passed!

Last updated: Mon, 26 Jan 2026 09:27:27 GMT

@github-actions
Copy link

github-actions bot commented Jan 26, 2026

Student Install Page

claude[bot]
claude bot previously requested changes Jan 26, 2026
Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review Summary - Consolidate Redwood and Journey Services

I've completed a comprehensive review of this PR which introduces a GraphQL proxy pattern where Journey forwards Redwood operations. This is a significant architectural change with both benefits and concerns.

🎯 Architecture Overview

What Changed:

  • Journey now acts as a proxy for all Redwood GraphQL operations
  • New ExecuteRedwoodQueryMutation wraps Redwood queries/mutations as JSON strings
  • Consolidated authentication using single Journey JWT token
  • Added region-aware URL resolution with {region} template placeholders
  • Removed separate RedwoodModule and related infrastructure

Files Changed: 27 files (+732/-488 lines)

  • Core changes in canvas-api-2 module
  • Integration test updates in horizon module
  • Login flow updates in login-api-2 module

🔴 Critical Issues (Must Fix)

  1. GraphQL Injection Risk - Proxy accepts arbitrary operations without validation
  2. Unvalidated JSON Deserialization - Missing error handling could cause crashes
  3. Missing Error Propagation - Redwood errors not checked before parsing
  4. No Unit Tests - JourneyRedwoodManagerImpl has complex logic but no dedicated tests

⚠️ Important Issues (Should Fix)

  1. Region Validation - No sanitization of region string used in URLs
  2. Null Handling Bug - canvasRegion could overwrite valid values with null
  3. Data Loss - Preference name change will lose existing tokens on upgrade
  4. Performance - Double JSON serialization adds overhead

✅ Positive Aspects

  • Clean service consolidation
  • Consistent Dagger Hilt patterns
  • Type-safe proxy maintains compile-time safety
  • All integration tests properly updated

📋 Recommendations

  1. Add operation allowlist for security validation
  2. Implement comprehensive error handling in proxy layer
  3. Create unit tests for JourneyRedwoodManagerImpl
  4. Add region format validation to prevent URL injection
  5. Document architectural tradeoffs in KDoc
  6. Consider feature flags for gradual rollout

📊 Testing Status

  • ✅ Integration tests updated (8 test files)
  • ❌ Unit tests missing for new manager class
  • ❌ Region resolution not tested
  • ❌ Proxy behavior not verified in integration tests

Please address the critical security and bug issues before merging. I've added inline comments with specific recommendations for each issue.

)

val mutation = ExecuteRedwoodQueryMutation(input)
val response = journeyClient.enqueueMutation(mutation).dataAssertNoErrors
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 CRITICAL - Security: GraphQL Injection Risk

The executeRedwoodQuery method forwards arbitrary GraphQL operations to the Journey proxy without validation. This creates a potential security vulnerability.

Recommendation: Add an allowlist of permitted Redwood operations:

companion object {
    private val ALLOWED_REDWOOD_OPERATIONS = setOf(
        "QueryNotes", "CreateNote", "UpdateNote", "DeleteNote",
        "GetWidgetByTypeAndUser", "CreateWidget"
    )
}

private suspend fun <D : Query.Data> executeRedwoodQuery(operation: Query<D>): D {
    val operationName = operation.name()
    require(operationName in ALLOWED_REDWOOD_OPERATIONS) {
        "Unauthorized Redwood operation: $operationName"
    }
    // ... rest of implementation
}

}

val jsonString = gson.toJson(data)
val buffer = Buffer().writeUtf8(jsonString)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 CRITICAL - Security: Unvalidated JSON Deserialization

The parseResponse method deserializes JSON from the Journey proxy without validation or error handling. This could lead to runtime crashes or type confusion.

Recommendation: Add proper error handling:

private fun <D : Operation.Data> parseResponse(
    data: Any?,
    operation: Operation<D>
): D {
    if (data == null) {
        throw IllegalStateException("Redwood query returned null data")
    }
    
    try {
        val jsonString = gson.toJson(data)
        val buffer = Buffer().writeUtf8(jsonString)
        val jsonReader = buffer.jsonReader()
        
        return operation.adapter().fromJson(jsonReader, customScalarAdapters)
    } catch (e: Exception) {
        Logger.e("Failed to parse Redwood response", e)
        throw IllegalStateException("Invalid Redwood response format", e)
    }
}

val input = RedwoodQueryInput(
query = queryString,
variables = Optional.presentIfNotNull(variables),
operationName = Optional.presentIfNotNull(operation.name())
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 HIGH - Bug: Missing Error Propagation

The code checks for Journey-level errors with dataAssertNoErrors but doesn't check for Redwood-level errors returned in the response. The schema includes errors: [JSON!] field that should be validated.

Recommendation:

val apolloResponse = journeyClient.enqueueMutation(mutation)
val response = apolloResponse.dataAssertNoErrors

// Check for Redwood-level errors
val redwoodErrors = response.executeRedwoodQuery.errors
if (!redwoodErrors.isNullOrEmpty()) {
    throw RedwoodApiException("Redwood query failed: $redwoodErrors")
}

return parseResponse(response.executeRedwoodQuery.data, operation)

return resolveRegion(baseUrlTemplate)
}

private fun resolveRegion(urlTemplate: String): String {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ MEDIUM - Security: Region String Injection Risk

The resolveRegion method performs string replacement without validating the region value. If canvasRegion ever becomes controllable or contains malicious characters, it could manipulate URLs.

Recommendation: Add validation:

private fun resolveRegion(urlTemplate: String): String {
    if (!urlTemplate.contains("{region}")) {
        return urlTemplate
    }
    
    val region = ApiPrefs.canvasRegion?.takeIf { it.isNotEmpty() } ?: "us-east-1"
    
    // Validate region format (alphanumeric + dashes only)
    if (!region.matches(Regex("^[a-z0-9-]+$"))) {
        Logger.w("Invalid region format: $region, using default")
        return urlTemplate.replace("{region}", "us-east-1")
    }
    
    return urlTemplate.replace("{region}", region)
}

logEvent(AnalyticsEventConstants.LOGIN_SUCCESS, bundle)
refreshToken = token!!.refreshToken!!
accessToken = token.accessToken!!
canvasRegion = token.canvasRegion
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ MEDIUM - Bug: Null Overwrites Valid Region

The canvasRegion assignment could overwrite a valid existing region with null if the token response doesn't include a region value.

Recommendation: Only update if present:

refreshToken = token!!.refreshToken!!
accessToken = token.accessToken!!
token.canvasRegion?.let { canvasRegion = it }  // Only update if present

}

object JourneyApiPref: DomainServicesApiPref("journey_api_prefs") {
object JourneyApiPref: DomainServicesApiPref("journey_api_pref") {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ MEDIUM - Data Loss: Preference Name Change

Changing the preference name from "journey_api_prefs" to "journey_api_pref" will cause existing Journey tokens to be lost on app upgrade, requiring users to re-authenticate.

Recommendation: Either revert to the original name or add migration logic:

object JourneyApiPref: DomainServicesApiPref("journey_api_pref") {
    override var token: String? by NStringPref(null, "journey_token")
    
    init {
        // Migration from old preference name
        if (token == null) {
            val oldPrefs = context.getSharedPreferences("journey_api_prefs", Context.MODE_PRIVATE)
            token = oldPrefs.getString("journey_token", null)
        }
    }
}

class JourneyRedwoodManagerImpl @Inject constructor(
@JourneyApolloClient private val journeyClient: ApolloClient
) : RedwoodApiManager {

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ LOW - Code Quality: Gson Configuration

Using default Gson() without configuration may lead to issues with null handling, date formats, or special types.

Recommendation:

private val gson = GsonBuilder()
    .setDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
    .serializeNulls()
    .create()

@domonkosadam domonkosadam merged commit e28fe3c into master Jan 26, 2026
34 checks passed
@domonkosadam domonkosadam deleted the CLX-3799-refactor-domain-services-api branch January 26, 2026 10:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants