r23779: Change from v2 or later to v3 or later.
[samba.git] / source3 / lib / ldb / modules / ldb_map_inbound.c
1 /*
2    ldb database mapping module
3
4    Copyright (C) Jelmer Vernooij 2005
5    Copyright (C) Martin Kuehl <mkhl@samba.org> 2006
6
7    * NOTICE: this module is NOT released under the GNU LGPL license as
8    * other ldb code. This module is release under the GNU GPL v2 or
9    * later license.
10
11    This program is free software; you can redistribute it and/or modify
12    it under the terms of the GNU General Public License as published by
13    the Free Software Foundation; either version 3 of the License, or
14    (at your option) any later version.
15    
16    This program is distributed in the hope that it will be useful,
17    but WITHOUT ANY WARRANTY; without even the implied warranty of
18    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19    GNU General Public License for more details.
20    
21    You should have received a copy of the GNU General Public License
22    along with this program; if not, write to the Free Software
23    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
24 */
25
26 #include "includes.h"
27 #include "ldb/include/includes.h"
28
29 #include "ldb/modules/ldb_map.h"
30 #include "ldb/modules/ldb_map_private.h"
31
32
33 /* Mapping message elements
34  * ======================== */
35
36 /* Map a message element into the remote partition. */
37 static struct ldb_message_element *ldb_msg_el_map_local(struct ldb_module *module, void *mem_ctx, const struct ldb_map_attribute *map, const struct ldb_message_element *old)
38 {
39         struct ldb_message_element *el;
40         int i;
41
42         el = talloc_zero(mem_ctx, struct ldb_message_element);
43         if (el == NULL) {
44                 map_oom(module);
45                 return NULL;
46         }
47
48         el->num_values = old->num_values;
49         el->values = talloc_array(el, struct ldb_val, el->num_values);
50         if (el->values == NULL) {
51                 talloc_free(el);
52                 map_oom(module);
53                 return NULL;
54         }
55
56         el->name = map_attr_map_local(el, map, old->name);
57
58         for (i = 0; i < el->num_values; i++) {
59                 el->values[i] = ldb_val_map_local(module, el->values, map, &old->values[i]);
60         }
61
62         return el;
63 }
64
65 /* Add a message element either to a local or to a remote message,
66  * depending on whether it goes into the local or remote partition. */
67 static int ldb_msg_el_partition(struct ldb_module *module, struct ldb_message *local, struct ldb_message *remote, const struct ldb_message *msg, const char *attr_name, /* const char * const names[], */ const struct ldb_message_element *old)
68 {
69         const struct ldb_map_context *data = map_get_context(module);
70         const struct ldb_map_attribute *map = map_attr_find_local(data, attr_name);
71         struct ldb_message_element *el=NULL;
72
73         /* Unknown attribute: ignore */
74         if (map == NULL) {
75                 ldb_debug(module->ldb, LDB_DEBUG_WARNING, "ldb_map: "
76                           "Not mapping attribute '%s': no mapping found\n",
77                           old->name);
78                 goto local;
79         }
80
81         switch (map->type) {
82         case MAP_IGNORE:
83                 goto local;
84
85         case MAP_CONVERT:
86                 if (map->u.convert.convert_local == NULL) {
87                         ldb_debug(module->ldb, LDB_DEBUG_WARNING, "ldb_map: "
88                                   "Not mapping attribute '%s': "
89                                   "'convert_local' not set\n",
90                                   map->local_name);
91                         goto local;
92                 }
93                 /* fall through */
94         case MAP_KEEP:
95         case MAP_RENAME:
96                 el = ldb_msg_el_map_local(module, remote, map, old);
97                 break;
98
99         case MAP_GENERATE:
100                 if (map->u.generate.generate_remote == NULL) {
101                         ldb_debug(module->ldb, LDB_DEBUG_WARNING, "ldb_map: "
102                                   "Not mapping attribute '%s': "
103                                   "'generate_remote' not set\n",
104                                   map->local_name);
105                         goto local;
106                 }
107
108                 /* TODO: if this attr requires context:
109                  *       make sure all context attrs are mappable (in 'names')
110                  *       make sure all context attrs have already been mapped?
111                  *       maybe postpone generation until they have been mapped?
112                  */
113
114                 map->u.generate.generate_remote(module, map->local_name, msg, remote, local);
115                 return 0;
116         }
117
118         if (el == NULL) {
119                 return -1;
120         }
121
122         return ldb_msg_add(remote, el, old->flags);
123
124 local:
125         el = talloc(local, struct ldb_message_element);
126         if (el == NULL) {
127                 map_oom(module);
128                 return -1;
129         }
130
131         *el = *old;                     /* copy the old element */
132
133         return ldb_msg_add(local, el, old->flags);
134 }
135
136 /* Mapping messages
137  * ================ */
138
139 /* Check whether a message will be (partially) mapped into the remote partition. */
140 static BOOL ldb_msg_check_remote(struct ldb_module *module, const struct ldb_message *msg)
141 {
142         const struct ldb_map_context *data = map_get_context(module);
143         BOOL ret;
144         int i;
145
146         for (i = 0; i < msg->num_elements; i++) {
147                 ret = map_attr_check_remote(data, msg->elements[i].name);
148                 if (ret) {
149                         return ret;
150                 }
151         }
152
153         return False;
154 }
155
156 /* Split message elements that stay in the local partition from those
157  * that are mapped into the remote partition. */
158 static int ldb_msg_partition(struct ldb_module *module, struct ldb_message *local, struct ldb_message *remote, const struct ldb_message *msg)
159 {
160         /* const char * const names[]; */
161         int i, ret;
162
163         for (i = 0; i < msg->num_elements; i++) {
164                 /* Skip 'IS_MAPPED' */
165                 if (ldb_attr_cmp(msg->elements[i].name, IS_MAPPED) == 0) {
166                         ldb_debug(module->ldb, LDB_DEBUG_WARNING, "ldb_map: "
167                                   "Skipping attribute '%s'\n",
168                                   msg->elements[i].name);
169                         continue;
170                 }
171
172                 ret = ldb_msg_el_partition(module, local, remote, msg, msg->elements[i].name, &msg->elements[i]);
173                 if (ret) {
174                         return ret;
175                 }
176         }
177
178         return 0;
179 }
180
181
182 /* Inbound requests: add, modify, rename, delete
183  * ============================================= */
184
185 /* Add the remote record. */
186 int map_add_do_remote(struct ldb_handle *handle)
187 {
188         struct map_context *ac;
189
190         ac = talloc_get_type(handle->private_data, struct map_context);
191
192         ldb_set_timeout_from_prev_req(ac->module->ldb, ac->orig_req, ac->remote_req);
193
194         ac->step = MAP_ADD_REMOTE;
195
196         handle->state = LDB_ASYNC_INIT;
197         handle->status = LDB_SUCCESS;
198
199         return ldb_next_remote_request(ac->module, ac->remote_req);
200 }
201
202 /* Add the local record. */
203 int map_add_do_local(struct ldb_handle *handle)
204 {
205         struct map_context *ac;
206
207         ac = talloc_get_type(handle->private_data, struct map_context);
208
209         ldb_set_timeout_from_prev_req(ac->module->ldb, ac->orig_req, ac->local_req);
210
211         ac->step = MAP_ADD_LOCAL;
212
213         handle->state = LDB_ASYNC_INIT;
214         handle->status = LDB_SUCCESS;
215
216         return ldb_next_request(ac->module, ac->local_req);
217 }
218
219 /* Add a record. */
220 int map_add(struct ldb_module *module, struct ldb_request *req)
221 {
222         const struct ldb_message *msg = req->op.add.message;
223         struct ldb_handle *h;
224         struct map_context *ac;
225         struct ldb_message *local, *remote;
226         const char *dn;
227
228         /* Do not manipulate our control entries */
229         if (ldb_dn_is_special(msg->dn)) {
230                 return ldb_next_request(module, req);
231         }
232
233         /* No mapping requested (perhaps no DN mapping specified), skip to next module */
234         if (!ldb_dn_check_local(module, msg->dn)) {
235                 return ldb_next_request(module, req);
236         }
237
238         /* No mapping needed, fail */
239         if (!ldb_msg_check_remote(module, msg)) {
240                 return LDB_ERR_OPERATIONS_ERROR;
241         }
242
243         /* Prepare context and handle */
244         h = map_init_handle(req, module);
245         if (h == NULL) {
246                 return LDB_ERR_OPERATIONS_ERROR;
247         }
248         ac = talloc_get_type(h->private_data, struct map_context);
249
250         /* Prepare the local operation */
251         ac->local_req = talloc(ac, struct ldb_request);
252         if (ac->local_req == NULL) {
253                 goto oom;
254         }
255
256         *(ac->local_req) = *req;        /* copy the request */
257
258         ac->local_req->context = NULL;
259         ac->local_req->callback = NULL;
260
261         /* Prepare the remote operation */
262         ac->remote_req = talloc(ac, struct ldb_request);
263         if (ac->remote_req == NULL) {
264                 goto oom;
265         }
266
267         *(ac->remote_req) = *req;       /* copy the request */
268
269         ac->remote_req->context = NULL;
270         ac->remote_req->callback = NULL;
271
272         /* Prepare the local message */
273         local = ldb_msg_new(ac->local_req);
274         if (local == NULL) {
275                 goto oom;
276         }
277         local->dn = msg->dn;
278
279         /* Prepare the remote message */
280         remote = ldb_msg_new(ac->remote_req);
281         if (remote == NULL) {
282                 goto oom;
283         }
284         remote->dn = ldb_dn_map_local(ac->module, remote, msg->dn);
285
286         /* Split local from remote message */
287         ldb_msg_partition(module, local, remote, msg);
288         ac->local_req->op.add.message = local;
289         ac->remote_req->op.add.message = remote;
290
291         if ((local->num_elements == 0) || (!map_check_local_db(ac->module))) {
292                 /* No local data or db, just run the remote request */
293                 talloc_free(ac->local_req);
294                 req->handle = h;        /* return our own handle to deal with this call */
295                 return map_add_do_remote(h);
296         }
297
298         /* Store remote DN in 'IS_MAPPED' */
299         /* TODO: use GUIDs here instead */
300         dn = ldb_dn_linearize(local, remote->dn);
301         if (ldb_msg_add_string(local, IS_MAPPED, dn) != 0) {
302                 goto failed;
303         }
304
305         req->handle = h;                /* return our own handle to deal with this call */
306         return map_add_do_local(h);
307
308 oom:
309         map_oom(module);
310 failed:
311         talloc_free(h);
312         return LDB_ERR_OPERATIONS_ERROR;
313 }
314
315 /* Modify the remote record. */
316 int map_modify_do_remote(struct ldb_handle *handle)
317 {
318         struct map_context *ac;
319
320         ac = talloc_get_type(handle->private_data, struct map_context);
321
322         ldb_set_timeout_from_prev_req(ac->module->ldb, ac->orig_req, ac->remote_req);
323
324         ac->step = MAP_MODIFY_REMOTE;
325
326         handle->state = LDB_ASYNC_INIT;
327         handle->status = LDB_SUCCESS;
328
329         return ldb_next_remote_request(ac->module, ac->remote_req);
330 }
331
332 /* Modify the local record. */
333 int map_modify_do_local(struct ldb_handle *handle)
334 {
335         struct map_context *ac;
336         struct ldb_message *msg;
337         char *dn;
338
339         ac = talloc_get_type(handle->private_data, struct map_context);
340
341         if (ac->local_dn == NULL) {
342                 /* No local record present, add it instead */
343                 msg = discard_const_p(struct ldb_message, ac->local_req->op.mod.message);
344
345                 /* Add local 'IS_MAPPED' */
346                 /* TODO: use GUIDs here instead */
347                 dn = ldb_dn_linearize(msg, ac->remote_req->op.mod.message->dn);
348                 if (ldb_msg_add_empty(msg, IS_MAPPED, LDB_FLAG_MOD_ADD, NULL) != 0) {
349                         return LDB_ERR_OPERATIONS_ERROR;
350                 }
351                 if (ldb_msg_add_string(msg, IS_MAPPED, dn) != 0) {
352                         return LDB_ERR_OPERATIONS_ERROR;
353                 }
354
355                 /* Turn request into 'add' */
356                 ac->local_req->operation = LDB_ADD;
357                 ac->local_req->op.add.message = msg;
358                 /* TODO: Could I just leave msg in there?  I think so,
359                  *       but it looks clearer this way. */
360         }
361
362         ldb_set_timeout_from_prev_req(ac->module->ldb, ac->orig_req, ac->local_req);
363
364         ac->step = MAP_MODIFY_LOCAL;
365
366         handle->state = LDB_ASYNC_INIT;
367         handle->status = LDB_SUCCESS;
368
369         return ldb_next_request(ac->module, ac->local_req);
370 }
371
372 /* Modify a record. */
373 int map_modify(struct ldb_module *module, struct ldb_request *req)
374 {
375         const struct ldb_message *msg = req->op.mod.message;
376         struct ldb_handle *h;
377         struct map_context *ac;
378         struct ldb_message *local, *remote;
379
380         /* Do not manipulate our control entries */
381         if (ldb_dn_is_special(msg->dn)) {
382                 return ldb_next_request(module, req);
383         }
384
385         /* No mapping requested (perhaps no DN mapping specified), skip to next module */
386         if (!ldb_dn_check_local(module, msg->dn)) {
387                 return ldb_next_request(module, req);
388         }
389
390         /* No mapping needed, skip to next module */
391         /* TODO: What if the remote part exists, the local doesn't,
392          *       and this request wants to modify local data and thus
393          *       add the local record? */
394         if (!ldb_msg_check_remote(module, msg)) {
395                 return LDB_ERR_OPERATIONS_ERROR;
396         }
397
398         /* Prepare context and handle */
399         h = map_init_handle(req, module);
400         if (h == NULL) {
401                 return LDB_ERR_OPERATIONS_ERROR;
402         }
403         ac = talloc_get_type(h->private_data, struct map_context);
404
405         /* Prepare the local operation */
406         ac->local_req = talloc(ac, struct ldb_request);
407         if (ac->local_req == NULL) {
408                 goto oom;
409         }
410
411         *(ac->local_req) = *req;        /* copy the request */
412
413         ac->local_req->context = NULL;
414         ac->local_req->callback = NULL;
415
416         /* Prepare the remote operation */
417         ac->remote_req = talloc(ac, struct ldb_request);
418         if (ac->remote_req == NULL) {
419                 goto oom;
420         }
421
422         *(ac->remote_req) = *req;       /* copy the request */
423
424         ac->remote_req->context = NULL;
425         ac->remote_req->callback = NULL;
426
427         /* Prepare the local message */
428         local = ldb_msg_new(ac->local_req);
429         if (local == NULL) {
430                 goto oom;
431         }
432         local->dn = msg->dn;
433
434         /* Prepare the remote message */
435         remote = ldb_msg_new(ac->remote_req);
436         if (remote == NULL) {
437                 goto oom;
438         }
439         remote->dn = ldb_dn_map_local(ac->module, remote, msg->dn);
440
441         /* Split local from remote message */
442         ldb_msg_partition(module, local, remote, msg);
443         ac->local_req->op.mod.message = local;
444         ac->remote_req->op.mod.message = remote;
445
446         if ((local->num_elements == 0) || (!map_check_local_db(ac->module))) {
447                 /* No local data or db, just run the remote request */
448                 talloc_free(ac->local_req);
449                 req->handle = h;        /* return our own handle to deal with this call */
450                 return map_modify_do_remote(h);
451         }
452
453         /* prepare the search operation */
454         ac->search_req = map_search_self_req(ac, msg->dn);
455         if (ac->search_req == NULL) {
456                 goto failed;
457         }
458
459         ac->step = MAP_SEARCH_SELF_MODIFY;
460
461         req->handle = h;                /* return our own handle to deal with this call */
462         return ldb_next_request(module, ac->search_req);
463
464 oom:
465         map_oom(module);
466 failed:
467         talloc_free(h);
468         return LDB_ERR_OPERATIONS_ERROR;
469 }
470
471 /* Delete the remote record. */
472 int map_delete_do_remote(struct ldb_handle *handle)
473 {
474         struct map_context *ac;
475
476         ac = talloc_get_type(handle->private_data, struct map_context);
477
478         ldb_set_timeout_from_prev_req(ac->module->ldb, ac->orig_req, ac->remote_req);
479
480         ac->step = MAP_DELETE_REMOTE;
481
482         handle->state = LDB_ASYNC_INIT;
483         handle->status = LDB_SUCCESS;
484
485         return ldb_next_remote_request(ac->module, ac->remote_req);
486 }
487
488 /* Delete the local record. */
489 int map_delete_do_local(struct ldb_handle *handle)
490 {
491         struct map_context *ac;
492
493         ac = talloc_get_type(handle->private_data, struct map_context);
494
495         /* No local record, continue remotely */
496         if (ac->local_dn == NULL) {
497                 return map_delete_do_remote(handle);
498         }
499
500         ldb_set_timeout_from_prev_req(ac->module->ldb, ac->orig_req, ac->local_req);
501
502         ac->step = MAP_DELETE_LOCAL;
503
504         handle->state = LDB_ASYNC_INIT;
505         handle->status = LDB_SUCCESS;
506
507         return ldb_next_request(ac->module, ac->local_req);
508 }
509
510 /* Delete a record. */
511 int map_delete(struct ldb_module *module, struct ldb_request *req)
512 {
513         struct ldb_handle *h;
514         struct map_context *ac;
515
516         /* Do not manipulate our control entries */
517         if (ldb_dn_is_special(req->op.del.dn)) {
518                 return ldb_next_request(module, req);
519         }
520
521         /* No mapping requested (perhaps no DN mapping specified), skip to next module */
522         if (!ldb_dn_check_local(module, req->op.del.dn)) {
523                 return ldb_next_request(module, req);
524         }
525
526         /* Prepare context and handle */
527         h = map_init_handle(req, module);
528         if (h == NULL) {
529                 return LDB_ERR_OPERATIONS_ERROR;
530         }
531         ac = talloc_get_type(h->private_data, struct map_context);
532
533         /* Prepare the local operation */
534         ac->local_req = talloc(ac, struct ldb_request);
535         if (ac->local_req == NULL) {
536                 goto oom;
537         }
538
539         *(ac->local_req) = *req;        /* copy the request */
540         ac->local_req->op.del.dn = req->op.del.dn;
541
542         ac->local_req->context = NULL;
543         ac->local_req->callback = NULL;
544
545         /* Prepare the remote operation */
546         ac->remote_req = talloc(ac, struct ldb_request);
547         if (ac->remote_req == NULL) {
548                 goto oom;
549         }
550
551         *(ac->remote_req) = *req;       /* copy the request */
552         ac->remote_req->op.del.dn = ldb_dn_map_local(module, ac->remote_req, req->op.del.dn);
553
554         /* No local db, just run the remote request */
555         if (!map_check_local_db(ac->module)) {
556                 req->handle = h;        /* return our own handle to deal with this call */
557                 return map_delete_do_remote(h);
558         }
559
560         ac->remote_req->context = NULL;
561         ac->remote_req->callback = NULL;
562
563         /* Prepare the search operation */
564         ac->search_req = map_search_self_req(ac, req->op.del.dn);
565         if (ac->search_req == NULL) {
566                 goto failed;
567         }
568
569         req->handle = h;                /* return our own handle to deal with this call */
570
571         ac->step = MAP_SEARCH_SELF_DELETE;
572
573         return ldb_next_request(module, ac->search_req);
574
575 oom:
576         map_oom(module);
577 failed:
578         talloc_free(h);
579         return LDB_ERR_OPERATIONS_ERROR;
580 }
581
582 /* Rename the remote record. */
583 int map_rename_do_remote(struct ldb_handle *handle)
584 {
585         struct map_context *ac;
586
587         ac = talloc_get_type(handle->private_data, struct map_context);
588
589         ldb_set_timeout_from_prev_req(ac->module->ldb, ac->orig_req, ac->remote_req);
590
591         ac->step = MAP_RENAME_REMOTE;
592
593         handle->state = LDB_ASYNC_INIT;
594         handle->status = LDB_SUCCESS;
595
596         return ldb_next_remote_request(ac->module, ac->remote_req);
597 }
598
599 /* Update the local 'IS_MAPPED' attribute. */
600 int map_rename_do_fixup(struct ldb_handle *handle)
601 {
602         struct map_context *ac;
603
604         ac = talloc_get_type(handle->private_data, struct map_context);
605
606         ldb_set_timeout_from_prev_req(ac->module->ldb, ac->orig_req, ac->down_req);
607
608         ac->step = MAP_RENAME_FIXUP;
609
610         handle->state = LDB_ASYNC_INIT;
611         handle->status = LDB_SUCCESS;
612
613         return ldb_next_request(ac->module, ac->down_req);
614 }
615
616 /* Rename the local record. */
617 int map_rename_do_local(struct ldb_handle *handle)
618 {
619         struct map_context *ac;
620
621         ac = talloc_get_type(handle->private_data, struct map_context);
622
623         /* No local record, continue remotely */
624         if (ac->local_dn == NULL) {
625                 return map_rename_do_remote(handle);
626         }
627
628         ldb_set_timeout_from_prev_req(ac->module->ldb, ac->orig_req, ac->local_req);
629
630         ac->step = MAP_RENAME_LOCAL;
631
632         handle->state = LDB_ASYNC_INIT;
633         handle->status = LDB_SUCCESS;
634
635         return ldb_next_request(ac->module, ac->local_req);
636 }
637
638 /* Rename a record. */
639 int map_rename(struct ldb_module *module, struct ldb_request *req)
640 {
641         struct ldb_handle *h;
642         struct map_context *ac;
643
644         /* Do not manipulate our control entries */
645         if (ldb_dn_is_special(req->op.rename.olddn)) {
646                 return ldb_next_request(module, req);
647         }
648
649         /* No mapping requested (perhaps no DN mapping specified), skip to next module */
650         if ((!ldb_dn_check_local(module, req->op.rename.olddn)) &&
651             (!ldb_dn_check_local(module, req->op.rename.newdn))) {
652                 return ldb_next_request(module, req);
653         }
654
655         /* Rename into/out of the mapped partition requested, bail out */
656         if (!ldb_dn_check_local(module, req->op.rename.olddn) ||
657             !ldb_dn_check_local(module, req->op.rename.newdn)) {
658                 return LDB_ERR_AFFECTS_MULTIPLE_DSAS;
659         }
660
661         /* Prepare context and handle */
662         h = map_init_handle(req, module);
663         if (h == NULL) {
664                 return LDB_ERR_OPERATIONS_ERROR;
665         }
666         ac = talloc_get_type(h->private_data, struct map_context);
667
668         /* Prepare the local operation */
669         ac->local_req = talloc(ac, struct ldb_request);
670         if (ac->local_req == NULL) {
671                 goto oom;
672         }
673
674         *(ac->local_req) = *req;        /* copy the request */
675         ac->local_req->op.rename.olddn = req->op.rename.olddn;
676         ac->local_req->op.rename.newdn = req->op.rename.newdn;
677
678         ac->local_req->context = NULL;
679         ac->local_req->callback = NULL;
680
681         /* Prepare the remote operation */
682         ac->remote_req = talloc(ac, struct ldb_request);
683         if (ac->remote_req == NULL) {
684                 goto oom;
685         }
686
687         *(ac->remote_req) = *req;       /* copy the request */
688         ac->remote_req->op.rename.olddn = ldb_dn_map_local(module, ac->remote_req, req->op.rename.olddn);
689         ac->remote_req->op.rename.newdn = ldb_dn_map_local(module, ac->remote_req, req->op.rename.newdn);
690
691         ac->remote_req->context = NULL;
692         ac->remote_req->callback = NULL;
693
694         /* No local db, just run the remote request */
695         if (!map_check_local_db(ac->module)) {
696                 req->handle = h;        /* return our own handle to deal with this call */
697                 return map_rename_do_remote(h);
698         }
699
700         /* Prepare the fixup operation */
701         /* TODO: use GUIDs here instead -- or skip it when GUIDs are used. */
702         ac->down_req = map_build_fixup_req(ac, req->op.rename.newdn, ac->remote_req->op.rename.newdn);
703         if (ac->down_req == NULL) {
704                 goto failed;
705         }
706
707         /* Prepare the search operation */
708         ac->search_req = map_search_self_req(ac, req->op.rename.olddn);
709         if (ac->search_req == NULL) {
710                 goto failed;
711         }
712
713         req->handle = h;                /* return our own handle to deal with this call */
714
715         ac->step = MAP_SEARCH_SELF_RENAME;
716
717         return ldb_next_request(module, ac->search_req);
718
719 oom:
720         map_oom(module);
721 failed:
722         talloc_free(h);
723         return LDB_ERR_OPERATIONS_ERROR;
724 }