|
| 1 | +package main |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "log" |
| 6 | + "strings" |
| 7 | + |
| 8 | + "github.com/MiLk/nsscache-go/cache" |
| 9 | + "github.com/MiLk/nsscache-go/source" |
| 10 | + |
| 11 | + pb "github.com/NetAuth/Protocol" |
| 12 | + "github.com/netauth/netauth/pkg/netauth" |
| 13 | + |
| 14 | + // We need a token cache available, even if no tokens will be |
| 15 | + // issued. |
| 16 | + _ "github.com/netauth/netauth/pkg/netauth/memory" |
| 17 | +) |
| 18 | + |
| 19 | +// A NetAuthCacheFiller satisfies the cache filler interface and uses |
| 20 | +// NetAuth as the data source. |
| 21 | +type NetAuthCacheFiller struct { |
| 22 | + entities map[string]*pb.Entity |
| 23 | + groups map[string]*pb.Group |
| 24 | + members map[string][]string |
| 25 | + pgroups map[string]uint32 |
| 26 | + |
| 27 | + // The MinUID and MinGID specify the numeric lower bound for |
| 28 | + // remote values to be loaded into the system. These values |
| 29 | + // should be set with a decent amount of headroom above the |
| 30 | + // local namespace on the machine. A default of 2000 is |
| 31 | + // recommended for both. |
| 32 | + MinUID int32 |
| 33 | + MinGID int32 |
| 34 | + |
| 35 | + // The DefaultShell is a mix between convenience and security. |
| 36 | + // On a secure system this will be /bin/false or |
| 37 | + // /sbin/nologin, whereas on a convenient system this will be |
| 38 | + // /bin/sh or /bin/bash. This shell will be substituted in if |
| 39 | + // the shell specified for a user isn't present in the list of |
| 40 | + // AllowedShells. |
| 41 | + DefaultShell string |
| 42 | + |
| 43 | + // This is the list of shells that are permitted on a given |
| 44 | + // host. This list should normally be populated with the list |
| 45 | + // from /etc/shells. |
| 46 | + AllowedShells []string |
| 47 | + |
| 48 | + // The DefaultHome is the location for user files to be |
| 49 | + // specified in the passwd map. This location can include the |
| 50 | + // magic token {UID} which will be replaced with the entity ID |
| 51 | + // during templating if no other home directory is specified. |
| 52 | + DefaultHome string |
| 53 | + |
| 54 | + c *netauth.Client |
| 55 | +} |
| 56 | + |
| 57 | +// NewCacheFiller returns an interface that can be used to fill caches |
| 58 | +// using the libnss library. |
| 59 | +func NewCacheFiller(minuid, mingid int32, defshell, defhome string, shells []string) (source.Source, error) { |
| 60 | + x := NetAuthCacheFiller{ |
| 61 | + entities: make(map[string]*pb.Entity), |
| 62 | + groups: make(map[string]*pb.Group), |
| 63 | + members: make(map[string][]string), |
| 64 | + pgroups: make(map[string]uint32), |
| 65 | + |
| 66 | + MinUID: minuid, |
| 67 | + MinGID: mingid, |
| 68 | + |
| 69 | + DefaultShell: defshell, |
| 70 | + AllowedShells: shells, |
| 71 | + |
| 72 | + DefaultHome: defhome, |
| 73 | + } |
| 74 | + |
| 75 | + ctx := context.Background() |
| 76 | + |
| 77 | + c, err := netauth.New() |
| 78 | + if err != nil { |
| 79 | + log.Println("Error during client initialization") |
| 80 | + return nil, err |
| 81 | + } |
| 82 | + c.SetServiceName("nsscache") |
| 83 | + x.c = c |
| 84 | + |
| 85 | + if err := x.findGroups(ctx); err != nil { |
| 86 | + return nil, err |
| 87 | + } |
| 88 | + if err := x.findEntities(ctx); err != nil { |
| 89 | + return nil, err |
| 90 | + } |
| 91 | + if err := x.findMembers(ctx); err != nil { |
| 92 | + return nil, err |
| 93 | + } |
| 94 | + |
| 95 | + return &x, nil |
| 96 | +} |
| 97 | + |
| 98 | +// FillShadowCache fills the shadow cache. Since NetAuth doesn't |
| 99 | +// provide a way to exfiltrate the secret hashes, the shadow cache |
| 100 | +// just gets filled with *'s. |
| 101 | +func (nc *NetAuthCacheFiller) FillShadowCache(c *cache.Cache) error { |
| 102 | + for i := range nc.entities { |
| 103 | + c.Add(&cache.ShadowEntry{Name: nc.entities[i].GetID(), Passwd: "*"}) |
| 104 | + } |
| 105 | + return nil |
| 106 | +} |
| 107 | + |
| 108 | +// FillGroupCache fills in the group cache using information from |
| 109 | +// NetAuth. |
| 110 | +func (nc *NetAuthCacheFiller) FillGroupCache(c *cache.Cache) error { |
| 111 | + for i := range nc.groups { |
| 112 | + c.Add(&cache.GroupEntry{ |
| 113 | + Name: nc.groups[i].GetName(), |
| 114 | + Passwd: "*", |
| 115 | + GID: uint32(nc.groups[i].GetNumber()), |
| 116 | + Mem: nc.members[nc.groups[i].GetName()], |
| 117 | + }) |
| 118 | + } |
| 119 | + return nil |
| 120 | +} |
| 121 | + |
| 122 | +// FillPasswdCache fills in the cache for normal users. This function |
| 123 | +// makes some choices about where home folders are located and what to |
| 124 | +// fill in for the user's shell if the values aren't fully specified. |
| 125 | +func (nc *NetAuthCacheFiller) FillPasswdCache(c *cache.Cache) error { |
| 126 | + for i := range nc.entities { |
| 127 | + c.Add(&cache.PasswdEntry{ |
| 128 | + Name: nc.entities[i].GetID(), |
| 129 | + Passwd: "x", |
| 130 | + UID: uint32(nc.entities[i].GetNumber()), |
| 131 | + GID: nc.pgroups[nc.entities[i].GetMeta().GetPrimaryGroup()], |
| 132 | + Dir: nc.entities[i].GetMeta().GetHome(), |
| 133 | + Shell: nc.entities[i].GetMeta().GetShell(), |
| 134 | + }) |
| 135 | + } |
| 136 | + return nil |
| 137 | +} |
| 138 | + |
| 139 | +// findGroups fetches a list of groups from the server and discards |
| 140 | +// groups with a GID below the specified minimum. The groups are |
| 141 | +// indexed by name targeting both the group struct and the number. |
| 142 | +func (nc *NetAuthCacheFiller) findGroups(ctx context.Context) error { |
| 143 | + grps, err := nc.c.GroupSearch(ctx, "*") |
| 144 | + if err != nil { |
| 145 | + return err |
| 146 | + } |
| 147 | + for i := range grps { |
| 148 | + if grps[i].GetNumber() < nc.MinGID { |
| 149 | + // Group number is too low, continue without |
| 150 | + // this one. |
| 151 | + log.Printf("Ignoring group %s, GID is below cutoff (%d < %d)", grps[i].GetName(), grps[i].GetNumber(), nc.MinGID) |
| 152 | + continue |
| 153 | + } |
| 154 | + nc.groups[grps[i].GetName()] = grps[i] |
| 155 | + nc.pgroups[grps[i].GetName()] = uint32(grps[i].GetNumber()) |
| 156 | + } |
| 157 | + return nil |
| 158 | +} |
| 159 | + |
| 160 | +// findEntities fetches a list of entities from the server and |
| 161 | +// discards entities with a UID below the specicified minimum or with |
| 162 | +// an invalid primary group. Then, the default shell is checked |
| 163 | +// against the shells on the system and optionally replaced with the |
| 164 | +// default. Finally, the home directory is checked and optionally |
| 165 | +// replaced with the default. |
| 166 | +func (nc *NetAuthCacheFiller) findEntities(ctx context.Context) error { |
| 167 | + ents, err := nc.c.EntitySearch(ctx, "*") |
| 168 | + if err != nil { |
| 169 | + return err |
| 170 | + } |
| 171 | + |
| 172 | + for i := range ents { |
| 173 | + if ents[i].GetNumber() < nc.MinUID { |
| 174 | + // The uidNumber was too low, continue without |
| 175 | + // this one. |
| 176 | + log.Printf("Ignoring entity %s, UID is below cutoff (%d < %d)", ents[i].GetID(), ents[i].GetNumber(), nc.MinUID) |
| 177 | + continue |
| 178 | + } |
| 179 | + if _, ok := nc.pgroups[ents[i].GetMeta().GetPrimaryGroup()]; !ok { |
| 180 | + // The primary group was invalid, continue |
| 181 | + // without this one. |
| 182 | + log.Printf("Ignoring entity %s, Primary Group is invalid", ents[i].GetID()) |
| 183 | + continue |
| 184 | + } |
| 185 | + if nc.hasBadShell(ents[i].GetMeta().GetShell()) { |
| 186 | + ents[i].Meta.Shell = &nc.DefaultShell |
| 187 | + } |
| 188 | + if ents[i].GetMeta().GetHome() == "" { |
| 189 | + t := strings.Replace(nc.DefaultHome, "{UID}", ents[i].GetID(), -1) |
| 190 | + ents[i].Meta.Home = &t |
| 191 | + } |
| 192 | + nc.entities[ents[i].GetID()] = ents[i] |
| 193 | + } |
| 194 | + return nil |
| 195 | +} |
| 196 | + |
| 197 | +// findMembers works out from the groups that are valid on the system |
| 198 | +// the effective memberships. This function is quite expensive to |
| 199 | +// call, so if this is causing performance problems in your |
| 200 | +// environment its recommended to have a central point compute the |
| 201 | +// cache files and distribute them securely. |
| 202 | +func (nc *NetAuthCacheFiller) findMembers(ctx context.Context) error { |
| 203 | + tmp := make(map[string]map[string]struct{}) |
| 204 | + for g := range nc.groups { |
| 205 | + tmp[g] = make(map[string]struct{}) |
| 206 | + members, err := nc.c.GroupMembers(ctx, g) |
| 207 | + if err != nil { |
| 208 | + return err |
| 209 | + } |
| 210 | + for i := range members { |
| 211 | + if _, ok := nc.entities[members[i].GetID()]; !ok { |
| 212 | + // This entity has already been |
| 213 | + // discarded for some reason. |
| 214 | + continue |
| 215 | + } |
| 216 | + tmp[g][members[i].GetID()] = struct{}{} |
| 217 | + } |
| 218 | + } |
| 219 | + |
| 220 | + // Add every entity to its primary group. This isn't |
| 221 | + // necessarily required by the specification, but it does |
| 222 | + // clear up a lot of really confusing corner cases, and is |
| 223 | + // generally what people expect. |
| 224 | + for i := range nc.entities { |
| 225 | + tmp[nc.entities[i].GetMeta().GetPrimaryGroup()][nc.entities[i].GetID()] = struct{}{} |
| 226 | + } |
| 227 | + |
| 228 | + for g, mem := range tmp { |
| 229 | + nc.members[g] = make([]string, len(tmp[g])) |
| 230 | + idx := 0 |
| 231 | + for i := range mem { |
| 232 | + nc.members[g][idx] = i |
| 233 | + idx++ |
| 234 | + } |
| 235 | + } |
| 236 | + return nil |
| 237 | +} |
| 238 | + |
| 239 | +// hasBadShell returns true if the provided test shell is not present |
| 240 | +// in the list of AllowedShells for this system. |
| 241 | +func (nc *NetAuthCacheFiller) hasBadShell(s string) bool { |
| 242 | + for i := range nc.AllowedShells { |
| 243 | + if nc.AllowedShells[i] == s { |
| 244 | + return false |
| 245 | + } |
| 246 | + } |
| 247 | + return true |
| 248 | +} |
0 commit comments