diff --git a/pkg/infra/log/log.go b/pkg/infra/log/log.go index 490d9f8a58f..cf25b1064d8 100644 --- a/pkg/infra/log/log.go +++ b/pkg/infra/log/log.go @@ -36,6 +36,13 @@ func New(logger string, ctx ...interface{}) Logger { return Root.New(params...) } +// NewWithLevel returns a new logger with a certain level. +func NewWithLevel(name string, level log15.Lvl) Logger { + logger := Root.New("logger", name) + logger.SetHandler(log15.LvlFilterHandler(level, log15.StreamHandler(os.Stdout, getLogFormat("console")))) + return logger +} + func Tracef(format string, v ...interface{}) { var message string if len(v) > 0 { diff --git a/pkg/login/social/generic_oauth.go b/pkg/login/social/generic_oauth.go index f71e7bd318b..4dc503d0dce 100644 --- a/pkg/login/social/generic_oauth.go +++ b/pkg/login/social/generic_oauth.go @@ -82,6 +82,7 @@ type UserInfoJson struct { Upn string `json:"upn"` Attributes map[string][]string `json:"attributes"` rawJSON []byte + source string } func (info *UserInfoJson) String() string { @@ -91,64 +92,97 @@ func (info *UserInfoJson) String() string { } func (s *SocialGenericOAuth) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) { - var data UserInfoJson - var err error + s.log.Debug("Getting user info") + tokenData := s.extractFromToken(token) + apiData := s.extractFromAPI(client) userInfo := &BasicUserInfo{} + for _, data := range []*UserInfoJson{tokenData, apiData} { + if data == nil { + continue + } - if s.extractToken(&data, token) { - s.fillUserInfo(userInfo, &data) - } + s.log.Debug("Processing external user info", "source", data.source, "data", data) - if s.extractAPI(&data, client) { - s.fillUserInfo(userInfo, &data) + if userInfo.Name == "" { + if data.Name != "" { + s.log.Debug("Setting user info name from name field") + userInfo.Name = data.Name + } else if data.DisplayName != "" { + s.log.Debug("Setting user info name from display name field") + userInfo.Name = data.DisplayName + } + } + + if userInfo.Login == "" { + if data.Login != "" { + s.log.Debug("Setting user info login from login field", "login", data.Login) + userInfo.Login = data.Login + } else { + if s.loginAttributePath != "" { + s.log.Debug("Searching for login among JSON", "loginAttributePath", s.loginAttributePath) + login, err := s.searchJSONForAttr(s.loginAttributePath, data.rawJSON) + if err != nil { + s.log.Error("Failed to search JSON for login attribute", "error", err) + } else if login != "" { + userInfo.Login = login + s.log.Debug("Setting user info login from login field", "login", login) + } + } + + if userInfo.Login == "" && data.Username != "" { + s.log.Debug("Setting user info login from username field", "username", data.Username) + userInfo.Login = data.Username + } + } + } + + if userInfo.Email == "" { + userInfo.Email = s.extractEmail(data) + if userInfo.Email != "" { + s.log.Debug("Set user info email from extracted email", "email", userInfo.Email) + } + } + + if userInfo.Role == "" { + role, err := s.extractRole(data) + if err != nil { + s.log.Error("Failed to extract role", "error", err) + } else if role != "" { + s.log.Debug("Setting user info role from extracted role") + userInfo.Role = role + } + } } if userInfo.Email == "" { + var err error userInfo.Email, err = s.FetchPrivateEmail(client) if err != nil { return nil, err } + s.log.Debug("Setting email from fetched private email", "email", userInfo.Email) } if userInfo.Login == "" { + s.log.Debug("Defaulting to using email for user info login", "email", userInfo.Email) userInfo.Login = userInfo.Email } if !s.IsTeamMember(client) { - return nil, errors.New("User not a member of one of the required teams") + return nil, errors.New("user not a member of one of the required teams") } if !s.IsOrganizationMember(client) { - return nil, errors.New("User not a member of one of the required organizations") + return nil, errors.New("user not a member of one of the required organizations") } s.log.Debug("User info result", "result", userInfo) return userInfo, nil } -func (s *SocialGenericOAuth) fillUserInfo(userInfo *BasicUserInfo, data *UserInfoJson) { - if userInfo.Email == "" { - userInfo.Email = s.extractEmail(data) - } - if userInfo.Role == "" { - role, err := s.extractRole(data) - if err != nil { - s.log.Error("Failed to extract role", "error", err) - } else { - userInfo.Role = role - } - } - if userInfo.Name == "" { - userInfo.Name = s.extractName(data) - } - if userInfo.Login == "" { - userInfo.Login = s.extractLogin(data) - } -} - -func (s *SocialGenericOAuth) extractToken(data *UserInfoJson, token *oauth2.Token) bool { - var err error +func (s *SocialGenericOAuth) extractFromToken(token *oauth2.Token) *UserInfoJson { + s.log.Debug("Extracting user info from OAuth token") idTokenAttribute := "id_token" if s.idTokenAttributeName != "" { @@ -159,50 +193,54 @@ func (s *SocialGenericOAuth) extractToken(data *UserInfoJson, token *oauth2.Toke idToken := token.Extra(idTokenAttribute) if idToken == nil { s.log.Debug("No id_token found", "token", token) - return false + return nil } jwtRegexp := regexp.MustCompile("^([-_a-zA-Z0-9=]+)[.]([-_a-zA-Z0-9=]+)[.]([-_a-zA-Z0-9=]+)$") matched := jwtRegexp.FindStringSubmatch(idToken.(string)) if matched == nil { s.log.Debug("id_token is not in JWT format", "id_token", idToken.(string)) - return false + return nil } - data.rawJSON, err = base64.RawURLEncoding.DecodeString(matched[2]) + rawJSON, err := base64.RawURLEncoding.DecodeString(matched[2]) if err != nil { s.log.Error("Error base64 decoding id_token", "raw_payload", matched[2], "error", err) - return false + return nil } - err = json.Unmarshal(data.rawJSON, data) - if err != nil { + var data UserInfoJson + if err := json.Unmarshal(rawJSON, &data); err != nil { s.log.Error("Error decoding id_token JSON", "raw_json", string(data.rawJSON), "error", err) - data.rawJSON = []byte{} - return false + return nil } + data.rawJSON = rawJSON + data.source = "token" s.log.Debug("Received id_token", "raw_json", string(data.rawJSON), "data", data) - return true + return &data } -func (s *SocialGenericOAuth) extractAPI(data *UserInfoJson, client *http.Client) bool { +func (s *SocialGenericOAuth) extractFromAPI(client *http.Client) *UserInfoJson { + s.log.Debug("Getting user info from API") rawUserInfoResponse, err := HttpGet(client, s.apiUrl) if err != nil { - s.log.Debug("Error getting user info", "url", s.apiUrl, "error", err) - return false - } - data.rawJSON = rawUserInfoResponse.Body - - err = json.Unmarshal(data.rawJSON, data) - if err != nil { - s.log.Error("Error decoding user info response", "raw_json", data.rawJSON, "error", err) - data.rawJSON = []byte{} - return false + s.log.Debug("Error getting user info from API", "url", s.apiUrl, "error", err) + return nil } - s.log.Debug("Received user info response", "raw_json", string(data.rawJSON), "data", data) - return true + rawJSON := rawUserInfoResponse.Body + + var data UserInfoJson + if err := json.Unmarshal(rawJSON, &data); err != nil { + s.log.Error("Error decoding user info response", "raw_json", rawJSON, "error", err) + return nil + } + + data.rawJSON = rawJSON + data.source = "API" + s.log.Debug("Received user info response from API", "raw_json", string(rawJSON), "data", data) + return &data } func (s *SocialGenericOAuth) extractEmail(data *UserInfoJson) string { @@ -247,39 +285,6 @@ func (s *SocialGenericOAuth) extractRole(data *UserInfoJson) (string, error) { return role, nil } -func (s *SocialGenericOAuth) extractLogin(data *UserInfoJson) string { - if data.Login != "" { - return data.Login - } - - if s.loginAttributePath != "" { - login, err := s.searchJSONForAttr(s.loginAttributePath, data.rawJSON) - if err != nil { - s.log.Error("Failed to search JSON for attribute", "error", err) - } else if login != "" { - return login - } - } - - if data.Username != "" { - return data.Username - } - - return "" -} - -func (s *SocialGenericOAuth) extractName(data *UserInfoJson) string { - if data.Name != "" { - return data.Name - } - - if data.DisplayName != "" { - return data.DisplayName - } - - return "" -} - func (s *SocialGenericOAuth) FetchPrivateEmail(client *http.Client) (string, error) { type Record struct { Email string `json:"email"` diff --git a/pkg/login/social/generic_oauth_test.go b/pkg/login/social/generic_oauth_test.go index bdeca63163b..c832f2c9bbe 100644 --- a/pkg/login/social/generic_oauth_test.go +++ b/pkg/login/social/generic_oauth_test.go @@ -6,6 +6,7 @@ import ( "net/http/httptest" "time" + "github.com/inconshreveable/log15" "github.com/stretchr/testify/require" "testing" @@ -18,7 +19,7 @@ func TestSearchJSONForEmail(t *testing.T) { t.Run("Given a generic OAuth provider", func(t *testing.T) { provider := SocialGenericOAuth{ SocialBase: &SocialBase{ - log: log.New("generic_oauth_test"), + log: log.NewWithLevel("generic_oauth_test", log15.LvlDebug), }, } @@ -106,7 +107,7 @@ func TestSearchJSONForRole(t *testing.T) { t.Run("Given a generic OAuth provider", func(t *testing.T) { provider := SocialGenericOAuth{ SocialBase: &SocialBase{ - log: log.New("generic_oauth_test"), + log: log.NewWithLevel("generic_oauth_test", log15.LvlDebug), }, } @@ -169,21 +170,21 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) { t.Run("Given a generic OAuth provider", func(t *testing.T) { provider := SocialGenericOAuth{ SocialBase: &SocialBase{ - log: log.New("generic_oauth_test"), + log: log.NewWithLevel("generic_oauth_test", log15.LvlDebug), }, emailAttributePath: "email", } tests := []struct { Name string - APIURLResponse interface{} + ResponseBody interface{} OAuth2Extra interface{} RoleAttributePath string ExpectedEmail string ExpectedRole string }{ { - Name: "Given a valid id_token, a valid role path, no api response, use id_token", + Name: "Given a valid id_token, a valid role path, no API response, use id_token", OAuth2Extra: map[string]interface{}{ // { "role": "Admin", "email": "john.doe@example.com" } "id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiQWRtaW4iLCJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIn0.9PtHcCaXxZa2HDlASyKIaFGfOKlw2ILQo32xlvhvhRg", @@ -193,7 +194,7 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) { ExpectedRole: "Admin", }, { - Name: "Given a valid id_token, no role path, no api response, use id_token", + Name: "Given a valid id_token, no role path, no API response, use id_token", OAuth2Extra: map[string]interface{}{ // { "email": "john.doe@example.com" } "id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIn0.k5GwPcZvGe2BE_jgwN0ntz0nz4KlYhEd0hRRLApkTJ4", @@ -203,7 +204,7 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) { ExpectedRole: "", }, { - Name: "Given a valid id_token, an invalid role path, no api response, use id_token", + Name: "Given a valid id_token, an invalid role path, no API response, use id_token", OAuth2Extra: map[string]interface{}{ // { "role": "Admin", "email": "john.doe@example.com" } "id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiQWRtaW4iLCJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIn0.9PtHcCaXxZa2HDlASyKIaFGfOKlw2ILQo32xlvhvhRg", @@ -213,8 +214,8 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) { ExpectedRole: "", }, { - Name: "Given no id_token, a valid role path, a valid api response, use api response", - APIURLResponse: map[string]interface{}{ + Name: "Given no id_token, a valid role path, a valid API response, use API response", + ResponseBody: map[string]interface{}{ "role": "Admin", "email": "john.doe@example.com", }, @@ -223,8 +224,8 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) { ExpectedRole: "Admin", }, { - Name: "Given no id_token, no role path, a valid api response, use api response", - APIURLResponse: map[string]interface{}{ + Name: "Given no id_token, no role path, a valid API response, use API response", + ResponseBody: map[string]interface{}{ "email": "john.doe@example.com", }, RoleAttributePath: "", @@ -232,8 +233,8 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) { ExpectedRole: "", }, { - Name: "Given no id_token, a role path, a valid api response without a role, use api response", - APIURLResponse: map[string]interface{}{ + Name: "Given no id_token, a role path, a valid API response without a role, use API response", + ResponseBody: map[string]interface{}{ "email": "john.doe@example.com", }, RoleAttributePath: "role", @@ -241,18 +242,18 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) { ExpectedRole: "", }, { - Name: "Given no id_token, a valid role path, no api response, no data", + Name: "Given no id_token, a valid role path, no API response, no data", RoleAttributePath: "role", ExpectedEmail: "", ExpectedRole: "", }, { - Name: "Given a valid id_token, a valid role path, a valid api response, prefer id_token", + Name: "Given a valid id_token, a valid role path, a valid API response, prefer id_token", OAuth2Extra: map[string]interface{}{ // { "role": "Admin", "email": "john.doe@example.com" } "id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiQWRtaW4iLCJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIn0.9PtHcCaXxZa2HDlASyKIaFGfOKlw2ILQo32xlvhvhRg", }, - APIURLResponse: map[string]interface{}{ + ResponseBody: map[string]interface{}{ "role": "FromResponse", "email": "from_response@example.com", }, @@ -261,12 +262,12 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) { ExpectedRole: "Admin", }, { - Name: "Given a valid id_token, an invalid role path, a valid api response, prefer id_token", + Name: "Given a valid id_token, an invalid role path, a valid API response, prefer id_token", OAuth2Extra: map[string]interface{}{ // { "role": "Admin", "email": "john.doe@example.com" } "id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiQWRtaW4iLCJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIn0.9PtHcCaXxZa2HDlASyKIaFGfOKlw2ILQo32xlvhvhRg", }, - APIURLResponse: map[string]interface{}{ + ResponseBody: map[string]interface{}{ "role": "FromResponse", "email": "from_response@example.com", }, @@ -275,12 +276,12 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) { ExpectedRole: "", }, { - Name: "Given a valid id_token with no email, a valid role path, a valid api response with no role, merge", + Name: "Given a valid id_token with no email, a valid role path, a valid API response with no role, merge", OAuth2Extra: map[string]interface{}{ // { "role": "Admin" } "id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiQWRtaW4ifQ.k5GwPcZvGe2BE_jgwN0ntz0nz4KlYhEd0hRRLApkTJ4", }, - APIURLResponse: map[string]interface{}{ + ResponseBody: map[string]interface{}{ "email": "from_response@example.com", }, RoleAttributePath: "role", @@ -288,12 +289,12 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) { ExpectedRole: "Admin", }, { - Name: "Given a valid id_token with no role, a valid role path, a valid api response with no email, merge", + Name: "Given a valid id_token with no role, a valid role path, a valid API response with no email, merge", OAuth2Extra: map[string]interface{}{ // { "email": "john.doe@example.com" } "id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIn0.k5GwPcZvGe2BE_jgwN0ntz0nz4KlYhEd0hRRLApkTJ4", }, - APIURLResponse: map[string]interface{}{ + ResponseBody: map[string]interface{}{ "role": "FromResponse", }, RoleAttributePath: "role", @@ -305,12 +306,12 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) { for _, test := range tests { provider.roleAttributePath = test.RoleAttributePath t.Run(test.Name, func(t *testing.T) { - response, err := json.Marshal(test.APIURLResponse) + body, err := json.Marshal(test.ResponseBody) require.NoError(t, err) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Header().Set("Content-Type", "application/json") - _, err = w.Write(response) + _, err = w.Write(body) require.NoError(t, err) })) provider.apiUrl = ts.URL @@ -336,20 +337,20 @@ func TestUserInfoSearchesForLogin(t *testing.T) { t.Run("Given a generic OAuth provider", func(t *testing.T) { provider := SocialGenericOAuth{ SocialBase: &SocialBase{ - log: log.New("generic_oauth_test"), + log: log.NewWithLevel("generic_oauth_test", log15.LvlDebug), }, loginAttributePath: "login", } tests := []struct { Name string - APIURLResponse interface{} + ResponseBody interface{} OAuth2Extra interface{} LoginAttributePath string ExpectedLogin string }{ { - Name: "Given a valid id_token, a valid login path, no api response, use id_token", + Name: "Given a valid id_token, a valid login path, no API response, use id_token", OAuth2Extra: map[string]interface{}{ // { "login": "johndoe", "email": "john.doe@example.com" } "id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbiI6ImpvaG5kb2UiLCJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIn0.sg4sRJCNpax_76XMgr277fdxhjjtNSWXKIOFv4_GJN8", @@ -358,7 +359,7 @@ func TestUserInfoSearchesForLogin(t *testing.T) { ExpectedLogin: "johndoe", }, { - Name: "Given a valid id_token, no login path, no api response, use id_token", + Name: "Given a valid id_token, no login path, no API response, use id_token", OAuth2Extra: map[string]interface{}{ // { "login": "johndoe", "email": "john.doe@example.com" } "id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbiI6ImpvaG5kb2UiLCJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIn0.sg4sRJCNpax_76XMgr277fdxhjjtNSWXKIOFv4_GJN8", @@ -367,8 +368,8 @@ func TestUserInfoSearchesForLogin(t *testing.T) { ExpectedLogin: "johndoe", }, { - Name: "Given no id_token, a valid login path, a valid api response, use api response", - APIURLResponse: map[string]interface{}{ + Name: "Given no id_token, a valid login path, a valid API response, use API response", + ResponseBody: map[string]interface{}{ "user_uid": "johndoe", "email": "john.doe@example.com", }, @@ -376,23 +377,23 @@ func TestUserInfoSearchesForLogin(t *testing.T) { ExpectedLogin: "johndoe", }, { - Name: "Given no id_token, no login path, a valid api response, use api response", - APIURLResponse: map[string]interface{}{ + Name: "Given no id_token, no login path, a valid API response, use API response", + ResponseBody: map[string]interface{}{ "login": "johndoe", }, LoginAttributePath: "", ExpectedLogin: "johndoe", }, { - Name: "Given no id_token, a login path, a valid api response without a login, use api response", - APIURLResponse: map[string]interface{}{ + Name: "Given no id_token, a login path, a valid API response without a login, use API response", + ResponseBody: map[string]interface{}{ "username": "john.doe", }, LoginAttributePath: "login", ExpectedLogin: "john.doe", }, { - Name: "Given no id_token, a valid login path, no api response, no data", + Name: "Given no id_token, a valid login path, no API response, no data", LoginAttributePath: "login", ExpectedLogin: "", }, @@ -401,12 +402,13 @@ func TestUserInfoSearchesForLogin(t *testing.T) { for _, test := range tests { provider.loginAttributePath = test.LoginAttributePath t.Run(test.Name, func(t *testing.T) { - response, err := json.Marshal(test.APIURLResponse) + body, err := json.Marshal(test.ResponseBody) require.NoError(t, err) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Header().Set("Content-Type", "application/json") - _, err = w.Write(response) + t.Log("Writing fake API response body", "body", test.ResponseBody) + _, err = w.Write(body) require.NoError(t, err) })) provider.apiUrl = ts.URL