Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions pkg/api/admin_users.go
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,52 @@ func (hs *HTTPServer) AdminRevokeUserAuthToken(c *contextmodel.ReqContext) respo
return hs.revokeUserAuthTokenInternal(c, userID, cmd)
}

// swagger:route PUT /admin/users/{user_id}/permissions admin_users AdminAddUserOAuth
//
// # User OAuth mapping added
//
// Only works with Basic Authentication (username and password). See introduction for an explanation.
// If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users.permissions:update` and scope `global.users:*`.
//
// Responses:
// 200: okResponse
// 400: badRequestError
// 401: unauthorisedError
// 403: forbiddenError
// 500: internalServerError
func (hs *HTTPServer) AdminAddUserOAuth(c *contextmodel.ReqContext) response.Response {
form := dtos.AdminAddUserOAuthForm{}
if err := web.Bind(c.Req, &form); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
userID, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "id is invalid", err)
}

if authInfo, err := hs.authInfoService.GetAuthInfo(c.Req.Context(), &login.GetAuthInfoQuery{UserId: userID}); err == nil && authInfo != nil {
oauthInfo := hs.SocialService.GetOAuthInfoProvider(authInfo.AuthModule)
if login.IsGrafanaAdminExternallySynced(hs.Cfg, oauthInfo, authInfo.AuthModule) {
return response.Error(http.StatusForbidden, "Cannot change Grafana Admin role for externally synced user", nil)
}
}

err = hs.userService.UpdateAuthModule(c.Req.Context(), &user.UpdateAuthModuleCommand{
UserID: userID,
AuthModule: form.AuthModule,
AuthID: form.AuthID,
})
if err != nil {
if errors.Is(err, user.ErrLastGrafanaAdmin) {
return response.Error(http.StatusBadRequest, user.ErrLastGrafanaAdmin.Error(), nil)
}

return response.Error(http.StatusInternalServerError, "Failed to create user OAuth mapping", err)
}

return response.Success("User OAuth mapping added")
}

// swagger:parameters adminUpdateUserPassword
type AdminUpdateUserPasswordParams struct {
// in:body
Expand Down
3 changes: 3 additions & 0 deletions pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,9 @@ func (hs *HTTPServer) registerRoutes() {
adminUserRoute.Post("/:id/logout", authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersLogout, userIDScope)), routing.Wrap(hs.AdminLogoutUser))
adminUserRoute.Get("/:id/auth-tokens", authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersAuthTokenList, userIDScope)), routing.Wrap(hs.AdminGetUserAuthTokens))
adminUserRoute.Post("/:id/revoke-auth-token", authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersAuthTokenUpdate, userIDScope)), routing.Wrap(hs.AdminRevokeUserAuthToken))

adminUserRoute.Post("/:id/oauth", reqGrafanaAdmin, authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersPermissionsUpdate, userIDScope)), routing.Wrap(hs.AdminAddUserOAuth))

}, reqSignedIn)

// rendering
Expand Down
6 changes: 6 additions & 0 deletions pkg/api/dtos/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,9 @@ type UserLookupDTO struct {
Login string `json:"login"`
AvatarURL string `json:"avatarUrl"`
}

type AdminAddUserOAuthForm struct {
UserID int64 `json:"user_id"`
AuthModule string `json:"auth_module"`
AuthID string `json:"auth_id"`
}
2 changes: 1 addition & 1 deletion pkg/api/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ func (hs *HTTPServer) tryAutoLogin(c *contextmodel.ReqContext) bool {

for providerName, provider := range oauthInfos {
if provider.AutoLogin || hs.Cfg.OAuthAutoLogin {
redirectUrl := hs.Cfg.AppSubURL + "/login/" + providerName + "?autologin=true"
redirectUrl := hs.Cfg.AppSubURL + "/login/" + providerName
if hs.Features.IsEnabledGlobally(featuremgmt.FlagUseSessionStorageForRedirection) {
redirectUrl += hs.getRedirectToForAutoLogin(c)
}
Expand Down
17 changes: 8 additions & 9 deletions pkg/api/login_oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package api

import (
// "github.com/grafana/grafana/pkg/apimachinery/errutil"
"strings"

"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/middleware/cookies"
Expand Down Expand Up @@ -49,14 +48,14 @@ func (hs *HTTPServer) OAuthLogin(reqCtx *contextmodel.ReqContext) {
cookies.WriteCookie(reqCtx.Resp, OauthPKCECookieName, pkce, hs.Cfg.OAuthCookieMaxAge, hs.CookieOptionsFromCfg)
}

autoLogin := reqCtx.Query("autologin")
if autoLogin == "true" {
if strings.Contains(redirect.URL, "?") {
redirect.URL += "&prompt=none"
} else {
redirect.URL += "?prompt=none"
}
}
// autoLogin := reqCtx.Query("autologin")
// if autoLogin == "true" {
// if strings.Contains(redirect.URL, "?") {
// redirect.URL += "&prompt=none"
// } else {
// redirect.URL += "?prompt=none"
// }
// }
reqCtx.Redirect(redirect.URL)
return
}
Expand Down
7 changes: 7 additions & 0 deletions pkg/events/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ type UserUpdated struct {
Email string `json:"email"`
}

type UserAuthUpdated struct {
Timestamp time.Time `json:"timestamp"`
UserID int64 `json:"user_id"`
AuthModule string `json:"auth_module"`
AuthID string `json:"auth_id"`
}

type DataSourceDeleted struct {
Timestamp time.Time `json:"timestamp"`
Name string `json:"name"`
Expand Down
15 changes: 15 additions & 0 deletions pkg/services/user/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ type User struct {
LastSeenAt time.Time
}

type UserAuth struct {
ID int64 `xorm:"pk autoincr 'id'" json:"id"`
UserID int64 `xorm:"user_id" json:"user_id"`
AuthModule string `xorm:"auth_module" json:"auth_module"`
AuthID string `xorm:"auth_id" json:"auth_id"`
Created time.Time `xorm:"created" json:"created"`
}

type CreateUserCommand struct {
UID string
Email string
Expand Down Expand Up @@ -93,6 +101,13 @@ type UpdateUserCommand struct {
HelpFlags1 *HelpFlags1 `json:"-"`
}

type UpdateAuthModuleCommand struct {
UserID int64 `json:"user_id"`
AuthModule string `json:"auth_module"`
AuthID string `json:"auth_id"`
Created time.Time `xorm:"created" json:"created"`
}

type UpdateUserLastSeenAtCommand struct {
UserID int64
OrgID int64
Expand Down
1 change: 1 addition & 0 deletions pkg/services/user/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type Service interface {
GetByLogin(context.Context, *GetUserByLoginQuery) (*User, error)
GetByEmail(context.Context, *GetUserByEmailQuery) (*User, error)
Update(context.Context, *UpdateUserCommand) error
UpdateAuthModule(context.Context, *UpdateAuthModuleCommand) error
UpdateLastSeenAt(context.Context, *UpdateUserLastSeenAtCommand) error
GetSignedInUser(context.Context, *GetSignedInUserQuery) (*SignedInUser, error)
Search(context.Context, *SearchUsersQuery) (*SearchUserQueryResult, error)
Expand Down
38 changes: 38 additions & 0 deletions pkg/services/user/userimpl/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type store interface {
Delete(context.Context, int64) error
LoginConflict(ctx context.Context, login, email string) error
Update(context.Context, *user.UpdateUserCommand) error
UpdateAuthModule(context.Context, *user.UpdateAuthModuleCommand) error
UpdateLastSeenAt(context.Context, *user.UpdateUserLastSeenAtCommand) error
GetSignedInUser(context.Context, *user.GetSignedInUserQuery) (*user.SignedInUser, error)
GetProfile(context.Context, *user.GetUserProfileQuery) (*user.UserProfileDTO, error)
Expand Down Expand Up @@ -281,6 +282,43 @@ func (ss *sqlStore) Update(ctx context.Context, cmd *user.UpdateUserCommand) err
})
}

func (ss *sqlStore) UpdateAuthModule(ctx context.Context, cmd *user.UpdateAuthModuleCommand) error {
// enforcement of lowercase due to forcement of caseinsensitive login
return ss.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
usr := user.UserAuth{
UserID: cmd.UserID,
AuthModule: cmd.AuthModule,
AuthID: cmd.AuthID,
Created: time.Now(),
}

// 기존 레코드 조회 (서비스 계정 필터 제거)
q := sess.ID(cmd.UserID)

rows, err := q.Update(&usr)
if err != nil {
return err
}

// 존재하지 않으면 Insert
if rows == 0 {
if _, err := sess.Insert(&usr); err != nil {
return err
}
}

// 이벤트 publish
sess.PublishAfterCommit(&events.UserAuthUpdated{
Timestamp: usr.Created,
UserID: usr.UserID,
AuthModule: usr.AuthModule,
AuthID: usr.AuthID,
})

return nil
})
}

func (ss *sqlStore) UpdateLastSeenAt(ctx context.Context, cmd *user.UpdateUserLastSeenAtCommand) error {
if cmd.UserID <= 0 {
return user.ErrUpdateInvalidID
Expand Down
16 changes: 16 additions & 0 deletions pkg/services/user/userimpl/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,22 @@ func (s *Service) Update(ctx context.Context, cmd *user.UpdateUserCommand) error
return s.store.Update(ctx, cmd)
}

func (s *Service) UpdateAuthModule(ctx context.Context, cmd *user.UpdateAuthModuleCommand) error {
ctx, span := s.tracer.Start(ctx, "user.UpdateAuthModule", trace.WithAttributes(
attribute.Int64("userID", cmd.UserID),
))
defer span.End()

_, err := s.store.GetByID(ctx, cmd.UserID)
if err != nil {
return err
}

cmd.Created = time.Now().UTC()

return s.store.UpdateAuthModule(ctx, cmd)
}

func (s *Service) UpdateLastSeenAt(ctx context.Context, cmd *user.UpdateUserLastSeenAtCommand) error {
ctx, span := s.tracer.Start(ctx, "user.UpdateLastSeen", trace.WithAttributes(
attribute.Int64("userID", cmd.UserID),
Expand Down
Loading