s4:schema Provide a way to reference a loaded schema between ldbs
[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 = int(res[0]["userAccountControl"][0])
85         if (userAccountControl & 0x2):
86             userAccountControl = userAccountControl & ~0x2 # remove disabled bit
87         if (userAccountControl & 0x20):
88             userAccountControl = userAccountControl & ~0x20 # remove 'no password required' bit
89
90         mod = """
91 dn: %s
92 changetype: modify
93 replace: userAccountControl
94 userAccountControl: %u
95 """ % (user_dn, userAccountControl)
96         self.modify_ldif(mod)
97
98         
99     def force_password_change_at_next_login(self, user_dn):
100         """Force a password change at next login
101         
102         :param user_dn: Dn of the account to force password change on
103         """
104         mod = """
105 dn: %s
106 changetype: modify
107 replace: pwdLastSet
108 pwdLastSet: 0
109 """ % (user_dn)
110         self.modify_ldif(mod)
111
112     def domain_dn(self):
113         # find the DNs for the domain and the domain users group
114         res = self.search("", scope=ldb.SCOPE_BASE, 
115                           expression="(defaultNamingContext=*)", 
116                           attrs=["defaultNamingContext"])
117         assert(len(res) == 1 and res[0]["defaultNamingContext"] is not None)
118         return res[0]["defaultNamingContext"][0]
119
120     def newuser(self, username, unixname, password, force_password_change_at_next_login=False):
121         """add a new user record.
122         
123         :param username: Name of the new user.
124         :param unixname: Name of the unix user to map to.
125         :param password: Password for the new user
126         """
127         # connect to the sam 
128         self.transaction_start()
129         try:
130             domain_dn = self.domain_dn()
131             assert(domain_dn is not None)
132             user_dn = "CN=%s,CN=Users,%s" % (username, domain_dn)
133
134             #
135             #  the new user record. note the reliance on the samdb module to 
136             #  fill in a sid, guid etc
137             #
138             #  now the real work
139             self.add({"dn": user_dn, 
140                 "sAMAccountName": username,
141                 "userPassword": password,
142                 "objectClass": "user"})
143
144             res = self.search(user_dn, scope=ldb.SCOPE_BASE,
145                               expression="objectclass=*",
146                               attrs=["objectSid"])
147             assert len(res) == 1
148             user_sid = self.schema_format_value("objectSid", res[0]["objectSid"][0])
149             
150             try:
151                 idmap = IDmapDB(lp=self.lp)
152
153                 user = pwd.getpwnam(unixname)
154                 # setup ID mapping for this UID
155                 
156                 idmap.setup_name_mapping(user_sid, idmap.TYPE_UID, user[2])
157
158             except KeyError:
159                 pass
160
161             if force_password_change_at_next_login:
162                 self.force_password_change_at_next_login(user_dn)
163
164             #  modify the userAccountControl to remove the disabled bit
165             self.enable_account(user_dn)
166         except:
167             self.transaction_cancel()
168             raise
169         self.transaction_commit()
170
171     def setpassword(self, filter, password, force_password_change_at_next_login=False):
172         """Set a password on a user record
173         
174         :param filter: LDAP filter to find the user (eg samccountname=name)
175         :param password: Password for the user
176         """
177         # connect to the sam 
178         self.transaction_start()
179         try:
180             # find the DNs for the domain
181             res = self.search("", scope=ldb.SCOPE_BASE, 
182                               expression="(defaultNamingContext=*)", 
183                               attrs=["defaultNamingContext"])
184             assert(len(res) == 1 and res[0]["defaultNamingContext"] is not None)
185             domain_dn = res[0]["defaultNamingContext"][0]
186             assert(domain_dn is not None)
187
188             res = self.search(domain_dn, scope=ldb.SCOPE_SUBTREE, 
189                               expression=filter)
190             assert(len(res) == 1)
191             user_dn = res[0].dn
192
193             setpw = """
194 dn: %s
195 changetype: modify
196 replace: userPassword
197 userPassword:: %s
198 """ % (user_dn, base64.b64encode(password))
199
200             self.modify_ldif(setpw)
201
202             if force_password_change_at_next_login:
203                 self.force_password_change_at_next_login(user_dn)
204
205             #  modify the userAccountControl to remove the disabled bit
206             self.enable_account(user_dn)
207         except:
208             self.transaction_cancel()
209             raise
210         self.transaction_commit()
211
212     def set_domain_sid(self, sid):
213         """Change the domain SID used by this SamDB.
214
215         :param sid: The new domain sid to use.
216         """
217         glue.samdb_set_domain_sid(self, sid)
218
219     def set_schema_from_ldif(self, pf, df):
220         glue.dsdb_set_schema_from_ldif(self, pf, df)
221
222     def set_schema_from_ldb(self, ldb):
223         glue.dsdb_set_schema_from_ldb(self, ldb)
224
225     def convert_schema_to_openldap(self, target, mapping):
226         return glue.dsdb_convert_schema_to_openldap(self, target, mapping)
227
228     def set_invocation_id(self, invocation_id):
229         """Set the invocation id for this SamDB handle.
230         
231         :param invocation_id: GUID of the invocation id.
232         """
233         glue.dsdb_set_ntds_invocation_id(self, invocation_id)
234
235     def set_opaque_integer(self, name, value):
236         """Set an integer as an opaque (a flag or other value) value on the database
237         
238         :param name: The name for the opaque value
239         :param value: The integer value
240         """
241         glue.dsdb_set_opaque_integer(self, name, value)
242
243     def setexpiry(self, user, expiry_seconds, noexpiry):
244         """Set the account expiry for a user
245         
246         :param expiry_seconds: expiry time from now in seconds
247         :param noexpiry: if set, then don't expire password
248         """
249         self.transaction_start()
250         try:
251             res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
252                               expression=("(samAccountName=%s)" % user),
253                               attrs=["userAccountControl", "accountExpires"])
254             assert len(res) == 1
255             userAccountControl = int(res[0]["userAccountControl"][0])
256             accountExpires     = int(res[0]["accountExpires"][0])
257             if noexpiry:
258                 userAccountControl = userAccountControl | 0x10000
259                 accountExpires = 0
260             else:
261                 userAccountControl = userAccountControl & ~0x10000
262                 accountExpires = glue.unix2nttime(expiry_seconds + int(time.time()))
263
264             mod = """
265 dn: %s
266 changetype: modify
267 replace: userAccountControl
268 userAccountControl: %u
269 replace: accountExpires
270 accountExpires: %u
271 """ % (res[0].dn, userAccountControl, accountExpires)
272             # now change the database
273             self.modify_ldif(mod)
274         except:
275             self.transaction_cancel()
276             raise
277         self.transaction_commit();
278