1package authproxy
2
3import (
4	"context"
5	"encoding/hex"
6	"errors"
7	"fmt"
8	"hash/fnv"
9	"net"
10	"net/mail"
11	"path"
12	"reflect"
13	"strings"
14	"time"
15
16	"github.com/grafana/grafana/pkg/bus"
17	"github.com/grafana/grafana/pkg/infra/log"
18	"github.com/grafana/grafana/pkg/infra/remotecache"
19	"github.com/grafana/grafana/pkg/models"
20	"github.com/grafana/grafana/pkg/services/ldap"
21	"github.com/grafana/grafana/pkg/services/multildap"
22	"github.com/grafana/grafana/pkg/setting"
23	"github.com/grafana/grafana/pkg/util"
24)
25
26const (
27
28	// CachePrefix is a prefix for the cache key
29	CachePrefix = "auth-proxy-sync-ttl:%s"
30)
31
32// getLDAPConfig gets LDAP config
33var getLDAPConfig = ldap.GetConfig
34
35// isLDAPEnabled checks if LDAP is enabled
36var isLDAPEnabled = func(cfg *setting.Cfg) bool {
37	if cfg != nil {
38		return cfg.LDAPEnabled
39	}
40
41	return setting.LDAPEnabled
42}
43
44// newLDAP creates multiple LDAP instance
45var newLDAP = multildap.New
46
47// supportedHeaders states the supported headers configuration fields
48var supportedHeaderFields = []string{"Name", "Email", "Login", "Groups", "Role"}
49
50// AuthProxy struct
51type AuthProxy struct {
52	cfg         *setting.Cfg
53	remoteCache *remotecache.RemoteCache
54	ctx         *models.ReqContext
55	orgID       int64
56	header      string
57}
58
59// Error auth proxy specific error
60type Error struct {
61	Message      string
62	DetailsError error
63}
64
65// newError returns an Error.
66func newError(message string, err error) Error {
67	return Error{
68		Message:      message,
69		DetailsError: err,
70	}
71}
72
73// Error returns the error message.
74func (err Error) Error() string {
75	return err.Message
76}
77
78// Options for the AuthProxy
79type Options struct {
80	RemoteCache *remotecache.RemoteCache
81	Ctx         *models.ReqContext
82	OrgID       int64
83}
84
85// New instance of the AuthProxy.
86func New(cfg *setting.Cfg, options *Options) *AuthProxy {
87	header := options.Ctx.Req.Header.Get(cfg.AuthProxyHeaderName)
88	return &AuthProxy{
89		remoteCache: options.RemoteCache,
90		cfg:         cfg,
91		ctx:         options.Ctx,
92		orgID:       options.OrgID,
93		header:      header,
94	}
95}
96
97// IsEnabled checks if the auth proxy is enabled.
98func (auth *AuthProxy) IsEnabled() bool {
99	// Bail if the setting is not enabled
100	return auth.cfg.AuthProxyEnabled
101}
102
103// HasHeader checks if the we have specified header
104func (auth *AuthProxy) HasHeader() bool {
105	return len(auth.header) != 0
106}
107
108// IsAllowedIP returns whether provided IP is allowed.
109func (auth *AuthProxy) IsAllowedIP() error {
110	ip := auth.ctx.Req.RemoteAddr
111
112	if len(strings.TrimSpace(auth.cfg.AuthProxyWhitelist)) == 0 {
113		return nil
114	}
115
116	proxies := strings.Split(auth.cfg.AuthProxyWhitelist, ",")
117	var proxyObjs []*net.IPNet
118	for _, proxy := range proxies {
119		result, err := coerceProxyAddress(proxy)
120		if err != nil {
121			return newError("could not get the network", err)
122		}
123
124		proxyObjs = append(proxyObjs, result)
125	}
126
127	sourceIP, _, err := net.SplitHostPort(ip)
128	if err != nil {
129		return newError("could not parse address", err)
130	}
131	sourceObj := net.ParseIP(sourceIP)
132
133	for _, proxyObj := range proxyObjs {
134		if proxyObj.Contains(sourceObj) {
135			return nil
136		}
137	}
138
139	return newError("proxy authentication required", fmt.Errorf(
140		"request for user (%s) from %s is not from the authentication proxy", auth.header,
141		sourceIP,
142	))
143}
144
145func HashCacheKey(key string) (string, error) {
146	hasher := fnv.New128a()
147	if _, err := hasher.Write([]byte(key)); err != nil {
148		return "", err
149	}
150	return hex.EncodeToString(hasher.Sum(nil)), nil
151}
152
153// getKey forms a key for the cache based on the headers received as part of the authentication flow.
154// Our configuration supports multiple headers. The main header contains the email or username.
155// And the additional ones that allow us to specify extra attributes: Name, Email, Role, or Groups.
156func (auth *AuthProxy) getKey() (string, error) {
157	key := strings.TrimSpace(auth.header) // start the key with the main header
158
159	auth.headersIterator(func(_, header string) {
160		key = strings.Join([]string{key, header}, "-") // compose the key with any additional headers
161	})
162
163	hashedKey, err := HashCacheKey(key)
164	if err != nil {
165		return "", err
166	}
167	return fmt.Sprintf(CachePrefix, hashedKey), nil
168}
169
170// Login logs in user ID by whatever means possible.
171func (auth *AuthProxy) Login(logger log.Logger, ignoreCache bool) (int64, error) {
172	if !ignoreCache {
173		// Error here means absent cache - we don't need to handle that
174		id, err := auth.GetUserViaCache(logger)
175		if err == nil && id != 0 {
176			return id, nil
177		}
178	}
179
180	if isLDAPEnabled(auth.cfg) {
181		id, err := auth.LoginViaLDAP()
182		if err != nil {
183			if errors.Is(err, ldap.ErrInvalidCredentials) {
184				return 0, newError("proxy authentication required", ldap.ErrInvalidCredentials)
185			}
186			return 0, newError("failed to get the user", err)
187		}
188
189		return id, nil
190	}
191
192	id, err := auth.LoginViaHeader()
193	if err != nil {
194		return 0, newError("failed to log in as user, specified in auth proxy header", err)
195	}
196
197	return id, nil
198}
199
200// GetUserViaCache gets user ID from cache.
201func (auth *AuthProxy) GetUserViaCache(logger log.Logger) (int64, error) {
202	cacheKey, err := auth.getKey()
203	if err != nil {
204		return 0, err
205	}
206	logger.Debug("Getting user ID via auth cache", "cacheKey", cacheKey)
207	userID, err := auth.remoteCache.Get(cacheKey)
208	if err != nil {
209		logger.Debug("Failed getting user ID via auth cache", "error", err)
210		return 0, err
211	}
212
213	logger.Debug("Successfully got user ID via auth cache", "id", userID)
214	return userID.(int64), nil
215}
216
217// RemoveUserFromCache removes user from cache.
218func (auth *AuthProxy) RemoveUserFromCache(logger log.Logger) error {
219	cacheKey, err := auth.getKey()
220	if err != nil {
221		return err
222	}
223	logger.Debug("Removing user from auth cache", "cacheKey", cacheKey)
224	if err := auth.remoteCache.Delete(cacheKey); err != nil {
225		return err
226	}
227
228	logger.Debug("Successfully removed user from auth cache", "cacheKey", cacheKey)
229	return nil
230}
231
232// LoginViaLDAP logs in user via LDAP request
233func (auth *AuthProxy) LoginViaLDAP() (int64, error) {
234	config, err := getLDAPConfig(auth.cfg)
235	if err != nil {
236		return 0, newError("failed to get LDAP config", err)
237	}
238
239	mldap := newLDAP(config.Servers)
240	extUser, _, err := mldap.User(auth.header)
241	if err != nil {
242		return 0, err
243	}
244
245	// Have to sync grafana and LDAP user during log in
246	upsert := &models.UpsertUserCommand{
247		ReqContext:    auth.ctx,
248		SignupAllowed: auth.cfg.LDAPAllowSignup,
249		ExternalUser:  extUser,
250	}
251	if err := bus.Dispatch(upsert); err != nil {
252		return 0, err
253	}
254
255	return upsert.Result.Id, nil
256}
257
258// LoginViaHeader logs in user from the header only
259func (auth *AuthProxy) LoginViaHeader() (int64, error) {
260	extUser := &models.ExternalUserInfo{
261		AuthModule: "authproxy",
262		AuthId:     auth.header,
263	}
264
265	switch auth.cfg.AuthProxyHeaderProperty {
266	case "username":
267		extUser.Login = auth.header
268
269		emailAddr, emailErr := mail.ParseAddress(auth.header) // only set Email if it can be parsed as an email address
270		if emailErr == nil {
271			extUser.Email = emailAddr.Address
272		}
273	case "email":
274		extUser.Email = auth.header
275		extUser.Login = auth.header
276	default:
277		return 0, fmt.Errorf("auth proxy header property invalid")
278	}
279
280	auth.headersIterator(func(field string, header string) {
281		switch field {
282		case "Groups":
283			extUser.Groups = util.SplitString(header)
284		case "Role":
285			// If Role header is specified, we update the user role of the default org
286			if header != "" {
287				rt := models.RoleType(header)
288				if rt.IsValid() {
289					extUser.OrgRoles = map[int64]models.RoleType{}
290					orgID := int64(1)
291					if setting.AutoAssignOrg && setting.AutoAssignOrgId > 0 {
292						orgID = int64(setting.AutoAssignOrgId)
293					}
294					extUser.OrgRoles[orgID] = rt
295				}
296			}
297		default:
298			reflect.ValueOf(extUser).Elem().FieldByName(field).SetString(header)
299		}
300	})
301
302	upsert := &models.UpsertUserCommand{
303		ReqContext:    auth.ctx,
304		SignupAllowed: auth.cfg.AuthProxyAutoSignUp,
305		ExternalUser:  extUser,
306	}
307
308	err := bus.Dispatch(upsert)
309	if err != nil {
310		return 0, err
311	}
312
313	return upsert.Result.Id, nil
314}
315
316// headersIterator iterates over all non-empty supported additional headers
317func (auth *AuthProxy) headersIterator(fn func(field string, header string)) {
318	for _, field := range supportedHeaderFields {
319		h := auth.cfg.AuthProxyHeaders[field]
320		if h == "" {
321			continue
322		}
323
324		if value := auth.ctx.Req.Header.Get(h); value != "" {
325			fn(field, strings.TrimSpace(value))
326		}
327	}
328}
329
330// GetSignedUser gets full signed in user info.
331func (auth *AuthProxy) GetSignedInUser(userID int64) (*models.SignedInUser, error) {
332	query := &models.GetSignedInUserQuery{
333		OrgId:  auth.orgID,
334		UserId: userID,
335	}
336
337	if err := bus.DispatchCtx(context.Background(), query); err != nil {
338		return nil, err
339	}
340
341	return query.Result, nil
342}
343
344// Remember user in cache
345func (auth *AuthProxy) Remember(id int64) error {
346	key, err := auth.getKey()
347	if err != nil {
348		return err
349	}
350
351	// Check if user already in cache
352	userID, err := auth.remoteCache.Get(key)
353	if err == nil && userID != nil {
354		return nil
355	}
356
357	expiration := time.Duration(auth.cfg.AuthProxySyncTTL) * time.Minute
358
359	if err := auth.remoteCache.Set(key, id, expiration); err != nil {
360		return err
361	}
362
363	return nil
364}
365
366// coerceProxyAddress gets network of the presented CIDR notation
367func coerceProxyAddress(proxyAddr string) (*net.IPNet, error) {
368	proxyAddr = strings.TrimSpace(proxyAddr)
369	if !strings.Contains(proxyAddr, "/") {
370		proxyAddr = path.Join(proxyAddr, "32")
371	}
372
373	_, network, err := net.ParseCIDR(proxyAddr)
374	if err != nil {
375		return nil, fmt.Errorf("could not parse the network: %w", err)
376	}
377	return network, nil
378}
379