bc76cd3c5fdbbf576c590234cb880fa336112af1
[ira/wip.git] / source4 / scripting / python / samba / samdb.py
1 #!/usr/bin/python
2
3 # Unix SMB/CIFS implementation.
4 # Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2008
5 #
6 # Based on the original in EJS:
7 # Copyright (C) Andrew Tridgell <tridge@samba.org> 2005
8 #   
9 # This program is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 3 of the License, or
12 # (at your option) any later version.
13 #   
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 # GNU General Public License for more details.
18 #   
19 # You should have received a copy of the GNU General Public License
20 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
21 #
22
23 """Convenience functions for using the SAM."""
24
25 import samba
26 import glue
27 import ldb
28 from samba.idmap import IDmapDB
29 import pwd
30 import time
31 import base64
32
33 __docformat__ = "restructuredText"
34
35 class SamDB(samba.Ldb):
36     """The SAM database."""
37
38     def __init__(self, url=None, session_info=None, credentials=None, 
39                  modules_dir=None, lp=None, options=None):
40         """Open the Sam Database.
41
42         :param url: URL of the database.
43         """
44         self.lp = lp
45         super(SamDB, self).__init__(session_info=session_info, credentials=credentials,
46                                     modules_dir=modules_dir, lp=lp, options=options)
47         glue.dsdb_set_global_schema(self)
48         if url:
49             self.connect(url)
50         else:
51             self.connect(lp.get("sam database"))
52
53     def connect(self, url):
54         super(SamDB, self).connect(self.lp.private_path(url))
55
56     def add_foreign(self, domaindn, sid, desc):
57         """Add a foreign security principle."""
58         add = """
59 dn: CN=%s,CN=ForeignSecurityPrincipals,%s
60 objectClass: top
61 objectClass: foreignSecurityPrincipal
62 description: %s
63 """ % (sid, domaindn, desc)
64         # deliberately ignore errors from this, as the records may
65         # already exist
66         for msg in self.parse_ldif(add):
67             self.add(msg[1])
68
69     def add_stock_foreign_sids(self):
70         domaindn = self.domain_dn()
71         self.add_foreign(domaindn, "S-1-5-7", "Anonymous")
72         self.add_foreign(domaindn, "S-1-1-0", "World")
73         self.add_foreign(domaindn, "S-1-5-2", "Network")
74         self.add_foreign(domaindn, "S-1-5-18", "System")
75         self.add_foreign(domaindn, "S-1-5-11", "Authenticated Users")
76
77     def enable_account(self, user_dn):
78         """Enable an account.
79         
80         :param user_dn: Dn of the account to enable.
81         """
82         res = self.search(user_dn, ldb.SCOPE_BASE, None, ["userAccountControl"])
83         assert len(res) == 1
84         userAccountControl = res[0]["userAccountControl"][0]
85         userAccountControl = int(userAccountControl)
86         if (userAccountControl & 0x2):
87             userAccountControl = userAccountControl & ~0x2 # remove disabled bit
88         if (userAccountControl & 0x20):
89             userAccountControl = userAccountControl & ~0x20 # remove 'no password required' bit
90
91         mod = """
92 dn: %s
93 changetype: modify
94 replace: userAccountControl
95 userAccountControl: %u
96 """ % (user_dn, userAccountControl)
97         self.modify_ldif(mod)
98
99         
100     def force_password_change_at_next_login(self, user_dn):
101         """Force a password change at next login
102         
103         :param user_dn: Dn of the account to force password change on
104         """
105         mod = """
106 dn: %s
107 changetype: modify
108 replace: pwdLastSet
109 pwdLastSet: 0
110 """ % (user_dn)
111         self.modify_ldif(mod)
112
113     def domain_dn(self):
114         # find the DNs for the domain and the domain users group
115         res = self.search("", scope=ldb.SCOPE_BASE, 
116                           expression="(defaultNamingContext=*)", 
117                           attrs=["defaultNamingContext"])
118         assert(len(res) == 1 and res[0]["defaultNamingContext"] is not None)
119         return res[0]["defaultNamingContext"][0]
120
121     def newuser(self, username, unixname, password, force_password_change_at_next_login=False):
122         """add a new user record.
123         
124         :param username: Name of the new user.
125         :param unixname: Name of the unix user to map to.
126         :param password: Password for the new user
127         """
128         # connect to the sam 
129         self.transaction_start()
130         try:
131             domain_dn = self.domain_dn()
132             assert(domain_dn is not None)
133             user_dn = "CN=%s,CN=Users,%s" % (username, domain_dn)
134
135             #
136             #  the new user record. note the reliance on the samdb module to 
137             #  fill in a sid, guid etc
138             #
139             #  now the real work
140             self.add({"dn": user_dn, 
141                 "sAMAccountName": username,
142                 "userPassword": password,
143                 "objectClass": "user"})
144
145             res = self.search(user_dn, scope=ldb.SCOPE_BASE,
146                               expression="objectclass=*",
147                               attrs=["objectSid"])
148             assert len(res) == 1
149             user_sid = self.schema_format_value("objectSid", res[0]["objectSid"][0])
150             
151             try:
152                 idmap = IDmapDB(lp=self.lp)
153
154                 user = pwd.getpwnam(unixname)
155                 # setup ID mapping for this UID
156                 
157                 idmap.setup_name_mapping(user_sid, idmap.TYPE_UID, user[2])
158
159             except KeyError:
160                 pass
161
162             if force_password_change_at_next_login:
163                 self.force_password_change_at_next_login(user_dn)
164
165             #  modify the userAccountControl to remove the disabled bit
166             self.enable_account(user_dn)
167         except:
168             self.transaction_cancel()
169             raise
170         self.transaction_commit()
171
172     def setpassword(self, filter, password, force_password_change_at_next_login=False):
173         """Set a password on a user record
174         
175         :param filter: LDAP filter to find the user (eg samccountname=name)
176         :param password: Password for the user
177         """
178         # connect to the sam 
179         self.transaction_start()
180         try:
181             # find the DNs for the domain
182             res = self.search("", scope=ldb.SCOPE_BASE, 
183                               expression="(defaultNamingContext=*)", 
184                               attrs=["defaultNamingContext"])
185             assert(len(res) == 1 and res[0]["defaultNamingContext"] is not None)
186             domain_dn = res[0]["defaultNamingContext"][0]
187             assert(domain_dn is not None)
188
189             res = self.search(domain_dn, scope=ldb.SCOPE_SUBTREE, 
190                               expression=filter,
191                               attrs=[])
192             assert(len(res) == 1)
193             user_dn = res[0].dn
194
195             setpw = """
196 dn: %s
197 changetype: modify
198 replace: userPassword
199 userPassword:: %s
200 """ % (user_dn, base64.b64encode(password))
201
202             self.modify_ldif(setpw)
203
204             if force_password_change_at_next_login:
205                 self.force_password_change_at_next_login(user_dn)
206
207             #  modify the userAccountControl to remove the disabled bit
208             self.enable_account(user_dn)
209         except:
210             self.transaction_cancel()
211             raise
212         self.transaction_commit()
213
214     def set_domain_sid(self, sid):
215         """Change the domain SID used by this SamDB.
216
217         :param sid: The new domain sid to use.
218         """
219         glue.samdb_set_domain_sid(self, sid)
220
221     def attach_schema_from_ldif(self, pf, df):
222         glue.dsdb_attach_schema_from_ldif(self, pf, df)
223
224     def convert_schema_to_openldap(self, target, mapping):
225         return glue.dsdb_convert_schema_to_openldap(self, target, mapping)
226
227     def set_invocation_id(self, invocation_id):
228         """Set the invocation id for this SamDB handle.
229         
230         :param invocation_id: GUID of the invocation id.
231         """
232         glue.dsdb_set_ntds_invocation_id(self, invocation_id)
233
234     def setexpiry(self, user, expiry_seconds, noexpiry):
235         """Set the account expiry for a user
236         
237         :param expiry_seconds: expiry time from now in seconds
238         :param noexpiry: if set, then don't expire password
239         """
240         self.transaction_start()
241         try:
242             res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
243                               expression=("(samAccountName=%s)" % user),
244                               attrs=["userAccountControl", "accountExpires"])
245             assert len(res) == 1
246             userAccountControl = int(res[0]["userAccountControl"][0])
247             accountExpires     = int(res[0]["accountExpires"][0])
248             if noexpiry:
249                 userAccountControl = userAccountControl | 0x10000
250                 accountExpires = 0
251             else:
252                 userAccountControl = userAccountControl & ~0x10000
253                 accountExpires = glue.unix2nttime(expiry_seconds + int(time.time()))
254
255             mod = """
256 dn: %s
257 changetype: modify
258 replace: userAccountControl
259 userAccountControl: %u
260 replace: accountExpires
261 accountExpires: %u
262 """ % (res[0].dn, userAccountControl, accountExpires)
263             # now change the database
264             self.modify_ldif(mod)
265         except:
266             self.transaction_cancel()
267             raise
268         self.transaction_commit();
269