Skip to content

Commit bf1cbb9

Browse files
committed
Refactor to use nsscache-go to fill from NetAuth2
1 parent 0d532b2 commit bf1cbb9

File tree

5 files changed

+884
-315
lines changed

5 files changed

+884
-315
lines changed

cachefiller.go

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
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

Comments
 (0)