diff --git a/pkg/api/admin_users.go b/pkg/api/admin_users.go index 456484d54f8ca..638c8b80afa15 100644 --- a/pkg/api/admin_users.go +++ b/pkg/api/admin_users.go @@ -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 diff --git a/pkg/api/api.go b/pkg/api/api.go index e79526134cc5e..326864416cf5f 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -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 diff --git a/pkg/api/dtos/user.go b/pkg/api/dtos/user.go index 35cdb3c78b039..e7409954cca67 100644 --- a/pkg/api/dtos/user.go +++ b/pkg/api/dtos/user.go @@ -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"` +} diff --git a/pkg/api/login.go b/pkg/api/login.go index 991a2c096b508..338ccf5a24e36 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -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) } diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go index 8ad9c04a0764c..2bc3c0509a8d3 100644 --- a/pkg/api/login_oauth.go +++ b/pkg/api/login_oauth.go @@ -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" @@ -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 } diff --git a/pkg/events/events.go b/pkg/events/events.go index 38b6dce4090ac..73b12a691407a 100644 --- a/pkg/events/events.go +++ b/pkg/events/events.go @@ -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"` diff --git a/pkg/services/user/model.go b/pkg/services/user/model.go index fe9d6e75cc7d4..57a3d9638d144 100644 --- a/pkg/services/user/model.go +++ b/pkg/services/user/model.go @@ -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 @@ -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 diff --git a/pkg/services/user/user.go b/pkg/services/user/user.go index 69df82b5b2c3b..78176dba55400 100644 --- a/pkg/services/user/user.go +++ b/pkg/services/user/user.go @@ -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) diff --git a/pkg/services/user/userimpl/store.go b/pkg/services/user/userimpl/store.go index 5cfc775d16d31..8eb383367a697 100644 --- a/pkg/services/user/userimpl/store.go +++ b/pkg/services/user/userimpl/store.go @@ -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) @@ -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 diff --git a/pkg/services/user/userimpl/user.go b/pkg/services/user/userimpl/user.go index e50855b48b5de..050ad73c44b7c 100644 --- a/pkg/services/user/userimpl/user.go +++ b/pkg/services/user/userimpl/user.go @@ -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),