James Moger
2014-05-07 4aa595cd4307d40e32fdb12f5178122caaa8c643
commit | author | age
75bca8 1 /*
234933 2  * Copyright 2013 gitblit.com.
75bca8 3  *
JM 4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.gitblit.git;
17
234933 18 import static org.eclipse.jgit.transport.BasePackPushConnection.CAPABILITY_SIDE_BAND_64K;
75bca8 19 import groovy.lang.Binding;
JM 20 import groovy.util.GroovyScriptEngine;
21
22 import java.io.File;
23 import java.io.IOException;
24 import java.text.MessageFormat;
25 import java.util.Collection;
26 import java.util.LinkedHashSet;
27 import java.util.List;
28 import java.util.Set;
234933 29 import java.util.concurrent.TimeUnit;
75bca8 30
234933 31 import org.eclipse.jgit.lib.BatchRefUpdate;
JM 32 import org.eclipse.jgit.lib.NullProgressMonitor;
75bca8 33 import org.eclipse.jgit.lib.PersonIdent;
234933 34 import org.eclipse.jgit.lib.ProgressMonitor;
JM 35 import org.eclipse.jgit.lib.Repository;
75bca8 36 import org.eclipse.jgit.revwalk.RevCommit;
JM 37 import org.eclipse.jgit.transport.PostReceiveHook;
38 import org.eclipse.jgit.transport.PreReceiveHook;
39 import org.eclipse.jgit.transport.ReceiveCommand;
40 import org.eclipse.jgit.transport.ReceiveCommand.Result;
41 import org.eclipse.jgit.transport.ReceivePack;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
44
f01bc5 45 import com.gitblit.Constants;
75bca8 46 import com.gitblit.Constants.AccessRestrictionType;
db4f6b 47 import com.gitblit.IStoredSettings;
75bca8 48 import com.gitblit.Keys;
JM 49 import com.gitblit.client.Translation;
819efd 50 import com.gitblit.extensions.ReceiveHook;
3a9e76 51 import com.gitblit.manager.IGitblit;
75bca8 52 import com.gitblit.models.RepositoryModel;
JM 53 import com.gitblit.models.UserModel;
5e3521 54 import com.gitblit.tickets.BranchTicketService;
978820 55 import com.gitblit.utils.ArrayUtils;
75bca8 56 import com.gitblit.utils.ClientLogger;
f01bc5 57 import com.gitblit.utils.CommitCache;
75bca8 58 import com.gitblit.utils.JGitUtils;
ff7d3c 59 import com.gitblit.utils.RefLogUtils;
75bca8 60 import com.gitblit.utils.StringUtils;
JM 61
234933 62
75bca8 63 /**
234933 64  * GitblitReceivePack processes receive commands.  It also executes Groovy pre-
JM 65  * and post- receive hooks.
66  *
67  * The general execution flow is:
68  * <ol>
69  *    <li>onPreReceive()</li>
70  *    <li>executeCommands()</li>
71  *    <li>onPostReceive()</li>
72  * </ol>
73  *
74  * @author Android Open Source Project
75bca8 75  * @author James Moger
234933 76  *
75bca8 77  */
234933 78 public class GitblitReceivePack extends ReceivePack implements PreReceiveHook, PostReceiveHook {
75bca8 79
234933 80     private static final Logger LOGGER = LoggerFactory.getLogger(GitblitReceivePack.class);
75bca8 81
234933 82     protected final RepositoryModel repository;
JM 83
84     protected final UserModel user;
85
86     protected final File groovyDir;
75bca8 87
JM 88     protected String gitblitUrl;
89
234933 90     protected GroovyScriptEngine gse;
75bca8 91
d1cb28 92     protected final IStoredSettings settings;
cacf8b 93
d1cb28 94     protected final IGitblit gitblit;
cacf8b 95
JM 96     public GitblitReceivePack(
3a9e76 97             IGitblit gitblit,
cacf8b 98             Repository db,
JM 99             RepositoryModel repository,
100             UserModel user) {
101
234933 102         super(db);
325396 103         this.settings = gitblit.getSettings();
JM 104         this.gitblit = gitblit;
234933 105         this.repository = repository;
f8f6aa 106         this.user = user;
325396 107         this.groovyDir = gitblit.getHooksFolder();
75bca8 108         try {
JM 109             // set Grape root
325396 110             File grapeRoot = gitblit.getGrapesFolder();
75bca8 111             grapeRoot.mkdirs();
JM 112             System.setProperty("grape.root", grapeRoot.getAbsolutePath());
234933 113             this.gse = new GroovyScriptEngine(groovyDir.getAbsolutePath());
75bca8 114         } catch (IOException e) {
JM 115         }
234933 116
JM 117         // set advanced ref permissions
118         setAllowCreates(user.canCreateRef(repository));
119         setAllowDeletes(user.canDeleteRef(repository));
120         setAllowNonFastForwards(user.canRewindRef(repository));
699e71 121
7baf2e 122         int maxObjectSz = settings.getInteger(Keys.git.maxObjectSizeLimit, -1);
JM 123         if (maxObjectSz >= 0) {
124             setMaxObjectSizeLimit(maxObjectSz);
125         }
126         int maxPackSz = settings.getInteger(Keys.git.maxPackSizeLimit, -1);
127         if (maxPackSz >= 0) {
128             setMaxPackSizeLimit(maxPackSz);
129         }
130         setCheckReceivedObjects(settings.getBoolean(Keys.git.checkReceivedObjects, true));
131         setCheckReferencedObjectsAreReachable(settings.getBoolean(Keys.git.checkReferencedObjectsAreReachable, true));
132
234933 133         // setup pre and post receive hook
JM 134         setPreReceiveHook(this);
135         setPostReceiveHook(this);
75bca8 136     }
JM 137
138     /**
a66312 139      * Returns true if the user is permitted to apply the receive commands to
JM 140      * the repository.
141      *
142      * @param commands
143      * @return true if the user may push these commands
144      */
145     protected boolean canPush(Collection<ReceiveCommand> commands) {
146         // TODO Consider supporting branch permissions here (issue-36)
147         // Not sure if that should be Gerrit-style, refs/meta/config, or
148         // gitolite-style, permissions in users.conf
149         //
150         // How could commands be empty?
151         //
152         // Because a subclass, like PatchsetReceivePack, filters receive
153         // commands before this method is called.  This makes it possible for
154         // this method to test an empty list.  In this case, we assume that the
155         // subclass receive pack properly enforces push restrictions. for the
156         // ref.
157         //
158         // The empty test is not explicitly required, it's written here to
159         // clarify special-case behavior.
160
161         return commands.isEmpty() ? true : user.canPush(repository);
162     }
163
164     /**
75bca8 165      * Instrumentation point where the incoming push event has been parsed,
JM 166      * validated, objects created BUT refs have not been updated. You might
167      * use this to enforce a branch-write permissions model.
168      */
169     @Override
170     public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
819efd 171
JM 172         if (commands.size() == 0) {
173             // no receive commands to process
174             // this can happen if receive pack subclasses intercept and filter
175             // the commands
176             LOGGER.debug("skipping pre-receive processing, no refs created, updated, or removed");
177             return;
178         }
234933 179
c44dd0 180         if (repository.isMirror) {
JM 181             // repository is a mirror
182             for (ReceiveCommand cmd : commands) {
183                 sendRejection(cmd, "Gitblit does not allow pushes to \"{0}\" because it is a mirror!", repository.name);
184             }
185             return;
186         }
187
75bca8 188         if (repository.isFrozen) {
JM 189             // repository is frozen/readonly
190             for (ReceiveCommand cmd : commands) {
234933 191                 sendRejection(cmd, "Gitblit does not allow pushes to \"{0}\" because it is frozen!", repository.name);
75bca8 192             }
JM 193             return;
194         }
234933 195
75bca8 196         if (!repository.isBare) {
JM 197             // repository has a working copy
198             for (ReceiveCommand cmd : commands) {
234933 199                 sendRejection(cmd, "Gitblit does not allow pushes to \"{0}\" because it has a working copy!", repository.name);
75bca8 200             }
JM 201             return;
202         }
203
a66312 204         if (!canPush(commands)) {
75bca8 205             // user does not have push permissions
JM 206             for (ReceiveCommand cmd : commands) {
234933 207                 sendRejection(cmd, "User \"{0}\" does not have push permissions for \"{1}\"!", user.username, repository.name);
75bca8 208             }
JM 209             return;
210         }
211
212         if (repository.accessRestriction.atLeast(AccessRestrictionType.PUSH) && repository.verifyCommitter) {
213             // enforce committer verification
214             if (StringUtils.isEmpty(user.emailAddress)) {
f19b78 215                 // reject the push because the pushing account does not have an email address
JM 216                 for (ReceiveCommand cmd : commands) {
217                     sendRejection(cmd, "Sorry, the account \"{0}\" does not have an email address set for committer verification!", user.username);
218                 }
219                 return;
75bca8 220             }
JM 221
234933 222             // Optionally enforce that the committer of first parent chain
75bca8 223             // match the account being used to push the commits.
234933 224             //
75bca8 225             // This requires all merge commits are executed with the "--no-ff"
JM 226             // option to force a merge commit even if fast-forward is possible.
234933 227             // This ensures that the chain first parents has the commit
75bca8 228             // identity of the merging user.
JM 229             boolean allRejected = false;
230             for (ReceiveCommand cmd : commands) {
234933 231                 String firstParent = null;
75bca8 232                 try {
JM 233                     List<RevCommit> commits = JGitUtils.getRevLog(rp.getRepository(), cmd.getOldId().name(), cmd.getNewId().name());
234                     for (RevCommit commit : commits) {
234933 235
JM 236                         if (firstParent != null) {
237                             if (!commit.getName().equals(firstParent)) {
d1dc77 238                                 // ignore: commit is right-descendant of a merge
JM 239                                 continue;
240                             }
241                         }
234933 242
d1dc77 243                         // update expected next commit id
JM 244                         if (commit.getParentCount() == 0) {
234933 245                             firstParent = null;
d1dc77 246                         } else {
234933 247                             firstParent = commit.getParents()[0].getId().getName();
d1dc77 248                         }
234933 249
75bca8 250                         PersonIdent committer = commit.getCommitterIdent();
JM 251                         if (!user.is(committer.getName(), committer.getEmailAddress())) {
f19b78 252                             // verification failed
JM 253                             String reason = MessageFormat.format("{0} by {1} <{2}> was not committed by {3} ({4}) <{5}>",
254                                     commit.getId().name(), committer.getName(), StringUtils.isEmpty(committer.getEmailAddress()) ? "?":committer.getEmailAddress(), user.getDisplayName(), user.username, user.emailAddress);
234933 255                             LOGGER.warn(reason);
75bca8 256                             cmd.setResult(Result.REJECTED_OTHER_REASON, reason);
JM 257                             allRejected &= true;
258                             break;
259                         } else {
260                             allRejected = false;
261                         }
262                     }
263                 } catch (Exception e) {
234933 264                     LOGGER.error("Failed to verify commits were made by pushing user", e);
75bca8 265                 }
JM 266             }
267
268             if (allRejected) {
269                 // all ref updates rejected, abort
270                 return;
271             }
272         }
234933 273
f01bc5 274         for (ReceiveCommand cmd : commands) {
JM 275             String ref = cmd.getRefName();
276             if (ref.startsWith(Constants.R_HEADS)) {
277                 switch (cmd.getType()) {
278                 case UPDATE_NONFASTFORWARD:
279                 case DELETE:
d1cb28 280                     // reset branch commit cache on REWIND and DELETE
f01bc5 281                     CommitCache.instance().clear(repository.name, ref);
JM 282                     break;
283                 default:
284                     break;
285                 }
5e3521 286             } else if (ref.equals(BranchTicketService.BRANCH)) {
JM 287                 // ensure pushing user is an administrator OR an owner
288                 // i.e. prevent ticket tampering
289                 boolean permitted = user.canAdmin() || repository.isOwner(user.username);
290                 if (!permitted) {
291                     sendRejection(cmd, "{0} is not permitted to push to {1}", user.username, ref);
292                 }
293             } else if (ref.startsWith(Constants.R_FOR)) {
294                 // prevent accidental push to refs/for
295                 sendRejection(cmd, "{0} is not configured to receive patchsets", repository.name);
f01bc5 296             }
JM 297         }
75bca8 298
819efd 299         // call pre-receive plugins
JM 300         for (ReceiveHook hook : gitblit.getExtensions(ReceiveHook.class)) {
301             try {
302                 hook.onPreReceive(this, commands);
303             } catch (Exception e) {
304                 LOGGER.error("Failed to execute extension", e);
305             }
306         }
307
75bca8 308         Set<String> scripts = new LinkedHashSet<String>();
325396 309         scripts.addAll(gitblit.getPreReceiveScriptsInherited(repository));
978820 310         if (!ArrayUtils.isEmpty(repository.preReceiveScripts)) {
JM 311             scripts.addAll(repository.preReceiveScripts);
312         }
234933 313         runGroovy(commands, scripts);
75bca8 314         for (ReceiveCommand cmd : commands) {
JM 315             if (!Result.NOT_ATTEMPTED.equals(cmd.getResult())) {
234933 316                 LOGGER.warn(MessageFormat.format("{0} {1} because \"{2}\"", cmd.getNewId()
75bca8 317                         .getName(), cmd.getResult(), cmd.getMessage()));
JM 318             }
319         }
320     }
321
322     /**
323      * Instrumentation point where the incoming push has been applied to the
324      * repository. This is the point where we would trigger a Jenkins build
325      * or send an email.
326      */
327     @Override
328     public void onPostReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
329         if (commands.size() == 0) {
819efd 330             LOGGER.debug("skipping post-receive processing, no refs created, updated, or removed");
75bca8 331             return;
JM 332         }
333
ce07c4 334         boolean isRefCreationOrDeletion = false;
JM 335
75bca8 336         // log ref changes
JM 337         for (ReceiveCommand cmd : commands) {
234933 338
75bca8 339             if (Result.OK.equals(cmd.getResult())) {
JM 340                 // add some logging for important ref changes
341                 switch (cmd.getType()) {
342                 case DELETE:
234933 343                     LOGGER.info(MessageFormat.format("{0} DELETED {1} in {2} ({3})", user.username, cmd.getRefName(), repository.name, cmd.getOldId().name()));
ce07c4 344                     isRefCreationOrDeletion = true;
75bca8 345                     break;
JM 346                 case CREATE:
234933 347                     LOGGER.info(MessageFormat.format("{0} CREATED {1} in {2}", user.username, cmd.getRefName(), repository.name));
ce07c4 348                     isRefCreationOrDeletion = true;
75bca8 349                     break;
JM 350                 case UPDATE:
234933 351                     LOGGER.info(MessageFormat.format("{0} UPDATED {1} in {2} (from {3} to {4})", user.username, cmd.getRefName(), repository.name, cmd.getOldId().name(), cmd.getNewId().name()));
75bca8 352                     break;
JM 353                 case UPDATE_NONFASTFORWARD:
234933 354                     LOGGER.info(MessageFormat.format("{0} UPDATED NON-FAST-FORWARD {1} in {2} (from {3} to {4})", user.username, cmd.getRefName(), repository.name, cmd.getOldId().name(), cmd.getNewId().name()));
75bca8 355                     break;
JM 356                 default:
357                     break;
358                 }
359             }
360         }
361
ce07c4 362         if (isRefCreationOrDeletion) {
JM 363             gitblit.resetRepositoryCache(repository.name);
364         }
365
75bca8 366         if (repository.useIncrementalPushTags) {
JM 367             // tag each pushed branch tip
368             String emailAddress = user.emailAddress == null ? rp.getRefLogIdent().getEmailAddress() : user.emailAddress;
369             PersonIdent userIdent = new PersonIdent(user.getDisplayName(), emailAddress);
370
371             for (ReceiveCommand cmd : commands) {
234933 372                 if (!cmd.getRefName().startsWith(Constants.R_HEADS)) {
75bca8 373                     // only tag branch ref changes
JM 374                     continue;
375                 }
376
377                 if (!ReceiveCommand.Type.DELETE.equals(cmd.getType())
378                         && ReceiveCommand.Result.OK.equals(cmd.getResult())) {
379                     String objectId = cmd.getNewId().getName();
234933 380                     String branch = cmd.getRefName().substring(Constants.R_HEADS.length());
75bca8 381                     // get translation based on the server's locale setting
JM 382                     String template = Translation.get("gb.incrementalPushTagMessage");
383                     String msg = MessageFormat.format(template, branch);
384                     String prefix;
385                     if (StringUtils.isEmpty(repository.incrementalPushTagPrefix)) {
db4f6b 386                         prefix = settings.getString(Keys.git.defaultIncrementalPushTagPrefix, "r");
75bca8 387                     } else {
JM 388                         prefix = repository.incrementalPushTagPrefix;
389                     }
390
391                     JGitUtils.createIncrementalRevisionTag(
392                             rp.getRepository(),
393                             objectId,
394                             userIdent,
395                             prefix,
396                             "0",
397                             msg);
398                 }
234933 399             }
75bca8 400         }
JM 401
402         // update push log
403         try {
ff7d3c 404             RefLogUtils.updateRefLog(user, rp.getRepository(), commands);
234933 405             LOGGER.debug(MessageFormat.format("{0} push log updated", repository.name));
75bca8 406         } catch (Exception e) {
234933 407             LOGGER.error(MessageFormat.format("Failed to update {0} pushlog", repository.name), e);
75bca8 408         }
988334 409
e462bb 410         // check for updates pushed to the BranchTicketService branch
988334 411         // if the BranchTicketService is active it will reindex, as appropriate
e462bb 412         for (ReceiveCommand cmd : commands) {
JM 413             if (Result.OK.equals(cmd.getResult())
414                     && BranchTicketService.BRANCH.equals(cmd.getRefName())) {
988334 415                 rp.getRepository().fireEvent(new ReceiveCommandEvent(repository, cmd));
e462bb 416             }
JM 417         }
db4f6b 418
819efd 419         // call post-receive plugins
JM 420         for (ReceiveHook hook : gitblit.getExtensions(ReceiveHook.class)) {
421             try {
422                 hook.onPostReceive(this, commands);
423             } catch (Exception e) {
424                 LOGGER.error("Failed to execute extension", e);
425             }
426         }
427
234933 428         // run Groovy hook scripts
75bca8 429         Set<String> scripts = new LinkedHashSet<String>();
325396 430         scripts.addAll(gitblit.getPostReceiveScriptsInherited(repository));
978820 431         if (!ArrayUtils.isEmpty(repository.postReceiveScripts)) {
JM 432             scripts.addAll(repository.postReceiveScripts);
433         }
234933 434         runGroovy(commands, scripts);
JM 435     }
436
437     /** Execute commands to update references. */
438     @Override
439     protected void executeCommands() {
440         List<ReceiveCommand> toApply = filterCommands(Result.NOT_ATTEMPTED);
441         if (toApply.isEmpty()) {
442             return;
443         }
444
445         ProgressMonitor updating = NullProgressMonitor.INSTANCE;
446         boolean sideBand = isCapabilityEnabled(CAPABILITY_SIDE_BAND_64K);
447         if (sideBand) {
448             SideBandProgressMonitor pm = new SideBandProgressMonitor(msgOut);
449             pm.setDelayStart(250, TimeUnit.MILLISECONDS);
450             updating = pm;
451         }
452
453         BatchRefUpdate batch = getRepository().getRefDatabase().newBatchUpdate();
454         batch.setAllowNonFastForwards(isAllowNonFastForwards());
455         batch.setRefLogIdent(getRefLogIdent());
456         batch.setRefLogMessage("push", true);
457
458         for (ReceiveCommand cmd : toApply) {
459             if (Result.NOT_ATTEMPTED != cmd.getResult()) {
460                 // Already rejected by the core receive process.
461                 continue;
462             }
463             batch.addCommand(cmd);
464         }
465
466         if (!batch.getCommands().isEmpty()) {
467             try {
468                 batch.execute(getRevWalk(), updating);
469             } catch (IOException err) {
470                 for (ReceiveCommand cmd : toApply) {
471                     if (cmd.getResult() == Result.NOT_ATTEMPTED) {
472                         sendRejection(cmd, "lock error: {0}", err.getMessage());
473                     }
474                 }
475             }
476         }
477     }
478
479     protected void setGitblitUrl(String url) {
480         this.gitblitUrl = url;
481     }
482
819efd 483     public void sendRejection(final ReceiveCommand cmd, final String why, Object... objects) {
234933 484         String text;
JM 485         if (ArrayUtils.isEmpty(objects)) {
486             text = why;
487         } else {
488             text = MessageFormat.format(why, objects);
489         }
490         cmd.setResult(Result.REJECTED_OTHER_REASON, text);
491         LOGGER.error(text + " (" + user.username + ")");
492     }
493
819efd 494     public void sendHeader(String msg, Object... objects) {
d1cb28 495         sendInfo("--> ", msg, objects);
4360b3 496     }
JM 497
819efd 498     public void sendInfo(String msg, Object... objects) {
d1cb28 499         sendInfo("    ", msg, objects);
4360b3 500     }
JM 501
819efd 502     private void sendInfo(String prefix, String msg, Object... objects) {
234933 503         String text;
JM 504         if (ArrayUtils.isEmpty(objects)) {
505             text = msg;
4360b3 506             super.sendMessage(prefix + msg);
234933 507         } else {
JM 508             text = MessageFormat.format(msg, objects);
4360b3 509             super.sendMessage(prefix + text);
234933 510         }
da9941 511         if (!StringUtils.isEmpty(msg)) {
JM 512             LOGGER.info(text + " (" + user.username + ")");
513         }
234933 514     }
JM 515
819efd 516     public void sendError(String msg, Object... objects) {
234933 517         String text;
JM 518         if (ArrayUtils.isEmpty(objects)) {
519             text = msg;
520             super.sendError(msg);
521         } else {
522             text = MessageFormat.format(msg, objects);
523             super.sendError(text);
524         }
da9941 525         if (!StringUtils.isEmpty(msg)) {
JM 526             LOGGER.error(text + " (" + user.username + ")");
527         }
75bca8 528     }
JM 529
530     /**
531      * Runs the specified Groovy hook scripts.
234933 532      *
75bca8 533      * @param repository
JM 534      * @param user
535      * @param commands
536      * @param scripts
537      */
4360b3 538     private void runGroovy(Collection<ReceiveCommand> commands, Set<String> scripts) {
75bca8 539         if (scripts == null || scripts.size() == 0) {
JM 540             // no Groovy scripts to execute
541             return;
542         }
543
544         Binding binding = new Binding();
325396 545         binding.setVariable("gitblit", gitblit);
75bca8 546         binding.setVariable("repository", repository);
234933 547         binding.setVariable("receivePack", this);
75bca8 548         binding.setVariable("user", user);
JM 549         binding.setVariable("commands", commands);
550         binding.setVariable("url", gitblitUrl);
234933 551         binding.setVariable("logger", LOGGER);
JM 552         binding.setVariable("clientLogger", new ClientLogger(this));
75bca8 553         for (String script : scripts) {
JM 554             if (StringUtils.isEmpty(script)) {
555                 continue;
556             }
557             // allow script to be specified without .groovy extension
558             // this is easier to read in the settings
559             File file = new File(groovyDir, script);
560             if (!file.exists() && !script.toLowerCase().endsWith(".groovy")) {
561                 file = new File(groovyDir, script + ".groovy");
562                 if (file.exists()) {
563                     script = file.getName();
564                 }
565             }
566             try {
567                 Object result = gse.run(script, binding);
568                 if (result instanceof Boolean) {
569                     if (!((Boolean) result)) {
234933 570                         LOGGER.error(MessageFormat.format(
75bca8 571                                 "Groovy script {0} has failed!  Hook scripts aborted.", script));
JM 572                         break;
573                     }
574                 }
575             } catch (Exception e) {
234933 576                 LOGGER.error(
75bca8 577                         MessageFormat.format("Failed to execute Groovy script {0}", script), e);
JM 578             }
579         }
580     }
819efd 581
JM 582     public IGitblit getGitblit() {
583         return gitblit;
584     }
585
586     public RepositoryModel getRepositoryModel() {
587         return repository;
588     }
589
590     public UserModel getUserModel() {
591         return user;
592     }
234933 593 }