Skip to content
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

Client-supported features end-point #1503

Merged
merged 15 commits into from
May 19, 2021
1 change: 1 addition & 0 deletions docs/reference/cassandra-schema.cql
Original file line number Diff line number Diff line change
Expand Up @@ -1151,6 +1151,7 @@ CREATE TABLE brig_test.password_reset (
CREATE TABLE brig_test.clients (
user uuid,
client text,
capabilities set<int>,
class int,
cookie text,
ip inet,
Expand Down
119 changes: 116 additions & 3 deletions libs/wire-api/src/Wire/API/User/Client.hs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@
-- with this program. If not, see <https://www.gnu.org/licenses/>.

module Wire.API.User.Client
( -- * UserClients
( -- * ClientCapability
ClientCapability (..),
ClientCapabilityList (..),

-- * UserClients
UserClientMap (..),
QualifiedUserClientMap (..),
UserClients (..),
Expand Down Expand Up @@ -51,13 +55,16 @@ module Wire.API.User.Client
modelUserClients,
modelNewClient,
modelUpdateClient,
modelClientCapabilityList,
typeClientCapability,
modelDeleteClient,
modelClient,
modelSigkeys,
modelLocation, -- re-export from types-common
)
where

import qualified Cassandra as Cql
import Control.Lens ((?~), (^.))
import Data.Aeson
import Data.Domain (Domain)
Expand All @@ -67,6 +74,8 @@ import Data.Json.Util
import qualified Data.Map.Strict as Map
import Data.Misc (Latitude (..), Location, Longitude (..), PlainTextPassword (..), latitude, location, longitude, modelLocation)
import Data.Proxy (Proxy (..))
import qualified Data.Schema as Schema
import qualified Data.Set as Set
import Data.Swagger (HasExample (example), NamedSchema (..), ToSchema (..), declareSchema)
import qualified Data.Swagger as Swagger
import qualified Data.Swagger.Build.Api as Doc
Expand All @@ -75,12 +84,109 @@ import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text.E
import Data.Typeable (typeRep)
import Data.UUID (toASCIIBytes)
import Deriving.Swagger (CamelToSnake, ConstructorTagModifier, CustomSwagger, FieldLabelModifier, LabelMapping ((:->)), LabelMappings, LowerCase, StripPrefix, StripSuffix)
import Deriving.Swagger
( CamelToSnake,
ConstructorTagModifier,
CustomSwagger,
FieldLabelModifier,
LabelMapping ((:->)),
LabelMappings,
LowerCase,
StripPrefix,
StripSuffix,
)
import Imports
import Wire.API.Arbitrary (Arbitrary (arbitrary), GenericUniform (..), generateExample, mapOf', setOf')
import Wire.API.User.Auth (CookieLabel)
import Wire.API.User.Client.Prekey as Prekey

----------------------------------------------------------------------
-- ClientCapability, ClientCapabilityList

-- | Names of capabilities clients can claim to support in order to be treated differently by
-- the backend.
--
-- **The cost of capability keywords**
--
-- Avoid this wherever possible. Adding capability keywords in the backend code makes testing
-- exponentially more expensive (in principle, you should always test all combinations of
-- supported capabilitiess. But even if you only test those known to occur in the wild, it will
-- still make your life harder.)
--
-- Consider dropping support for clients without ancient capabilitiess if you have "enough" clients
-- that are younger. This will always be disruptive for a minority of users, but maybe this
-- can be mitigated by giving those users clear feedback that they need to upgrade in order to
-- get their expected UX back.
--
-- **An alternative design**
--
-- Consider replacing 'ClientCapability' with platform and version in formation (I
-- played with @data Platform = Android | IOS | WebApp | TeamSettings | AccountPages@ and
-- @Version@ from the `semver` package in https://github.com/wireapp/wire-server/pull/1503,
-- but ended up deciding against it). This data could be passed in a similar way as the
-- 'ClientCapabilityList' is now (similar end-point, different path, different body
-- type), and the two approaches could be used in parallel indefinitely.
--
-- Capability keywords reveal the minimum amount of information necessary to handle the client,
-- making it harder to fingerprint and track clients; they are straight-forward and
-- self-documenting (to an extent), and make it easier to release a capability on the backend and
-- clients independently.
--
-- Platform/version info is if you have many different capability keywords, even though it
-- doesn't solve the problem of having to explore the entire capability space in your tests.
-- They give you a better idea of the time line, and how to gently discontinue support for
-- ancient capabilities.
data ClientCapability
= -- | Clients have minimum support for LH, but not for explicit consent. Implicit consent
-- is granted via the galley server config (see '_setLegalHoldTeamsWhitelist').
ClientSupportsLegalholdImplicitConsent
deriving stock (Eq, Ord, Bounded, Enum, Show, Generic)
deriving (Arbitrary) via (GenericUniform ClientCapability)
deriving (ToJSON, FromJSON, Swagger.ToSchema) via Schema.Schema ClientCapability

instance Schema.ToSchema ClientCapability where
schema =
Schema.enum @Text "ClientCapability" $
Schema.element "legalhold-implicit-consent" ClientSupportsLegalholdImplicitConsent

typeClientCapability :: Doc.DataType
typeClientCapability =
Doc.string $
Doc.enum
[ "legalhold-implicit-consent"
]

instance Cql.Cql ClientCapability where
ctype = Cql.Tagged Cql.IntColumn

toCql ClientSupportsLegalholdImplicitConsent = Cql.CqlInt 1

fromCql (Cql.CqlInt i) = case i of
1 -> return ClientSupportsLegalholdImplicitConsent
n -> Left $ "Unexpected ClientCapability value: " ++ show n
fromCql _ = Left "ClientCapability value: int expected"

-- FUTUREWORK: add golden tests for this?
data ClientCapabilityList = ClientCapabilityList {fromClientCapabilityList :: Set ClientCapability}
deriving stock (Eq, Ord, Show, Generic)
deriving (Arbitrary) via (GenericUniform ClientCapabilityList)
deriving (ToJSON, FromJSON, Swagger.ToSchema) via (Schema.Schema ClientCapabilityList)

instance Schema.ToSchema ClientCapabilityList where
schema =
Schema.objectWithDocModifier "ClientCapabilityList" mods $
ClientCapabilityList
<$> (Set.toList . fromClientCapabilityList)
Schema..= Schema.field "capabilities" (Set.fromList <$> Schema.array Schema.schema)
where
mods = Schema.description ?~ ("Hints provided by the client for the backend so it can behavior in a backwards-compatible way." :: Text)

modelClientCapabilityList :: Doc.Model
modelClientCapabilityList = Doc.defineModel "ClientCapabilityList" $ do
Doc.description "Hints provided by the client for the backend so it can behavior in a backwards-compatible way."
Doc.property "capabilities" (Doc.array typeClientCapability) $ do
Doc.description "Array containing all capabilities supported by a client."

--------------------------------------------------------------------------------
-- UserClientMap

Expand Down Expand Up @@ -517,7 +623,9 @@ instance FromJSON NewClient where
data UpdateClient = UpdateClient
{ updateClientPrekeys :: [Prekey],
updateClientLastKey :: Maybe LastPrekey,
updateClientLabel :: Maybe Text
updateClientLabel :: Maybe Text,
-- | see haddocks for 'ClientCapability'
updateClientCapabilities :: Maybe (Set ClientCapability)
}
deriving stock (Eq, Show, Generic)
deriving (Arbitrary) via (GenericUniform UpdateClient)
Expand All @@ -540,13 +648,17 @@ modelUpdateClient = Doc.defineModel "UpdateClient" $ do
Doc.property "label" Doc.string' $ do
Doc.description "A new name for this client."
Doc.optional
Doc.property "capabilities" typeClientCapability $ do
Doc.description "Hints for the backend so it can behave in a backwards-compatible way."
Doc.optional

instance ToJSON UpdateClient where
toJSON c =
object $
"prekeys" .= updateClientPrekeys c
# "lastkey" .= updateClientLastKey c
# "label" .= updateClientLabel c
# "capabilities" .= updateClientCapabilities c
# []

instance FromJSON UpdateClient where
Expand All @@ -555,6 +667,7 @@ instance FromJSON UpdateClient where
<$> o .:? "prekeys" .!= []
<*> o .:? "lastkey"
<*> o .:? "label"
<*> o .:? "capabilities"

--------------------------------------------------------------------------------
-- RmClient
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"lastkey":{"key":"\u0014 }Kg\u000be3","id":65535},"prekeys":[{"key":"","id":1},{"key":"","id":0},{"key":"","id":0},{"key":"","id":1},{"key":"","id":0},{"key":"","id":0},{"key":"","id":0}],"label":"\u001b\u0004\u0001ccn\u001f{Y5"}
{"lastkey":{"key":"\u0014 }Kg\u000be3","id":65535},"prekeys":[{"key":"","id":1},{"key":"","id":0},{"key":"","id":0},{"key":"","id":1},{"key":"","id":0},{"key":"","id":0},{"key":"","id":0}],"capabilities":["legalhold-implicit-consent"],"label":"\u001b\u0004\u0001ccn\u001f{Y5"}
Loading