Initial import
[samba] / source / passdb / pdb_sql.c
1 /*
2  * Common PDB SQL backend functions
3  * Copyright (C) Jelmer Vernooij 2003-2004
4  * 
5  * This program is free software; you can redistribute it and/or modify it under
6  * the terms of the GNU General Public License as published by the Free
7  * Software Foundation; either version 2 of the License, or (at your option)
8  * any later version.
9  * 
10  * This program is distributed in the hope that it will be useful, but WITHOUT
11  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
13  * more details.
14  * 
15  * You should have received a copy of the GNU General Public License along with
16  * this program; if not, write to the Free Software Foundation, Inc., 675
17  * Mass Ave, Cambridge, MA 02139, USA.
18  */
19
20 #include "includes.h"
21
22 #define CONFIG_TABLE_DEFAULT                            "user"
23 #define CONFIG_LOGON_TIME_DEFAULT                       "logon_time"
24 #define CONFIG_LOGOFF_TIME_DEFAULT                      "logoff_time"
25 #define CONFIG_KICKOFF_TIME_DEFAULT                     "kickoff_time"
26 #define CONFIG_PASS_LAST_SET_TIME_DEFAULT               "pass_last_set_time"
27 #define CONFIG_PASS_CAN_CHANGE_TIME_DEFAULT             "pass_can_change_time"
28 #define CONFIG_PASS_MUST_CHANGE_TIME_DEFAULT            "pass_must_change_time"
29 #define CONFIG_USERNAME_DEFAULT                         "username"
30 #define CONFIG_DOMAIN_DEFAULT                           "domain"
31 #define CONFIG_NT_USERNAME_DEFAULT                      "nt_username"
32 #define CONFIG_FULLNAME_DEFAULT                         "nt_fullname"
33 #define CONFIG_HOME_DIR_DEFAULT                         "home_dir"
34 #define CONFIG_DIR_DRIVE_DEFAULT                        "dir_drive"
35 #define CONFIG_LOGON_SCRIPT_DEFAULT                     "logon_script"
36 #define CONFIG_PROFILE_PATH_DEFAULT                     "profile_path"
37 #define CONFIG_ACCT_DESC_DEFAULT                        "acct_desc"
38 #define CONFIG_WORKSTATIONS_DEFAULT                     "workstations"
39 #define CONFIG_UNKNOWN_STR_DEFAULT                      "unknown_str"
40 #define CONFIG_MUNGED_DIAL_DEFAULT                      "munged_dial"
41 #define CONFIG_USER_SID_DEFAULT                         "user_sid"
42 #define CONFIG_GROUP_SID_DEFAULT                        "group_sid"
43 #define CONFIG_LM_PW_DEFAULT                            "lm_pw"
44 #define CONFIG_NT_PW_DEFAULT                            "nt_pw"
45 #define CONFIG_PLAIN_PW_DEFAULT                         "NULL"
46 #define CONFIG_ACCT_CTRL_DEFAULT                        "acct_ctrl"
47 #define CONFIG_LOGON_DIVS_DEFAULT                       "logon_divs"
48 #define CONFIG_HOURS_LEN_DEFAULT                        "hours_len"
49 #define CONFIG_BAD_PASSWORD_COUNT_DEFAULT               "bad_password_count"
50 #define CONFIG_LOGON_COUNT_DEFAULT                      "logon_count"
51 #define CONFIG_UNKNOWN_6_DEFAULT                        "unknown_6"
52 #define CONFIG_LOGON_HOURS                              "logon_hours"
53
54 /* Used to construct insert and update queries */
55
56 typedef struct pdb_sql_query {
57         char update;
58         char *part1;
59         char *part2;
60 } pdb_sql_query;
61
62 static void pdb_sql_int_field(struct pdb_sql_query *q, const char *name, int value)
63 {
64         if (!name || strchr(name, '\''))
65                 return;                 /* This field shouldn't be set by us */
66
67         if (q->update) {
68                 q->part1 =
69                         talloc_asprintf_append(q->part1,
70                                                                    "%s = %d,", name, value);
71         } else {
72                 q->part1 =
73                         talloc_asprintf_append(q->part1, "%s,", name);
74                 q->part2 =
75                         talloc_asprintf_append(q->part2, "%d,", value);
76         }
77 }
78
79 char *sql_escape_string(TALLOC_CTX *mem_ctx, const char *unesc)
80 {
81         char *esc = talloc_array(mem_ctx, char, strlen(unesc) * 2 + 3);
82         size_t pos_unesc = 0, pos_esc = 0;
83
84         for(pos_unesc = 0; unesc[pos_unesc]; pos_unesc++) {
85                 switch(unesc[pos_unesc]) {
86                 case '\\':
87                 case '\'':
88                 case '"':
89                         esc[pos_esc] = '\\'; pos_esc++;
90                 default:
91                         esc[pos_esc] = unesc[pos_unesc]; pos_esc++;
92                         break;
93                 }
94         }
95
96         esc[pos_esc] = '\0';
97         
98         return esc;
99 }
100
101 static NTSTATUS pdb_sql_string_field(struct pdb_sql_query *q,
102                                            const char *name, const char *value)
103 {
104         char *esc_value;
105
106         if (!name || !value || !strcmp(value, "") || strchr(name, '\''))
107                 return NT_STATUS_INVALID_PARAMETER;   /* This field shouldn't be set by module */
108
109         esc_value = sql_escape_string(q, value);
110
111         if (q->update) {
112                 q->part1 =
113                         talloc_asprintf_append(q->part1,
114                                                                    "%s = '%s',", name, esc_value);
115         } else {
116                 q->part1 =
117                         talloc_asprintf_append(q->part1, "%s,", name);
118                 q->part2 =
119                         talloc_asprintf_append(q->part2, "'%s',",
120                                                                    esc_value);
121         }
122
123         talloc_free(esc_value);
124
125         return NT_STATUS_OK;
126 }
127
128 #define config_value(data,name,default_value) \
129         lp_parm_const_string(GLOBAL_SECTION_SNUM, data, name, default_value)
130
131 static const char * config_value_write(const char *location, const char *name, const char *default_value) 
132 {
133         char const *v = NULL;
134         char const *swrite = NULL;
135
136         v = lp_parm_const_string(GLOBAL_SECTION_SNUM, location, name, default_value);
137
138         if (!v)
139                 return NULL;
140
141         swrite = strrchr(v, ':');
142
143         /* Default to the same field as read field */
144         if (!swrite) {
145
146                 /* Updating NULL does not make much sense */
147                 if (!strcmp(v, "NULL")) 
148                         return NULL;
149
150                 return v;
151         }
152
153         swrite++;
154
155         /* If the field is 0 chars long, we shouldn't write to it */
156         if (!strlen(swrite) || !strcmp(swrite, "NULL"))
157                 return NULL;
158
159         /* Otherwise, use the additionally specified */
160         return swrite;
161 }
162
163 static const char * config_value_read(const char *location, const char *name, const char *default_value)
164 {
165         char *v = NULL;
166         char *swrite;
167
168         v = lp_parm_talloc_string(GLOBAL_SECTION_SNUM, location, name, default_value);
169
170         if (!v)
171                 return "NULL";
172
173         swrite = strrchr(v, ':');
174
175         /* If no write is specified, there are no problems */
176         if (!swrite) {
177                 if (strlen(v) == 0)
178                         return "NULL";
179                 return (const char *)v;
180         }
181
182         /* Otherwise, we have to cut the ':write_part' */
183         *swrite = '\0';
184         if (strlen(v) == 0)
185                 return "NULL";
186
187         return (const char *)v;
188 }
189
190 char *sql_account_query_select(TALLOC_CTX *mem_ctx, const char *data, BOOL update, enum sql_search_field field, const char *value)
191 {
192         const char *field_string;
193         char *query;
194
195         switch(field) {
196         case SQL_SEARCH_NONE: 
197                 field_string = "'1'"; 
198                 value = "1"; 
199                 break;
200                 
201         case SQL_SEARCH_USER_SID: 
202                 field_string = config_value_read(data, "user sid column", 
203                                                                                  CONFIG_USER_SID_DEFAULT); 
204                 break;
205                 
206         case SQL_SEARCH_USER_NAME: 
207                 field_string = config_value_read(data, "username column", 
208                                                                                  CONFIG_USERNAME_DEFAULT);
209                 break;
210         default:
211                 field_string = "unknown";
212                 break;
213         }
214
215         query = talloc_asprintf(mem_ctx,
216                          "SELECT %s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s FROM %s WHERE %s = '%s'",
217                          config_value_read(data, "logon time column",
218                                                            CONFIG_LOGON_TIME_DEFAULT),
219                          config_value_read(data, "logoff time column",
220                                                            CONFIG_LOGOFF_TIME_DEFAULT),
221                          config_value_read(data, "kickoff time column",
222                                                            CONFIG_KICKOFF_TIME_DEFAULT),
223                          config_value_read(data, "pass last set time column",
224                                                            CONFIG_PASS_LAST_SET_TIME_DEFAULT),
225                          config_value_read(data, "pass can change time column",
226                                                            CONFIG_PASS_CAN_CHANGE_TIME_DEFAULT),
227                          config_value_read(data, "pass must change time column",
228                                                            CONFIG_PASS_MUST_CHANGE_TIME_DEFAULT),
229                          config_value_read(data, "username column",
230                                                            CONFIG_USERNAME_DEFAULT),
231                          config_value_read(data, "domain column",
232                                                            CONFIG_DOMAIN_DEFAULT),
233                          config_value_read(data, "nt username column",
234                                                            CONFIG_NT_USERNAME_DEFAULT),
235                          config_value_read(data, "fullname column",
236                                                            CONFIG_FULLNAME_DEFAULT),
237                          config_value_read(data, "home dir column",
238                                                            CONFIG_HOME_DIR_DEFAULT),
239                          config_value_read(data, "dir drive column",
240                                                            CONFIG_DIR_DRIVE_DEFAULT),
241                          config_value_read(data, "logon script column",
242                                                            CONFIG_LOGON_SCRIPT_DEFAULT),
243                          config_value_read(data, "profile path column",
244                                                            CONFIG_PROFILE_PATH_DEFAULT),
245                          config_value_read(data, "acct desc column",
246                                                            CONFIG_ACCT_DESC_DEFAULT),
247                          config_value_read(data, "workstations column",
248                                                            CONFIG_WORKSTATIONS_DEFAULT),
249                          config_value_read(data, "unknown string column",
250                                                            CONFIG_UNKNOWN_STR_DEFAULT),
251                          config_value_read(data, "munged dial column",
252                                                            CONFIG_MUNGED_DIAL_DEFAULT),
253                          config_value_read(data, "user sid column",
254                                                            CONFIG_USER_SID_DEFAULT),
255                          config_value_read(data, "group sid column",
256                                                            CONFIG_GROUP_SID_DEFAULT),
257                          config_value_read(data, "lanman pass column",
258                                                            CONFIG_LM_PW_DEFAULT),
259                          config_value_read(data, "nt pass column",
260                                                            CONFIG_NT_PW_DEFAULT),
261                          config_value_read(data, "plain pass column",
262                                                            CONFIG_PLAIN_PW_DEFAULT),
263                          config_value_read(data, "acct ctrl column",
264                                                            CONFIG_ACCT_CTRL_DEFAULT),
265                          config_value_read(data, "logon divs column",
266                                                            CONFIG_LOGON_DIVS_DEFAULT),
267                          config_value_read(data, "hours len column",
268                                                            CONFIG_HOURS_LEN_DEFAULT),
269                          config_value_read(data, "bad password count column",
270                                                            CONFIG_BAD_PASSWORD_COUNT_DEFAULT),
271                          config_value_read(data, "logon count column",
272                                                            CONFIG_LOGON_COUNT_DEFAULT),
273                          config_value_read(data, "unknown 6 column",
274                                                            CONFIG_UNKNOWN_6_DEFAULT),
275                          config_value_read(data, "logon hours column",
276                                                            CONFIG_LOGON_HOURS),
277                          config_value(data, "table", CONFIG_TABLE_DEFAULT), 
278                          field_string, value
279                                  );
280          return query;
281 }
282
283 char *sql_account_query_delete(TALLOC_CTX *mem_ctx, const char *data, const char *esc) 
284 {
285         char *query;
286         
287         query = talloc_asprintf(mem_ctx, "DELETE FROM %s WHERE %s = '%s'",
288                          config_value(data, "table", CONFIG_TABLE_DEFAULT),
289                          config_value_read(data, "username column",
290                                                            CONFIG_USERNAME_DEFAULT), esc);
291         return query;
292 }
293
294 char *sql_account_query_update(TALLOC_CTX *mem_ctx, const char *location, const SAM_ACCOUNT *newpwd, char isupdate)
295 {
296         char *ret;
297         pstring temp;
298         fstring sid_str;
299         pdb_sql_query *query;
300         int some_field_affected = 0;
301
302         query = talloc(mem_ctx, pdb_sql_query);
303         query->update = isupdate;
304
305         /* I know this is somewhat overkill but only the talloc 
306          * functions have asprint_append and the 'normal' asprintf 
307          * is a GNU extension */
308         query->part2 = talloc_asprintf(query, "%s", "");
309         if (query->update) {
310                 query->part1 =
311                         talloc_asprintf(query, "UPDATE %s SET ",
312                                                         config_value(location, "table",
313                                                                                  CONFIG_TABLE_DEFAULT));
314         } else {
315                 query->part1 =
316                         talloc_asprintf(query, "INSERT INTO %s (",
317                                                         config_value(location, "table",
318                                                                                  CONFIG_TABLE_DEFAULT));
319         }
320
321         if (!isupdate || IS_SAM_CHANGED(newpwd, PDB_ACCTCTRL)) {
322                 some_field_affected = 1;
323                 pdb_sql_int_field(query,
324                                                 config_value_write(location, "acct ctrl column",
325                                                                                    CONFIG_ACCT_CTRL_DEFAULT),
326                                                 pdb_get_acct_ctrl(newpwd));
327         }
328
329         if (!isupdate || IS_SAM_CHANGED(newpwd, PDB_LOGONTIME)) {
330                 some_field_affected = 1;
331                 pdb_sql_int_field(query,
332                                                         config_value_write(location,
333                                                                                            "logon time column",
334                                                                                            CONFIG_LOGON_TIME_DEFAULT),
335                                                         pdb_get_logon_time(newpwd));
336         }
337
338         if (!isupdate || IS_SAM_CHANGED(newpwd, PDB_LOGOFFTIME)) {
339                 some_field_affected = 1;
340                 pdb_sql_int_field(query,
341                                                         config_value_write(location,
342                                                                                            "logoff time column",
343                                                                                            CONFIG_LOGOFF_TIME_DEFAULT),
344                                                         pdb_get_logoff_time(newpwd));
345         }
346
347         if (!isupdate || IS_SAM_CHANGED(newpwd, PDB_KICKOFFTIME)) {
348                 some_field_affected = 1;
349                 pdb_sql_int_field(query,
350                                                         config_value_write(location,
351                                                                                            "kickoff time column",
352                                                                                            CONFIG_KICKOFF_TIME_DEFAULT),
353                                                         pdb_get_kickoff_time(newpwd));
354         }
355
356         if (!isupdate || IS_SAM_CHANGED(newpwd, PDB_CANCHANGETIME)) {
357                 some_field_affected = 1;
358                 pdb_sql_int_field(query,
359                                                         config_value_write(location,
360                                                                                            "pass can change time column",
361                                                                                            CONFIG_PASS_CAN_CHANGE_TIME_DEFAULT),
362                                                         pdb_get_pass_can_change_time(newpwd));
363         }
364
365         if (!isupdate || IS_SAM_CHANGED(newpwd, PDB_MUSTCHANGETIME)) {
366                 some_field_affected = 1;
367                 pdb_sql_int_field(query,
368                                                         config_value_write(location,
369                                                                                            "pass must change time column",
370                                                                                            CONFIG_PASS_MUST_CHANGE_TIME_DEFAULT),
371                                                         pdb_get_pass_must_change_time(newpwd));
372         }
373
374         if (!isupdate || IS_SAM_CHANGED(newpwd, PDB_PASSLASTSET)) {
375                 some_field_affected = 1;
376                 pdb_sql_int_field(query,
377                                                         config_value_write(location,
378                                                                                            "pass last set time column",
379                                                                                            CONFIG_PASS_LAST_SET_TIME_DEFAULT),
380                                                         pdb_get_pass_last_set_time(newpwd));
381         }
382
383         if (!isupdate || IS_SAM_CHANGED(newpwd, PDB_HOURSLEN)) {
384                 some_field_affected = 1;
385                 pdb_sql_int_field(query,
386                                                         config_value_write(location,
387                                                                                            "hours len column",
388                                                                                            CONFIG_HOURS_LEN_DEFAULT),
389                                                         pdb_get_hours_len(newpwd));
390         }
391
392         if (!isupdate || IS_SAM_CHANGED(newpwd, PDB_LOGONDIVS)) {
393                 some_field_affected = 1;
394                 pdb_sql_int_field(query,
395                                                         config_value_write(location,
396                                                                                            "logon divs column",
397                                                                                            CONFIG_LOGON_DIVS_DEFAULT),
398                                                         pdb_get_logon_divs(newpwd));
399         }
400
401         if (!isupdate || IS_SAM_CHANGED(newpwd, PDB_USERSID)) {
402                 some_field_affected = 1;
403                 pdb_sql_string_field(query,
404                                                    config_value_write(location, "user sid column",
405                                                                                           CONFIG_USER_SID_DEFAULT),
406                                                    sid_to_string(sid_str, 
407                                                                                  pdb_get_user_sid(newpwd)));
408         }
409
410         if (!isupdate || IS_SAM_CHANGED(newpwd, PDB_GROUPSID)) {
411                 some_field_affected = 1;
412                 pdb_sql_string_field(query,
413                                                    config_value_write(location, "group sid column",
414                                                                                           CONFIG_GROUP_SID_DEFAULT),
415                                                    sid_to_string(sid_str,
416                                                                                  pdb_get_group_sid(newpwd)));
417         }
418
419         if (!isupdate || IS_SAM_CHANGED(newpwd, PDB_USERNAME)) {
420                 some_field_affected = 1;
421                 pdb_sql_string_field(query,
422                                                    config_value_write(location, "username column",
423                                                                                           CONFIG_USERNAME_DEFAULT),
424                                                    pdb_get_username(newpwd));
425         }
426
427         if (!isupdate || IS_SAM_CHANGED(newpwd, PDB_DOMAIN)) {
428                 some_field_affected = 1;
429                 pdb_sql_string_field(query,
430                                                    config_value_write(location, "domain column",
431                                                                                           CONFIG_DOMAIN_DEFAULT),
432                                                    pdb_get_domain(newpwd));
433         }
434
435         if (!isupdate || IS_SAM_CHANGED(newpwd, PDB_USERNAME)) {
436                 some_field_affected = 1;
437                 pdb_sql_string_field(query,
438                                                    config_value_write(location,
439                                                                                           "nt username column",
440                                                                                           CONFIG_NT_USERNAME_DEFAULT),
441                                                    pdb_get_nt_username(newpwd));
442         }
443
444         if (!isupdate || IS_SAM_CHANGED(newpwd, PDB_FULLNAME)) {
445                 some_field_affected = 1;
446                 pdb_sql_string_field(query,
447                                                    config_value_write(location, "fullname column",
448                                                                                           CONFIG_FULLNAME_DEFAULT),
449                                                    pdb_get_fullname(newpwd));
450         }
451
452         if (!isupdate || IS_SAM_CHANGED(newpwd, PDB_LOGONSCRIPT)) {
453                 some_field_affected = 1;
454                 pdb_sql_string_field(query,
455                                                    config_value_write(location,
456                                                                                           "logon script column",
457                                                                                           CONFIG_LOGON_SCRIPT_DEFAULT),
458                                                    pdb_get_logon_script(newpwd));
459         }
460
461         if (!isupdate || IS_SAM_CHANGED(newpwd, PDB_PROFILE)) {
462                 some_field_affected = 1;
463                 pdb_sql_string_field(query,
464                                                    config_value_write(location,
465                                                                                           "profile path column",
466                                                                                           CONFIG_PROFILE_PATH_DEFAULT),
467                                                    pdb_get_profile_path(newpwd));
468         }
469
470         if (!isupdate || IS_SAM_CHANGED(newpwd, PDB_DRIVE)) {
471                 some_field_affected = 1;
472                 pdb_sql_string_field(query,
473                                                    config_value_write(location, "dir drive column",
474                                                                                           CONFIG_DIR_DRIVE_DEFAULT),
475                                                    pdb_get_dir_drive(newpwd));
476         }
477
478         if (!isupdate || IS_SAM_CHANGED(newpwd, PDB_SMBHOME)) {
479                 some_field_affected = 1;
480                 pdb_sql_string_field(query,
481                                                    config_value_write(location, "home dir column",
482                                                                                           CONFIG_HOME_DIR_DEFAULT),
483                                                    pdb_get_homedir(newpwd));
484         }
485
486         if (!isupdate || IS_SAM_CHANGED(newpwd, PDB_WORKSTATIONS)) {
487                 some_field_affected = 1;
488                 pdb_sql_string_field(query,
489                                                    config_value_write(location,
490                                                                                           "workstations column",
491                                                                                           CONFIG_WORKSTATIONS_DEFAULT),
492                                                    pdb_get_workstations(newpwd));
493         }
494
495         if (!isupdate || IS_SAM_CHANGED(newpwd, PDB_UNKNOWNSTR)) {
496                 some_field_affected = 1;
497                 pdb_sql_string_field(query,
498                                                    config_value_write(location,
499                                                                                           "unknown string column",
500                                                                                           CONFIG_UNKNOWN_STR_DEFAULT),
501                                                    pdb_get_workstations(newpwd));
502         }
503
504         if (!isupdate || IS_SAM_CHANGED(newpwd, PDB_LMPASSWD)) {
505                 some_field_affected = 1;
506                 pdb_sethexpwd(temp, pdb_get_lanman_passwd(newpwd),
507                                           pdb_get_acct_ctrl(newpwd));
508                 pdb_sql_string_field(query,
509                                                    config_value_write(location,
510                                                                                           "lanman pass column",
511                                                                                           CONFIG_LM_PW_DEFAULT), temp);
512         }
513
514         if (!isupdate || IS_SAM_CHANGED(newpwd, PDB_NTPASSWD)) {
515                 some_field_affected = 1;
516                 pdb_sethexpwd(temp, pdb_get_nt_passwd(newpwd),
517                                           pdb_get_acct_ctrl(newpwd));
518                 pdb_sql_string_field(query,
519                                                    config_value_write(location, "nt pass column",
520                                                                                           CONFIG_NT_PW_DEFAULT), temp);
521         }
522
523         if (!isupdate || IS_SAM_CHANGED(newpwd, PDB_HOURS)) {
524                 some_field_affected = 1;
525                 pdb_sql_string_field(query,
526                                                         config_value_write(location,
527                                                                                            "logon hours column",
528                                                                                            CONFIG_LOGON_HOURS),
529                                                         (const char *)pdb_get_hours(newpwd));
530         }
531
532         if (!some_field_affected) {
533                 talloc_free(query);
534                 return NULL;
535         }
536
537         if (query->update) {
538                 query->part1[strlen(query->part1) - 1] = '\0';
539                 query->part1 = talloc_asprintf(
540                         mem_ctx, "%s WHERE %s = '%s'", query->part1,
541                         config_value_read(location,
542                                           "user sid column",
543                                           CONFIG_USER_SID_DEFAULT),
544                         sid_to_string(sid_str, pdb_get_user_sid (newpwd)));
545         } else {
546                 query->part2[strlen(query->part2) - 1] = ')';
547                 query->part1[strlen(query->part1) - 1] = ')';
548                 query->part1 =
549                         talloc_asprintf_append(query->part1,
550                                                                    " VALUES (%s", query->part2);
551         }
552
553         ret = talloc_strdup(mem_ctx, query->part1);
554         talloc_free(query);
555         return ret;
556 }
557
558 BOOL sql_account_config_valid(const char *data)
559 {
560         const char *sid_column, *username_column;
561         
562     sid_column = config_value_read(data, "user sid column", CONFIG_USER_SID_DEFAULT);
563     username_column = config_value_read(data, "username column", CONFIG_USERNAME_DEFAULT);
564         
565     if(!strcmp(sid_column,"NULL") || !strcmp(username_column, "NULL")) {
566         DEBUG(0,("Please specify both a valid 'user sid column' and a valid 'username column' in smb.conf\n"));
567         return False;
568     }
569
570         return True;
571 }