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

Hash SCIM tokens #1240

Merged
merged 11 commits into from
Nov 12, 2020
2 changes: 2 additions & 0 deletions services/spar/package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ dependencies:
- aeson
- aeson-pretty
- aeson-qq
- attoparsec
- base
- base64-bytestring
- bilge
Expand Down Expand Up @@ -51,6 +52,7 @@ dependencies:
- insert-ordered-containers
- interpolate
- lens
- memory
- metrics-core
- metrics-wai
- mtl
Expand Down
13 changes: 12 additions & 1 deletion services/spar/spar.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ cabal-version: 1.12
--
-- see: https://github.com/sol/hpack
--
-- hash: 54710b4d4da7f1d6f621e0c28ded0702090f208db2f230715c129480502db771
-- hash: 7a433aaa30026803e763166dffe8888607987a8374c359e18186353fbf7136b6

name: spar
version: 0.1
Expand Down Expand Up @@ -50,6 +50,7 @@ library
, aeson
, aeson-pretty
, aeson-qq
, attoparsec
, base
, base64-bytestring
, bilge
Expand Down Expand Up @@ -80,6 +81,7 @@ library
, insert-ordered-containers
, interpolate
, lens
, memory
, metrics-core
, metrics-wai
, mtl
Expand Down Expand Up @@ -128,6 +130,7 @@ executable spar
, aeson
, aeson-pretty
, aeson-qq
, attoparsec
, base
, base64-bytestring
, bilge
Expand Down Expand Up @@ -158,6 +161,7 @@ executable spar
, insert-ordered-containers
, interpolate
, lens
, memory
, metrics-core
, metrics-wai
, mtl
Expand Down Expand Up @@ -224,6 +228,7 @@ executable spar-integration
, aeson-pretty
, aeson-qq
, async
, attoparsec
, base
, base64-bytestring
, bilge
Expand Down Expand Up @@ -258,6 +263,7 @@ executable spar-integration
, interpolate
, lens
, lens-aeson
, memory
, metrics-core
, metrics-wai
, mtl
Expand Down Expand Up @@ -329,6 +335,7 @@ executable spar-schema
, aeson
, aeson-pretty
, aeson-qq
, attoparsec
, base
, base64-bytestring
, bilge
Expand Down Expand Up @@ -359,6 +366,7 @@ executable spar-schema
, insert-ordered-containers
, interpolate
, lens
, memory
, metrics-core
, metrics-wai
, mtl
Expand Down Expand Up @@ -403,6 +411,7 @@ test-suite spec
Test.Spar.APISpec
Test.Spar.DataSpec
Test.Spar.Intra.BrigSpec
Test.Spar.Roundtrip.ByteString
Test.Spar.ScimSpec
Test.Spar.TypesSpec
Paths_spar
Expand All @@ -416,6 +425,7 @@ test-suite spec
, aeson
, aeson-pretty
, aeson-qq
, attoparsec
, base
, base64-bytestring
, bilge
Expand Down Expand Up @@ -449,6 +459,7 @@ test-suite spec
, interpolate
, lens
, lens-aeson
, memory
, metrics-core
, metrics-wai
, mtl
Expand Down
125 changes: 82 additions & 43 deletions services/spar/src/Spar/Data.hs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ where

import Brig.Types.Common (Email, fromEmail)
import Cassandra as Cas
import Control.Arrow (Arrow ((&&&)))
import Control.Lens
import Control.Monad.Except
import Data.Id
Expand All @@ -105,6 +106,7 @@ import URI.ByteString
import qualified Web.Cookie as Cky
import Web.Scim.Schema.Common (WithId (..))
import Web.Scim.Schema.Meta (Meta (..), WithMeta (..))
import qualified Prelude

-- | A lower bound: @schemaVersion <= whatWeFoundOnCassandra@, not @==@.
schemaVersion :: Int32
Expand Down Expand Up @@ -579,12 +581,15 @@ deleteDefaultSsoCode = retry x5 . write del $ params Quorum ()
--
-- docs/developer/scim/storage.md {#DevScimStorageTokens}

type ScimTokenRow = (ScimToken, TeamId, ScimTokenId, UTCTime, Maybe SAML.IdPId, Text)
type ScimTokenRow = (ScimTokenLookupKey, TeamId, ScimTokenId, UTCTime, Maybe SAML.IdPId, Text)

fromScimTokenRow :: ScimTokenRow -> ScimTokenInfo
fromScimTokenRow (_, stiTeam, stiId, stiCreatedAt, stiIdP, stiDescr) =
ScimTokenInfo {..}

scimTokenLookupKey :: ScimTokenRow -> ScimTokenLookupKey
scimTokenLookupKey (key, _, _, _, _, _) = key

-- | Add a new SCIM provisioning token. The token should be random and
-- generated by the backend, not by the user.
insertScimToken ::
Expand All @@ -595,22 +600,23 @@ insertScimToken ::
insertScimToken token ScimTokenInfo {..} = retry x5 . batch $ do
setType BatchLogged
setConsistency Quorum
addPrepQuery insByToken (token, stiTeam, stiId, stiCreatedAt, stiIdP, stiDescr)
addPrepQuery insByTeam (token, stiTeam, stiId, stiCreatedAt, stiIdP, stiDescr)
where
insByToken, insByTeam :: PrepQuery W ScimTokenRow ()
insByToken =
[r|
INSERT INTO team_provisioning_by_token
(token_, team, id, created_at, idp, descr)
VALUES (?, ?, ?, ?, ?, ?)
|]
insByTeam =
[r|
INSERT INTO team_provisioning_by_team
(token_, team, id, created_at, idp, descr)
VALUES (?, ?, ?, ?, ?, ?)
|]
let tokenHash = hashScimToken token
addPrepQuery insByToken (ScimTokenLookupKeyHashed tokenHash, stiTeam, stiId, stiCreatedAt, stiIdP, stiDescr)
addPrepQuery insByTeam (ScimTokenLookupKeyHashed tokenHash, stiTeam, stiId, stiCreatedAt, stiIdP, stiDescr)

insByToken, insByTeam :: PrepQuery W ScimTokenRow ()
insByToken =
[r|
INSERT INTO team_provisioning_by_token
(token_, team, id, created_at, idp, descr)
VALUES (?, ?, ?, ?, ?, ?)
|]
insByTeam =
[r|
INSERT INTO team_provisioning_by_team
(token_, team, id, created_at, idp, descr)
VALUES (?, ?, ?, ?, ?, ?)
|]

-- | Check whether a token exists and if yes, what team and IdP are
-- associated with it.
Expand All @@ -619,15 +625,48 @@ lookupScimToken ::
ScimToken ->
m (Maybe ScimTokenInfo)
lookupScimToken token = do
mbRow <- retry x1 . query1 sel $ params Quorum (Identity token)
pure $ fmap fromScimTokenRow mbRow
where
sel :: PrepQuery R (Identity ScimToken) ScimTokenRow
let tokenHash = hashScimToken token
rows <- retry x1 . query sel $ params Quorum (tokenHash, token)
case fmap (scimTokenLookupKey &&& Prelude.id) rows of
[(ScimTokenLookupKeyHashed _, row)] ->
pure (Just (fromScimTokenRow row))
[(ScimTokenLookupKeyPlaintext plain, row)] ->
convert plain row
[(ScimTokenLookupKeyHashed _, _), (ScimTokenLookupKeyPlaintext plain, row)] ->
convert plain row
[(ScimTokenLookupKeyPlaintext plain, row), (ScimTokenLookupKeyHashed _', _)] ->
smatting marked this conversation as resolved.
Show resolved Hide resolved
convert plain row
_ -> pure Nothing
where
sel :: PrepQuery R (ScimTokenHash, ScimToken) ScimTokenRow
sel =
[r|
SELECT token_, team, id, created_at, idp, descr
FROM team_provisioning_by_token WHERE token_ = ?
|]
FROM team_provisioning_by_token WHERE token_ in (?, ?)
|]

convert :: MonadClient m => ScimToken -> ScimTokenRow -> m (Maybe ScimTokenInfo)
convert plain row = do
let tokenInfo = fromScimTokenRow row
connvertPlaintextToken plain tokenInfo
pure (Just tokenInfo)

connvertPlaintextToken ::
(HasCallStack, MonadClient m) =>
ScimToken ->
ScimTokenInfo ->
m ()
connvertPlaintextToken token ScimTokenInfo {..} = retry x5 . batch $ do
setType BatchLogged
setConsistency Quorum
addPrepQuery delByTokenLookup (Identity (ScimTokenLookupKeyPlaintext token))
smatting marked this conversation as resolved.
Show resolved Hide resolved
let tokenHash = hashScimToken token
-- enter by new lookup key
addPrepQuery insByToken (ScimTokenLookupKeyHashed tokenHash, stiTeam, stiId, stiCreatedAt, stiIdP, stiDescr)
-- update info table
addPrepQuery insByTeam (ScimTokenLookupKeyHashed tokenHash, stiTeam, stiId, stiCreatedAt, stiIdP, stiDescr)
-- remove old lookup key
addPrepQuery delByTokenLookup (Identity (ScimTokenLookupKeyPlaintext token))

-- | List all tokens associated with a team, in the order of their creation.
getScimTokens ::
Expand All @@ -645,7 +684,7 @@ getScimTokens team = do
[r|
SELECT token_, team, id, created_at, idp, descr
FROM team_provisioning_by_team WHERE team = ?
|]
|]

-- | Delete a token.
deleteScimToken ::
Expand All @@ -659,27 +698,29 @@ deleteScimToken team tokenid = do
setType BatchLogged
setConsistency Quorum
addPrepQuery delById (team, tokenid)
for_ mbToken $ \(Identity token) ->
addPrepQuery delByToken (Identity token)
for_ mbToken $ \(Identity key) ->
addPrepQuery delByTokenLookup (Identity key)
where
selById :: PrepQuery R (TeamId, ScimTokenId) (Identity ScimToken)
selById :: PrepQuery R (TeamId, ScimTokenId) (Identity ScimTokenLookupKey)
selById =
[r|
SELECT token_ FROM team_provisioning_by_team
WHERE team = ? AND id = ?
|]
delById :: PrepQuery W (TeamId, ScimTokenId) ()
delById =
[r|
DELETE FROM team_provisioning_by_team
WHERE team = ? AND id = ?
|]
delByToken :: PrepQuery W (Identity ScimToken) ()
delByToken =
[r|
DELETE FROM team_provisioning_by_token
WHERE token_ = ?
|]

delById :: PrepQuery W (TeamId, ScimTokenId) ()
delById =
[r|
DELETE FROM team_provisioning_by_team
WHERE team = ? AND id = ?
|]

delByTokenLookup :: PrepQuery W (Identity ScimTokenLookupKey) ()
delByTokenLookup =
[r|
DELETE FROM team_provisioning_by_token
WHERE token_ = ?
|]

-- | Delete all tokens belonging to a team.
deleteTeamScimTokens ::
Expand All @@ -692,14 +733,12 @@ deleteTeamScimTokens team = do
setType BatchLogged
setConsistency Quorum
addPrepQuery delByTeam (Identity team)
mapM_ (addPrepQuery delByToken) tokens
mapM_ (addPrepQuery delByTokenLookup) tokens
where
sel :: PrepQuery R (Identity TeamId) (Identity ScimToken)
sel :: PrepQuery R (Identity TeamId) (Identity ScimTokenLookupKey)
sel = "SELECT token_ FROM team_provisioning_by_team WHERE team = ?"
delByTeam :: PrepQuery W (Identity TeamId) ()
delByTeam = "DELETE FROM team_provisioning_by_team WHERE team = ?"
delByToken :: PrepQuery W (Identity ScimToken) ()
delByToken = "DELETE FROM team_provisioning_by_token WHERE token_ = ?"

----------------------------------------------------------------------
-- SCIM user records
Expand Down
16 changes: 16 additions & 0 deletions services/spar/src/Spar/Data/Instances.hs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ module Spar.Data.Instances
where

import Cassandra as Cas
import Data.ByteString.Conversion (fromByteString, toByteString)
import Data.String.Conversions
import Data.X509 (SignedCertificate)
import Imports
Expand Down Expand Up @@ -101,3 +102,18 @@ toVerdictFormat (VerdictFormatConMobile, Just succredir, Just errredir) = Just $
toVerdictFormat _ = Nothing

deriving instance Cql ScimToken

instance Cql ScimTokenHash where
ctype = Tagged TextColumn
toCql = CqlText . cs . toByteString
fromCql (CqlText t) = maybe (Left "ScimTokenHash: parse error") Right (fromByteString . cs $ t)
fromCql _ = Left "ScimTokenHash: expected CqlText"

instance Cql ScimTokenLookupKey where
ctype = Tagged TextColumn
toCql = \case
ScimTokenLookupKeyHashed h -> toCql h
ScimTokenLookupKeyPlaintext t -> toCql t
fromCql s@(CqlText _) =
ScimTokenLookupKeyHashed <$> fromCql s <|> ScimTokenLookupKeyPlaintext <$> fromCql s
fromCql _ = Left "ScimTokenLookupKey: expected CqlText"
23 changes: 23 additions & 0 deletions services/spar/src/Spar/Types.hs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,12 @@ module Spar.Types where

import Control.Lens (makeLenses)
import Control.Monad.Except
import Crypto.Hash (SHA512 (..), hash)
import Data.Aeson
import Data.Aeson.TH
import Data.Attoparsec.ByteString (string)
import qualified Data.Binary.Builder as BB (fromByteString)
import Data.ByteArray.Encoding (Base (..), convertToBase)
import qualified Data.ByteString.Builder as Builder
import Data.ByteString.Conversion
import Data.Id (ScimTokenId, TeamId, UserId)
Expand Down Expand Up @@ -143,6 +147,25 @@ instance ToJSON IdPMetadataInfo where
newtype ScimToken = ScimToken {fromScimToken :: Text}
deriving (Eq, Show, FromJSON, ToJSON, FromByteString, ToByteString)

newtype ScimTokenHash = ScimTokenHash {fromScimTokenHash :: Text}
deriving (Eq, Show)

instance FromByteString ScimTokenHash where
parser = string "sha512:" *> (ScimTokenHash <$> parser)

instance ToByteString ScimTokenHash where
builder (ScimTokenHash t) = BB.fromByteString "sha512:" <> builder t
Copy link
Contributor

Choose a reason for hiding this comment

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

Add a roundtrip test, like these?

(I admit that there are more gaps in the roundtrip test coverage, but we might as well start now to patch them...)

Copy link
Contributor

Choose a reason for hiding this comment

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

(we can also do that in a separate PR)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

added roundtrip test


data ScimTokenLookupKey
= ScimTokenLookupKeyHashed ScimTokenHash
| ScimTokenLookupKeyPlaintext ScimToken
deriving (Eq, Show)

hashScimToken :: ScimToken -> ScimTokenHash
hashScimToken token =
let digest = hash @ByteString @SHA512 (encodeUtf8 (fromScimToken token))
in ScimTokenHash (cs @ByteString @Text (convertToBase Base64 digest))

-- | Metadata that we store about each token.
data ScimTokenInfo = ScimTokenInfo
{ -- | Which team can be managed with the token
Expand Down
Loading