summaryrefslogtreecommitdiffabout
authorSergey Poznyakoff <gray@gnu.org.ua>2010-06-28 21:08:32 (GMT)
committer Sergey Poznyakoff <gray@gnu.org.ua>2010-06-28 21:09:40 (GMT)
commit2af5a2255dfcf46c06cb39010768863c5a2837aa (patch) (unidiff)
treedc354959ed5f909b560b2eb46fa002dc99c67aa7
parente696e8668cd2700825c42bf50c8a74a7292dcbf6 (diff)
downloadsmap-2af5a2255dfcf46c06cb39010768863c5a2837aa.tar.gz
smap-2af5a2255dfcf46c06cb39010768863c5a2837aa.tar.bz2
Implement user-defined variables in configs.
* src/cfg.c (asgn_p, asgn, find_env_var): New functions. (parse_config_loop): Handle user-defined variable assignments and expansions. * modules/mailutils/mailutils.c: Use wordsplit instead of the MU vartabs. * doc/ex-meta1.texi: Rewrite.
Diffstat (more/less context) (ignore whitespace changes)
-rw-r--r--doc/ex-meta1.texi145
-rw-r--r--modules/mailutils/mailutils.c180
-rw-r--r--src/cfg.c83
3 files changed, 248 insertions, 160 deletions
diff --git a/doc/ex-meta1.texi b/doc/ex-meta1.texi
index e558008..d74b49c 100644
--- a/doc/ex-meta1.texi
+++ b/doc/ex-meta1.texi
@@ -3,13 +3,36 @@
3@c See file smap.texi for copying conditions. 3@c See file smap.texi for copying conditions.
4@c ******************************************************************* 4@c *******************************************************************
5@cindex MeTA1 5@cindex MeTA1
6 In this appendix we will show how to use the @samp{mailutils} 6 In this appendix we will show how to use the @samp{mysql}
7module (@pxref{mailutils,mailutils module}) to configure local user 7module (@pxref{mysql,mysql module}) to configure local user
8and alias maps for @acronym{MeTA1}. For this purpose, we will assume 8and alias maps for @acronym{MeTA1}. For this purpose, we will assume
9that the actual data is stored in two tables in a @acronym{MySQL} 9that the actual data is stored in two tables in a @acronym{MySQL}
10database. The two maps will be served by two separate databases, each 10database. The two maps will be served by two separate databases, each
11of which uses a separate configuration file. 11of which uses a separate configuration file.
12 12
13 To reduce the number of connections to the @acronym{MySQL} server,
14the @acronym{MySQL} database will be opened at the module level and
15shared between the two smap databases. Thus, the module
16initialization in @file{smapd.conf} looks like:
17
18@example
19module mysql mysql open config-group=smap
20@end example
21
22The @samp{open} parameter instructs the module to open the requested
23databases. The @samp{config-group} parameter refers to a group
24name in the default @file{/etc/my.cnf} file that contains information
25about the @acronym{MySQL} database and credentials for accessing it.
26The following is a sample snippet from @file{/etc/my.cnf}:
27
28@example
29[smap]
30database = Mail
31user = smap
32password = guessme
33socket = /tmp/mysql.sock
34@end example
35
13@menu 36@menu
14* userdb-meta1:: Configure local_user_map. 37* userdb-meta1:: Configure local_user_map.
15* aliases-meta1:: Configure aliases. 38* aliases-meta1:: Configure aliases.
@@ -33,43 +56,25 @@ CREATE TABLE userdb (
33@end group 56@end group
34@end example 57@end example
35 58
36Module configuration file @file{/etc/mailutils.d/meta1-userdb} 59The smap database is defined as follows:
37begins with the following stanza:
38 60
39@example 61@example
40@group 62@group
41auth @{ 63database userdb mysql \
42 authentication clear; 64 defaultdb
43 authentication sql; 65 query="SELECT user FROM userdb WHERE user='$key'"
44 authorization clear; 66 positive-reply=OK
45 authorization sql;
46@}
47@end group 67@end group
48@end example 68@end example
49 69
50This clears any previous settings that the authorization engine might 70The @samp{defaultdb} parameter tells it to use the default SQL
51have read from the main configuration file, and requests that only 71database opened in the module initialization instruction. The
52@samp{sql} method be used for both authentication and authorization. 72@samp{query} parameter supplies the SQL query to run (the
53 73@samp{$@{key@}} variable will be expanded to the value of the actual
54Now, we need to supply a @samp{sql} statement. Mailutils requires 74lookup key, prior to executing the query). Finally,
55that the @code{getpwnam} query return at least six fields, whereas the 75@samp{positive-reply} defines the reply to give if the query returns
56@samp{userdb} table contains only two columns. So we will need to supply 76some tuples. The database only verifies whether the user is present
57defaults for the remaining four: 77or not, so no additional result is supplied in the reply.
58
59@example
60sql @{
61 interface mysql;
62 host sql.host.name
63 user smap;
64 passwd guessme;
65 db mail;
66 getpwnam "SELECT user as name, 'x' as passwd,10000 as uid, 10000 as gid, "
67 "'/nonexistent' as dir, '/sbin/nologin' as shell "
68 "FROM userdb WHERE user='$@{user@}'";
69@};
70@end example
71
72 That's all we need to have in @file{/etc/mailutils.d/meta1-userdb}.
73 78
74@node aliases-meta1 79@node aliases-meta1
75@appendixsec Configure aliases 80@appendixsec Configure aliases
@@ -87,74 +92,26 @@ CREATE TABLE userdb (
87@end group 92@end group
88@end example 93@end example
89 94
90It will be served by @samp{alias} database, which will read 95It will be served by @samp{alias} database, defined as follows:
91the configuration for Mailutils from the file
92@file{/etc/mailutils.d/meta1-alias}. This file is similar to
93@file{meta1-userdb}, but uses a different query in its @samp{sql}
94section:
95 96
96@example 97@example
97auth @{ 98@group
98 authentication clear; 99database alias mysql \
99 authentication sql; 100 defaultdb \
100 authorization clear; 101 query="SELECT alias FROM aliases WHERE user='$key'" \
101 authorization sql; 102 positive-reply="OK $alias"
102@} 103@end group
103
104sql @{
105 interface mysql;
106 host sql.host.name
107 user smap;
108 passwd guessme;
109 db mail;
110 getpwnam "SELECT alias as name, 'x' as passwd,1 as uid, 1 as gid, "
111 "'/nonexistent' as dir, '/sbin/nologin' as shell "
112 "FROM aliases WHERE name='$@{user@}'";
113@}
114@end example
115
116@node smapd-meta1
117@appendixsec Smapd configuration
118
119 Let's now configure @file{smapd.conf}. Suppose it will run a single
120server, which we will call @samp{local}. The server will listen on a
121UNIX socket @file{/var/spool/meta1/smap/userdb}. It is important that
122@samp{meta1} be able to read from and write to that socket, so we will make
123it owned by user @samp{meta1m}:
124
125@example
126server local unix:///var/spool/meta1/smap/userdb begin
127 user meta1m
128end
129@end example
130
131 Next task is to configure the databases. The @samp{userdb} database is
132pretty simple:
133
134@example
135database userdb mailutils mode=auth \
136 config-file=/etc/mailutils.d/meta1-userdb
137@end example 104@end example
138 105
139 It will return @samp{OK} if the user is found in the database and 106It differs from the @samp{userdb} database only in that it returns
140@samp{NOTFOUND} otherwise, which is exactly what the @acronym{MTA} needs. 107a @dfn{result section} with its positive reply.
141 108
142 The @samp{aliasdb} database is a bit different. In case of a
143positive reply, it must return the expanded alias value, so we need to
144supply a new @samp{positive-reply} template:
145 109
146@example 110@node smapd-meta1
147database aliasdb mailutils mode=auth \ 111@appendixsec Dispatch Rules
148 config-file=/usr/local/etc/mailutils.d/meta1-alias \
149 positive-reply="OK $@{name@}"
150@end example
151
152 The @samp{$@{name@}} will be replaced with the value of the first
153column in the tuple returned by the @acronym{SQL} database
154(@pxref{aliases-meta1, getpwnam}).
155 112
156 To dispatch queries to these databases, the following rules will 113 The following rules dispatch queries based on their map names to
157suffice: 114the two databases:
158 115
159@example 116@example
160dispatch map alias database aliasdb 117dispatch map alias database aliasdb
diff --git a/modules/mailutils/mailutils.c b/modules/mailutils/mailutils.c
index 10018c4..03966c1 100644
--- a/modules/mailutils/mailutils.c
+++ b/modules/mailutils/mailutils.c
@@ -25,6 +25,7 @@
25#include <smap/diag.h> 25#include <smap/diag.h>
26#include <smap/module.h> 26#include <smap/module.h>
27#include <smap/parseopt.h> 27#include <smap/parseopt.h>
28#include <smap/wordsplit.h>
28 29
29static char *dfl_positive_reply = "OK"; 30static char *dfl_positive_reply = "OK";
30static char *dfl_negative_reply = "NOTFOUND"; 31static char *dfl_negative_reply = "NOTFOUND";
@@ -66,58 +67,97 @@ _mu_smap_db_free(struct _mu_smap_db *db)
66 free(db); 67 free(db);
67} 68}
68 69
70static void
71free_env(char **env)
72{
73 int i;
74 for (i = 0; env[i]; i++)
75 free(env[i]);
76}
77
69static char * 78static char *
70expand_reply_text(const char *arg, struct _mu_smap_result *res) 79mkvar(const char *name, const char *val)
71{ 80{
72 int rc; 81 char *ptr = malloc(strlen(name) + strlen(val) + 2);
73 mu_vartab_t vtab; 82 if (ptr) {
74 char *reply = NULL; 83 strcpy(ptr, name);
75 char buf[512]; 84 strcat(ptr, "=");
76 struct mu_auth_data *auth = res->auth; 85 strcat(ptr, val);
86 }
87 return ptr;
88}
77 89
78 if (!arg) 90static int
79 return NULL; 91mkenv(char **env, struct _mu_smap_result *res)
80 mu_vartab_create(&vtab); 92{
81 mu_vartab_define(vtab, "db", res->db, 0); 93 int i = 0;
82 mu_vartab_define(vtab, "key", res->key, 0); 94 struct mu_auth_data *auth = res->auth;
83 mu_vartab_define(vtab, "map", res->map, 0); 95 char buf[512];
84 mu_vartab_define(vtab, MU_AUTH_NAME, auth ? auth->name : "", 0); 96
85 mu_vartab_define(vtab, MU_AUTH_PASSWD, auth ? auth->passwd : "", 0); 97 #define MKVAR(n, v) \
86 if (!auth) 98 do { \
87 strcpy(buf, "-1"); 99 if (!(env[i++] = mkvar(n, v)))\
88 else 100 return 1; \
101 } while (0)
102
103 MKVAR("db", res->db);
104 MKVAR("key", res->key);
105 MKVAR("map", res->map);
106 if (auth) {
107 MKVAR(MU_AUTH_NAME, auth->name);
108 MKVAR(MU_AUTH_PASSWD, auth->passwd);
89 snprintf(buf, sizeof buf, "%lu", (unsigned long) auth->uid); 109 snprintf(buf, sizeof buf, "%lu", (unsigned long) auth->uid);
90 mu_vartab_define(vtab, MU_AUTH_UID, buf, 0); 110 MKVAR(MU_AUTH_UID, buf);
91 if (!auth)
92 strcpy(buf, "-1");
93 else
94 snprintf(buf, sizeof buf, "%lu", (unsigned long) auth->gid); 111 snprintf(buf, sizeof buf, "%lu", (unsigned long) auth->gid);
95 mu_vartab_define(vtab, MU_AUTH_GID, buf, 0); 112 MKVAR(MU_AUTH_GID, buf);
96 mu_vartab_define(vtab, MU_AUTH_GECOS, 113 MKVAR(MU_AUTH_GECOS, auth->gecos);
97 auth ? auth->gecos : "", 0); 114 MKVAR(MU_AUTH_DIR, auth->dir);
98 mu_vartab_define(vtab, MU_AUTH_DIR, auth ? auth->dir : "", 0); 115 MKVAR(MU_AUTH_SHELL, auth->shell);
99 mu_vartab_define(vtab, MU_AUTH_SHELL, auth ? auth->shell : "", 0); 116 MKVAR(MU_AUTH_MAILBOX,
100 mu_vartab_define(vtab, MU_AUTH_MAILBOX, 117 auth->mailbox ? auth->mailbox :
101 (auth && auth->mailbox) ? auth->mailbox : 118 res->url ? mu_url_to_string(res->url) : "");
102 res->url ? mu_url_to_string(res->url) : "", 0);
103 if (!auth)
104 strcpy(buf, "NONE");
105 else
106 snprintf(buf, sizeof buf, "%lu", (unsigned long) auth->quota); 119 snprintf(buf, sizeof buf, "%lu", (unsigned long) auth->quota);
107 mu_vartab_define(vtab, MU_AUTH_QUOTA, buf, 0); 120 MKVAR(MU_AUTH_QUOTA, buf);
108 snprintf(buf, sizeof buf, "%lu", (unsigned long) res->mbsize); 121 snprintf(buf, sizeof buf, "%lu", (unsigned long) res->mbsize);
109 mu_vartab_define(vtab, "mbsize", buf, 0); 122 MKVAR("mbsize", buf);
110 snprintf(buf, sizeof buf, "%lu", (unsigned long) res->msgsize); 123 snprintf(buf, sizeof buf, "%lu", (unsigned long) res->msgsize);
111 mu_vartab_define(vtab, "msgsize", buf, 0); 124 MKVAR("msgsize", buf);
112 125 }
113 if (res->diag) 126 if (res->diag)
114 mu_vartab_define(vtab, "diag", res->diag, 1); 127 MKVAR("diag", res->diag);
115 rc = mu_vartab_expand(vtab, arg, &reply); 128
129 env[i] = NULL;
130 return 0;
131}
132
133static int
134expand_reply_text(const char *arg, struct _mu_smap_result *res, char **repl)
135{
136 int rc;
137 char *env[16];
138 struct wordsplit ws;
139
140 if (mkenv(env, res)) {
141 mu_error("not enough memory");
142 free_env(env);
143 return 1;
144 }
145
146 ws.ws_env = (const char **) env;
147 ws.ws_error = smap_error;
148 rc = wordsplit(arg, &ws,
149 WRDSF_NOSPLIT |
150 WRDSF_NOCMD |
151 WRDSF_ENV |
152 WRDSF_ERROR |
153 WRDSF_SHOWERR);
154 free_env(env);
116 if (rc) 155 if (rc)
117 mu_error("cannot expand string `%s': %s", 156 return 1;
118 arg, mu_strerror (rc)); 157 *repl = ws.ws_wordv[0];
119 mu_vartab_destroy(&vtab); 158 ws.ws_wordv = NULL;
120 return reply; 159 wordsplit_free(&ws);
160 return 0;
121} 161}
122 162
123static int 163static int
@@ -130,7 +170,8 @@ _mu_auth_query(smap_database_t dbp,
130 struct mu_auth_data *auth = mu_get_auth_by_name(key); 170 struct mu_auth_data *auth = mu_get_auth_by_name(key);
131 struct _mu_smap_result res; 171 struct _mu_smap_result res;
132 char *reply; 172 char *reply;
133 173 int rc;
174
134 res.db = mdb->id; 175 res.db = mdb->id;
135 res.map = map; 176 res.map = map;
136 res.key = key; 177 res.key = key;
@@ -140,14 +181,16 @@ _mu_auth_query(smap_database_t dbp,
140 res.diag = NULL; 181 res.diag = NULL;
141 res.url = NULL; 182 res.url = NULL;
142 if (!auth) 183 if (!auth)
143 reply = expand_reply_text(mdb->negative_reply, &res); 184 rc = expand_reply_text(mdb->negative_reply, &res, &reply);
144 else { 185 else {
145 reply = expand_reply_text(mdb->positive_reply, &res); 186 rc = expand_reply_text(mdb->positive_reply, &res, &reply);
146 mu_auth_data_free(auth); 187 mu_auth_data_free(auth);
147 } 188 }
148 smap_stream_printf(ostr, "%s\n", reply); 189 if (rc == 0) {
149 free(reply); 190 smap_stream_printf(ostr, "%s\n", reply);
150 return 0; 191 free(reply);
192 }
193 return rc;
151} 194}
152 195
153static int 196static int
@@ -181,32 +224,34 @@ switch_user_id(struct mu_auth_data *auth, int user)
181 return rc; 224 return rc;
182} 225}
183 226
184static char * 227static int
185checksize(struct _mu_smap_db *mdb, smap_stream_t ostr, 228checksize(struct _mu_smap_db *mdb, smap_stream_t ostr,
186 const char *user, struct _mu_smap_result *res) 229 const char *user, struct _mu_smap_result *res,
230 char **preply)
187{ 231{
188 struct mu_auth_data *auth; 232 struct mu_auth_data *auth;
189 mu_mailbox_t mbox; 233 mu_mailbox_t mbox;
190 int status; 234 int status;
191 char *reply_txt = NULL; 235
236 *preply = NULL;
192 237
193 auth = mu_get_auth_by_name(user); 238 auth = mu_get_auth_by_name(user);
194 res->auth = auth; 239 res->auth = auth;
195 if (!auth) { 240 if (!auth) {
196 res->diag = "user not found"; 241 res->diag = "user not found";
197 smap_debug(dbgid, 1, ("%s: user not found", user)); 242 smap_debug(dbgid, 1, ("%s: user not found", user));
198 return NULL; 243 return 0;
199 } 244 }
200 if (switch_user_id(auth, 1)) { 245 if (switch_user_id(auth, 1)) {
201 res->diag = "local system error"; 246 res->diag = "local system error";
202 return NULL; 247 return 0;
203 } 248 }
204 status = mu_mailbox_create_default(&mbox, auth->mailbox); 249 status = mu_mailbox_create_default(&mbox, auth->mailbox);
205 if (status) { 250 if (status) {
206 res->diag = "local system error"; 251 res->diag = "local system error";
207 mu_error("could not create mailbox `%s': %s", 252 mu_error("could not create mailbox `%s': %s",
208 auth->mailbox, mu_strerror(status)); 253 auth->mailbox, mu_strerror(status));
209 return NULL; 254 return 0;
210 } 255 }
211 256
212 mu_mailbox_get_url(mbox, &res->url); 257 mu_mailbox_get_url(mbox, &res->url);
@@ -245,14 +290,15 @@ checksize(struct _mu_smap_db *mdb, smap_stream_t ostr,
245 stat = mdb->positive_reply; 290 stat = mdb->positive_reply;
246 res->diag = "QUOTAOK"; 291 res->diag = "QUOTAOK";
247 } 292 }
248 reply_txt = expand_reply_text(stat, res); 293 if (expand_reply_text(stat, res, preply))
294 return 1;
249 } 295 }
250 switch_user_id(auth, 0); 296 switch_user_id(auth, 0);
251 mu_mailbox_close(mbox); 297 mu_mailbox_close(mbox);
252 } 298 }
253 mu_mailbox_destroy(&mbox); 299 mu_mailbox_destroy(&mbox);
254 mu_auth_data_free(auth); 300 mu_auth_data_free(auth);
255 return reply_txt; 301 return 0;
256} 302}
257 303
258static int 304static int
@@ -267,7 +313,8 @@ _mu_mbq_query(smap_database_t dbp,
267 size_t len; 313 size_t len;
268 struct _mu_smap_result res; 314 struct _mu_smap_result res;
269 char *reply; 315 char *reply;
270 316 int rc;
317
271 memset(&res, 0, sizeof(res)); 318 memset(&res, 0, sizeof(res));
272 res.db = mdb->id; 319 res.db = mdb->id;
273 res.map = map; 320 res.map = map;
@@ -290,13 +337,16 @@ _mu_mbq_query(smap_database_t dbp,
290 ("ignoring junk after %s", user + len)); 337 ("ignoring junk after %s", user + len));
291 } 338 }
292 339
293 reply = checksize(mdb, ostr, user, &res); 340 rc = checksize(mdb, ostr, user, &res, &reply);
294 if (!reply) 341
295 reply = expand_reply_text(mdb->onerror_reply, &res); 342 if (!rc && !reply)
296 smap_stream_printf(ostr, "%s\n", reply); 343 rc = expand_reply_text(mdb->onerror_reply, &res, &reply);
297 free(reply); 344 if (rc == 0) {
345 smap_stream_printf(ostr, "%s\n", reply);
346 free(reply);
347 }
298 free(user); 348 free(user);
299 return 0; 349 return rc;
300} 350}
301 351
302 352
diff --git a/src/cfg.c b/src/cfg.c
index 0809504..e0eea62 100644
--- a/src/cfg.c
+++ b/src/cfg.c
@@ -190,13 +190,78 @@ wrdse_to_ex(int code)
190 return EX_UNAVAILABLE; 190 return EX_UNAVAILABLE;
191} 191}
192 192
193static int
194asgn_p(char *buf)
195{
196 unsigned char *p = (unsigned char*)buf;
197 while (*p && (*p == ' ' || *p == '\t'))
198 p++;
199 if (!*p)
200 return 0;
201 if (*p < 127 && (isalpha(*p) || *p == '_')) {
202 while (*p && *p < 127 && (isalnum(*p) || *p == '_'))
203 p++;
204 return *p == '=';
205 }
206 return 0;
207}
208
209static size_t
210find_env_var(const char **env, char *name)
211{
212 size_t i;
213
214 for (i = 0; env[i]; i++) {
215 size_t j;
216 const char *var = env[i];
217
218 for (j = 0; name[j]; j++) {
219 if (name[j] != var[j])
220 break;
221 if (name[j] == '=')
222 return i;
223 }
224 }
225 return i;
226}
227
228static void
229asgn(struct wordsplit *ws, size_t *psize)
230{
231 size_t size = *psize;
232
233 if (size == 0) {
234 size = 16;
235 ws->ws_env = ecalloc(size, sizeof(ws->ws_env[0]));
236 ws->ws_env[0] = ws->ws_wordv[0];
237 } else {
238 size_t n = find_env_var(ws->ws_env, ws->ws_wordv[0]);
239 if (ws->ws_env[n])
240 free((char*)ws->ws_env[n]);
241 else if (n == size - 1) {
242 size *= 2;
243 ws->ws_env = erealloc(ws->ws_env,
244 size * sizeof(ws->ws_env[0]));
245 }
246 ws->ws_env[n] = ws->ws_wordv[0];
247 ws->ws_env[n++] = NULL;
248 }
249 ws->ws_wordv[0] = NULL;
250 *psize = size;
251}
252
253
193void 254void
194parse_config_loop(struct cfg_kw *kwtab, void *data) 255parse_config_loop(struct cfg_kw *kwtab, void *data)
195{ 256{
196 char *buf = NULL; 257 char *buf = NULL;
197 size_t size = 0; 258 size_t size = 0;
198 struct wordsplit ws; 259 struct wordsplit ws;
199 int wsflags = WRDSF_DEFFLAGS|WRDSF_ERROR|WRDSF_COMMENT|WRDSF_ENOMEMABRT; 260 int wsflags = WRDSF_DEFFLAGS |
261 WRDSF_ERROR |
262 WRDSF_COMMENT |
263 WRDSF_ENOMEMABRT;
264 size_t envsize = 0;
200 int eof = 0; 265 int eof = 0;
201 266
202 ws.ws_error = smap_error; 267 ws.ws_error = smap_error;
@@ -216,6 +281,20 @@ parse_config_loop(struct cfg_kw *kwtab, void *data)
216 if (ws.ws_wordc == 0) 281 if (ws.ws_wordc == 0)
217 continue; 282 continue;
218 283
284 if (asgn_p(ws.ws_wordv[0])) {
285 if (ws.ws_wordc > 1) {
286 smap_error("%s:%u: too many arguments "
287 "(unquoted assignment?)",
288 cfg_file_name, cfg_line);
289 cfg_errors = 1;
290 } else {
291 asgn(&ws, &envsize);
292 wsflags &= ~WRDSF_NOVAR;
293 wsflags |= WRDSF_ENV | WRDSF_KEEPUNDEF;
294 }
295 continue;
296 }
297
219 kwp = find_cfg_kw(kwtab, ws.ws_wordv[0]); 298 kwp = find_cfg_kw(kwtab, ws.ws_wordv[0]);
220 if (!kwp) { 299 if (!kwp) {
221 smap_error("%s:%u: unrecognized line", 300 smap_error("%s:%u: unrecognized line",
@@ -296,6 +375,8 @@ parse_config_loop(struct cfg_kw *kwtab, void *data)
296 } 375 }
297 if (wsflags & WRDSF_REUSE) 376 if (wsflags & WRDSF_REUSE)
298 wordsplit_free(&ws); 377 wordsplit_free(&ws);
378 if (envsize)
379 free(ws.ws_env);
299 free(buf); 380 free(buf);
300} 381}
301 382

Return to:

Send suggestions and report system problems to the System administrator.