1
2# group.py - group entry lookup routines
3#
4# Copyright (C) 2010-2019 Arthur de Jong
5#
6# This library is free software; you can redistribute it and/or
7# modify it under the terms of the GNU Lesser General Public
8# License as published by the Free Software Foundation; either
9# version 2.1 of the License, or (at your option) any later version.
10#
11# This library is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14# Lesser General Public License for more details.
15#
16# You should have received a copy of the GNU Lesser General Public
17# License along with this library; if not, write to the Free Software
18# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19# 02110-1301 USA
20
21import logging
22
23import ldap
24from ldap.filter import escape_filter_chars
25
26import cache
27import cfg
28import common
29import constants
30import passwd
31import search
32
33
34def clean(lst):
35    if lst:
36        for i in lst:
37            yield i.replace('\0', '')
38
39
40attmap = common.Attributes(
41    cn='cn',
42    userPassword='"*"',
43    gidNumber='gidNumber',
44    memberUid='memberUid',
45    member='member')
46filter = '(objectClass=posixGroup)'
47
48
49class Search(search.LDAPSearch):
50
51    case_sensitive = ('cn', )
52    limit_attributes = ('cn', 'gidNumber')
53
54    def __init__(self, *args, **kwargs):
55        super(Search, self).__init__(*args, **kwargs)
56        if (cfg.nss_getgrent_skipmembers or
57                'memberUid' in self.parameters or
58                'member' in self.parameters):
59            # set up our own attributes that leave out membership attributes
60            self.attributes = list(self.attributes)
61            if attmap['memberUid'] in self.attributes:
62                self.attributes.remove(attmap['memberUid'])
63            if attmap['member'] in self.attributes:
64                self.attributes.remove(attmap['member'])
65
66    def mk_filter(self):
67        # we still need a custom mk_filter because this is an | query
68        if attmap['member'] and 'memberUid' in self.parameters:
69            memberuid = self.parameters['memberUid']
70            entry = passwd.uid2entry(self.conn, memberuid)
71            if entry:
72                return '(&%s(|(%s=%s)(%s=%s)))' % (
73                    self.filter,
74                    attmap['memberUid'], escape_filter_chars(memberuid),
75                    attmap['member'], escape_filter_chars(entry[0]),
76                )
77        if 'gidNumber' in self.parameters:
78            self.parameters['gidNumber'] -= cfg.nss_gid_offset
79        return super(Search, self).mk_filter()
80
81
82class Cache(cache.Cache):
83
84    tables = ('group_cache', 'group_member_cache')
85
86    create_sql = '''
87        CREATE TABLE IF NOT EXISTS `group_cache`
88          ( `cn` TEXT PRIMARY KEY,
89            `userPassword` TEXT,
90            `gidNumber` INTEGER NOT NULL UNIQUE,
91            `mtime` TIMESTAMP NOT NULL );
92        CREATE TABLE IF NOT EXISTS `group_member_cache`
93          ( `group` TEXT NOT NULL,
94            `memberUid` TEXT NOT NULL,
95            FOREIGN KEY(`group`) REFERENCES `group_cache`(`cn`)
96            ON DELETE CASCADE ON UPDATE CASCADE );
97        CREATE INDEX IF NOT EXISTS `group_member_idx` ON `group_member_cache`(`group`);
98    '''
99
100    retrieve_sql = '''
101        SELECT `group_cache`.`cn` AS `cn`, `userPassword`, `gidNumber`,
102               `memberUid`, `mtime`
103        FROM `group_cache`
104        LEFT JOIN `group_member_cache`
105          ON `group_member_cache`.`group` = `group_cache`.`cn`
106    '''
107
108    retrieve_by = dict(
109        memberUid='''
110            `cn` IN (
111                SELECT `a`.`group`
112                FROM `group_member_cache` `a`
113                WHERE `a`.`memberUid` = ?)
114        ''',
115    )
116
117    group_by = (0, )  # cn
118    group_columns = (3, )  # memberUid
119
120
121class GroupRequest(common.Request):
122
123    def write(self, name, passwd, gid, members):
124        self.fp.write_string(name)
125        self.fp.write_string(passwd)
126        self.fp.write_int32(gid)
127        self.fp.write_stringlist(members)
128
129    def get_members(self, attributes, members, subgroups, seen):
130        # add the memberUid values
131        for member in clean(attributes['memberUid']):
132            if common.is_valid_name(member):
133                members.add(member)
134        # translate and add the member values
135        if attmap['member']:
136            for memberdn in clean(attributes['member']):
137                if memberdn in seen:
138                    continue
139                seen.add(memberdn)
140                member = passwd.dn2uid(self.conn, memberdn)
141                if member and common.is_valid_name(member):
142                    members.add(member)
143                elif cfg.nss_nested_groups:
144                    subgroups.append(memberdn)
145
146    def convert(self, dn, attributes, parameters):
147        # get group names and check against requested group name
148        names = attributes['cn']
149        # get group password
150        try:
151            passwd = attributes['userPassword'][0]
152        except IndexError:
153            passwd = None
154        if not passwd or self.calleruid != 0:
155            passwd = '*'
156        # get group id(s)
157        gids = [int(x) + cfg.nss_gid_offset for x in attributes['gidNumber']]
158        # build member list
159        members = set()
160        subgroups = []
161        seen = set([dn])
162        self.get_members(attributes, members, subgroups, seen)
163        # go over subgroups to find more members
164        while subgroups:
165            memberdn = subgroups.pop(0)
166            for dn2, attributes2 in self.search(self.conn, base=memberdn, scope=ldap.SCOPE_BASE):
167                self.get_members(attributes2, members, subgroups, seen)
168        # actually return the results
169        for name in names:
170            if not common.is_valid_name(name):
171                logging.warning('%s: %s: denied by validnames option', dn,
172                                attmap['cn'])
173            else:
174                for gid in gids:
175                    yield (name, passwd, gid, members)
176
177
178class GroupByNameRequest(GroupRequest):
179
180    action = constants.NSLCD_ACTION_GROUP_BYNAME
181
182    def read_parameters(self, fp):
183        name = fp.read_string()
184        common.validate_name(name)
185        return dict(cn=name)
186
187
188class GroupByGidRequest(GroupRequest):
189
190    action = constants.NSLCD_ACTION_GROUP_BYGID
191
192    def read_parameters(self, fp):
193        return dict(gidNumber=fp.read_int32())
194
195
196class GroupByMemberRequest(GroupRequest):
197
198    action = constants.NSLCD_ACTION_GROUP_BYMEMBER
199
200    def read_parameters(self, fp):
201        memberuid = fp.read_string()
202        common.validate_name(memberuid)
203        return dict(memberUid=memberuid)
204
205    def get_results(self, parameters):
206        seen = set()
207        for dn, attributes in self.search(self.conn, parameters=parameters):
208            seen.add(dn)
209            for values in self.convert(dn, attributes, parameters):
210                yield values
211        if cfg.nss_nested_groups and attmap['member']:
212            tocheck = list(seen)
213            # find parent groups
214            while tocheck:
215                group = tocheck.pop(0)
216                for dn, attributes in self.search(self.conn, parameters=dict(member=group)):
217                    if dn not in seen:
218                        seen.add(dn)
219                        tocheck.append(dn)
220                        for result in self.convert(dn, attributes, parameters):
221                            yield result
222
223    def handle_request(self, parameters):
224        # check whether requested user is in nss_initgroups_ignoreusers
225        if parameters['memberUid'] in cfg.nss_initgroups_ignoreusers:
226            # write the final result code to signify empty results
227            self.fp.write_int32(constants.NSLCD_RESULT_END)
228            return
229        return super(GroupByMemberRequest, self).handle_request(parameters)
230
231
232class GroupAllRequest(GroupRequest):
233
234    action = constants.NSLCD_ACTION_GROUP_ALL
235
236    def handle_request(self, parameters):
237        if not cfg.nss_disable_enumeration:
238            return super(GroupAllRequest, self).handle_request(parameters)
239