Skip to content
This repository has been archived by the owner on Sep 30, 2024. It is now read-only.

GitHub authn: allowSignup option #1155

Merged
merged 12 commits into from
Dec 7, 2018
Merged

GitHub authn: allowSignup option #1155

merged 12 commits into from
Dec 7, 2018

Conversation

beyang
Copy link
Member

@beyang beyang commented Nov 28, 2018

Change summary:

  • 41feb91: Rename and refactor the auth.CreateOrUpdateUser function to be auth.GetAndSaveUser. This makes the code clearer and gives clear contracts on how the user identity is inferred when signing in via external auth mechanism. See the docstring for that function.
  • 34ad4c6: Adds a allowSignup field to the GitHub auth provider config. This is analogous to the allowSignup field in the builtin auth provider. If set to true, users can sign up by authenticating via their GitHub account. If set to false, an existing account must exist that corresponds to the GitHub identity by verified email equivalency. Its default value is false (cc @sqs okay with you?)
  • 81f6270: Simplifies the auth.UpdateProviders method. Previously, there was a complicated (IMO) mechanism to ensure that existing Provider instances were not deleted/re-created if their config had not actually changed (i.e., reuse the same Provider instance if possible). Now, it just re-creates all auth.Provider instances on any config change. The added complexity didn't seem worth it to me, since none of the auth.Provider impls maintain internal state that would cause issues if it were discarded. There was a bug in the previous code that caused a panic on config update and I wasn't able to pin down the issue after about an hour of debugging. cc @sqs I think you were the original author—was there any other reason I missed why we could recreate all auth.Providers on every config refresh?
  • 755b97e: Docs, including guidance on when to use each of the current auth providers.
  • The remaining commits are minor.

This will allow us to enable GitHub auth on dogfood.

Copy link
Member

@sqs sqs left a comment

Choose a reason for hiding this comment

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

The change (allowSignup for the GitHub auth provider) looks good. (And in general, we can make site config changes much more cheaply/cavalierly now because https://github.com/sourcegraph/sourcegraph/issues/965 makes it possible for Sourcegraph to auto-migrate site config when we change the structure of it.)

I added some code review notes, though.


// CreateOrUpdateUser creates or updates a user using the provided information, looking up a user by
// UpdateUser creates or updates a user using the provided information, looking up a user by
Copy link
Member

Choose a reason for hiding this comment

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

UpdateUser creates or updates a user

meant to remove "creates or"?

Copy link
Member Author

Choose a reason for hiding this comment

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

To me, "UpdateUser" is clearer, since the caller has to specifically pass createIfNotExist=true if they want creation behavior.

// MockCreateOrUpdateUser is used in tests to mock CreateOrUpdateUser.
var MockCreateOrUpdateUser func(db.NewUser, extsvc.ExternalAccountSpec) (int32, error)
// MockUpdateUser is used in tests to mock UpdateUser.
var MockUpdateUser func(db.NewUser, extsvc.ExternalAccountSpec) (int32, error)
Copy link
Member

Choose a reason for hiding this comment

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

db.NewUser struct sounds confusing if it's passed to an "update" func (doesn't make sense to "update" a "new user")

@@ -109,3 +77,53 @@ func CreateOrUpdateUser(ctx context.Context, newOrUpdatedUser db.NewUser, extern
}
return user.ID, "", nil
}

func createUser(ctx context.Context, newUser db.NewUser, externalAccount extsvc.ExternalAccountSpec, data extsvc.ExternalAccountData) (
Copy link
Member

Choose a reason for hiding this comment

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

It doesn't make sense to call this func createUser (it was extracted from a func called CreateOrUpdateUser) because it doesn't just create a user. In some cases (the associateUser == true code path) it updates the user.

@beyang beyang changed the title GitHub authn: allowSignup option WIP GitHub authn: allowSignup option Nov 29, 2018
@beyang beyang requested review from sqs and removed request for keegancsmith, nicksnyder and sqs November 29, 2018 02:02
@beyang
Copy link
Member Author

beyang commented Nov 29, 2018

WIP'ing/unassigning to do a more extensive refactoring of the auth code to make this update possible.

createUserAndSaveErr error
associateUserAndSaveErr error
getByVerifiedEmailErr error
getByIDErr error

Choose a reason for hiding this comment

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

getByIDErr is unused (from structcheck)

associateUserAndSaveErr error
getByVerifiedEmailErr error
getByIDErr error
updateErr error

Choose a reason for hiding this comment

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

updateErr is unused (from structcheck)


type mockParams struct {
userInfos []userInfo
lookupUserAndSaveErr error

Choose a reason for hiding this comment

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

lookupUserAndSaveErr is unused (from structcheck)

type mockParams struct {
userInfos []userInfo
lookupUserAndSaveErr error
createUserAndSaveErr error

Choose a reason for hiding this comment

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

createUserAndSaveErr is unused (from structcheck)

userInfos []userInfo
lookupUserAndSaveErr error
createUserAndSaveErr error
associateUserAndSaveErr error

Choose a reason for hiding this comment

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

associateUserAndSaveErr is unused (from structcheck)

lookupUserAndSaveErr error
createUserAndSaveErr error
associateUserAndSaveErr error
getByVerifiedEmailErr error

Choose a reason for hiding this comment

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

getByVerifiedEmailErr is unused (from structcheck)

@beyang beyang force-pushed the bl/gh-no-signup branch 4 times, most recently from af467a3 to 34ad4c6 Compare December 2, 2018 21:10
func GetProviderByConfigID(id ProviderConfigID) Provider {
var ps []Provider
if MockProviders != nil {
ps = MockProviders

Choose a reason for hiding this comment

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

ineffectual assignment to ps (from ineffassign)

<-allProvidersRegistered
allProvidersMu.RLock()
defer allProvidersMu.RUnlock()
ps = allProviders

Choose a reason for hiding this comment

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

ineffectual assignment to ps (from ineffassign)

close(allProvidersRegistered)
})
func GetProviderByConfigID(id ProviderConfigID) Provider {
if MockProviders != nil {

Choose a reason for hiding this comment

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

unnecessary nil check around range (from megacheck)

@beyang beyang force-pushed the bl/gh-no-signup branch 2 times, most recently from 7a5cf69 to 81f6270 Compare December 3, 2018 19:31
@beyang beyang changed the title WIP GitHub authn: allowSignup option GitHub authn: allowSignup option Dec 4, 2018
@codecov-io
Copy link

codecov-io commented Dec 4, 2018

Codecov Report

Merging #1155 into master will increase coverage by 0.15%.
The diff coverage is 52.58%.

Impacted Files Coverage Δ
cmd/frontend/db/users_mock.go 0% <ø> (ø) ⬆️
cmd/frontend/db/users.go 47.04% <0%> (-0.29%) ⬇️
cmd/frontend/db/test_util.go 0% <0%> (ø)
pkg/extsvc/github/user_emails.go 0% <0%> (ø)
enterprise/cmd/frontend/auth/gitlaboauth/config.go 36.58% <0%> (+9.79%) ⬆️
enterprise/cmd/frontend/auth/githuboauth/config.go 45.23% <0%> (+11.3%) ⬆️
...nterprise/cmd/frontend/auth/gitlaboauth/session.go 0% <0%> (ø) ⬆️
...erprise/cmd/frontend/auth/httpheader/middleware.go 50% <100%> (+5.88%) ⬆️
cmd/frontend/internal/auth/override.go 64.7% <100%> (+9.15%) ⬆️
enterprise/cmd/frontend/auth/openidconnect/user.go 74.46% <100%> (+3.03%) ⬆️
... and 13 more

@beyang
Copy link
Member Author

beyang commented Dec 4, 2018

@sqs @nicksnyder PTAL

@beyang beyang requested review from slimsag and removed request for nicksnyder and keegancsmith December 5, 2018 18:40
@sqs
Copy link
Member

sqs commented Dec 5, 2018

Re: recreating all auth.Providers:

since none of the auth.Provider impls maintain internal state that would cause issues if it were discarded

If that is true, then it is fine to recreate them all each time. I think the state they maintain is things like the openid-configuration document. It probably isn't bad to fetch that a few times.

Are there any other things blocked on me?


// CreateOrUpdateUser creates or updates a user using the provided information, looking up a user by
// the external account provided.
type GetUserOp struct {
Copy link
Member

Choose a reason for hiding this comment

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

Should this be GetAndSaveUserOp? GetUserOp implies to me that the fields within this struct can be incorrect without repercussion, but that is not true.

for _, p := range providers {
k, err := json.Marshal(p.Config())
if err != nil {
log15.Error("Omitting auth provider, because could not JSON-marshal its config", "error", err, "configID", p.ConfigID())
Copy link
Member

@slimsag slimsag Dec 6, 2018

Choose a reason for hiding this comment

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

Suggested change
log15.Error("Omitting auth provider, because could not JSON-marshal its config", "error", err, "configID", p.ConfigID())
log15.Error("Omitting auth provider (failed to marshal its JSON config)", "error", err, "configID", p.ConfigID())

Copy link
Member

Choose a reason for hiding this comment

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

Should this include the label of the provider? for improved debugging

Copy link
Member Author

Choose a reason for hiding this comment

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

p.ConfigID() identifies it

// curProviders is a map (label -> (config string -> Provider)). The first key is the label
// under which the provider was registered. The second key is the normalized JSON serialization
// of Provider.Config().
curProviders = map[string]map[string]Provider{}
Copy link
Member

Choose a reason for hiding this comment

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

I feel like this double nested map is too complex for what we are doing. I am slightly worried about the lack of types (using string) too. Is there a way we could make it cleaner by using a struct or anything?

Copy link
Member

Choose a reason for hiding this comment

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

We should rename label to pkgName where we use it

for _, p := range providers {
k, err := json.Marshal(p.Config())
if err != nil {
log15.Error("Omitting auth provider, because could not JSON-marshal its config", "error", err, "configID", p.ConfigID())
Copy link
Member

Choose a reason for hiding this comment

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

Should this include the label of the provider? for improved debugging

// registered via UpdateProviders).
func GetProviderByConfigID(id ProviderConfigID) Provider {
var ps []Provider
func Providers() []Provider {
Copy link
Member

Choose a reason for hiding this comment

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

Let's add docs to Providers and clarify what it returns when there are no providers?

}
if !op {
panic(fmt.Sprintf("UpdateProviders: provider to remove did not exist: %+v", p))
providers := make([]Provider, 0, ct)
Copy link
Member

Choose a reason for hiding this comment

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

note: The double nested map curProviders makes this hard to read. I thought lp was the label name (human readable name) and not the submap of providers

Do we need the complexity here of calculating the array preallocation length? Why not just use something like 64 or even 128? (these are all pointer allocations, 128*8 == 1024 bytes memory, so very cheap anyway)

aj := allProviders[j].ConfigID()
return ai.Type < aj.Type || (ai.Type == aj.Type && ai.ID < aj.ID)
})
sort.Sort(sortProviders(providers))
Copy link
Member

Choose a reason for hiding this comment

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

Document why sorting these is important or not. If it's not important, why do we do it?

Do we need stable sorting here?

Copy link
Member Author

Choose a reason for hiding this comment

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

For the UI, will doc.

func (p sortProviders) Len() int {
return len(p)
}
func (p sortProviders) Less(i, j int) bool {
Copy link
Member

Choose a reason for hiding this comment

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

This less func is hard to read (that is fine / expected) but the intent is not documented. If there is a bug here, it would be hard to catch without knowing the intent. If I read it correctly, the intent is: builtin first, then by ConfigID().Type, then by ConfigID().ID

cmd/frontend/internal/auth/override.go Show resolved Hide resolved
cmd/frontend/internal/auth/override_test.go Show resolved Hide resolved
if MockCreateOrUpdateUser != nil {
userID, err = MockCreateOrUpdateUser(newOrUpdatedUser, externalAccount)
return userID, "", err
func GetAndSaveUser(ctx context.Context, op GetUserOp) (userID int32, safeErrMsg string, err error) {
Copy link
Member

Choose a reason for hiding this comment

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

As it stands this function returns two error message parameters that are easy to mix up by accident. A better approach would be to return a struct with all of these values, which means to access them you MUST acknowledge their names (result.UserID, result.SafeErrMsg, result.Err)

Copy link
Member Author

Choose a reason for hiding this comment

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

I'll follow up with this refactor in a later PR (want to get this deployed to dogfood for testing)

auth.UpdateProviders("builtin", nil)
} else {
auth.UpdateProviders("builtin", []auth.Provider{&provider{c: newPC}})
}
Copy link
Member

Choose a reason for hiding this comment

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

Return inside newPC == nil conditional instead and remove else?

if newPC == nil {
auth.UpdateProviders("httpheader", nil)
} else {
auth.UpdateProviders("httpheader", []auth.Provider{&provider{c: newPC}})
Copy link
Member

Choose a reason for hiding this comment

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

drop else / use early return here

}
userID, userSaved, extAcctSaved, safeErrMsg, err := func() (int32, bool, bool, string, error) {
Copy link
Member

Choose a reason for hiding this comment

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

Probably makes sense to use a struct type here (e.g. declared inline). Otherwise it is hard to read + easy to mix up e.g. userSaved and extAcctSaved by accident

Copy link
Member

@slimsag slimsag left a comment

Choose a reason for hiding this comment

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

LGTM aside from my unresolved inline comments

@beyang
Copy link
Member Author

beyang commented Dec 7, 2018

@sqs nothing in particular, but you've "requested changes"—if everything looks fine, can you approve?

@beyang beyang merged commit 6392504 into master Dec 7, 2018
@beyang beyang deleted the bl/gh-no-signup branch December 7, 2018 21:31
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants