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