aucoalesce/id_lookup.go (165 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package aucoalesce
import (
"math"
"os/user"
"strings"
"sync"
"time"
)
const cacheTimeout = time.Minute
var (
userLookup = NewUserCache(cacheTimeout)
groupLookup = NewGroupCache(cacheTimeout)
// noExpiration = time.Unix(math.MaxInt64, 0)
// The above breaks time.Before and time.After due to overflows.
// See https://stackoverflow.com/questions/25065055/what-is-the-maximum-time-time-in-go
//
// Safe alternative:
noExpiration = time.Unix(0, 0).Add(math.MaxInt64 - 1)
)
type stringItem struct {
timeout time.Time
value string
}
func (i *stringItem) isExpired() bool {
return time.Now().After(i.timeout)
}
// EntityCache is a cache of IDs and usernames.
type EntityCache struct {
byID, byName stringCache
}
// NewUserCache returns a new EntityCache to resolve users. EntityCache is thread-safe.
func NewUserCache(expiration time.Duration) *EntityCache {
return &EntityCache{
byID: stringCache{
expiration: expiration,
data: map[string]stringItem{
"0": {timeout: noExpiration, value: "root"},
},
lookupFn: func(s string) string {
user, err := user.LookupId(s)
if err != nil {
return ""
}
return user.Username
},
},
byName: stringCache{
expiration: expiration,
data: map[string]stringItem{
"root": {timeout: noExpiration, value: "0"},
},
lookupFn: func(s string) string {
user, err := user.Lookup(s)
if err != nil {
return ""
}
return user.Uid
},
},
}
}
// LookupID looks up an UID/GID and returns the user/group name associated with it. If
// no name could be found an empty string is returned. The value will be
// cached for a minute.
func (c *EntityCache) LookupID(uid string) string {
return c.byID.lookup(uid)
}
// LookupName looks up an user/group name and returns the ID associated with it. If
// no ID could be found an empty string is returned. The value will be
// cached for a minute. This requires cgo on Linux.
func (c *EntityCache) LookupName(name string) string {
return c.byName.lookup(name)
}
// NewGroupCache returns a new EntityCache to resolve groups. EntityCache is thread-safe.
func NewGroupCache(expiration time.Duration) *EntityCache {
return &EntityCache{
byID: stringCache{
expiration: expiration,
data: map[string]stringItem{
"0": {timeout: noExpiration, value: "root"},
},
lookupFn: func(s string) string {
grp, err := user.LookupGroupId(s)
if err != nil {
return ""
}
return grp.Name
},
},
byName: stringCache{
expiration: expiration,
data: map[string]stringItem{
"root": {timeout: noExpiration, value: "0"},
},
lookupFn: func(s string) string {
grp, err := user.LookupGroup(s)
if err != nil {
return ""
}
return grp.Gid
},
},
}
}
// ResolveIDs translates all uid and gid values to their associated names.
// Prior to Go 1.9 this requires cgo on Linux. UID and GID values are cached
// for 60 seconds from the time they are read.
func ResolveIDs(event *Event) {
ResolveIDsFromCaches(event, userLookup, groupLookup)
}
// ResolveIDsFromCaches translates all uid and gid values to their associated
// names using the provided caches. Prior to Go 1.9 this requires cgo on Linux.
func ResolveIDsFromCaches(event *Event, users, groups *EntityCache) {
// Actor
if v := users.LookupID(event.Summary.Actor.Primary); v != "" {
event.Summary.Actor.Primary = v
}
if v := users.LookupID(event.Summary.Actor.Secondary); v != "" {
event.Summary.Actor.Secondary = v
}
// User
names := map[string]string{}
for key, id := range event.User.IDs {
if strings.HasSuffix(key, "uid") {
if v := users.LookupID(id); v != "" {
names[key] = v
}
} else if strings.HasSuffix(key, "gid") {
if v := groups.LookupID(id); v != "" {
names[key] = v
}
}
}
if len(names) > 0 {
event.User.Names = names
}
// File owner/group
if event.File != nil {
if event.File.UID != "" {
event.File.Owner = users.LookupID(event.File.UID)
}
if event.File.GID != "" {
event.File.Group = groups.LookupID(event.File.GID)
}
}
// ECS User and groups
event.ECS.User.lookup(users)
event.ECS.Group.lookup(groups)
}
// HardcodeUsers is useful for injecting values for testing.
func HardcodeUsers(users ...user.User) {
for _, usr := range users {
userLookup.byID.hardcode(usr.Uid, usr.Username)
userLookup.byName.hardcode(usr.Username, usr.Uid)
}
}
// HardcodeGroups is useful for injecting values for testing.
func HardcodeGroups(groups ...user.Group) {
for _, grp := range groups {
groupLookup.byID.hardcode(grp.Gid, grp.Name)
groupLookup.byName.hardcode(grp.Name, grp.Gid)
}
}
type stringCache struct {
mutex sync.Mutex
expiration time.Duration
data map[string]stringItem
lookupFn func(string) string
}
func (c *stringCache) lookup(key string) string {
if key == "" || key == "unset" {
return ""
}
c.mutex.Lock()
defer c.mutex.Unlock()
if item, found := c.data[key]; found && !item.isExpired() {
return item.value
}
// Cache the result (even on error).
resolved := c.lookupFn(key)
c.data[key] = stringItem{timeout: time.Now().Add(c.expiration), value: resolved}
return resolved
}
func (c *stringCache) hardcode(key, value string) {
c.mutex.Lock()
defer c.mutex.Unlock()
c.data[key] = stringItem{
timeout: noExpiration,
value: value,
}
}