Skip to content

Commit 7345e89

Browse files
authored
Merge pull request #56 from unisoncomputing/cp/user-creation-tests
Skip Github flow on local for easier testing of user creation flows
2 parents 0b536b8 + f2a21bf commit 7345e89

7 files changed

Lines changed: 100 additions & 34 deletions

File tree

docker/docker-compose.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,13 @@ services:
4848

4949
environment:
5050
# Placeholder values for development
51-
- SHARE_API_ORIGIN=http://share-api
51+
- SHARE_DEPLOYMENT=local
52+
- SHARE_API_ORIGIN=http://localhost:5424
5253
- SHARE_SERVER_PORT=5424
5354
- SHARE_REDIS=redis://redis:6379
5455
- SHARE_POSTGRES=postgresql://postgres:sekrit@postgres:5432
5556
- SHARE_HMAC_KEY=hmac-key-test-key-test-key-test-
5657
- SHARE_EDDSA_KEY=eddsa-key-test-key-test-key-test
57-
- SHARE_DEPLOYMENT=local
5858
- SHARE_POSTGRES_CONN_TTL=30
5959
- SHARE_POSTGRES_CONN_MAX=10
6060
- SHARE_SHARE_UI_ORIGIN=http://localhost:1234

src/Share/Github.hs

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -45,44 +45,44 @@ instance ToServerError GithubError where
4545
GithubDecodeError _ce -> (ErrorID "github:decode-error", err500 {errBody = "Github returned an unexpected response. Please try again."})
4646

4747
data GithubUser = GithubUser
48-
{ github_user_login :: Text,
49-
github_user_id :: Int64,
50-
github_user_avatar_url :: URIParam,
51-
github_user_name :: Maybe Text
48+
{ githubHandle :: Text,
49+
githubUserId :: Int64,
50+
githubUserAvatarUrl :: URIParam,
51+
githubUserName :: Maybe Text
5252
}
5353
deriving (Show)
5454

5555
instance FromJSON GithubUser where
5656
parseJSON = withObject "GithubUser" $ \u ->
5757
GithubUser
5858
<$> u
59-
.: "login"
59+
.: "login"
6060
<*> u
61-
.: "id"
61+
.: "id"
6262
<*> u
63-
.: "avatar_url"
63+
.: "avatar_url"
6464
-- We don't use this email because it's the "publicly visible" email, instead we fetch
6565
-- the primary email using the emails API.
6666
-- <*> u .: "email"
6767
<*> u
68-
.:? "name"
68+
.:? "name"
6969

7070
data GithubEmail = GithubEmail
71-
{ github_email_email :: Text,
72-
github_email_primary :: Bool,
73-
github_email_verified :: Bool
71+
{ githubEmailEmail :: Text,
72+
githubEmailIsPrimary :: Bool,
73+
githubEmailIsVerified :: Bool
7474
}
7575
deriving (Show)
7676

7777
instance FromJSON GithubEmail where
7878
parseJSON = withObject "GithubEmail" $ \u ->
7979
GithubEmail
8080
<$> u
81-
.: "email"
81+
.: "email"
8282
<*> u
83-
.: "primary"
83+
.: "primary"
8484
<*> u
85-
.: "verified"
85+
.: "verified"
8686

8787
type GithubTokenApi =
8888
"login"
@@ -190,7 +190,7 @@ primaryGithubEmail auth = do
190190
emails <- runGithubClient githubAPIBaseURL (githubEmailsClient auth)
191191
-- Github's api docs suggest there will always be a primary email.
192192
-- https://docs.github.com/en/rest/users/emails#list-email-addresses-for-the-authenticated-user
193-
case find github_email_primary emails of
193+
case find githubEmailIsPrimary emails of
194194
Nothing -> respondError GithubUserWithoutPrimaryEmail
195195
Just email -> pure email
196196

src/Share/Postgres/Users/Queries.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ userByHandle handle = do
197197

198198
createFromGithubUser :: AuthZ.AuthZReceipt -> GithubUser -> GithubEmail -> Maybe UserHandle -> PG.Transaction UserCreationError User
199199
createFromGithubUser !authzReceipt (GithubUser githubHandle githubUserId avatar_url user_name) primaryEmail mayPreferredHandle = do
200-
let (GithubEmail {github_email_email = user_email, github_email_verified = emailVerified}) = primaryEmail
200+
let (GithubEmail {githubEmailEmail = user_email, githubEmailIsVerified = emailVerified}) = primaryEmail
201201
userHandle <- case mayPreferredHandle of
202202
Just handle -> pure handle
203203
Nothing -> case IDs.fromText @UserHandle (Text.toLower githubHandle) of

src/Share/Web/OAuth/Impl.hs

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ import Control.Monad.Reader
1919
import Data.Aeson (ToJSON (..))
2020
import Data.Aeson qualified as Aeson
2121
import Data.Map qualified as Map
22+
import Data.Maybe (fromJust)
2223
import Data.Set qualified as Set
24+
import Network.URI (parseURI)
2325
import Servant
2426
import Share.App (shareAud, shareIssuer)
2527
import Share.Env qualified as Env
@@ -38,11 +40,11 @@ import Share.OAuth.Session qualified as Session
3840
import Share.OAuth.Types (AccessToken, AuthenticationRequest (..), Code, GrantType (AuthorizationCode), OAuth2State, OAuthClientConfig (..), OAuthClientId, PKCEChallenge, PKCEChallengeMethod, RedirectReceiverErr (..), ResponseType (ResponseTypeCode), TokenRequest (..), TokenResponse (..), TokenType (BearerToken))
3941
import Share.OAuth.Types qualified as OAuth
4042
import Share.Postgres qualified as PG
41-
import Share.Postgres.Ops qualified as PGO
4243
import Share.Postgres.Users.Queries qualified as UserQ
4344
import Share.Prelude
4445
import Share.User (User (User))
4546
import Share.User qualified as User
47+
import Share.Utils.Deployment qualified as Deployment
4648
import Share.Utils.Logging qualified as Logging
4749
import Share.Utils.Servant
4850
import Share.Utils.Servant.Cookies (CookieVal, cookieVal)
@@ -139,18 +141,20 @@ redirectReceiverEndpoint _mayGithubCode _mayStatePSID (Just errorType) mayErrorD
139141
otherErrType -> do
140142
Logging.logErrorText ("Github authentication error: " <> otherErrType <> " " <> fold mayErrorDescription)
141143
errorRedirect UnspecifiedError
142-
redirectReceiverEndpoint mayGithubCode mayStatePSID _errorType@Nothing _mayErrorDescription mayCookiePSID existingAuthSession = do
144+
redirectReceiverEndpoint mayGithubCode mayStatePSID _errorType@Nothing _mayErrorDescription mayCookiePSID _existingAuthSession = do
143145
cookiePSID <- case cookieVal mayCookiePSID of
144146
Nothing -> respondError $ MissingOrExpiredPendingSession
145147
Just psid -> pure psid
146148
PendingSession {loginRequest, returnToURI = unvalidatedReturnToURI, additionalData} <- ensurePendingSession cookiePSID
147-
newOrPreExistingUser <- case (mayGithubCode, mayStatePSID, existingAuthSession) of
148-
-- The user has an already valid session, we can use that.
149-
(_, _, Just session) -> do
150-
user <- (PGO.expectUserById (sessionUserId session))
151-
pure (UserQ.PreExisting user)
149+
newOrPreExistingUser <- case (mayGithubCode, mayStatePSID) of
152150
-- The user has completed the Github flow, we can log them in or create a new user.
153-
(Just githubCode, Just statePSID, _noSession) -> do
151+
(Just githubCode, Just statePSID) -> do
152+
(ghUser, ghEmail) <-
153+
-- Skip the github flow when developing locally, and just use some dummy github user
154+
-- data.
155+
if Deployment.onLocal
156+
then pure localGithubUserInfo
157+
else getGithubUserInfo githubCode statePSID cookiePSID
154158
mayPreferredHandle <- runMaybeT do
155159
obj <- hoistMaybe additionalData
156160
case Aeson.fromJSON obj of
@@ -160,10 +164,10 @@ redirectReceiverEndpoint mayGithubCode mayStatePSID _errorType@Nothing _mayError
160164
Aeson.Success m -> do
161165
handle <- hoistMaybe $ Map.lookup ("handle" :: Text) m
162166
hoistMaybe . eitherToMaybe $ IDs.fromText handle
163-
completeGithubFlow githubCode statePSID cookiePSID mayPreferredHandle
164-
(Nothing, _, _) -> do
167+
completeGithubFlow ghUser ghEmail mayPreferredHandle
168+
(Nothing, _) -> do
165169
respondError $ MissingCode
166-
(_, Nothing, _) -> do
170+
(_, Nothing) -> do
167171
respondError $ MissingState
168172
let (User {User.user_id = uid}) = UserQ.getNewOrPreExisting newOrPreExistingUser
169173
when (UserQ.isNew newOrPreExistingUser) do
@@ -198,20 +202,37 @@ redirectReceiverEndpoint mayGithubCode mayStatePSID _errorType@Nothing _mayError
198202
Nothing -> respondError $ InternalServerError "session-create-failure" ("Failed to create session" :: Text)
199203
Just setAuthHeaders -> pure . clearPendingSessionCookie cookieSettings $ setAuthHeaders response
200204
where
201-
completeGithubFlow ::
205+
localGithubUserInfo :: (Github.GithubUser, Github.GithubEmail)
206+
localGithubUserInfo =
207+
( Github.GithubUser
208+
{ githubHandle = "LocalGithubUser",
209+
githubUserId = 1,
210+
githubUserAvatarUrl = URIParam $ fromJust $ parseURI "https://avatars.githubusercontent.com/u/0?v=4",
211+
githubUserName = Just "Local Github User"
212+
},
213+
Github.GithubEmail
214+
{ githubEmailEmail = "local@example.com",
215+
githubEmailIsPrimary = True,
216+
githubEmailIsVerified = True
217+
}
218+
)
219+
220+
getGithubUserInfo ::
202221
( OAuth.Code ->
203222
PendingSessionId ->
204223
PendingSessionId ->
205-
Maybe UserHandle ->
206-
WebApp (UserQ.NewOrPreExisting User)
224+
WebApp (Github.GithubUser, Github.GithubEmail)
207225
)
208-
completeGithubFlow githubCode statePSID cookiePSID mayPreferredHandle = do
226+
getGithubUserInfo githubCode statePSID cookiePSID = do
209227
when (statePSID /= cookiePSID) do
210228
Redis.liftRedis $ Redis.failPendingSession cookiePSID
211229
respondError (MismatchedState cookiePSID statePSID)
212230
token <- Github.githubTokenForCode githubCode
213231
ghUser <- Github.githubUser token
214232
ghEmail <- Github.primaryGithubEmail token
233+
pure (ghUser, ghEmail)
234+
completeGithubFlow :: Github.GithubUser -> Github.GithubEmail -> Maybe UserHandle -> WebApp (UserQ.NewOrPreExisting User)
235+
completeGithubFlow ghUser ghEmail mayPreferredHandle = do
215236
PG.tryRunTransaction (UserQ.findOrCreateGithubUser AuthZ.userCreationOverride ghUser ghEmail mayPreferredHandle) >>= \case
216237
Left (UserQ.UserHandleTaken _) -> do
217238
errorRedirect AccountCreationHandleAlreadyTaken
@@ -269,8 +290,19 @@ loginWithGithub ::
269290
NoContent
270291
)
271292
loginWithGithub psid = do
272-
githubAuthURI <- Github.githubAuthenticationURI psid
293+
githubAuthURI <-
294+
if Deployment.onLocal
295+
then skipGithubLoginURL psid
296+
else Github.githubAuthenticationURI psid
273297
pure $ redirectTo githubAuthURI
298+
where
299+
skipGithubLoginURL :: OAuth2State -> WebApp URI
300+
skipGithubLoginURL oauth2State = do
301+
sharePathQ ["oauth", "redirect"] $
302+
Map.fromList
303+
[ ("code", "code"),
304+
("state", toQueryParam oauth2State)
305+
]
274306

275307
-- | Log out the user by telling the browser to clear the session cookies.
276308
-- Note that this doesn't (yet) invalidate the session itself, it just removes its cookie from the

transcripts/run-transcripts.zsh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ transcripts=(
1616
contribution-merge transcripts/share-apis/contribution-merge/
1717
search transcripts/share-apis/search/
1818
users transcripts/share-apis/users/
19+
user-creation transcripts/share-apis/user-creation/
1920
contribution-diffs transcripts/share-apis/contribution-diffs/
2021
definition-diffs transcripts/share-apis/definition-diffs/
2122
tickets transcripts/share-apis/tickets/
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"body": {
3+
"avatarUrl": "https://avatars.githubusercontent.com/u/0?v=4",
4+
"completedTours": [],
5+
"handle": "localgithubuser",
6+
"isSuperadmin": false,
7+
"name": "Local Github User",
8+
"organizationMemberships": [],
9+
"primaryEmail": "local@example.com",
10+
"userId": "U-<UUID>"
11+
},
12+
"status": [
13+
{
14+
"status_code": 200
15+
}
16+
]
17+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/usr/bin/env zsh
2+
3+
set -e
4+
5+
source "../../transcript_helpers.sh"
6+
7+
# Create a cookie jar we can use to store cookies for the new user we're goin to create.
8+
new_user_cookie_jar=$(cookie_jar_for_user_id "new_user")
9+
10+
# Should be able to create a new user via the login flow by following redirects.
11+
# Note that the end of the redirect chain ends up on the Share UI (localhost:1234) which may or may not be running, so
12+
# we just ignore bad status codes from that server.
13+
curl -v -L -I -o /dev/null -w '{"result_url": "%{url_effective}"}' --request "GET" --cookie "$new_user_cookie_jar" --cookie-jar "$new_user_cookie_jar" "http://localhost:5424/login" || true
14+
15+
# user should now be logged in as the local github user.
16+
fetch "new_user" GET new-user-profile /account

0 commit comments

Comments
 (0)