Add a way to set an opaque integer onto a samdb
[samba.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 set_opaque_integer(self, name, value):
235         """Set an integer as an opaque (a flag or other value) value on the database
236         
237         :param name: The name for the opaque value
238         :param value: The integer value
239         """
240         glue.dsdb_set_opaque_integer(self, name, value)
241
242     def setexpiry(self, user, expiry_seconds, noexpiry):
243         """Set the account expiry for a user
244         
245         :param expiry_seconds: expiry time from now in seconds
246         :param noexpiry: if set, then don't expire password
247         """
248         self.transaction_start()
249         try:
250             res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
251                               expression=("(samAccountName=%s)" % user),
252                               attrs=["userAccountControl", "accountExpires"])
253             assert len(res) == 1
254             userAccountControl = int(res[0]["userAccountControl"][0])
255             accountExpires     = int(res[0]["accountExpires"][0])
256             if noexpiry:
257                 userAccountControl = userAccountControl | 0x10000
258                 accountExpires = 0
259             else:
260                 userAccountControl = userAccountControl & ~0x10000
261                 accountExpires = glue.unix2nttime(expiry_seconds + int(time.time()))
262
263             mod = """
264 dn: %s
265 changetype: modify
266 replace: userAccountControl
267 userAccountControl: %u
268 replace: accountExpires
269 accountExpires: %u
270 """ % (res[0].dn, userAccountControl, accountExpires)
271             # now change the database
272             self.modify_ldif(mod)
273         except:
274             self.transaction_cancel()
275             raise
276         self.transaction_commit();
277