mirror of https://github.com/grafana/grafana.git
				
				
				
			
		
			
				
	
	
		
			363 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			363 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
| package accesscontrol
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"context"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"net/http"
 | |
| 	"net/url"
 | |
| 	"regexp"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 	"text/template"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/grafana/grafana/pkg/apimachinery/identity"
 | |
| 	"github.com/grafana/grafana/pkg/infra/tracing"
 | |
| 	"github.com/grafana/grafana/pkg/middleware/cookies"
 | |
| 	"github.com/grafana/grafana/pkg/models/usertoken"
 | |
| 	"github.com/grafana/grafana/pkg/services/authn"
 | |
| 	contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
 | |
| 	"github.com/grafana/grafana/pkg/services/org"
 | |
| 	"github.com/grafana/grafana/pkg/setting"
 | |
| 	"github.com/grafana/grafana/pkg/util"
 | |
| 	"github.com/grafana/grafana/pkg/web"
 | |
| )
 | |
| 
 | |
| func Middleware(ac AccessControl) func(Evaluator) web.Handler {
 | |
| 	return func(evaluator Evaluator) web.Handler {
 | |
| 		return func(c *contextmodel.ReqContext) {
 | |
| 			if c.AllowAnonymous {
 | |
| 				forceLogin, _ := strconv.ParseBool(c.Req.URL.Query().Get("forceLogin")) // ignoring error, assuming false for non-true values is ok.
 | |
| 				orgID, err := strconv.ParseInt(c.Req.URL.Query().Get("orgId"), 10, 64)
 | |
| 				if err == nil && orgID > 0 && orgID != c.SignedInUser.GetOrgID() {
 | |
| 					forceLogin = true
 | |
| 				}
 | |
| 
 | |
| 				if !c.IsSignedIn && forceLogin {
 | |
| 					unauthorized(c)
 | |
| 					return
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			if c.LookupTokenErr != nil {
 | |
| 				var revokedErr *usertoken.TokenRevokedError
 | |
| 				if errors.As(c.LookupTokenErr, &revokedErr) {
 | |
| 					tokenRevoked(c, revokedErr)
 | |
| 					return
 | |
| 				}
 | |
| 
 | |
| 				unauthorized(c)
 | |
| 				return
 | |
| 			}
 | |
| 
 | |
| 			authorize(c, ac, c.SignedInUser, evaluator)
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func authorize(c *contextmodel.ReqContext, ac AccessControl, user identity.Requester, evaluator Evaluator) {
 | |
| 	injected, err := evaluator.MutateScopes(c.Req.Context(), scopeInjector(scopeParams{
 | |
| 		OrgID:     user.GetOrgID(),
 | |
| 		URLParams: web.Params(c.Req),
 | |
| 	}))
 | |
| 	if err != nil {
 | |
| 		c.JsonApiErr(http.StatusInternalServerError, "Internal server error", err)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	hasAccess, err := ac.Evaluate(c.Req.Context(), user, injected)
 | |
| 	if !hasAccess || err != nil {
 | |
| 		deny(c, injected, err)
 | |
| 		return
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func deny(c *contextmodel.ReqContext, evaluator Evaluator, err error) {
 | |
| 	id := newID()
 | |
| 	if err != nil {
 | |
| 		c.Logger.Error("Error from access control system", "error", err, "accessErrorID", id)
 | |
| 	} else {
 | |
| 		namespace, identifier := c.SignedInUser.GetNamespacedID()
 | |
| 		c.Logger.Info(
 | |
| 			"Access denied",
 | |
| 			"namespace", namespace,
 | |
| 			"userID", identifier,
 | |
| 			"accessErrorID", id,
 | |
| 			"permissions", evaluator.GoString(),
 | |
| 		)
 | |
| 	}
 | |
| 
 | |
| 	if !c.IsApiRequest() {
 | |
| 		// TODO(emil): I'd like to show a message after this redirect, not sure how that can be done?
 | |
| 		writeRedirectCookie(c)
 | |
| 		c.Redirect(setting.AppSubUrl + "/")
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	message := ""
 | |
| 	if evaluator != nil {
 | |
| 		message = evaluator.String()
 | |
| 	}
 | |
| 
 | |
| 	// If the user triggers an error in the access control system, we
 | |
| 	// don't want the user to be aware of that, so the user gets the
 | |
| 	// same information from the system regardless of if it's an
 | |
| 	// internal server error or access denied.
 | |
| 	c.JSON(http.StatusForbidden, map[string]string{
 | |
| 		"title":         "Access denied", // the component needs to pick this up
 | |
| 		"message":       fmt.Sprintf("You'll need additional permissions to perform this action. Permissions needed: %s", message),
 | |
| 		"accessErrorId": id,
 | |
| 	})
 | |
| }
 | |
| 
 | |
| func unauthorized(c *contextmodel.ReqContext) {
 | |
| 	if c.IsApiRequest() {
 | |
| 		c.WriteErrOrFallback(http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized), c.LookupTokenErr)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	writeRedirectCookie(c)
 | |
| 	if errors.Is(c.LookupTokenErr, authn.ErrTokenNeedsRotation) {
 | |
| 		c.Redirect(setting.AppSubUrl + "/user/auth-tokens/rotate")
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	c.Redirect(setting.AppSubUrl + "/login")
 | |
| }
 | |
| 
 | |
| func tokenRevoked(c *contextmodel.ReqContext, err *usertoken.TokenRevokedError) {
 | |
| 	if c.IsApiRequest() {
 | |
| 		c.JSON(http.StatusUnauthorized, map[string]any{
 | |
| 			"message": "Token revoked",
 | |
| 			"error": map[string]any{
 | |
| 				"id":                    "ERR_TOKEN_REVOKED",
 | |
| 				"maxConcurrentSessions": err.MaxConcurrentSessions,
 | |
| 			},
 | |
| 		})
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	writeRedirectCookie(c)
 | |
| 	c.Redirect(setting.AppSubUrl + "/login")
 | |
| }
 | |
| 
 | |
| func writeRedirectCookie(c *contextmodel.ReqContext) {
 | |
| 	redirectTo := c.Req.RequestURI
 | |
| 	if setting.AppSubUrl != "" && !strings.HasPrefix(redirectTo, setting.AppSubUrl) {
 | |
| 		redirectTo = setting.AppSubUrl + c.Req.RequestURI
 | |
| 	}
 | |
| 
 | |
| 	// remove any forceLogin=true params
 | |
| 	redirectTo = removeForceLoginParams(redirectTo)
 | |
| 
 | |
| 	cookies.WriteCookie(c.Resp, "redirect_to", url.QueryEscape(redirectTo), 0, nil)
 | |
| }
 | |
| 
 | |
| var forceLoginParamsRegexp = regexp.MustCompile(`&?forceLogin=true`)
 | |
| 
 | |
| func removeForceLoginParams(str string) string {
 | |
| 	return forceLoginParamsRegexp.ReplaceAllString(str, "")
 | |
| }
 | |
| 
 | |
| func newID() string {
 | |
| 	// Less ambiguity than alphanumerical.
 | |
| 	numerical := []byte("0123456789")
 | |
| 	id, err := util.GetRandomString(10, numerical...)
 | |
| 	if err != nil {
 | |
| 		// this should not happen, but if it does, a timestamp is as
 | |
| 		// useful as anything.
 | |
| 		id = fmt.Sprintf("%d", time.Now().UnixNano())
 | |
| 	}
 | |
| 	return "ACE" + id
 | |
| }
 | |
| 
 | |
| type OrgIDGetter func(c *contextmodel.ReqContext) (int64, error)
 | |
| 
 | |
| func AuthorizeInOrgMiddleware(ac AccessControl, authnService authn.Service) func(OrgIDGetter, Evaluator) web.Handler {
 | |
| 	return func(getTargetOrg OrgIDGetter, evaluator Evaluator) web.Handler {
 | |
| 		return func(c *contextmodel.ReqContext) {
 | |
| 			targetOrgID, err := getTargetOrg(c)
 | |
| 			if err != nil {
 | |
| 				if errors.Is(err, ErrInvalidRequestBody) {
 | |
| 					c.JSON(http.StatusBadRequest, map[string]string{
 | |
| 						"message": err.Error(),
 | |
| 						"traceID": tracing.TraceIDFromContext(c.Req.Context(), false),
 | |
| 					})
 | |
| 					return
 | |
| 				}
 | |
| 				deny(c, nil, fmt.Errorf("failed to get target org: %w", err))
 | |
| 				return
 | |
| 			}
 | |
| 
 | |
| 			var orgUser identity.Requester = c.SignedInUser
 | |
| 			if targetOrgID != c.SignedInUser.GetOrgID() {
 | |
| 				orgUser, err = authnService.ResolveIdentity(c.Req.Context(), targetOrgID, c.SignedInUser.GetID())
 | |
| 				if err != nil {
 | |
| 					deny(c, nil, fmt.Errorf("failed to authenticate user in target org: %w", err))
 | |
| 					return
 | |
| 				}
 | |
| 			}
 | |
| 			authorize(c, ac, orgUser, evaluator)
 | |
| 
 | |
| 			// guard against nil map
 | |
| 			if c.SignedInUser.Permissions == nil {
 | |
| 				c.SignedInUser.Permissions = make(map[int64]map[string][]string)
 | |
| 			}
 | |
| 			c.SignedInUser.Permissions[orgUser.GetOrgID()] = orgUser.GetPermissions()
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func UseOrgFromContextParams(c *contextmodel.ReqContext) (int64, error) {
 | |
| 	orgID, err := strconv.ParseInt(web.Params(c.Req)[":orgId"], 10, 64)
 | |
| 
 | |
| 	// Special case of macaron handling invalid params
 | |
| 	if err != nil {
 | |
| 		return 0, org.ErrOrgNotFound.Errorf("failed to get organization from context: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	if orgID == 0 {
 | |
| 		return 0, org.ErrOrgNotFound.Errorf("empty org ID")
 | |
| 	}
 | |
| 
 | |
| 	return orgID, nil
 | |
| }
 | |
| 
 | |
| func UseGlobalOrg(c *contextmodel.ReqContext) (int64, error) {
 | |
| 	return GlobalOrgID, nil
 | |
| }
 | |
| 
 | |
| // UseGlobalOrSingleOrg returns the global organization or the current organization in a single organization setup
 | |
| func UseGlobalOrSingleOrg(cfg *setting.Cfg) OrgIDGetter {
 | |
| 	return func(c *contextmodel.ReqContext) (int64, error) {
 | |
| 		if cfg.RBAC.SingleOrganization {
 | |
| 			return c.GetOrgID(), nil
 | |
| 		}
 | |
| 		return GlobalOrgID, nil
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // UseOrgFromRequestData returns the organization from the request data.
 | |
| // If no org is specified, then the org where user is logged in is returned.
 | |
| func UseOrgFromRequestData(c *contextmodel.ReqContext) (int64, error) {
 | |
| 	query, err := getOrgQueryFromRequest(c)
 | |
| 	if err != nil {
 | |
| 		// Special case of macaron handling invalid params
 | |
| 		return NoOrgID, org.ErrOrgNotFound.Errorf("failed to get organization from context: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	if query.OrgId == nil {
 | |
| 		return c.SignedInUser.GetOrgID(), nil
 | |
| 	}
 | |
| 
 | |
| 	return *query.OrgId, nil
 | |
| }
 | |
| 
 | |
| // UseGlobalOrgFromRequestData returns global org if `global` flag is set or the org where user is logged in.
 | |
| // If RBACSingleOrganization is set, the org where user is logged in is returned - this is intended only for cloud workflows, where instances are limited to a single organization.
 | |
| func UseGlobalOrgFromRequestData(cfg *setting.Cfg) OrgIDGetter {
 | |
| 	return func(c *contextmodel.ReqContext) (int64, error) {
 | |
| 		query, err := getOrgQueryFromRequest(c)
 | |
| 		if err != nil {
 | |
| 			if errors.Is(err, ErrInvalidRequestBody) {
 | |
| 				return NoOrgID, err
 | |
| 			}
 | |
| 			// Special case of macaron handling invalid params
 | |
| 			return NoOrgID, org.ErrOrgNotFound.Errorf("failed to get organization from context: %w", err)
 | |
| 		}
 | |
| 
 | |
| 		// We only check permissions in the global organization if we are not running a SingleOrganization setup
 | |
| 		// That allows Organization Admins to modify global roles and make global assignments.
 | |
| 		if query.Global && !cfg.RBAC.SingleOrganization {
 | |
| 			return GlobalOrgID, nil
 | |
| 		}
 | |
| 
 | |
| 		return c.SignedInUser.GetOrgID(), nil
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // UseGlobalOrgFromRequestParams returns global org if `global` flag is set or the org where user is logged in.
 | |
| func UseGlobalOrgFromRequestParams(cfg *setting.Cfg) OrgIDGetter {
 | |
| 	return func(c *contextmodel.ReqContext) (int64, error) {
 | |
| 		// We only check permissions in the global organization if we are not running a SingleOrganization setup
 | |
| 		// That allows Organization Admins to modify global roles and make global assignments, and is intended for use in hosted Grafana.
 | |
| 		if c.QueryBool("global") && !cfg.RBAC.SingleOrganization {
 | |
| 			return GlobalOrgID, nil
 | |
| 		}
 | |
| 
 | |
| 		return c.SignedInUser.GetOrgID(), nil
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func getOrgQueryFromRequest(c *contextmodel.ReqContext) (*QueryWithOrg, error) {
 | |
| 	query := &QueryWithOrg{}
 | |
| 
 | |
| 	req, err := CloneRequest(c.Req)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	if err := web.Bind(req, query); err != nil {
 | |
| 		if err.Error() == "unexpected EOF" {
 | |
| 			return nil, fmt.Errorf("%w: unexpected end of JSON input", ErrInvalidRequestBody)
 | |
| 		}
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	return query, nil
 | |
| }
 | |
| 
 | |
| // CloneRequest creates request copy including request body
 | |
| func CloneRequest(req *http.Request) (*http.Request, error) {
 | |
| 	// Get copy of body to prevent error when reading closed body in request handler
 | |
| 	bodyCopy, err := CopyRequestBody(req)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	reqCopy := req.Clone(req.Context())
 | |
| 	reqCopy.Body = bodyCopy
 | |
| 	return reqCopy, nil
 | |
| }
 | |
| 
 | |
| // CopyRequestBody returns copy of request body and keeps the original one to prevent error when reading closed body
 | |
| func CopyRequestBody(req *http.Request) (io.ReadCloser, error) {
 | |
| 	if req.Body == nil {
 | |
| 		return nil, nil
 | |
| 	}
 | |
| 
 | |
| 	body := req.Body
 | |
| 	var buf bytes.Buffer
 | |
| 	if _, err := buf.ReadFrom(body); err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	if err := body.Close(); err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	req.Body = io.NopCloser(&buf)
 | |
| 	return io.NopCloser(bytes.NewReader(buf.Bytes())), nil
 | |
| }
 | |
| 
 | |
| // scopeParams holds the parameters used to fill in scope templates
 | |
| type scopeParams struct {
 | |
| 	OrgID     int64
 | |
| 	URLParams map[string]string
 | |
| }
 | |
| 
 | |
| // scopeInjector inject request params into the templated scopes. e.g. "settings:" + eval.Parameters(":id")
 | |
| func scopeInjector(params scopeParams) ScopeAttributeMutator {
 | |
| 	return func(_ context.Context, scope string) ([]string, error) {
 | |
| 		tmpl, err := template.New("scope").Parse(scope)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		var buf bytes.Buffer
 | |
| 		if err = tmpl.Execute(&buf, params); err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		return []string{buf.String()}, nil
 | |
| 	}
 | |
| }
 |