diff --git a/changelog.d/5-internal/pr-3305 b/changelog.d/5-internal/pr-3305 new file mode 100644 index 0000000000..88ca47e731 --- /dev/null +++ b/changelog.d/5-internal/pr-3305 @@ -0,0 +1 @@ +Register/Update OAuth client via backoffice/stern diff --git a/integration/test/API/BrigInternal.hs b/integration/test/API/BrigInternal.hs index 1e73f4366c..77201aa09d 100644 --- a/integration/test/API/BrigInternal.hs +++ b/integration/test/API/BrigInternal.hs @@ -43,3 +43,30 @@ createUser domain cu = do | cu.team ] ) + +registerOAuthClient :: (HasCallStack, MakesValue user, MakesValue name, MakesValue url) => user -> name -> url -> App Response +registerOAuthClient user name url = do + req <- baseRequest user Brig Unversioned "i/oauth/clients" + applicationName <- asString name + redirectUrl <- asString url + submit "POST" (req & addJSONObject ["application_name" .= applicationName, "redirect_url" .= redirectUrl]) + +getOAuthClient :: (HasCallStack, MakesValue user, MakesValue cid) => user -> cid -> App Response +getOAuthClient user cid = do + clientId <- objId cid + req <- baseRequest user Brig Unversioned $ "i/oauth/clients/" <> clientId + submit "GET" req + +updateOAuthClient :: (HasCallStack, MakesValue user, MakesValue cid, MakesValue name, MakesValue url) => user -> cid -> name -> url -> App Response +updateOAuthClient user cid name url = do + clientId <- objId cid + req <- baseRequest user Brig Unversioned $ "i/oauth/clients/" <> clientId + applicationName <- asString name + redirectUrl <- asString url + submit "PUT" (req & addJSONObject ["application_name" .= applicationName, "redirect_url" .= redirectUrl]) + +deleteOAuthClient :: (HasCallStack, MakesValue user, MakesValue cid) => user -> cid -> App Response +deleteOAuthClient user cid = do + clientId <- objId cid + req <- baseRequest user Brig Unversioned $ "i/oauth/clients/" <> clientId + submit "DELETE" req diff --git a/integration/test/Test/Brig.hs b/integration/test/Test/Brig.hs index 170017c8f8..61a39af82d 100644 --- a/integration/test/Test/Brig.hs +++ b/integration/test/Test/Brig.hs @@ -18,3 +18,26 @@ testSearchContactForExternalUsers = do bindResponse (Public.searchContacts partner (owner %. "name")) $ \resp -> resp.status `shouldMatchInt` 403 + +testCrudOAuthClient :: HasCallStack => App () +testCrudOAuthClient = do + user <- randomUser ownDomain def + let appName = "foobar" + let url = "https://example.com/callback.html" + clientId <- bindResponse (Internal.registerOAuthClient user appName url) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "client_id" + bindResponse (Internal.getOAuthClient user clientId) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "application_name" `shouldMatch` appName + resp.json %. "redirect_url" `shouldMatch` url + let newName = "barfoo" + let newUrl = "https://example.com/callback2.html" + bindResponse (Internal.updateOAuthClient user clientId newName newUrl) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "application_name" `shouldMatch` newName + resp.json %. "redirect_url" `shouldMatch` newUrl + bindResponse (Internal.deleteOAuthClient user clientId) $ \resp -> do + resp.status `shouldMatchInt` 200 + bindResponse (Internal.getOAuthClient user clientId) $ \resp -> do + resp.status `shouldMatchInt` 404 diff --git a/libs/wire-api/src/Wire/API/OAuth.hs b/libs/wire-api/src/Wire/API/OAuth.hs index 21d8b3a85e..d5c9333070 100644 --- a/libs/wire-api/src/Wire/API/OAuth.hs +++ b/libs/wire-api/src/Wire/API/OAuth.hs @@ -100,18 +100,18 @@ newtype OAuthApplicationName = OAuthApplicationName {unOAuthApplicationName :: R instance ToSchema OAuthApplicationName where schema = OAuthApplicationName <$> unOAuthApplicationName .= schema -data RegisterOAuthClientRequest = RegisterOAuthClientRequest +data OAuthClientConfig = OAuthClientConfig { applicationName :: OAuthApplicationName, redirectUrl :: RedirectUrl } deriving (Eq, Show, Generic) - deriving (Arbitrary) via (GenericUniform RegisterOAuthClientRequest) - deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema RegisterOAuthClientRequest) + deriving (Arbitrary) via (GenericUniform OAuthClientConfig) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema OAuthClientConfig) -instance ToSchema RegisterOAuthClientRequest where +instance ToSchema OAuthClientConfig where schema = - object "RegisterOAuthClientRequest" $ - RegisterOAuthClientRequest + object "OAuthClientConfig" $ + OAuthClientConfig <$> applicationName .= fieldWithDocModifier "application_name" applicationNameDescription schema <*> (.redirectUrl) .= fieldWithDocModifier "redirect_url" redirectUrlDescription schema where diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Brig/OAuth.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Brig/OAuth.hs index a4e24b58ec..a8a2747af7 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig/OAuth.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig/OAuth.hs @@ -17,6 +17,7 @@ module Wire.API.Routes.Internal.Brig.OAuth where +import Data.Id (OAuthClientId) import Servant (JSON) import Servant hiding (Handler, JSON, Tagged, addHeader, respond) import Servant.Swagger.Internal.Orphans () @@ -34,6 +35,37 @@ type OAuthAPI = :> CanThrow 'OAuthFeatureDisabled :> "oauth" :> "clients" - :> ReqBody '[JSON] RegisterOAuthClientRequest + :> ReqBody '[JSON] OAuthClientConfig :> Post '[JSON] OAuthClientCredentials ) + :<|> Named + "get-oauth-client" + ( Summary "Get OAuth client by id" + :> CanThrow 'OAuthFeatureDisabled + :> CanThrow 'OAuthClientNotFound + :> "oauth" + :> "clients" + :> Capture "id" OAuthClientId + :> Get '[JSON] OAuthClient + ) + :<|> Named + "update-oauth-client" + ( Summary "Update OAuth client" + :> CanThrow 'OAuthFeatureDisabled + :> CanThrow 'OAuthClientNotFound + :> "oauth" + :> "clients" + :> Capture "id" OAuthClientId + :> ReqBody '[JSON] OAuthClientConfig + :> Put '[JSON] OAuthClient + ) + :<|> Named + "delete-oauth-client" + ( Summary "Delete OAuth client" + :> CanThrow 'OAuthFeatureDisabled + :> CanThrow 'OAuthClientNotFound + :> "oauth" + :> "clients" + :> Capture "id" OAuthClientId + :> Delete '[JSON] () + ) diff --git a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs index 23b7ebdb7d..7464f2e3b9 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs @@ -149,7 +149,7 @@ tests = testRoundTrip @Message.ClientMismatch, testRoundTrip @OAuth.RedirectUrl, testRoundTrip @OAuth.OAuthApplicationName, - testRoundTrip @OAuth.RegisterOAuthClientRequest, + testRoundTrip @OAuth.OAuthClientConfig, testRoundTrip @OAuth.OAuthClient, testRoundTrip @OAuth.CreateOAuthAuthorizationCodeRequest, testRoundTrip @OAuth.OAuthAccessTokenRequest, diff --git a/services/brig/src/Brig/API/OAuth.hs b/services/brig/src/Brig/API/OAuth.hs index 84df200afc..d20c929fd1 100644 --- a/services/brig/src/Brig/API/OAuth.hs +++ b/services/brig/src/Brig/API/OAuth.hs @@ -62,6 +62,9 @@ import qualified Wire.Sem.Now as Now internalOauthAPI :: ServerT I.OAuthAPI (Handler r) internalOauthAPI = Named @"create-oauth-client" registerOAuthClient + :<|> Named @"get-oauth-client" getOAuthClientById + :<|> Named @"update-oauth-client" updateOAuthClient + :<|> Named @"delete-oauth-client" deleteOAuthClient -------------------------------------------------------------------------------- -- API Public @@ -78,8 +81,8 @@ oauthAPI = -------------------------------------------------------------------------------- -- Handlers -registerOAuthClient :: RegisterOAuthClientRequest -> (Handler r) OAuthClientCredentials -registerOAuthClient (RegisterOAuthClientRequest name uri) = do +registerOAuthClient :: OAuthClientConfig -> (Handler r) OAuthClientCredentials +registerOAuthClient (OAuthClientConfig name uri) = do unlessM (Opt.setOAuthEnabled <$> view settings) $ throwStd $ errorToWai @'OAuthFeatureDisabled credentials@(OAuthClientCredentials cid secret) <- OAuthClientCredentials <$> randomId <*> createSecret safeSecret <- liftIO $ hashClientSecret secret @@ -95,6 +98,23 @@ registerOAuthClient (RegisterOAuthClientRequest name uri) = do rand32Bytes :: MonadIO m => m AsciiBase16 rand32Bytes = liftIO . fmap encodeBase16 $ randBytes 32 +getOAuthClientById :: OAuthClientId -> (Handler r) OAuthClient +getOAuthClientById cid = do + unlessM (Opt.setOAuthEnabled <$> view settings) $ throwStd $ errorToWai @'OAuthFeatureDisabled + mClient <- lift $ wrapClient $ lookupOauthClient cid + maybe (throwStd $ errorToWai @'OAuthClientNotFound) pure mClient + +updateOAuthClient :: OAuthClientId -> OAuthClientConfig -> (Handler r) OAuthClient +updateOAuthClient cid config = do + void $ getOAuthClientById cid + lift $ wrapClient $ updateOAuthClient' cid config.applicationName config.redirectUrl + getOAuthClientById cid + +deleteOAuthClient :: OAuthClientId -> (Handler r) () +deleteOAuthClient cid = do + void $ getOAuthClientById cid + lift $ wrapClient $ deleteOAuthClient' cid + -------------------------------------------------------------------------------- getOAuthClient :: UserId -> OAuthClientId -> (Handler r) (Maybe OAuthClient) @@ -284,6 +304,18 @@ revokeOAuthAccountAccess uid cid = do -------------------------------------------------------------------------------- -- DB +deleteOAuthClient' :: (MonadClient m) => OAuthClientId -> m () +deleteOAuthClient' cid = retry x5 . write q $ params LocalQuorum (Identity cid) + where + q :: PrepQuery W (Identity OAuthClientId) () + q = "DELETE FROM oauth_client WHERE id = ?" + +updateOAuthClient' :: (MonadClient m) => OAuthClientId -> OAuthApplicationName -> RedirectUrl -> m () +updateOAuthClient' cid name uri = retry x5 . write q $ params LocalQuorum (name, uri, cid) + where + q :: PrepQuery W (OAuthApplicationName, RedirectUrl, OAuthClientId) () + q = "UPDATE oauth_client SET name = ?, redirect_uri = ? WHERE id = ?" + insertOAuthClient :: (MonadClient m) => OAuthClientId -> OAuthApplicationName -> RedirectUrl -> Password -> m () insertOAuthClient cid name uri pw = retry x5 . write q $ params LocalQuorum (cid, name, uri, pw) where diff --git a/services/brig/test/integration/API/OAuth.hs b/services/brig/test/integration/API/OAuth.hs index 3efdd15fa4..a57a9171e8 100644 --- a/services/brig/test/integration/API/OAuth.hs +++ b/services/brig/test/integration/API/OAuth.hs @@ -130,7 +130,7 @@ tests m db b n o = do testRegisterNewOAuthClient :: Brig -> Http () testRegisterNewOAuthClient brig = do - let newOAuthClient@(RegisterOAuthClientRequest expectedAppName expectedUrl) = newOAuthClientRequestBody "E Corp" "https://example.com" + let newOAuthClient@(OAuthClientConfig expectedAppName expectedUrl) = newOAuthClientRequestBody "E Corp" "https://example.com" c <- registerNewOAuthClient brig newOAuthClient uid <- randomId oauthClientInfo <- getOAuthClientInfo brig uid c.clientId @@ -140,7 +140,7 @@ testRegisterNewOAuthClient brig = do testCreateOAuthCodeSuccess :: Brig -> Http () testCreateOAuthCodeSuccess brig = do - let newOAuthClient@(RegisterOAuthClientRequest _ redirectUrl) = newOAuthClientRequestBody "E Corp" "https://example.com" + let newOAuthClient@(OAuthClientConfig _ redirectUrl) = newOAuthClientRequestBody "E Corp" "https://example.com" c <- registerNewOAuthClient brig newOAuthClient uid <- randomId let scope = OAuthScopes $ Set.fromList [WriteConversations, WriteConversationsCode] @@ -739,17 +739,17 @@ authHeader = bearer "Authorization" bearer :: ToHttpApiData a => HeaderName -> a -> Request -> Request bearer name = header name . toHeader . Bearer -newOAuthClientRequestBody :: Text -> Text -> RegisterOAuthClientRequest +newOAuthClientRequestBody :: Text -> Text -> OAuthClientConfig newOAuthClientRequestBody name url = let redirectUrl = mkUrl (cs url) applicationName = OAuthApplicationName (unsafeRange name) - in RegisterOAuthClientRequest applicationName redirectUrl + in OAuthClientConfig applicationName redirectUrl -registerNewOAuthClient :: (MonadIO m, MonadHttp m, MonadCatch m, HasCallStack) => Brig -> RegisterOAuthClientRequest -> m OAuthClientCredentials +registerNewOAuthClient :: (MonadIO m, MonadHttp m, MonadCatch m, HasCallStack) => Brig -> OAuthClientConfig -> m OAuthClientCredentials registerNewOAuthClient brig reqBody = responseJsonError =<< registerNewOAuthClient' brig reqBody Brig -> RegisterOAuthClientRequest -> m ResponseLBS +registerNewOAuthClient' :: (MonadHttp m) => Brig -> OAuthClientConfig -> m ResponseLBS registerNewOAuthClient' brig reqBody = post (brig . paths ["i", "oauth", "clients"] . json reqBody) @@ -800,7 +800,7 @@ generateOAuthClientAndAuthorizationCode = generateOAuthClientAndAuthorizationCod generateOAuthClientAndAuthorizationCode' :: (MonadIO m, MonadHttp m, MonadCatch m, HasCallStack) => OAuthCodeChallenge -> Brig -> UserId -> OAuthScopes -> RedirectUrl -> m (OAuthClientId, OAuthAuthorizationCode) generateOAuthClientAndAuthorizationCode' chal brig uid scope url = do - let newOAuthClient = RegisterOAuthClientRequest (OAuthApplicationName (unsafeRange "E Corp")) url + let newOAuthClient = OAuthClientConfig (OAuthApplicationName (unsafeRange "E Corp")) url OAuthClientCredentials cid _ <- registerNewOAuthClient brig newOAuthClient (cid,) <$> generateOAuthAuthorizationCode' chal brig uid cid scope url diff --git a/tools/stern/src/Stern/API.hs b/tools/stern/src/Stern/API.hs index f5b671e0db..c9b4be4220 100644 --- a/tools/stern/src/Stern/API.hs +++ b/tools/stern/src/Stern/API.hs @@ -165,6 +165,10 @@ sitemap' = :<|> Named @"post-team-billing-info" setTeamBillingInfo :<|> Named @"get-consent-log" getConsentLog :<|> Named @"get-user-meta-info" getUserData + :<|> Named @"register-oauth-client" Intra.registerOAuthClient + :<|> Named @"get-oauth-client" Intra.getOAuthClient + :<|> Named @"update-oauth-client" Intra.updateOAuthClient + :<|> Named @"delete-oauth-client" Intra.deleteOAuthClient sitemapInternal :: Servant.Server SternAPIInternal sitemapInternal = diff --git a/tools/stern/src/Stern/API/Routes.hs b/tools/stern/src/Stern/API/Routes.hs index 4318a88e8e..af94471b03 100644 --- a/tools/stern/src/Stern/API/Routes.hs +++ b/tools/stern/src/Stern/API/Routes.hs @@ -43,6 +43,7 @@ import Servant.Swagger (HasSwagger (toSwagger)) import Servant.Swagger.Internal.Orphans () import Servant.Swagger.UI import Stern.Types +import Wire.API.OAuth import Wire.API.Routes.Internal.Brig.Connection (ConnectionStatus) import qualified Wire.API.Routes.Internal.Brig.EJPD as EJPD import Wire.API.Routes.Named @@ -380,6 +381,43 @@ type SternAPI = :> QueryParam' [Required, Strict, Description "A valid UserId"] "id" UserId :> Post '[JSON] UserMetaInfo ) + :<|> Named + "register-oauth-client" + ( Summary "Register an OAuth client" + :> "i" + :> "oauth" + :> "clients" + :> ReqBody '[JSON] OAuthClientConfig + :> Post '[JSON] OAuthClientCredentials + ) + :<|> Named + "get-oauth-client" + ( Summary "Get OAuth client by id" + :> "i" + :> "oauth" + :> "clients" + :> Capture "id" OAuthClientId + :> Get '[JSON] OAuthClient + ) + :<|> Named + "update-oauth-client" + ( Summary "Update OAuth client" + :> "i" + :> "oauth" + :> "clients" + :> Capture "id" OAuthClientId + :> ReqBody '[JSON] OAuthClientConfig + :> Put '[JSON] OAuthClient + ) + :<|> Named + "delete-oauth-client" + ( Summary "Delete OAuth client" + :> "i" + :> "oauth" + :> "clients" + :> Capture "id" OAuthClientId + :> Delete '[JSON] () + ) ------------------------------------------------------------------------------- -- Swagger diff --git a/tools/stern/src/Stern/Intra.hs b/tools/stern/src/Stern/Intra.hs index 5f311d1f93..e9c7e95964 100644 --- a/tools/stern/src/Stern/Intra.hs +++ b/tools/stern/src/Stern/Intra.hs @@ -59,6 +59,10 @@ module Stern.Intra getUserClients, getUserCookies, getUserNotifications, + registerOAuthClient, + getOAuthClient, + updateOAuthClient, + deleteOAuthClient, ) where @@ -98,6 +102,7 @@ import UnliftIO.Exception hiding (Handler) import Wire.API.Connection import Wire.API.Conversation import Wire.API.Internal.Notification +import Wire.API.OAuth (OAuthClient, OAuthClientConfig, OAuthClientCredentials) import Wire.API.Properties import Wire.API.Routes.Internal.Brig.Connection import qualified Wire.API.Routes.Internal.Brig.EJPD as EJPD @@ -853,3 +858,64 @@ getUserNotifications uid = do 404 -> parseResponse (mkError status502 "bad-upstream") r _ -> throwE (mkError status502 "bad-upstream" "") batchSize = 100 :: Int + +registerOAuthClient :: OAuthClientConfig -> Handler OAuthClientCredentials +registerOAuthClient conf = do + b <- view brig + r <- + catchRpcErrors $ + rpc' + "brig" + b + ( method POST + . Bilge.paths ["i", "oauth", "clients"] + . Bilge.json conf + . contentJson + . expect2xx + ) + parseResponse (mkError status502 "bad-upstream") r + +getOAuthClient :: OAuthClientId -> Handler OAuthClient +getOAuthClient cid = do + b <- view brig + r <- + rpc' + "brig" + b + ( method GET + . Bilge.paths ["i", "oauth", "clients", toByteString' cid] + ) + case statusCode r of + 200 -> parseResponse (mkError status502 "bad-upstream") r + 404 -> throwE (mkError status404 "bad-upstream" "not-found") + _ -> throwE (mkError status502 "bad-upstream" (cs $ show r)) + +updateOAuthClient :: OAuthClientId -> OAuthClientConfig -> Handler OAuthClient +updateOAuthClient cid conf = do + b <- view brig + r <- + catchRpcErrors $ + rpc' + "brig" + b + ( method PUT + . Bilge.paths ["i", "oauth", "clients", toByteString' cid] + . Bilge.json conf + . contentJson + . expect2xx + ) + parseResponse (mkError status502 "bad-upstream") r + +deleteOAuthClient :: OAuthClientId -> Handler () +deleteOAuthClient cid = do + b <- view brig + r <- + catchRpcErrors $ + rpc' + "brig" + b + ( method DELETE + . Bilge.paths ["i", "oauth", "clients", toByteString' cid] + . expect2xx + ) + parseResponse (mkError status502 "bad-upstream") r diff --git a/tools/stern/test/integration/API.hs b/tools/stern/test/integration/API.hs index ca62050c10..7b67d1062d 100644 --- a/tools/stern/test/integration/API.hs +++ b/tools/stern/test/integration/API.hs @@ -31,6 +31,7 @@ import Data.Aeson (ToJSON, Value) import Data.ByteString.Conversion import Data.Handle import Data.Id +import Data.Range (unsafeRange) import Data.Schema import qualified Data.Set as Set import Data.String.Conversions @@ -42,6 +43,7 @@ import Test.Tasty import Test.Tasty.HUnit import TestSetup import Util +import Wire.API.OAuth (OAuthApplicationName (OAuthApplicationName), OAuthClientConfig (..), OAuthClientCredentials (..)) import Wire.API.Properties (PropertyKey) import Wire.API.Routes.Internal.Brig.Connection import qualified Wire.API.Routes.Internal.Brig.EJPD as EJPD @@ -93,7 +95,8 @@ tests s = test s "GET /i/consent" testGetConsentLog, test s "GET /teams/:id" testGetTeamInfo, test s "GET i/user/meta-info?id=..." testGetUserMetaInfo, - test s "/teams/:tid/search-visibility" testSearchVisibility + test s "/teams/:tid/search-visibility" testSearchVisibility, + test s "i/oauth/clients" testCrudOAuthClient -- The following endpoints can not be tested because they require ibis: -- - `GET /teams/:tid/billing` -- - `GET /teams/:tid/invoice/:inr` @@ -101,6 +104,25 @@ tests s = -- - `POST /teams/:tid/billing` ] +testCrudOAuthClient :: TestM () +testCrudOAuthClient = do + let url = fromMaybe (error "invalid url") . fromByteString $ "https://example.com" + let name = OAuthApplicationName (unsafeRange "foobar") + cred <- registerOAuthClient (OAuthClientConfig name url) + c <- getOAuthClient cred.clientId + liftIO $ do + c.applicationName @?= name + c.redirectUrl @?= url + let newName = OAuthApplicationName (unsafeRange "barfoo") + let newUrl = fromMaybe (error "invalid url") . fromByteString $ "https://example.org" + updateOAuthClient cred.clientId (OAuthClientConfig newName newUrl) + c' <- getOAuthClient cred.clientId + liftIO $ do + c'.applicationName @?= newName + c'.redirectUrl @?= newUrl + deleteOAuthClient cred.clientId + getOAuthClient' cred.clientId !!! const 404 === statusCode + testSearchVisibility :: TestM () testSearchVisibility = do (_, tid, _) <- createTeamWithNMembers 10 @@ -623,3 +645,30 @@ putUserProperty :: UserId -> PropertyKey -> Value -> TestM () putUserProperty uid k v = do b <- view tsBrig void $ put (b . paths ["properties", toByteString' k] . json v . zUser uid . zConn "123" . expect2xx) + +registerOAuthClient :: OAuthClientConfig -> TestM OAuthClientCredentials +registerOAuthClient cfg = do + s <- view tsStern + r <- post (s . paths ["i", "oauth", "clients"] . json cfg . expect2xx) + pure $ responseJsonUnsafe r + +getOAuthClient' :: OAuthClientId -> TestM ResponseLBS +getOAuthClient' cid = do + s <- view tsStern + get (s . paths ["i", "oauth", "clients", toByteString' cid]) + +getOAuthClient :: OAuthClientId -> TestM OAuthClientConfig +getOAuthClient cid = do + s <- view tsStern + r <- get (s . paths ["i", "oauth", "clients", toByteString' cid] . expect2xx) + pure $ responseJsonUnsafe r + +updateOAuthClient :: OAuthClientId -> OAuthClientConfig -> TestM () +updateOAuthClient cid cfg = do + s <- view tsStern + void $ put (s . paths ["i", "oauth", "clients", toByteString' cid] . json cfg . expect2xx) + +deleteOAuthClient :: OAuthClientId -> TestM () +deleteOAuthClient cid = do + s <- view tsStern + void $ delete (s . paths ["i", "oauth", "clients", toByteString' cid] . expect2xx)