The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/services/oauthserver/oasimpl/token.go

351 lines
12 KiB

package oasimpl
import (
"context"
"errors"
"fmt"
"net/http"
"strconv"
"github.com/ory/fosite"
"github.com/ory/fosite/handler/oauth2"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/oauthserver"
"github.com/grafana/grafana/pkg/services/oauthserver/utils"
"github.com/grafana/grafana/pkg/services/team"
"github.com/grafana/grafana/pkg/services/user"
)
// HandleTokenRequest handles the client's OAuth2 query to obtain an access_token by presenting its authorization
// grant (ex: client_credentials, jwtbearer)
func (s *OAuth2ServiceImpl) HandleTokenRequest(rw http.ResponseWriter, req *http.Request) {
// This context will be passed to all methods.
ctx := req.Context()
// Create an empty session object which will be passed to the request handlers
oauthSession := NewAuthSession()
// This will create an access request object and iterate through the registered TokenEndpointHandlers to validate the request.
accessRequest, err := s.oauthProvider.NewAccessRequest(ctx, req, oauthSession)
if err != nil {
s.writeAccessError(ctx, rw, accessRequest, err)
return
}
client, err := s.GetExternalService(ctx, accessRequest.GetClient().GetID())
if err != nil || client == nil {
s.oauthProvider.WriteAccessError(ctx, rw, accessRequest, &fosite.RFC6749Error{
DescriptionField: "Could not find the requested subject.",
ErrorField: "not_found",
CodeField: http.StatusBadRequest,
})
return
}
oauthSession.JWTClaims.Add("client_id", client.ClientID)
errClientCred := s.handleClientCredentials(ctx, accessRequest, oauthSession, client)
if errClientCred != nil {
s.writeAccessError(ctx, rw, accessRequest, errClientCred)
return
}
errJWTBearer := s.handleJWTBearer(ctx, accessRequest, oauthSession, client)
if errJWTBearer != nil {
s.writeAccessError(ctx, rw, accessRequest, errJWTBearer)
return
}
// All tokens we generate in this service should target Grafana's API.
accessRequest.GrantAudience(s.cfg.AppURL)
// Prepare response, fosite handlers will populate the token.
response, err := s.oauthProvider.NewAccessResponse(ctx, accessRequest)
if err != nil {
s.writeAccessError(ctx, rw, accessRequest, err)
return
}
s.oauthProvider.WriteAccessResponse(ctx, rw, accessRequest, response)
}
// writeAccessError logs the error then uses fosite to write the error back to the user.
func (s *OAuth2ServiceImpl) writeAccessError(ctx context.Context, rw http.ResponseWriter, accessRequest fosite.AccessRequester, err error) {
var fositeErr *fosite.RFC6749Error
if errors.As(err, &fositeErr) {
s.logger.Error("Description", fositeErr.DescriptionField, "hint", fositeErr.HintField, "error", fositeErr.ErrorField)
} else {
s.logger.Error("Error", err)
}
s.oauthProvider.WriteAccessError(ctx, rw, accessRequest, err)
}
// splitOAuthScopes sort scopes that are generic (profile, email, groups, entitlements) from scopes
// that are RBAC actions (used to further restrict the entitlements embedded in the access_token)
func splitOAuthScopes(requestedScopes fosite.Arguments) (map[string]bool, map[string]bool) {
actionsFilter := map[string]bool{}
claimsFilter := map[string]bool{}
for _, scope := range requestedScopes {
switch scope {
case "profile", "email", "groups", "entitlements":
claimsFilter[scope] = true
default:
actionsFilter[scope] = true
}
}
return actionsFilter, claimsFilter
}
// handleJWTBearer populates the "impersonation" access_token generated by fosite to match the rfc9068 specifications (entitlements, groups).
// It ensures that the user can be impersonated, that the generated token audiences only contain Grafana's AppURL (and token endpoint)
// and that entitlements solely contain the user's permissions that the client is allowed to have.
func (s *OAuth2ServiceImpl) handleJWTBearer(ctx context.Context, accessRequest fosite.AccessRequester, oauthSession *oauth2.JWTSession, client *oauthserver.ExternalService) error {
if !accessRequest.GetGrantTypes().ExactOne(string(fosite.GrantTypeJWTBearer)) {
return nil
}
userID, err := utils.ParseUserIDFromSubject(oauthSession.Subject)
if err != nil {
return &fosite.RFC6749Error{
DescriptionField: "Could not find the requested subject.",
ErrorField: "not_found",
CodeField: http.StatusBadRequest,
}
}
// Check audiences list only contains the AppURL and the token endpoint
for _, aud := range accessRequest.GetGrantedAudience() {
if aud != fmt.Sprintf("%voauth2/token", s.cfg.AppURL) && aud != s.cfg.AppURL {
return &fosite.RFC6749Error{
DescriptionField: "Client is not allowed to target this Audience.",
HintField: "The audience must be the AppURL or the token endpoint.",
ErrorField: "invalid_request",
CodeField: http.StatusForbidden,
}
}
}
// If the client was not allowed to impersonate the user we would not have reached this point given allowed scopes would have been empty
// But just in case we check again
ev := ac.EvalPermission(ac.ActionUsersImpersonate, ac.Scope("users", "id", strconv.FormatInt(userID, 10)))
hasAccess, errAccess := s.accessControl.Evaluate(ctx, client.SignedInUser, ev)
if errAccess != nil || !hasAccess {
return &fosite.RFC6749Error{
DescriptionField: "Client is not allowed to impersonate subject.",
ErrorField: "restricted_access",
CodeField: http.StatusForbidden,
}
}
// Populate claims' suject from the session subject
oauthSession.JWTClaims.Subject = oauthSession.Subject
// Get the user
query := user.GetUserByIDQuery{ID: userID}
dbUser, err := s.userService.GetByID(ctx, &query)
if err != nil {
if errors.Is(err, user.ErrUserNotFound) {
return &fosite.RFC6749Error{
DescriptionField: "Could not find the requested subject.",
ErrorField: "not_found",
CodeField: http.StatusBadRequest,
}
}
return &fosite.RFC6749Error{
DescriptionField: "The request subject could not be processed.",
ErrorField: "server_error",
CodeField: http.StatusInternalServerError,
}
}
oauthSession.Username = dbUser.Login
// Split scopes into actions and claims
actionsFilter, claimsFilter := splitOAuthScopes(accessRequest.GetGrantedScopes())
teams := []*team.TeamDTO{}
// Fetch teams if the groups scope is requested or if we need to populate it in the entitlements
if claimsFilter["groups"] ||
(claimsFilter["entitlements"] && (len(actionsFilter) == 0 || actionsFilter["teams:read"])) {
var errGetTeams error
teams, errGetTeams = s.teamService.GetTeamsByUser(ctx, &team.GetTeamsByUserQuery{
OrgID: oauthserver.TmpOrgID,
UserID: dbUser.ID,
// Fetch teams without restriction on permissions
SignedInUser: &user.SignedInUser{
OrgID: oauthserver.TmpOrgID,
Permissions: map[int64]map[string][]string{
oauthserver.TmpOrgID: {
ac.ActionTeamsRead: {ac.ScopeTeamsAll},
},
},
},
})
if errGetTeams != nil {
return &fosite.RFC6749Error{
DescriptionField: "The teams scope could not be processed.",
ErrorField: "server_error",
CodeField: http.StatusInternalServerError,
}
}
}
if claimsFilter["profile"] {
oauthSession.JWTClaims.Add("name", dbUser.Name)
oauthSession.JWTClaims.Add("login", dbUser.Login)
oauthSession.JWTClaims.Add("updated_at", dbUser.Updated.Unix())
}
if claimsFilter["email"] {
oauthSession.JWTClaims.Add("email", dbUser.Email)
}
if claimsFilter["groups"] {
teamNames := make([]string, 0, len(teams))
for _, team := range teams {
teamNames = append(teamNames, team.Name)
}
oauthSession.JWTClaims.Add("groups", teamNames)
}
if claimsFilter["entitlements"] {
// Get the user permissions (apply the actions filter)
permissions, errGetPermission := s.filteredUserPermissions(ctx, userID, actionsFilter)
if errGetPermission != nil {
return errGetPermission
}
// Compute the impersonated permissions (apply the actions filter, replace the scope self with the user id)
impPerms := s.filteredImpersonatePermissions(client.ImpersonatePermissions, userID, teams, actionsFilter)
// Intersect the permissions with the client permissions
intesect := ac.Intersect(permissions, impPerms)
oauthSession.JWTClaims.Add("entitlements", intesect)
}
return nil
}
// filteredUserPermissions gets the user permissions and applies the actions filter
func (s *OAuth2ServiceImpl) filteredUserPermissions(ctx context.Context, userID int64, actionsFilter map[string]bool) ([]ac.Permission, error) {
permissions, err := s.acService.SearchUserPermissions(ctx, oauthserver.TmpOrgID, ac.SearchOptions{UserID: userID})
if err != nil {
return nil, &fosite.RFC6749Error{
DescriptionField: "The permissions scope could not be processed.",
ErrorField: "server_error",
CodeField: http.StatusInternalServerError,
}
}
// Apply the actions filter
if len(actionsFilter) > 0 {
filtered := []ac.Permission{}
for i := range permissions {
if actionsFilter[permissions[i].Action] {
filtered = append(filtered, permissions[i])
}
}
permissions = filtered
}
return permissions, nil
}
// filteredImpersonatePermissions computes the impersonated permissions.
// It applies the actions filter and replaces the "self RBAC scopes" (~ scope templates) by the correct user id/team id.
func (*OAuth2ServiceImpl) filteredImpersonatePermissions(impersonatePermissions []ac.Permission, userID int64, teams []*team.TeamDTO, actionsFilter map[string]bool) []ac.Permission {
// Compute the impersonated permissions
impPerms := impersonatePermissions
// Apply the actions filter
if len(actionsFilter) > 0 {
filtered := []ac.Permission{}
for i := range impPerms {
if actionsFilter[impPerms[i].Action] {
filtered = append(filtered, impPerms[i])
}
}
impPerms = filtered
}
// Replace the scope self with the user id
correctScopes := []ac.Permission{}
for i := range impPerms {
switch impPerms[i].Scope {
case oauthserver.ScopeGlobalUsersSelf:
correctScopes = append(correctScopes, ac.Permission{
Action: impPerms[i].Action,
Scope: ac.Scope("global.users", "id", strconv.FormatInt(userID, 10)),
})
case oauthserver.ScopeUsersSelf:
correctScopes = append(correctScopes, ac.Permission{
Action: impPerms[i].Action,
Scope: ac.Scope("users", "id", strconv.FormatInt(userID, 10)),
})
case oauthserver.ScopeTeamsSelf:
for t := range teams {
correctScopes = append(correctScopes, ac.Permission{
Action: impPerms[i].Action,
Scope: ac.Scope("teams", "id", strconv.FormatInt(teams[t].ID, 10)),
})
}
default:
correctScopes = append(correctScopes, impPerms[i])
}
continue
}
return correctScopes
}
// handleClientCredentials populates the client's access_token generated by fosite to match the rfc9068 specifications (entitlements, groups)
func (s *OAuth2ServiceImpl) handleClientCredentials(ctx context.Context, accessRequest fosite.AccessRequester, oauthSession *oauth2.JWTSession, client *oauthserver.ExternalService) error {
if !accessRequest.GetGrantTypes().ExactOne("client_credentials") {
return nil
}
// Set the subject to the service account associated to the client
oauthSession.JWTClaims.Subject = fmt.Sprintf("user:id:%d", client.ServiceAccountID)
sa := client.SignedInUser
if sa == nil {
return &fosite.RFC6749Error{
DescriptionField: "Could not find the service account of the client",
ErrorField: "not_found",
CodeField: http.StatusNotFound,
}
}
oauthSession.Username = sa.Login
// For client credentials, scopes are not marked as granted by fosite but the request would have been rejected
// already if the client was not allowed to request them
for _, scope := range accessRequest.GetRequestedScopes() {
accessRequest.GrantScope(scope)
}
// Split scopes into actions and claims
actionsFilter, claimsFilter := splitOAuthScopes(accessRequest.GetGrantedScopes())
if claimsFilter["profile"] {
oauthSession.JWTClaims.Add("name", sa.Name)
oauthSession.JWTClaims.Add("login", sa.Login)
}
if claimsFilter["email"] {
s.logger.Debug("Service accounts have no emails")
}
if claimsFilter["groups"] {
s.logger.Debug("Service accounts have no groups")
}
if claimsFilter["entitlements"] {
s.logger.Debug("Processing client entitlements")
if sa.Permissions != nil && sa.Permissions[oauthserver.TmpOrgID] != nil {
perms := sa.Permissions[oauthserver.TmpOrgID]
if len(actionsFilter) > 0 {
filtered := map[string][]string{}
for action := range actionsFilter {
if _, ok := perms[action]; ok {
filtered[action] = perms[action]
}
}
perms = filtered
}
oauthSession.JWTClaims.Add("entitlements", perms)
} else {
s.logger.Debug("Client has no permissions")
}
}
return nil
}