Commit e16f04cf authored by Tom Lane's avatar Tom Lane

Make CREATE/ALTER/DROP USER/GROUP transaction-safe, or at least pretty

nearly so, by postponing write of flat password file until transaction
commit.
parent de9d7f4b
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
* *
* *
* IDENTIFICATION * IDENTIFICATION
* $Header: /cvsroot/pgsql/src/backend/access/transam/xact.c,v 1.132 2002/09/04 20:31:13 momjian Exp $ * $Header: /cvsroot/pgsql/src/backend/access/transam/xact.c,v 1.133 2002/10/21 19:46:45 tgl Exp $
* *
* NOTES * NOTES
* Transaction aborts can now occur two ways: * Transaction aborts can now occur two ways:
...@@ -167,6 +167,7 @@ ...@@ -167,6 +167,7 @@
#include "catalog/namespace.h" #include "catalog/namespace.h"
#include "commands/async.h" #include "commands/async.h"
#include "commands/trigger.h" #include "commands/trigger.h"
#include "commands/user.h"
#include "executor/spi.h" #include "executor/spi.h"
#include "libpq/be-fsstubs.h" #include "libpq/be-fsstubs.h"
#include "miscadmin.h" #include "miscadmin.h"
...@@ -959,18 +960,25 @@ CommitTransaction(void) ...@@ -959,18 +960,25 @@ CommitTransaction(void)
s->state = TRANS_COMMIT; s->state = TRANS_COMMIT;
/* /*
* do commit processing * Do pre-commit processing (most of this stuff requires database
* access, and in fact could still cause an error...)
*/ */
AtEOXact_portals();
/* handle commit for large objects [ PA, 7/17/98 ] */ /* handle commit for large objects [ PA, 7/17/98 ] */
/* XXX probably this does not belong here */
lo_commit(true); lo_commit(true);
/* NOTIFY commit must also come before lower-level cleanup */ /* NOTIFY commit must come before lower-level cleanup */
AtCommit_Notify(); AtCommit_Notify();
AtEOXact_portals(); /* Update the flat password file if we changed pg_shadow or pg_group */
AtEOXact_UpdatePasswordFile(true);
/* Here is where we really truly commit. */ /*
* Here is where we really truly commit.
*/
RecordTransactionCommit(); RecordTransactionCommit();
/* /*
...@@ -1013,7 +1021,6 @@ CommitTransaction(void) ...@@ -1013,7 +1021,6 @@ CommitTransaction(void)
AtEOXact_CatCache(true); AtEOXact_CatCache(true);
AtCommit_Memory(); AtCommit_Memory();
AtEOXact_Buffers(true); AtEOXact_Buffers(true);
smgrabort();
AtEOXact_Files(); AtEOXact_Files();
/* Count transaction commit in statistics collector */ /* Count transaction commit in statistics collector */
...@@ -1080,9 +1087,10 @@ AbortTransaction(void) ...@@ -1080,9 +1087,10 @@ AbortTransaction(void)
* do abort processing * do abort processing
*/ */
DeferredTriggerAbortXact(); DeferredTriggerAbortXact();
AtEOXact_portals();
lo_commit(false); /* 'false' means it's abort */ lo_commit(false); /* 'false' means it's abort */
AtAbort_Notify(); AtAbort_Notify();
AtEOXact_portals(); AtEOXact_UpdatePasswordFile(false);
/* Advertise the fact that we aborted in pg_clog. */ /* Advertise the fact that we aborted in pg_clog. */
RecordTransactionAbort(); RecordTransactionAbort();
...@@ -1114,6 +1122,7 @@ AbortTransaction(void) ...@@ -1114,6 +1122,7 @@ AbortTransaction(void)
AtEOXact_CatCache(false); AtEOXact_CatCache(false);
AtAbort_Memory(); AtAbort_Memory();
AtEOXact_Buffers(false); AtEOXact_Buffers(false);
smgrabort();
AtEOXact_Files(); AtEOXact_Files();
AtAbort_Locks(); AtAbort_Locks();
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
* Portions Copyright (c) 1996-2002, PostgreSQL Global Development Group * Portions Copyright (c) 1996-2002, PostgreSQL Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California * Portions Copyright (c) 1994, Regents of the University of California
* *
* $Header: /cvsroot/pgsql/src/backend/commands/user.c,v 1.112 2002/09/14 13:46:23 petere Exp $ * $Header: /cvsroot/pgsql/src/backend/commands/user.c,v 1.113 2002/10/21 19:46:45 tgl Exp $
* *
*------------------------------------------------------------------------- *-------------------------------------------------------------------------
*/ */
...@@ -37,8 +37,16 @@ ...@@ -37,8 +37,16 @@
#include "utils/syscache.h" #include "utils/syscache.h"
#define PWD_FILE "pg_pwd"
#define USER_GROUP_FILE "pg_group"
extern bool Password_encryption; extern bool Password_encryption;
static bool user_file_update_needed = false;
static bool group_file_update_needed = false;
static void CheckPgUserAclNotNull(void); static void CheckPgUserAclNotNull(void);
static void UpdateGroupMembership(Relation group_rel, HeapTuple group_tuple, static void UpdateGroupMembership(Relation group_rel, HeapTuple group_tuple,
List *members); List *members);
...@@ -67,7 +75,6 @@ fputs_quote(char *str, FILE *fp) ...@@ -67,7 +75,6 @@ fputs_quote(char *str, FILE *fp)
} }
/* /*
* group_getfilename --- get full pathname of group file * group_getfilename --- get full pathname of group file
* *
...@@ -88,9 +95,9 @@ group_getfilename(void) ...@@ -88,9 +95,9 @@ group_getfilename(void)
} }
/* /*
* Get full pathname of password file. * Get full pathname of password file.
*
* Note that result string is palloc'd, and should be freed by the caller. * Note that result string is palloc'd, and should be freed by the caller.
*/ */
char * char *
...@@ -108,12 +115,11 @@ user_getfilename(void) ...@@ -108,12 +115,11 @@ user_getfilename(void)
} }
/* /*
* write_group_file for trigger update_pg_pwd_and_pg_group * write_group_file: update the flat group file
*/ */
static void static void
write_group_file(Relation urel, Relation grel) write_group_file(Relation grel)
{ {
char *filename, char *filename,
*tempname; *tempname;
...@@ -132,15 +138,19 @@ write_group_file(Relation urel, Relation grel) ...@@ -132,15 +138,19 @@ write_group_file(Relation urel, Relation grel)
filename = group_getfilename(); filename = group_getfilename();
bufsize = strlen(filename) + 12; bufsize = strlen(filename) + 12;
tempname = (char *) palloc(bufsize); tempname = (char *) palloc(bufsize);
snprintf(tempname, bufsize, "%s.%d", filename, MyProcPid); snprintf(tempname, bufsize, "%s.%d", filename, MyProcPid);
oumask = umask((mode_t) 077); oumask = umask((mode_t) 077);
fp = AllocateFile(tempname, "w"); fp = AllocateFile(tempname, "w");
umask(oumask); umask(oumask);
if (fp == NULL) if (fp == NULL)
elog(ERROR, "write_group_file: unable to write %s: %m", tempname); elog(ERROR, "write_group_file: unable to write %s: %m", tempname);
/* read table */ /*
* Read pg_group and write the file. Note we use SnapshotSelf to ensure
* we see all effects of current transaction. (Perhaps could do a
* CommandCounterIncrement beforehand, instead?)
*/
scan = heap_beginscan(grel, SnapshotSelf, 0, NULL); scan = heap_beginscan(grel, SnapshotSelf, 0, NULL);
while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL) while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
{ {
...@@ -244,19 +254,8 @@ write_group_file(Relation urel, Relation grel) ...@@ -244,19 +254,8 @@ write_group_file(Relation urel, Relation grel)
} }
/* /*
* write_password_file for trigger update_pg_pwd_and_pg_group * write_user_file: update the flat password file
*
* copy the modified contents of pg_shadow to a file used by the postmaster
* for user authentication. The file is stored as $PGDATA/global/pg_pwd.
*
* This function set is both a trigger function for direct updates to pg_shadow
* as well as being called directly from create/alter/drop user.
*
* We raise an error to force transaction rollback if we detect an illegal
* username or password --- illegal being defined as values that would
* mess up the pg_pwd parser.
*/ */
static void static void
write_user_file(Relation urel) write_user_file(Relation urel)
...@@ -278,15 +277,19 @@ write_user_file(Relation urel) ...@@ -278,15 +277,19 @@ write_user_file(Relation urel)
filename = user_getfilename(); filename = user_getfilename();
bufsize = strlen(filename) + 12; bufsize = strlen(filename) + 12;
tempname = (char *) palloc(bufsize); tempname = (char *) palloc(bufsize);
snprintf(tempname, bufsize, "%s.%d", filename, MyProcPid); snprintf(tempname, bufsize, "%s.%d", filename, MyProcPid);
oumask = umask((mode_t) 077); oumask = umask((mode_t) 077);
fp = AllocateFile(tempname, "w"); fp = AllocateFile(tempname, "w");
umask(oumask); umask(oumask);
if (fp == NULL) if (fp == NULL)
elog(ERROR, "write_password_file: unable to write %s: %m", tempname); elog(ERROR, "write_user_file: unable to write %s: %m", tempname);
/* read table */ /*
* Read pg_shadow and write the file. Note we use SnapshotSelf to ensure
* we see all effects of current transaction. (Perhaps could do a
* CommandCounterIncrement beforehand, instead?)
*/
scan = heap_beginscan(urel, SnapshotSelf, 0, NULL); scan = heap_beginscan(urel, SnapshotSelf, 0, NULL);
while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL) while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
{ {
...@@ -328,10 +331,16 @@ write_user_file(Relation urel) ...@@ -328,10 +331,16 @@ write_user_file(Relation urel)
*/ */
i = strcspn(usename, "\n"); i = strcspn(usename, "\n");
if (usename[i] != '\0') if (usename[i] != '\0')
elog(ERROR, "Invalid user name '%s'", usename); {
elog(LOG, "Invalid user name '%s'", usename);
continue;
}
i = strcspn(passwd, "\n"); i = strcspn(passwd, "\n");
if (passwd[i] != '\0') if (passwd[i] != '\0')
elog(ERROR, "Invalid user password '%s'", passwd); {
elog(LOG, "Invalid user password '%s'", passwd);
continue;
}
/* /*
* The extra columns we emit here are not really necessary. To * The extra columns we emit here are not really necessary. To
...@@ -367,31 +376,84 @@ write_user_file(Relation urel) ...@@ -367,31 +376,84 @@ write_user_file(Relation urel)
} }
/*
/* This is the wrapper for triggers. */ * This trigger is fired whenever someone modifies pg_shadow or pg_group
* via general-purpose INSERT/UPDATE/DELETE commands.
*
* XXX should probably have two separate triggers.
*/
Datum Datum
update_pg_pwd_and_pg_group(PG_FUNCTION_ARGS) update_pg_pwd_and_pg_group(PG_FUNCTION_ARGS)
{ {
user_file_update_needed = true;
group_file_update_needed = true;
return PointerGetDatum(NULL);
}
/*
* This routine is called during transaction commit or abort.
*
* On commit, if we've written pg_shadow or pg_group during the current
* transaction, update the flat files and signal the postmaster.
*
* On abort, just reset the static flags so we don't try to do it on the
* next successful commit.
*
* NB: this should be the last step before actual transaction commit.
* If any error aborts the transaction after we run this code, the postmaster
* will still have received and cached the changed data; so minimize the
* window for such problems.
*/
void
AtEOXact_UpdatePasswordFile(bool isCommit)
{
Relation urel = NULL;
Relation grel = NULL;
if (! (user_file_update_needed || group_file_update_needed))
return;
if (! isCommit)
{
user_file_update_needed = false;
group_file_update_needed = false;
return;
}
/* /*
* ExclusiveLock ensures no one modifies pg_shadow while we read it, * We use ExclusiveLock to ensure that only one backend writes the flat
* and that only one backend rewrites the flat file at a time. It's * file(s) at a time. That's sufficient because it's okay to allow plain
* OK to allow normal reads of pg_shadow in parallel, however. * reads of the tables in parallel. There is some chance of a deadlock
* here (if we were triggered by a user update of pg_shadow or pg_group,
* which likely won't have gotten a strong enough lock), so get the locks
* we need before writing anything.
*/ */
Relation urel = heap_openr(ShadowRelationName, ExclusiveLock); if (user_file_update_needed)
Relation grel = heap_openr(GroupRelationName, ExclusiveLock); urel = heap_openr(ShadowRelationName, ExclusiveLock);
if (group_file_update_needed)
grel = heap_openr(GroupRelationName, ExclusiveLock);
write_user_file(urel); /* Okay to write the files */
write_group_file(urel, grel); if (user_file_update_needed)
/* OK to release lock, since we did not modify the relation */ {
heap_close(grel, ExclusiveLock); user_file_update_needed = false;
heap_close(urel, ExclusiveLock); write_user_file(urel);
heap_close(urel, NoLock);
}
if (group_file_update_needed)
{
group_file_update_needed = false;
write_group_file(grel);
heap_close(grel, NoLock);
}
/* /*
* Signal the postmaster to reload its password & group-file cache. * Signal the postmaster to reload its password & group-file cache.
*/ */
SendPostmasterSignal(PMSIGNAL_PASSWORD_CHANGE); SendPostmasterSignal(PMSIGNAL_PASSWORD_CHANGE);
return PointerGetDatum(NULL);
} }
...@@ -515,7 +577,7 @@ CreateUser(CreateUserStmt *stmt) ...@@ -515,7 +577,7 @@ CreateUser(CreateUserStmt *stmt)
* Scan the pg_shadow relation to be certain the user or id doesn't * Scan the pg_shadow relation to be certain the user or id doesn't
* already exist. Note we secure exclusive lock, because we also need * already exist. Note we secure exclusive lock, because we also need
* to be sure of what the next usesysid should be, and we need to * to be sure of what the next usesysid should be, and we need to
* protect our update of the flat password file. * protect our eventual update of the flat password file.
*/ */
pg_shadow_rel = heap_openr(ShadowRelationName, ExclusiveLock); pg_shadow_rel = heap_openr(ShadowRelationName, ExclusiveLock);
pg_shadow_dsc = RelationGetDescr(pg_shadow_rel); pg_shadow_dsc = RelationGetDescr(pg_shadow_rel);
...@@ -619,14 +681,15 @@ CreateUser(CreateUserStmt *stmt) ...@@ -619,14 +681,15 @@ CreateUser(CreateUserStmt *stmt)
} }
/* /*
* Now we can clean up; but keep lock until commit. * Now we can clean up; but keep lock until commit (to avoid possible
* deadlock when commit code tries to acquire lock).
*/ */
heap_close(pg_shadow_rel, NoLock); heap_close(pg_shadow_rel, NoLock);
/* /*
* Write the updated pg_shadow and pg_group data to the flat file. * Set flag to update flat password file at commit.
*/ */
update_pg_pwd_and_pg_group(NULL); user_file_update_needed = true;
} }
...@@ -718,10 +781,6 @@ AlterUser(AlterUserStmt *stmt) ...@@ -718,10 +781,6 @@ AlterUser(AlterUserStmt *stmt)
strcmp(GetUserNameFromId(GetUserId()), stmt->user) == 0)) strcmp(GetUserNameFromId(GetUserId()), stmt->user) == 0))
elog(ERROR, "ALTER USER: permission denied"); elog(ERROR, "ALTER USER: permission denied");
/* changes to the flat password file cannot be rolled back */
if (IsTransactionBlock() && password)
elog(NOTICE, "ALTER USER: password changes cannot be rolled back");
/* /*
* Scan the pg_shadow relation to be certain the user exists. Note we * Scan the pg_shadow relation to be certain the user exists. Note we
* secure exclusive lock to protect our update of the flat password * secure exclusive lock to protect our update of the flat password
...@@ -807,14 +866,15 @@ AlterUser(AlterUserStmt *stmt) ...@@ -807,14 +866,15 @@ AlterUser(AlterUserStmt *stmt)
heap_freetuple(new_tuple); heap_freetuple(new_tuple);
/* /*
* Now we can clean up. * Now we can clean up; but keep lock until commit (to avoid possible
* deadlock when commit code tries to acquire lock).
*/ */
heap_close(pg_shadow_rel, NoLock); heap_close(pg_shadow_rel, NoLock);
/* /*
* Write the updated pg_shadow and pg_group data to the flat file. * Set flag to update flat password file at commit.
*/ */
update_pg_pwd_and_pg_group(NULL); user_file_update_needed = true;
} }
...@@ -902,9 +962,6 @@ DropUser(DropUserStmt *stmt) ...@@ -902,9 +962,6 @@ DropUser(DropUserStmt *stmt)
if (!superuser()) if (!superuser())
elog(ERROR, "DROP USER: permission denied"); elog(ERROR, "DROP USER: permission denied");
if (IsTransactionBlock())
elog(NOTICE, "DROP USER cannot be rolled back completely");
/* /*
* Scan the pg_shadow relation to find the usesysid of the user to be * Scan the pg_shadow relation to find the usesysid of the user to be
* deleted. Note we secure exclusive lock, because we need to protect * deleted. Note we secure exclusive lock, because we need to protect
...@@ -1017,14 +1074,15 @@ DropUser(DropUserStmt *stmt) ...@@ -1017,14 +1074,15 @@ DropUser(DropUserStmt *stmt)
} }
/* /*
* Now we can clean up. * Now we can clean up; but keep lock until commit (to avoid possible
* deadlock when commit code tries to acquire lock).
*/ */
heap_close(pg_shadow_rel, NoLock); heap_close(pg_shadow_rel, NoLock);
/* /*
* Write the updated pg_shadow and pg_group data to the flat file. * Set flag to update flat password file at commit.
*/ */
update_pg_pwd_and_pg_group(NULL); user_file_update_needed = true;
} }
...@@ -1125,6 +1183,12 @@ CreateGroup(CreateGroupStmt *stmt) ...@@ -1125,6 +1183,12 @@ CreateGroup(CreateGroupStmt *stmt)
elog(ERROR, "CREATE GROUP: group name \"%s\" is reserved", elog(ERROR, "CREATE GROUP: group name \"%s\" is reserved",
stmt->name); stmt->name);
/*
* Scan the pg_group relation to be certain the group or id doesn't
* already exist. Note we secure exclusive lock, because we also need
* to be sure of what the next grosysid should be, and we need to
* protect our eventual update of the flat group file.
*/
pg_group_rel = heap_openr(GroupRelationName, ExclusiveLock); pg_group_rel = heap_openr(GroupRelationName, ExclusiveLock);
pg_group_dsc = RelationGetDescr(pg_group_rel); pg_group_dsc = RelationGetDescr(pg_group_rel);
...@@ -1200,16 +1264,19 @@ CreateGroup(CreateGroupStmt *stmt) ...@@ -1200,16 +1264,19 @@ CreateGroup(CreateGroupStmt *stmt)
/* Update indexes */ /* Update indexes */
CatalogUpdateIndexes(pg_group_rel, tuple); CatalogUpdateIndexes(pg_group_rel, tuple);
/*
* Now we can clean up; but keep lock until commit (to avoid possible
* deadlock when commit code tries to acquire lock).
*/
heap_close(pg_group_rel, NoLock); heap_close(pg_group_rel, NoLock);
/* /*
* Write the updated pg_shadow and pg_group data to the flat file. * Set flag to update flat group file at commit.
*/ */
update_pg_pwd_and_pg_group(NULL); group_file_update_needed = true;
} }
/* /*
* ALTER GROUP * ALTER GROUP
*/ */
...@@ -1231,6 +1298,9 @@ AlterGroup(AlterGroupStmt *stmt, const char *tag) ...@@ -1231,6 +1298,9 @@ AlterGroup(AlterGroupStmt *stmt, const char *tag)
if (!superuser()) if (!superuser())
elog(ERROR, "%s: permission denied", tag); elog(ERROR, "%s: permission denied", tag);
/*
* Secure exclusive lock to protect our update of the flat group file.
*/
pg_group_rel = heap_openr(GroupRelationName, ExclusiveLock); pg_group_rel = heap_openr(GroupRelationName, ExclusiveLock);
pg_group_dsc = RelationGetDescr(pg_group_rel); pg_group_dsc = RelationGetDescr(pg_group_rel);
...@@ -1345,14 +1415,15 @@ AlterGroup(AlterGroupStmt *stmt, const char *tag) ...@@ -1345,14 +1415,15 @@ AlterGroup(AlterGroupStmt *stmt, const char *tag)
ReleaseSysCache(group_tuple); ReleaseSysCache(group_tuple);
/* /*
* Write the updated pg_shadow and pg_group data to the flat files. * Now we can clean up; but keep lock until commit (to avoid possible
* deadlock when commit code tries to acquire lock).
*/ */
heap_close(pg_group_rel, NoLock); heap_close(pg_group_rel, NoLock);
/* /*
* Write the updated pg_shadow and pg_group data to the flat file. * Set flag to update flat group file at commit.
*/ */
update_pg_pwd_and_pg_group(NULL); group_file_update_needed = true;
} }
/* /*
...@@ -1465,10 +1536,12 @@ DropGroup(DropGroupStmt *stmt) ...@@ -1465,10 +1536,12 @@ DropGroup(DropGroupStmt *stmt)
elog(ERROR, "DROP GROUP: permission denied"); elog(ERROR, "DROP GROUP: permission denied");
/* /*
* Drop the group. * Secure exclusive lock to protect our update of the flat group file.
*/ */
pg_group_rel = heap_openr(GroupRelationName, ExclusiveLock); pg_group_rel = heap_openr(GroupRelationName, ExclusiveLock);
/* Find and delete the group. */
tuple = SearchSysCacheCopy(GRONAME, tuple = SearchSysCacheCopy(GRONAME,
PointerGetDatum(stmt->name), PointerGetDatum(stmt->name),
0, 0, 0); 0, 0, 0);
...@@ -1477,10 +1550,14 @@ DropGroup(DropGroupStmt *stmt) ...@@ -1477,10 +1550,14 @@ DropGroup(DropGroupStmt *stmt)
simple_heap_delete(pg_group_rel, &tuple->t_self); simple_heap_delete(pg_group_rel, &tuple->t_self);
/*
* Now we can clean up; but keep lock until commit (to avoid possible
* deadlock when commit code tries to acquire lock).
*/
heap_close(pg_group_rel, NoLock); heap_close(pg_group_rel, NoLock);
/* /*
* Write the updated pg_shadow and pg_group data to the flat file. * Set flag to update flat group file at commit.
*/ */
update_pg_pwd_and_pg_group(NULL); group_file_update_needed = true;
} }
/*------------------------------------------------------------------------- /*-------------------------------------------------------------------------
* *
* user.h * user.h
* Commands for manipulating users and groups.
* *
* *
* $Id: user.h,v 1.19 2002/09/04 20:31:42 momjian Exp $ * $Id: user.h,v 1.20 2002/10/21 19:46:45 tgl Exp $
* *
*------------------------------------------------------------------------- *-------------------------------------------------------------------------
*/ */
...@@ -13,13 +14,10 @@ ...@@ -13,13 +14,10 @@
#include "fmgr.h" #include "fmgr.h"
#include "nodes/parsenodes.h" #include "nodes/parsenodes.h"
#define PWD_FILE "pg_pwd"
#define USER_GROUP_FILE "pg_group"
extern char *group_getfilename(void); extern char *group_getfilename(void);
extern char *user_getfilename(void); extern char *user_getfilename(void);
extern void CreateUser(CreateUserStmt *stmt); extern void CreateUser(CreateUserStmt *stmt);
extern void AlterUser(AlterUserStmt *stmt); extern void AlterUser(AlterUserStmt *stmt);
extern void AlterUserSet(AlterUserSetStmt *stmt); extern void AlterUserSet(AlterUserSetStmt *stmt);
...@@ -31,4 +29,6 @@ extern void DropGroup(DropGroupStmt *stmt); ...@@ -31,4 +29,6 @@ extern void DropGroup(DropGroupStmt *stmt);
extern Datum update_pg_pwd_and_pg_group(PG_FUNCTION_ARGS); extern Datum update_pg_pwd_and_pg_group(PG_FUNCTION_ARGS);
extern void AtEOXact_UpdatePasswordFile(bool isCommit);
#endif /* USER_H */ #endif /* USER_H */
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment