Skip to content

Commit 1684bc6

Browse files
authored
Merge pull request #1053 from Zibbp/oauth-nil-user-panic
fix: prevent oauth nil user panic
2 parents cc44649 + 858bb9c commit 1684bc6

File tree

3 files changed

+80
-34
lines changed

3 files changed

+80
-34
lines changed

internal/auth/auth.go

Lines changed: 43 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -179,51 +179,60 @@ func (s *Service) OAuthUserCheck(ctx context.Context, userClaims OIDCCLaims) (*e
179179

180180
log.Debug().Msgf("OAuth user not found, creating user: %v", userClaims.PreferredUsername)
181181

182-
// Determine role from groups
183-
role := utils.UserRole
184-
for _, group := range userClaims.Groups {
185-
if strings.HasPrefix(group, "ganymede-") {
186-
groupRole := strings.TrimPrefix(group, "ganymede-")
187-
if utils.IsValidRole(groupRole) {
188-
log.Debug().Msgf("Found Ganymede role in user group %v", group)
189-
role = utils.Role(groupRole)
190-
break
191-
}
192-
}
182+
// Determine role from groups for newly created users.
183+
// If no mapped role is present, default to user.
184+
role, ok := roleFromGroups(userClaims.Groups)
185+
if !ok {
186+
role = utils.UserRole
193187
}
194188

195-
// Create new user
196-
if _, err := s.Store.Client.User.Create().
189+
createdUser, err := s.Store.Client.User.Create().
197190
SetSub(userClaims.Sub).
198191
SetUsername(userClaims.PreferredUsername).
199192
SetRole(role).
200193
SetOauth(true).
201-
Save(ctx); err != nil {
194+
Save(ctx)
195+
if err != nil {
202196
return nil, fmt.Errorf("failed to create user: %w", err)
203197
}
204-
return user, nil
205-
}
206-
207-
// Determine role from groups
208-
newRole := utils.UserRole
209-
for _, group := range userClaims.Groups {
210-
if strings.HasPrefix(group, "ganymede-") {
211-
groupRole := strings.TrimPrefix(group, "ganymede-")
212-
if utils.IsValidRole(groupRole) {
213-
log.Debug().Msgf("Found Ganymede role in user group %v", group)
214-
newRole = utils.Role(groupRole)
215-
break
216-
}
217-
}
198+
return createdUser, nil
218199
}
219200

220-
// Update existing user
221-
if _, err := s.Store.Client.User.UpdateOne(user).
222-
SetUsername(userClaims.PreferredUsername).
223-
SetRole(newRole).
224-
Save(ctx); err != nil {
201+
// Always keep username in sync with OIDC claim.
202+
update := s.Store.Client.User.UpdateOne(user).SetUsername(userClaims.PreferredUsername)
203+
204+
// Only sync role when provider explicitly sends a ganymede-* or ganymede_* role group.
205+
// This prevents manual role changes from being reset on each login.
206+
if newRole, ok := roleFromGroups(userClaims.Groups); ok {
207+
update = update.SetRole(newRole)
208+
}
209+
210+
updatedUser, err := update.Save(ctx)
211+
if err != nil {
225212
return nil, fmt.Errorf("failed to update user: %w", err)
226213
}
227214

228-
return user, nil
215+
return updatedUser, nil
216+
}
217+
218+
func roleFromGroups(groups []string) (utils.Role, bool) {
219+
for _, group := range groups {
220+
var groupRole string
221+
switch {
222+
case strings.HasPrefix(group, "ganymede-"):
223+
groupRole = strings.TrimPrefix(group, "ganymede-")
224+
case strings.HasPrefix(group, "ganymede_"):
225+
groupRole = strings.TrimPrefix(group, "ganymede_")
226+
default:
227+
continue
228+
}
229+
if !utils.IsValidRole(groupRole) {
230+
continue
231+
}
232+
233+
log.Debug().Msgf("Found Ganymede role in user group %v", group)
234+
return utils.Role(groupRole), true
235+
}
236+
237+
return "", false
229238
}

internal/auth/auth_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package auth
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/zibbp/ganymede/internal/utils"
8+
)
9+
10+
func TestRoleFromGroups(t *testing.T) {
11+
t.Run("returns role when valid ganymede group exists", func(t *testing.T) {
12+
role, ok := roleFromGroups([]string{"users", "ganymede-admin"})
13+
assert.True(t, ok)
14+
assert.Equal(t, utils.AdminRole, role)
15+
})
16+
17+
t.Run("returns role when valid ganymede underscore group exists", func(t *testing.T) {
18+
role, ok := roleFromGroups([]string{"users", "ganymede_editor"})
19+
assert.True(t, ok)
20+
assert.Equal(t, utils.EditorRole, role)
21+
})
22+
23+
t.Run("skips invalid role groups", func(t *testing.T) {
24+
role, ok := roleFromGroups([]string{"ganymede-invalid"})
25+
assert.False(t, ok)
26+
assert.Empty(t, role)
27+
})
28+
29+
t.Run("returns false when no ganymede role group exists", func(t *testing.T) {
30+
role, ok := roleFromGroups([]string{"users", "staff"})
31+
assert.False(t, ok)
32+
assert.Empty(t, role)
33+
})
34+
}

internal/transport/http/auth.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,9 @@ func (h *Handler) OAuthCallback(c echo.Context) error {
212212
if err != nil {
213213
return ErrorResponse(c, http.StatusInternalServerError, err.Error())
214214
}
215+
if user == nil {
216+
return ErrorResponse(c, http.StatusInternalServerError, "oauth callback returned nil user")
217+
}
215218

216219
h.SessionManager.Put(c.Request().Context(), "user_id", user.ID.String())
217220

0 commit comments

Comments
 (0)