BIT-636: Rectify sync api response model (#145)

This commit is contained in:
Ramsey Smith 2023-10-23 12:01:50 -06:00 committed by Álison Fernandes
parent bd8357a0c4
commit db30504b70
12 changed files with 633 additions and 41 deletions

View file

@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.platform.datasource.network.di
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import com.x8bit.bitwarden.data.platform.datasource.network.core.ResultCallAdapterFactory
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
import com.x8bit.bitwarden.data.platform.datasource.network.serializer.LocalDateTimeSerializer
import com.x8bit.bitwarden.data.platform.datasource.network.service.ConfigService
import com.x8bit.bitwarden.data.platform.datasource.network.service.ConfigServiceImpl
import dagger.Module
@ -11,11 +12,13 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.create
import java.time.LocalDateTime
import javax.inject.Named
import javax.inject.Singleton
@ -98,5 +101,8 @@ object NetworkModule {
// We allow for nullable values to have keys missing in the JSON response.
explicitNulls = false
serializersModule = SerializersModule {
contextual(LocalDateTime::class, LocalDateTimeSerializer())
}
}
}

View file

@ -0,0 +1,38 @@
package com.x8bit.bitwarden.data.platform.datasource.network.serializer
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeParseException
/**
* Used to serialize and deserialize [LocalDateTime].
*/
class LocalDateTimeSerializer : KSerializer<LocalDateTime> {
private val localDateTimeFormatter =
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SS'Z'")
private val localDateTimeFormatterNanoSeconds =
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSS'Z'")
override val descriptor: SerialDescriptor
get() = PrimitiveSerialDescriptor(serialName = "LocalDateTime", kind = PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): LocalDateTime =
decoder.decodeString().let { dateString ->
try {
LocalDateTime
.parse(dateString, localDateTimeFormatter)
} catch (exception: DateTimeParseException) {
LocalDateTime
.parse(dateString, localDateTimeFormatterNanoSeconds)
}
}
override fun serialize(encoder: Encoder, value: LocalDateTime) {
encoder.encodeString(localDateTimeFormatter.format(value))
}
}

View file

@ -0,0 +1,26 @@
package com.x8bit.bitwarden.data.vault.datasource.network.model
import com.x8bit.bitwarden.data.platform.datasource.network.serializer.BaseEnumeratedIntSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Represents different types of cipher repromt.
*/
@Serializable(CipherRepromptTypeSerializer::class)
enum class CipherRepromptTypeJson {
/**
* No re-prompt is necessary.
*/
@SerialName("0")
NONE,
/**
* The user should be prompted for their master password prior to using the cipher password.
*/
@SerialName("1")
PASSWORD,
}
private class CipherRepromptTypeSerializer :
BaseEnumeratedIntSerializer<CipherRepromptTypeJson>(CipherRepromptTypeJson.values())

View file

@ -0,0 +1,38 @@
package com.x8bit.bitwarden.data.vault.datasource.network.model
import com.x8bit.bitwarden.data.platform.datasource.network.serializer.BaseEnumeratedIntSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Represents different types of ciphers.
*/
@Serializable(CipherTypeSerializer::class)
enum class CipherTypeJson {
/**
* A login containing a username and password.
*/
@SerialName("1")
LOGIN,
/**
* A secure note.
*/
@SerialName("2")
SECURE_NOTE,
/**
* A credit/debit card.
*/
@SerialName("3")
CARD,
/**
* Personal information for filling out forms.
*/
@SerialName("4")
IDENTITY,
}
private class CipherTypeSerializer :
BaseEnumeratedIntSerializer<CipherTypeJson>(CipherTypeJson.values())

View file

@ -0,0 +1,38 @@
package com.x8bit.bitwarden.data.vault.datasource.network.model
import com.x8bit.bitwarden.data.platform.datasource.network.serializer.BaseEnumeratedIntSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Represents different types of fields.
*/
@Serializable(FieldTypeSerializer::class)
enum class FieldTypeJson {
/**
* The field stores freeform input.
*/
@SerialName("0")
TEXT,
/**
* The field stores freeform input that is hidden from view.
*/
@SerialName("1")
HIDDEN,
/**
* The field stores a boolean value.
*/
@SerialName("2")
BOOLEAN,
/**
* The field value is linked to the item's username or password.
*/
@SerialName("3")
LINKED,
}
private class FieldTypeSerializer :
BaseEnumeratedIntSerializer<FieldTypeJson>(FieldTypeJson.values())

View file

@ -0,0 +1,182 @@
package com.x8bit.bitwarden.data.vault.datasource.network.model
import com.x8bit.bitwarden.data.platform.datasource.network.serializer.BaseEnumeratedIntSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Represents different fields that a custom cipher field can be linked to.
*/
@Serializable(LinkedIdTypeSerializer::class)
enum class LinkedIdTypeJson {
// region LOGIN
/**
* The field is linked to the login's username.
*/
@SerialName("100")
LOGIN_USERNAME,
/**
* The field is linked to the login's password.
*/
@SerialName("101")
LOGIN_PASSWORD,
// endregion LOGIN
// region CARD
/**
* The field is linked to the card's cardholder name.
*/
@SerialName("300")
CARD_CARDHOLDER_NAME,
/**
* The field is linked to the card's expiration month.
*/
@SerialName("301")
CARD_EXP_MONTH,
/**
* The field is linked to the card's expiration year.
*/
@SerialName("302")
CARD_EXP_YEAR,
/**
* The field is linked to the card's code.
*/
@SerialName("303")
CARD_CODE,
/**
* The field is linked to the card's brand.
*/
@SerialName("304")
CARD_BRAND,
/**
* The field is linked to the card's number.
*/
@SerialName("305")
CARD_NUMBER,
// endregion CARD
// region IDENTITY
/**
* The field is linked to the identity's title.
*/
@SerialName("400")
IDENTITY_TITLE,
/**
* The field is linked to the identity's middle name.
*/
@SerialName("401")
IDENTITY_MIDDLE_NAME,
/**
* The field is linked to the identity's address line 1.
*/
@SerialName("402")
IDENTITY_ADDRESS_1,
/**
* The field is linked to the identity's address line 2.
*/
@SerialName("403")
IDENTITY_ADDRESS_2,
/**
* The field is linked to the identity's address line 3.
*/
@SerialName("404")
IDENTITY_ADDRESS_3,
/**
* The field is linked to the identity's city.
*/
@SerialName("405")
IDENTITY_CITY,
/**
* The field is linked to the identity's state.
*/
@SerialName("406")
IDENTITY_STATE,
/**
* The field is linked to the identity's postal code
*/
@SerialName("407")
IDENTITY_POSTAL_CODE,
/**
* The field is linked to the identity's country.
*/
@SerialName("408")
IDENTITY_COUNTRY,
/**
* The field is linked to the identity's company.
*/
@SerialName("409")
IDENTITY_COMPANY,
/**
* The field is linked to the identity's email.
*/
@SerialName("410")
IDENTITY_EMAIL,
/**
* The field is linked to the identity's phone.
*/
@SerialName("411")
IDENTITY_PHONE,
/**
* The field is linked to the identity's SSN.
*/
@SerialName("412")
IDENTITY_SSN,
/**
* The field is linked to the identity's username.
*/
@SerialName("413")
IDENTITY_USERNAME,
/**
* The field is linked to the identity's passport number.
*/
@SerialName("414")
IDENTITY_PASSPORT_NUMBER,
/**
* The field is linked to the identity's license number.
*/
@SerialName("415")
IDENTITY_LICENSE_NUMBER,
/**
* The field is linked to the identity's first name.
*/
@SerialName("416")
IDENTITY_FIRST_NAME,
/**
* The field is linked to the identity's last name.
*/
@SerialName("417")
IDENTITY_LAST_NAME,
/**
* The field is linked to the identity's full name.
*/
@SerialName("418")
IDENTITY_FULL_NAME,
// endregion IDENTITY
}
private class LinkedIdTypeSerializer :
BaseEnumeratedIntSerializer<LinkedIdTypeJson>(LinkedIdTypeJson.values())

View file

@ -0,0 +1,80 @@
package com.x8bit.bitwarden.data.vault.datasource.network.model
import com.x8bit.bitwarden.data.platform.datasource.network.serializer.BaseEnumeratedIntSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Represents different types of policies.
*/
@Serializable(PolicyTypeSerializer::class)
enum class PolicyTypeJson {
/**
* Requires users to have 2FA enabled.
*/
@SerialName("0")
TWO_FACTOR_AUTHENTICATION,
/**
* Sets minimum requirements for master password complexity.
*/
@SerialName("1")
MASTER_PASSWORD,
/**
* Sets minimum requirements/default type for generated passwords/passphrases.
*/
@SerialName("2")
PASSWORD_GENERATOR,
/**
* Allows users to only be apart of one organization.
*/
@SerialName("3")
ONLY_ORG,
/**
* Requires users to authenticate with SSO.
*/
@SerialName("4")
REQUIRE_SSO,
/**
* Disables personal vault ownership for adding/cloning items.
*/
@SerialName("5")
PERSONAL_OWNERSHIP,
/**
* Disables the ability to create and edit Sends.
*/
@SerialName("6")
DISABLE_SEND,
/**
* Sets restrictions or defaults for Bitwarden Sends.
*/
@SerialName("7")
SEND_OPTIONS,
/**
* Allows orgs to use reset password : also can enable auto-enrollment during invite flow.
*/
@SerialName("8")
RESET_PASSWORD,
/**
* Sets the maximum allowed vault timeout.
*/
@SerialName("9")
MAXIMUM_VAULT_TIMEOUT,
/**
* Disable personal vault export.
*/
@SerialName("10")
DISABLE_PERSONAL_VAULT_EXPORT,
}
private class PolicyTypeSerializer :
BaseEnumeratedIntSerializer<PolicyTypeJson>(PolicyTypeJson.values())

View file

@ -0,0 +1,20 @@
package com.x8bit.bitwarden.data.vault.datasource.network.model
import com.x8bit.bitwarden.data.platform.datasource.network.serializer.BaseEnumeratedIntSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Represents different types of secure notes.
*/
@Serializable(SecureNoteTypeSerializer::class)
enum class SecureNoteTypeJson {
/**
* A generic note.
*/
@SerialName("0")
GENERIC,
}
private class SecureNoteTypeSerializer :
BaseEnumeratedIntSerializer<SecureNoteTypeJson>(SecureNoteTypeJson.values())

View file

@ -0,0 +1,26 @@
package com.x8bit.bitwarden.data.vault.datasource.network.model
import com.x8bit.bitwarden.data.platform.datasource.network.serializer.BaseEnumeratedIntSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Represents different types of send.
*/
@Serializable(SendTypeSerializer::class)
enum class SendTypeJson {
/**
* The send contains text data.
*/
@SerialName("0")
TEXT,
/**
* The send contains an attached file.
*/
@SerialName("1")
FILE,
}
private class SendTypeSerializer :
BaseEnumeratedIntSerializer<SendTypeJson>(SendTypeJson.values())

View file

@ -1,7 +1,9 @@
package com.x8bit.bitwarden.data.vault.datasource.network.model
import kotlinx.serialization.Contextual
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.time.LocalDateTime
/**
* Represents the response model for vault data fetched from the server.
@ -14,7 +16,6 @@ import kotlinx.serialization.Serializable
* @property domains A domains object associated with the vault data.
* @property sends A list of send objects associated with the vault data (nullable).
*/
// TODO determine encrypted params and rename them to emphasize encryption in BIT-636
@Serializable
data class SyncResponseJson(
@SerialName("folders")
@ -68,7 +69,6 @@ data class SyncResponseJson(
@SerialName("domains")
val domains: List<String>?,
// TODO Parse type enum in BIT-636
@SerialName("type")
val type: Int,
)
@ -83,9 +83,9 @@ data class SyncResponseJson(
*/
@Serializable
data class Folder(
// TODO Serialize revision date in BIT-636
@SerialName("revisionDate")
val revisionDate: String?,
@Contextual
val revisionDate: LocalDateTime?, // Date
@SerialName("name")
val name: String?,
@ -110,9 +110,8 @@ data class SyncResponseJson(
@SerialName("id")
val id: String,
// TODO Parse type enum in BIT-636
@SerialName("type")
val type: Int,
val type: PolicyTypeJson,
@SerialName("enabled")
val isEnabled: Boolean,
@ -257,7 +256,7 @@ data class SyncResponseJson(
@SerialName("keyConnectorUrl")
val keyConnectorUrl: String?,
// TODO Parse type enum in BIT-636
@SerialName("type")
val type: Int,
@ -267,7 +266,6 @@ data class SyncResponseJson(
@SerialName("enabled")
val isEnabled: Boolean,
// TODO Parse provider type enum in BIT-636
@SerialName("providerType")
val providerType: Int,
@ -331,7 +329,6 @@ data class SyncResponseJson(
@SerialName("useResetPassword")
val shouldUseResetPassword: Boolean,
// TODO Parse plan product type enum in BIT-636
@SerialName("planProductType")
val planProductType: Int,
@ -362,9 +359,9 @@ data class SyncResponseJson(
@SerialName("useTotp")
val shouldUseTotp: Boolean,
// TODO Serialize family sponsorship last sync date in BIT-636
@SerialName("familySponsorshipLastSyncDate")
val familySponsorshipLastSyncDate: String?,
@Contextual
val familySponsorshipLastSyncDate: LocalDateTime?,
@SerialName("useScim")
val shouldUseScim: Boolean,
@ -412,7 +409,6 @@ data class SyncResponseJson(
@SerialName("id")
val id: String,
// TODO Parse type enum in BIT-636
@SerialName("type")
val type: Int,
@ -532,7 +528,7 @@ data class SyncResponseJson(
val shouldOrganizationUseTotp: Boolean,
@SerialName("reprompt")
val reprompt: Int,
val reprompt: CipherRepromptTypeJson,
@SerialName("edit")
val shouldEdit: Boolean,
@ -540,20 +536,19 @@ data class SyncResponseJson(
@SerialName("passwordHistory")
val passwordHistory: List<PasswordHistory>?,
// TODO Serialize revision date in BIT-636
@SerialName("revisionDate")
val revisionDate: String?,
@Contextual
val revisionDate: LocalDateTime?,
// TODO Parse type enum in BIT-636
@SerialName("type")
val type: Int,
val type: CipherTypeJson,
@SerialName("login")
val login: Login,
// TODO Serialize creation date in BIT-636
@SerialName("creationDate")
val creationDate: String?,
@Contextual
val creationDate: LocalDateTime?,
@SerialName("secureNote")
val secureNote: SecureNote,
@ -564,9 +559,9 @@ data class SyncResponseJson(
@SerialName("organizationId")
val organizationId: String?,
// TODO Serialize deleted date in BIT-636
@SerialName("deletedDate")
val deletedDate: String?,
@Contextual
val deletedDate: LocalDateTime?,
@SerialName("identity")
val identity: Identity,
@ -657,7 +652,7 @@ data class SyncResponseJson(
/**
* Represents a field in the vault response.
*
* @property linkedId The linked ID of the field (nullable).
* @property linkedIdType The linked ID of the field (nullable).
* @property name The name of the field (nullable).
* @property type The type of field.
* @property value The value of the field (nullable).
@ -665,14 +660,13 @@ data class SyncResponseJson(
@Serializable
data class Field(
@SerialName("linkedId")
val linkedId: String?,
val linkedIdType: LinkedIdTypeJson?,
@SerialName("name")
val name: String?,
// TODO Parse type enum in BIT-636
@SerialName("type")
val type: Int,
val type: FieldTypeJson,
@SerialName("value")
val value: String?,
@ -779,9 +773,9 @@ data class SyncResponseJson(
@SerialName("password")
val password: String?,
// TODO Serialize password revision date in BIT-636
@SerialName("passwordRevisionDate")
val passwordRevisionDate: String?,
@Contextual
val passwordRevisionDate: LocalDateTime?,
@SerialName("autofillOnPageLoad")
val shouldAutofillOnPageLoad: Boolean?,
@ -795,13 +789,13 @@ data class SyncResponseJson(
/**
* Represents a URI in the vault response.
*
* @property match The match of the URI.
* @property uriMatchType The match type of the URI.
* @property uri The actual string representing the URI (nullable).
*/
@Serializable
data class Uri(
@SerialName("match")
val match: Int,
val uriMatchType: UriMatchTypeJson,
@SerialName("uri")
val uri: String?,
@ -819,9 +813,9 @@ data class SyncResponseJson(
@SerialName("password")
val password: String,
// TODO Serialize last used date in BIT-636
@SerialName("lastUsedDate")
val lastUsedDate: String,
@Contextual
val lastUsedDate: LocalDateTime,
)
/**
@ -831,9 +825,8 @@ data class SyncResponseJson(
*/
@Serializable
data class SecureNote(
// TODO Parse type enum in BIT-636
@SerialName("type")
val type: Int,
val type: SecureNoteTypeJson,
)
}
@ -865,9 +858,9 @@ data class SyncResponseJson(
@SerialName("notes")
val notes: String?,
// TODO Serialize revision date in BIT-636
@SerialName("revisionDate")
val revisionDate: String,
@Contextual
val revisionDate: LocalDateTime,
@SerialName("maxAccessCount")
val maxAccessCount: Int?,
@ -875,9 +868,8 @@ data class SyncResponseJson(
@SerialName("hideEmail")
val shouldHideEmail: Boolean,
// TODO Parse type enum in BIT-636
@SerialName("type")
val type: Int,
val type: SendTypeJson,
@SerialName("accessId")
val accessId: String?,
@ -888,9 +880,9 @@ data class SyncResponseJson(
@SerialName("file")
val file: File,
// TODO Serialize deletion date in BIT-636
@SerialName("deletionDate")
val deletionDate: String,
@Contextual
val deletionDate: LocalDateTime,
@SerialName("name")
val name: String?,
@ -907,9 +899,9 @@ data class SyncResponseJson(
@SerialName("key")
val key: String?,
// TODO Serialize expiration date in BIT-636
@SerialName("expirationDate")
val expirationDate: String?,
@Contextual
val expirationDate: LocalDateTime?,
) {
/**
* Represents a file in the vault response.

View file

@ -0,0 +1,50 @@
package com.x8bit.bitwarden.data.vault.datasource.network.model
import com.x8bit.bitwarden.data.platform.datasource.network.serializer.BaseEnumeratedIntSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Represents how a URI should be matched for autofill to occur.
*/
@Serializable(UriMatchTypeSerializer::class)
enum class UriMatchTypeJson {
/**
* Matching of the URI is based on the domain.
*/
@SerialName("0")
DOMAIN,
/**
* Matching of the URI is based on the host.
*/
@SerialName("1")
HOST,
/**
* Matching of the URI is based the start of resource.
*/
@SerialName("2")
STARTS_WITH,
/**
* Matching of the URI requires an exact match.
*/
@SerialName("3")
EXACT,
/**
* Requires users to authenticate with SSO.
*/
@SerialName("4")
REGULAR_EXPRESSION,
/**
* The URI should never be autofilled.
*/
@SerialName("5")
NEVER,
}
private class UriMatchTypeSerializer :
BaseEnumeratedIntSerializer<UriMatchTypeJson>(UriMatchTypeJson.values())

View file

@ -0,0 +1,96 @@
package com.x8bit.bitwarden.data.platform.datasource.network.serializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.encodeToJsonElement
import kotlinx.serialization.modules.SerializersModule
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import java.time.LocalDateTime
class LocalDateTimeSerializerTest {
private val json = Json {
serializersModule = SerializersModule {
contextual(LocalDateTime::class, LocalDateTimeSerializer())
}
}
@Test
fun `properly deserializes raw JSON to LocalDate`() {
assertEquals(
LocalDateTimeData(
dataAsLocalDateTime = LocalDateTime.of(
2023,
10,
6,
17,
22,
28,
440000000,
),
),
json.decodeFromString<LocalDateTimeData>(
"""
{
"dataAsLocalDateTime": "2023-10-06T17:22:28.44Z"
}
""",
),
)
}
@Test
fun `properly deserializes raw JSON with nano seconds to LocalDate`() {
assertEquals(
LocalDateTimeData(
dataAsLocalDateTime = LocalDateTime.of(
2023,
10,
6,
17,
22,
28,
446666700,
),
),
json.decodeFromString<LocalDateTimeData>(
"""
{
"dataAsLocalDateTime": "2023-10-06T17:22:28.4466667Z"
}
""",
),
)
}
@Test
fun `properly serializes external model back to raw JSON`() {
assertEquals(
json.parseToJsonElement(
"""
{
"dataAsLocalDateTime": "2023-10-06T17:22:28.44Z"
}
""",
),
json.encodeToJsonElement(
LocalDateTimeData(
dataAsLocalDateTime = LocalDateTime.of(
2023,
10,
6,
17,
22,
28,
440000000,
),
),
),
)
}
}
@Serializable
private data class LocalDateTimeData(
@Serializable(LocalDateTimeSerializer::class)
val dataAsLocalDateTime: LocalDateTime,
)