Tarantool development patches archive
 help / color / mirror / Atom feed
From: Ilya Markov <imarkov@tarantool.org>
To: kostja@tarantool.org
Cc: georgy@tarantool.org, tarantool-patches@freelists.org
Subject: [tarantool-patches] [security 2/2] security: Refactor system space access checks
Date: Thu, 17 May 2018 19:15:17 +0300	[thread overview]
Message-ID: <3c7c9be6d93b22c46681eb8fc78a29fd19005f2f.1526573595.git.imarkov@tarantool.org> (raw)
In-Reply-To: <cover.1526573595.git.imarkov@tarantool.org>
In-Reply-To: <cover.1526573595.git.imarkov@tarantool.org>

Processes of creating, dropping objects contain work with
system spaces which so far must be available only to admin or user who has
write access to universe.

This patch removes the need of write accesses to universe.

Introduce access_check virtual function for space objects.
The patch separates access checks of system and user spaces.

The access_check functions of system spaces verifies mostly
usage and read access, if needed.

All specific checks on write to system spaces(in fact ddl checks) are performed
in on_replace triggers of that spaces.

For backward compatibility, write and read on universe are equivalent to
create, alter, drop privileges.

Refactor tests with changing write to universe privilege to create/drop
ones.

Follow-up #945
In scope of #3250
---
 src/box/alter.cc                    | 156 ++++++++++++++++++++++--------------
 src/box/schema.cc                   |  90 +++++++++++++++++++++
 src/box/schema.h                    |  10 +++
 src/box/space.c                     |   3 +-
 src/box/space.h                     |  17 +++-
 src/box/sysview_engine.c            |   1 +
 test/box/access.result              |  49 ++++++++---
 test/box/access.test.lua            |  29 ++++---
 test/box/access_escalation.result   |  41 +++++++++-
 test/box/access_escalation.test.lua |  17 +++-
 test/box/access_misc.result         |  82 ++++++++++++++-----
 test/box/access_misc.test.lua       |  27 +++++--
 test/box/net.box.result             |  10 +--
 test/box/net.box.test.lua           |   6 +-
 test/box/role.result                |  25 +++++-
 test/box/role.test.lua              |  13 ++-
 test/box/sequence.result            |  22 ++---
 test/box/sequence.test.lua          |  13 +--
 test/engine/truncate.result         |   2 +-
 19 files changed, 468 insertions(+), 145 deletions(-)

diff --git a/src/box/alter.cc b/src/box/alter.cc
index 8766c81..f26ca1b 100644
--- a/src/box/alter.cc
+++ b/src/box/alter.cc
@@ -80,34 +80,22 @@ access_check_ddl(const char *name, uint32_t owner_uid,
 	 * were added in 1.7.7 only.
 	 */
 	if (is_17_compat_mode && has_access & PRIV_R && has_access & PRIV_W)
-		has_access |= PRIV_C | PRIV_A;
-
-	user_access_t access = ((PRIV_U | (user_access_t) priv_type) &
-				~has_access);
+		has_access |= PRIV_C | PRIV_A | PRIV_D;
+	user_access_t access = ((user_access_t) priv_type & ~has_access);
 	bool is_owner = owner_uid == cr->uid || cr->uid == ADMIN;
 	/*
 	 * Only the owner of the object or someone who has
-	 * specific DDL privilege on the object can execute
-	 * DDL. If a user has no USAGE access and is owner,
-	 * deny access as well.
+	 * specific DDL privilege on the object can execute DDL.
 	 */
-	if (access == 0 || (is_owner && !(access & PRIV_U)))
+	if (access == 0 || is_owner)
 		return; /* Access granted. */
 
 	struct user *user = user_find_xc(cr->uid);
-	if (is_owner) {
-		tnt_raise(AccessDeniedError,
-			  priv_name(PRIV_U),
-			  schema_object_name(SC_UNIVERSE),
-			  "",
-			  user->def->name);
-	} else {
-		tnt_raise(AccessDeniedError,
-			  priv_name(access),
-			  schema_object_name(type),
-			  name,
-			  user->def->name);
-	}
+	tnt_raise(AccessDeniedError,
+		  priv_name(access),
+		  schema_object_name(type),
+		  name,
+		  user->def->name);
 }
 
 /**
@@ -1551,7 +1539,8 @@ on_replace_dd_space(struct trigger * /* trigger */, void *event)
 		struct space_def *def =
 			space_def_new_from_tuple(new_tuple, ER_CREATE_SPACE,
 						 region);
-		access_check_ddl(def->name, def->uid, SC_SPACE, PRIV_C, true);
+		/* When object is not created, we assume ADMIN owns it */
+		access_check_ddl(def->name, ADMIN, SC_SPACE, PRIV_C, true);
 		auto def_guard =
 			make_scoped_guard([=] { space_def_delete(def); });
 		RLIST_HEAD(empty_list);
@@ -1908,9 +1897,17 @@ on_replace_dd_truncate(struct trigger * /* trigger */, void *event)
 			  space_name(old_space));
 
 	/*
-	 * Check if a write privilege was given, raise an error if not.
+	 * Perform checks on write or drop specific for space being truncated.
+	 * Then check ddl if the user is owner.
 	 */
-	access_check_space_xc(old_space, PRIV_W);
+	credentials *cr = effective_user();
+	uint32_t has_access = cr->universal_access |
+			      old_space->access[cr->auth_token].effective;
+	if (!(has_access & (PRIV_W | PRIV_D))) {
+		access_check_ddl(old_space->def->name, old_space->def->uid,
+				 SC_SPACE,
+				 PRIV_D, true);
+	}
 
 	struct alter_space *alter = alter_space_new(old_space);
 	auto scoped_guard =
@@ -2110,7 +2107,8 @@ on_replace_dd_user(struct trigger * /* trigger */, void *event)
 	struct user *old_user = user_by_id(uid);
 	if (new_tuple != NULL && old_user == NULL) { /* INSERT */
 		struct user_def *user = user_def_new_from_tuple(new_tuple);
-		access_check_ddl(user->name, user->owner, SC_USER, PRIV_C, true);
+		/* When object is not created, we assume ADMIN owns it */
+		access_check_ddl(user->name, ADMIN, SC_USER, PRIV_C, true);
 		auto def_guard = make_scoped_guard([=] { free(user); });
 		(void) user_cache_replace(user);
 		def_guard.is_active = false;
@@ -2248,7 +2246,8 @@ on_replace_dd_func(struct trigger * /* trigger */, void *event)
 	struct func *old_func = func_by_id(fid);
 	if (new_tuple != NULL && old_func == NULL) { /* INSERT */
 		struct func_def *def = func_def_new_from_tuple(new_tuple);
-		access_check_ddl(def->name, def->uid, SC_FUNCTION, PRIV_C, true);
+		/* When object is not created, we assume ADMIN owns it */
+		access_check_ddl(def->name, ADMIN, SC_FUNCTION, PRIV_C, true);
 		auto def_guard = make_scoped_guard([=] { free(def); });
 		func_cache_replace(def);
 		def_guard.is_active = false;
@@ -2496,9 +2495,11 @@ priv_def_create_from_tuple(struct priv_def *priv, struct tuple *tuple)
 static void
 priv_def_check(struct priv_def *priv, enum priv_type priv_type)
 {
-	struct user *grantor = user_find_xc(priv->grantor_id);
 	/* May be a role */
 	struct user *grantee = user_by_id(priv->grantee_id);
+	credentials *cr = effective_user();
+	struct user *grantor = user_find_xc(cr->uid);
+
 	if (grantee == NULL) {
 		tnt_raise(ClientError, ER_NO_SUCH_USER,
 			  int2str(priv->grantee_id));
@@ -2561,16 +2562,23 @@ priv_def_check(struct priv_def *priv, enum priv_type priv_type)
 				  int2str(priv->object_id));
 		}
 		/*
-		 * Only the creator of the role can grant or revoke it.
-		 * Everyone can grant 'PUBLIC' role.
+		 * Only the creator or owner of grantee of the role
+		 * or user with appropriate privileges can grant or revoke it.
+		 * If role is 'PUBLIC' it is allowed to grant for anyone,
+		 * Revoke 'PUBLIC' is allowed only to users who has drop access.
+		 * The latter case is required for dropping users.
 		 */
 		if (role->def->owner != grantor->def->uid &&
-		    grantor->def->uid != ADMIN &&
-		    (role->def->uid != PUBLIC || priv->access != PRIV_X)) {
-			tnt_raise(AccessDeniedError,
-				  priv_name(priv_type),
-				  schema_object_name(SC_ROLE), name,
-				  grantor->def->name);
+		    grantor->def->uid != grantee->def->owner &&
+			!((role->def->uid == PUBLIC) &&
+			  (((cr->universal_access & PRIV_D)
+			    || priv_type == PRIV_GRANT)
+			   && priv->access == PRIV_X))) {
+				tnt_raise(AccessDeniedError,
+					  priv_name(priv_type),
+					  schema_object_name(SC_ROLE),
+					  name,
+					  grantor->def->name);
 		}
 		/* Not necessary to do during revoke, but who cares. */
 		role_check(grantee, role);
@@ -2716,31 +2724,60 @@ on_replace_dd_schema(struct trigger * /* trigger */, void *event)
 	struct tuple *new_tuple = stmt->new_tuple;
 	const char *key = tuple_field_cstr_xc(new_tuple ? new_tuple : old_tuple,
 					      BOX_SCHEMA_FIELD_KEY);
-	if (strcmp(key, "cluster") == 0) {
-		if (new_tuple == NULL)
-			tnt_raise(ClientError, ER_REPLICASET_UUID_IS_RO);
-		tt_uuid uu;
-		tuple_field_uuid_xc(new_tuple, BOX_CLUSTER_FIELD_UUID, &uu);
-		REPLICASET_UUID = uu;
-	} else if (strcmp(key, "version") == 0) {
-		if (new_tuple != NULL) {
-			uint32_t major, minor, patch;
-			if (tuple_field_u32(new_tuple, 1, &major) != 0 ||
-			    tuple_field_u32(new_tuple, 2, &minor) != 0)
-				tnt_raise(ClientError, ER_WRONG_DD_VERSION);
-			/* Version can be major.minor with no patch. */
-			if (tuple_field_u32(new_tuple, 3, &patch) != 0)
-				patch = 0;
-			dd_version_id = version_id(major, minor, patch);
-		} else {
-			assert(old_tuple != NULL);
-			/*
-			 * _schema:delete({'version'}) for
-			 * example, for box.internal.bootstrap().
-			 */
-			dd_version_id = tarantool_version_id();
+	credentials *cr = effective_user();
+	/*
+	 * Access check to _schema is following.
+	 * If users have create or write privileges on universe or write on _schema,
+	 * they can't update max_id key.
+	 * Otherwise, simple write check is performed.
+	 */
+	uint32_t access = cr->universal_access;
+	struct space *schema_space = space_by_id(BOX_SCHEMA_ID);
+	access |= schema_space->access[cr->auth_token].effective;
+	if (strncmp(key, "max_id", 6) == 0) {
+		if (!(access & (PRIV_W | PRIV_C)))
+			goto error;
+	} else {
+		if (~access & PRIV_W)
+			goto error;
+		else if (strncmp(key, "cluster", 7) == 0) {
+			if (new_tuple == NULL)
+				tnt_raise(ClientError,
+					  ER_REPLICASET_UUID_IS_RO);
+			tt_uuid uu;
+			tuple_field_uuid_xc(new_tuple, BOX_CLUSTER_FIELD_UUID,
+					    &uu);
+			REPLICASET_UUID = uu;
+		} else if (strncmp(key, "version", 7) == 0) {
+			if (new_tuple != NULL) {
+				uint32_t major, minor, patch;
+				if (tuple_field_u32(new_tuple, 1, &major) !=
+				    0 ||
+				    tuple_field_u32(new_tuple, 2, &minor) != 0)
+					tnt_raise(ClientError,
+						  ER_WRONG_DD_VERSION);
+				/* Version can be major.minor with no patch. */
+				if (tuple_field_u32(new_tuple, 3, &patch) != 0)
+					patch = 0;
+				dd_version_id = version_id(major, minor, patch);
+			} else {
+				assert(old_tuple != NULL);
+				/*
+				 * _schema:delete({'version'}) for
+				 * example, for box.internal.bootstrap().
+				 */
+				dd_version_id = tarantool_version_id();
+			}
 		}
 	}
+	return;
+error:
+	struct user *user = user_find_xc(cr->uid);
+	tnt_raise(AccessDeniedError,
+		  priv_name(PRIV_W),
+		  schema_object_name(SC_SPACE),
+		  "_schema",
+		  user->def->name);
 }
 
 /**
@@ -2967,6 +3004,9 @@ on_replace_dd_sequence(struct trigger * /* trigger */, void *event)
 		new_def = sequence_def_new_from_tuple(new_tuple,
 						      ER_CREATE_SEQUENCE);
 		assert(sequence_by_id(new_def->id) == NULL);
+		/* When object is not created, we assume ADMIN owns it */
+		access_check_ddl(new_def->name, ADMIN, SC_SEQUENCE,
+				 PRIV_C, false);
 		sequence_cache_replace(new_def);
 		alter->new_def = new_def;
 	} else if (old_tuple != NULL && new_tuple == NULL) {	/* DELETE */
diff --git a/src/box/schema.cc b/src/box/schema.cc
index 1b96f97..fc7840f 100644
--- a/src/box/schema.cc
+++ b/src/box/schema.cc
@@ -37,6 +37,7 @@
 #include "scoped_guard.h"
 #include "version.h"
 #include "user.h"
+#include "session.h"
 #include <stdio.h>
 /**
  * @module Data Dictionary
@@ -520,6 +521,70 @@ sequence_cache_delete(uint32_t id)
 	}
 }
 
+
+/**
+ * Runs simple read, usage checks.
+ * All other checks are postponed.
+ */
+static int
+sys_read_access_check(struct space *space, user_access_t access)
+{
+	credentials *cr = effective_user();
+	if (~cr->universal_access & PRIV_U) {
+		struct user *user = user_find(cr->uid);
+		if (user != NULL)
+			diag_set(AccessDeniedError,
+				 priv_name(PRIV_U),
+				 schema_object_name(SC_UNIVERSE),
+				 "",
+				 user->def->name);
+		return -1;
+	}
+	if (access == PRIV_R) {
+		uint32_t access = cr->universal_access |
+			space->access[cr->auth_token].effective;
+		if (~access & PRIV_R) {
+			struct user *user = user_find(cr->uid);
+			if (user != NULL)
+				diag_set(AccessDeniedError,
+					 priv_name(PRIV_R),
+					 schema_object_name(SC_SPACE),
+					 space->def->name,
+					 user->def->name);
+			return -1;
+		}
+	}
+	/* Other checks are postponed to trigger*/
+	return 0;
+}
+
+/**
+ * Runs simple read, usage, write checks.
+ * All other checks are postponed.
+ */
+static int
+sys_read_write_access_check(struct space *space, user_access_t access)
+{
+	credentials *cr = effective_user();
+	if (access == PRIV_W) {
+		uint32_t has_access = space->access[cr->auth_token].effective
+				      | cr->universal_access;
+		if (~has_access & access) {
+			struct user *user = user_find(cr->uid);
+			if (user != NULL) {
+				diag_set(AccessDeniedError,
+					 priv_name(access),
+					 schema_object_name(SC_SPACE),
+					 space->def->name,
+					 user->def->name);
+			}
+			return -1;
+		}
+		return 0;
+	}
+	return sys_read_access_check(space, access);
+}
+
 const char *
 schema_find_name(enum schema_object_type type, uint32_t object_id)
 {
@@ -562,3 +627,28 @@ schema_find_name(enum schema_object_type type, uint32_t object_id)
 	return "(nil)";
 }
 
+access_check_func_t
+get_access_check_func(uint32_t space_id)
+{
+	switch (space_id) {
+	case BOX_CLUSTER_ID:
+	case BOX_COLLATION_ID:
+		return sys_read_write_access_check;
+	case BOX_SCHEMA_ID:
+	case BOX_SPACE_ID:
+	case BOX_TRUNCATE_ID:
+	case BOX_INDEX_ID:
+	case BOX_FUNC_ID:
+	case BOX_USER_ID:
+	case BOX_SEQUENCE_ID:
+	case BOX_SEQUENCE_DATA_ID:
+	case BOX_SPACE_SEQUENCE_ID:
+	case BOX_PRIV_ID:
+		/*
+		 * Specialized access checks will be performed in trigger.
+		 */
+		return sys_read_access_check;
+	default:
+		return access_check_user_space;
+	}
+}
diff --git a/src/box/schema.h b/src/box/schema.h
index 2b87f5f..d3fb7ce 100644
--- a/src/box/schema.h
+++ b/src/box/schema.h
@@ -36,6 +36,7 @@
 #include "error.h"
 #include "space.h"
 #include "latch.h"
+#include "user.h"
 
 #if defined(__cplusplus)
 extern "C" {
@@ -103,6 +104,9 @@ schema_find_name(enum schema_object_type type, uint32_t object_id);
  */
 struct sequence *
 sequence_by_id(uint32_t id);
+
+int
+access_check_user_space(struct space *space, user_access_t access);
 #if defined(__cplusplus)
 } /* extern "C" */
 
@@ -205,6 +209,12 @@ sequence_cache_delete(uint32_t id);
 #endif /* defined(__cplusplus) */
 
 /**
+ * Utility function used to specify access_check function for system space.
+ */
+access_check_func_t
+get_access_check_func(uint32_t space_id);
+
+/**
  * Triggers fired after committing a change in space definition.
  * The space is passed to the trigger callback in the event
  * argument. It is the new space in case of create/update or
diff --git a/src/box/space.c b/src/box/space.c
index 02a9792..d72ce29 100644
--- a/src/box/space.c
+++ b/src/box/space.c
@@ -44,7 +44,7 @@
 #include "iproto_constants.h"
 
 int
-access_check_space(struct space *space, user_access_t access)
+access_check_user_space(struct space *space, user_access_t access)
 {
 	struct credentials *cr = effective_user();
 	/* Any space access also requires global USAGE privilege. */
@@ -132,6 +132,7 @@ space_create(struct space *space, struct engine *engine,
 	rlist_create(&space->on_replace);
 	rlist_create(&space->on_stmt_begin);
 	space->run_triggers = true;
+	space->access_check = get_access_check_func(def->id);
 
 	space->format = format;
 	if (format != NULL)
diff --git a/src/box/space.h b/src/box/space.h
index a024fdc..5139a76 100644
--- a/src/box/space.h
+++ b/src/box/space.h
@@ -50,6 +50,8 @@ struct port;
 struct tuple;
 struct tuple_format;
 
+typedef int (*access_check_func_t)(struct space *space, user_access_t access);
+
 struct space_vtab {
 	/** Free a space instance. */
 	void (*destroy)(struct space *);
@@ -127,6 +129,7 @@ struct space_vtab {
 struct space {
 	/** Virtual function table. */
 	const struct space_vtab *vtab;
+	access_check_func_t access_check;
 	/** Cached runtime access information. */
 	struct access access[BOX_USER_MAX];
 	/** Engine used by this space. */
@@ -273,8 +276,11 @@ index_name_by_id(struct space *space, uint32_t id);
  * Check whether or not the current user can be granted
  * the requested access to the space.
  */
-int
-access_check_space(struct space *space, user_access_t access);
+static inline int
+access_check_space(struct space *space, user_access_t access)
+{
+	return space->access_check(space, access);
+}
 
 static inline int
 space_apply_initial_join_row(struct space *space, struct request *request)
@@ -390,6 +396,12 @@ space_dump_def(const struct space *space, struct rlist *key_list);
 void
 space_fill_index_map(struct space *space);
 
+/*
+ * Utility function used to specify access_check function for system space.
+ */
+access_check_func_t
+get_access_check_func(uint32_t space_id);
+
 #if defined(__cplusplus)
 } /* extern "C" */
 
@@ -499,4 +511,5 @@ space_prepare_alter_xc(struct space *old_space, struct space *new_space)
 
 #endif /* defined(__cplusplus) */
 
+
 #endif /* TARANTOOL_BOX_SPACE_H_INCLUDED */
diff --git a/src/box/sysview_engine.c b/src/box/sysview_engine.c
index 5aea87e..f77ce99 100644
--- a/src/box/sysview_engine.c
+++ b/src/box/sysview_engine.c
@@ -205,6 +205,7 @@ sysview_engine_create_space(struct engine *engine, struct space_def *def,
 		free(space);
 		return NULL;
 	}
+	space->access_check = access_check_user_space;
 	return space;
 }
 
diff --git a/test/box/access.result b/test/box/access.result
index 131a215..aa18193 100644
--- a/test/box/access.result
+++ b/test/box/access.result
@@ -1389,10 +1389,32 @@ box.schema.func.create('test_func')
 box.session.su("admin")
 ---
 ...
+-- failed create because of auto_increment
+box.session.su("tester")
+---
+...
+box.schema.space.create("test_space")
+---
+- error: Write access to space '_schema' is denied for user 'tester'
+...
+box.schema.user.create('test_user')
+---
+- error: Read access to space '_user' is denied for user 'tester'
+...
+box.schema.func.create('test_func')
+---
+- error: Read access to space '_func' is denied for user 'tester'
+...
+box.schema.sequence.create('test_seq')
+---
+- error: Read access to space '_sequence' is denied for user 'tester'
+...
+box.session.su("admin")
+---
+...
 box.schema.user.grant("tester", "read", "universe")
 ---
 ...
--- failed create
 box.session.su("tester")
 ---
 ...
@@ -1402,27 +1424,26 @@ box.schema.space.create("test_space")
 ...
 box.schema.user.create('test_user')
 ---
-- error: Write access to space '_user' is denied for user 'tester'
+- error: Create access to user 'test_user' is denied for user 'tester'
 ...
 box.schema.func.create('test_func')
 ---
-- error: Write access to space '_func' is denied for user 'tester'
+- error: Create access to function 'test_func' is denied for user 'tester'
+...
+box.schema.sequence.create('test_seq')
+---
+- error: Create access to sequence 'test_seq' is denied for user 'tester'
 ...
 box.session.su("admin")
 ---
 ...
---
--- FIXME 2.0: we still need to grant 'write' on universe
--- explicitly since we still use process_rw to write to system
--- tables from ddl
---
-box.schema.user.grant("tester", "create,write", "universe")
+box.schema.user.grant("tester", "create", "universe")
 ---
 ...
 box.session.su("tester")
 ---
 ...
--- successful create
+-- successful create. Note user still needs read privilege because of auto_increment.
 s1 = box.schema.space.create("test_space")
 ---
 ...
@@ -1477,10 +1498,16 @@ box.schema.func.drop("test")
 box.session.su("admin")
 ---
 ...
+box.schema.user.revoke("tester", "create", "universe")
+---
+...
 box.schema.user.grant("tester", "drop", "universe")
 ---
 ...
--- successful drop
+-- successful truncate, drop
+box.session.su("tester", s.truncate, s)
+---
+...
 box.session.su("tester", s.drop, s)
 ---
 ...
diff --git a/test/box/access.test.lua b/test/box/access.test.lua
index 4bd34e4..38019db 100644
--- a/test/box/access.test.lua
+++ b/test/box/access.test.lua
@@ -520,22 +520,25 @@ box.schema.space.create("test_space")
 box.schema.user.create('test_user')
 box.schema.func.create('test_func')
 box.session.su("admin")
-box.schema.user.grant("tester", "read", "universe")
--- failed create
+-- failed create because of auto_increment
 box.session.su("tester")
 box.schema.space.create("test_space")
 box.schema.user.create('test_user')
 box.schema.func.create('test_func')
+box.schema.sequence.create('test_seq')
+
 box.session.su("admin")
+box.schema.user.grant("tester", "read", "universe")
+box.session.su("tester")
 
---
--- FIXME 2.0: we still need to grant 'write' on universe
--- explicitly since we still use process_rw to write to system
--- tables from ddl
---
-box.schema.user.grant("tester", "create,write", "universe")
+box.schema.space.create("test_space")
+box.schema.user.create('test_user')
+box.schema.func.create('test_func')
+box.schema.sequence.create('test_seq')
+box.session.su("admin")
+box.schema.user.grant("tester", "create", "universe")
 box.session.su("tester")
--- successful create
+-- successful create. Note user still needs read privilege because of auto_increment.
 s1 = box.schema.space.create("test_space")
 _ = s1:create_index("primary")
 _ = box.schema.user.create('test_user')
@@ -557,14 +560,18 @@ box.schema.func.drop('test_func')
 
 -- failed drop
 -- box.session.su("tester", s.drop, s)
+
 s:drop()
 seq:drop()
 box.schema.user.drop("test")
 box.schema.func.drop("test")
-
 box.session.su("admin")
+
+
+box.schema.user.revoke("tester", "create", "universe")
 box.schema.user.grant("tester", "drop", "universe")
--- successful drop
+-- successful truncate, drop
+box.session.su("tester", s.truncate, s)
 box.session.su("tester", s.drop, s)
 box.session.su("tester", seq.drop, seq)
 box.session.su("tester", box.schema.user.drop, "test")
diff --git a/test/box/access_escalation.result b/test/box/access_escalation.result
index 9d6cb99..a776e98 100644
--- a/test/box/access_escalation.result
+++ b/test/box/access_escalation.result
@@ -81,7 +81,7 @@ connection:close()
 box.schema.user.create('underprivileged')
 ---
 ...
-box.schema.user.grant('underprivileged', 'read,write', 'space', '_func')
+box.schema.user.grant('underprivileged', 'read,write', 'universe')
 ---
 ...
 box.session.su('underprivileged')
@@ -90,6 +90,45 @@ box.session.su('underprivileged')
 box.schema.func.create('setuid', {setuid=true})
 ---
 ...
+box.schema.func.drop('setuid')
+---
+...
+box.session.su('admin')
+---
+...
+-- user needs read access because of auto_increment in func.create.
+box.schema.user.revoke('underprivileged', 'write', 'universe')
+---
+...
+box.schema.user.grant('underprivileged', 'create', 'universe')
+---
+...
+-- check drop other user's func
+box.schema.func.create('setuid2', {setuid=true})
+---
+...
+box.session.su('underprivileged')
+---
+...
+box.schema.func.create('setuid', {setuid=true})
+---
+...
+box.schema.func.drop('setuid2')
+---
+- error: Drop access to function 'setuid2' is denied for user 'underprivileged'
+...
+box.session.su('admin')
+---
+...
+box.schema.user.grant('underprivileged', 'drop', 'universe')
+---
+...
+box.session.su('underprivileged')
+---
+...
+box.schema.func.drop('setuid2')
+---
+...
 box.session.su('admin')
 ---
 ...
diff --git a/test/box/access_escalation.test.lua b/test/box/access_escalation.test.lua
index 8b30870..36cd6fb 100644
--- a/test/box/access_escalation.test.lua
+++ b/test/box/access_escalation.test.lua
@@ -60,10 +60,25 @@ connection:close()
 -- create a deprived user
 
 box.schema.user.create('underprivileged')
-box.schema.user.grant('underprivileged', 'read,write', 'space', '_func')
+box.schema.user.grant('underprivileged', 'read,write', 'universe')
 box.session.su('underprivileged')
 box.schema.func.create('setuid', {setuid=true})
+box.schema.func.drop('setuid')
+box.session.su('admin')
+-- user needs read access because of auto_increment in func.create.
+box.schema.user.revoke('underprivileged', 'write', 'universe')
+box.schema.user.grant('underprivileged', 'create', 'universe')
+-- check drop other user's func
+box.schema.func.create('setuid2', {setuid=true})
+box.session.su('underprivileged')
+box.schema.func.create('setuid', {setuid=true})
+box.schema.func.drop('setuid2')
 box.session.su('admin')
+box.schema.user.grant('underprivileged', 'drop', 'universe')
+box.session.su('underprivileged')
+box.schema.func.drop('setuid2')
+box.session.su('admin')
+
 --
 -- create a deprived function
 --
diff --git a/test/box/access_misc.result b/test/box/access_misc.result
index 8bf99f2..1d6ccfb 100644
--- a/test/box/access_misc.result
+++ b/test/box/access_misc.result
@@ -61,6 +61,9 @@ box.schema.user.grant('testus', 'read', 'space', 'admin_space')
 ---
 - error: User 'testus' already has read access on space 'admin_space'
 ...
+box.schema.user.grant('testus', 'read', 'universe')
+---
+...
 session.su('testus')
 ---
 ...
@@ -78,7 +81,7 @@ s:delete(1)
 ...
 s:drop()
 ---
-- error: Write access to space '_space_sequence' is denied for user 'testus'
+- error: Drop access to space 'admin_space' is denied for user 'testus'
 ...
 --
 -- Check double revoke
@@ -93,6 +96,9 @@ box.schema.user.revoke('testus', 'read', 'space', 'admin_space')
 ---
 - error: User 'testus' does not have read access on space 'admin_space'
 ...
+box.schema.user.revoke('testus', 'read', 'universe')
+---
+...
 session.su('testus')
 ---
 ...
@@ -124,9 +130,18 @@ s:insert({3})
 ---
 - [3]
 ...
+session.su('admin')
+---
+...
+box.schema.user.grant('testus', 'read', 'universe')
+---
+...
+session.su('testus')
+---
+...
 s:drop()
 ---
-- error: Write access to space '_space_sequence' is denied for user 'testus'
+- error: Drop access to space 'admin_space' is denied for user 'testus'
 ...
 session.su('admin')
 ---
@@ -167,28 +182,26 @@ s:delete({3})
 ---
 - error: Write access to space 'admin_space' is denied for user 'guest'
 ...
-s:drop()
+session.su('admin')
 ---
-- error: Write access to space '_space_sequence' is denied for user 'guest'
 ...
-gs = box.schema.space.create('guest_space')
+box.schema.user.grant('guest', 'read', 'universe')
 ---
-- error: Write access to space '_schema' is denied for user 'guest'
 ...
---
--- FIXME: object create calls system space auto_increment, which requires
--- read and write privileges. Create privilege must solve this.
---
-box.schema.func.create('guest_func')
+session.su('guest')
 ---
-- error: Read access to space '_func' is denied for user 'guest'
 ...
-session.su('admin', box.schema.user.grant, "guest", "read", "universe")
+s:drop()
+---
+- error: Drop access to space 'admin_space' is denied for user 'guest'
+...
+gs = box.schema.space.create('guest_space')
 ---
+- error: Write access to space '_schema' is denied for user 'guest'
 ...
 box.schema.func.create('guest_func')
 ---
-- error: Read access to space '_func' is denied for user 'guest'
+- error: Create access to function 'guest_func' is denied for user 'guest'
 ...
 session.su('admin')
 ---
@@ -291,7 +304,7 @@ session.su('admin')
 box.schema.user.create('someuser')
 ---
 ...
-box.schema.user.grant('someuser', 'read, write, execute', 'universe')
+box.schema.user.grant('someuser', 'read', 'universe')
 ---
 ...
 session.su('someuser')
@@ -360,12 +373,38 @@ _ = box.space._user:delete(2)
 ---
 - error: Drop access to user 'public' is denied for user 'testuser'
 ...
+session.su('admin')
+---
+...
+box.schema.user.grant('testuser', 'drop', 'universe')
+---
+...
+session.su('testuser')
+---
+...
+_ = box.space._user:delete(2)
+---
+- error: 'Failed to drop user or role ''public'': the user or the role is a system'
+...
 box.space._user:select(1)
 ---
 - error: Read access to space '_user' is denied for user 'testuser'
 ...
 uid = box.space._user:insert{maxuid+1, session.uid(), 'someone', 'user', EMPTY_MAP}[1]
 ---
+- error: Create access to user 'someone' is denied for user 'testuser'
+...
+session.su('admin')
+---
+...
+box.schema.user.grant('testuser', 'create', 'universe')
+---
+...
+session.su('testuser')
+---
+...
+uid = box.space._user:insert{maxuid+1, session.uid(), 'someone', 'user', EMPTY_MAP}[1]
+---
 ...
 _ = box.space._user:delete(uid)
 ---
@@ -384,6 +423,12 @@ _ = box.space._user:delete(testuser_uid)
 box.schema.user.revoke('testuser', 'write', 'space', '_user')
 ---
 ...
+box.schema.user.revoke('testuser', 'drop', 'universe')
+---
+...
+box.schema.user.revoke('testuser', 'create', 'universe')
+---
+...
 --
 -- Check read grant on _user
 --
@@ -395,7 +440,7 @@ session.su('testuser')
 ...
 _  = box.space._user:delete(2)
 ---
-- error: Write access to space '_user' is denied for user 'testuser'
+- error: Drop access to user 'public' is denied for user 'testuser'
 ...
 box.space._user:select(1)
 ---
@@ -403,7 +448,8 @@ box.space._user:select(1)
 ...
 box.space._user:insert{uid, session.uid(), 'someone2', 'user'}
 ---
-- error: Write access to space '_user' is denied for user 'testuser'
+- error: Tuple field count 4 is less than required by space format or defined indexes
+    (expected at least 5)
 ...
 session.su('admin')
 ---
@@ -423,7 +469,7 @@ box.space._index:select(272)
 ...
 box.space._index:insert{512, 1,'owner','tree', 1, 1, 0,'unsigned'}
 ---
-- error: Write access to space '_index' is denied for user 'testuser'
+- error: 'Tuple field 5 type does not match one required by operation: expected map'
 ...
 session.su('admin')
 ---
diff --git a/test/box/access_misc.test.lua b/test/box/access_misc.test.lua
index 27064c4..8c26b98 100644
--- a/test/box/access_misc.test.lua
+++ b/test/box/access_misc.test.lua
@@ -27,6 +27,7 @@ s:insert({2})
 --
 box.schema.user.grant('testus', 'read', 'space', 'admin_space')
 box.schema.user.grant('testus', 'read', 'space', 'admin_space')
+box.schema.user.grant('testus', 'read', 'universe')
 
 session.su('testus')
 s:select(1)
@@ -39,6 +40,7 @@ s:drop()
 session.su('admin')
 box.schema.user.revoke('testus', 'read', 'space', 'admin_space')
 box.schema.user.revoke('testus', 'read', 'space', 'admin_space')
+box.schema.user.revoke('testus', 'read', 'universe')
 
 session.su('testus')
 s:select(1)
@@ -52,6 +54,9 @@ session.su('testus')
 s:select(1)
 s:delete(1)
 s:insert({3})
+session.su('admin')
+box.schema.user.grant('testus', 'read', 'universe')
+session.su('testus')
 s:drop()
 session.su('admin')
 --
@@ -68,14 +73,12 @@ box.space._user:select(1)
 s:select(1)
 s:insert({4})
 s:delete({3})
+session.su('admin')
+box.schema.user.grant('guest', 'read', 'universe')
+session.su('guest')
 s:drop()
 gs = box.schema.space.create('guest_space')
---
--- FIXME: object create calls system space auto_increment, which requires
--- read and write privileges. Create privilege must solve this.
---
-box.schema.func.create('guest_func')
-session.su('admin', box.schema.user.grant, "guest", "read", "universe")
+
 box.schema.func.create('guest_func')
 session.su('admin')
 box.schema.user.revoke("guest", "read", "universe")
@@ -123,7 +126,7 @@ box.schema.func.create('uniuser_func')
 
 session.su('admin')
 box.schema.user.create('someuser')
-box.schema.user.grant('someuser', 'read, write, execute', 'universe')
+box.schema.user.grant('someuser', 'read', 'universe')
 session.su('someuser')
 --
 -- Check drop objects of another user
@@ -150,14 +153,24 @@ box.schema.user.grant('testuser', 'write', 'space', '_user')
 session.su('testuser')
 testuser_uid = session.uid()
 _ = box.space._user:delete(2)
+session.su('admin')
+box.schema.user.grant('testuser', 'drop', 'universe')
+session.su('testuser')
+_ = box.space._user:delete(2)
 box.space._user:select(1)
 uid = box.space._user:insert{maxuid+1, session.uid(), 'someone', 'user', EMPTY_MAP}[1]
+session.su('admin')
+box.schema.user.grant('testuser', 'create', 'universe')
+session.su('testuser')
+uid = box.space._user:insert{maxuid+1, session.uid(), 'someone', 'user', EMPTY_MAP}[1]
 _ = box.space._user:delete(uid)
 
 session.su('admin')
 box.space._user:select(1)
 _ = box.space._user:delete(testuser_uid)
 box.schema.user.revoke('testuser', 'write', 'space', '_user')
+box.schema.user.revoke('testuser', 'drop', 'universe')
+box.schema.user.revoke('testuser', 'create', 'universe')
 --
 -- Check read grant on _user
 --
diff --git a/test/box/net.box.result b/test/box/net.box.result
index 1674c27..b6bb383 100644
--- a/test/box/net.box.result
+++ b/test/box/net.box.result
@@ -2140,10 +2140,7 @@ box.session.on_disconnect(nil, on_disconnect)
 -- gh-2666: check that netbox.call is not repeated on schema
 -- change.
 --
-box.schema.user.grant('guest', 'write', 'space', '_space')
----
-...
-box.schema.user.grant('guest', 'write', 'space', '_schema')
+box.schema.user.grant('guest', 'create', 'universe')
 ---
 ...
 count = 0
@@ -2188,10 +2185,7 @@ box.space.test2:drop()
 box.space.test3:drop()
 ---
 ...
-box.schema.user.revoke('guest', 'write', 'space', '_space')
----
-...
-box.schema.user.revoke('guest', 'write', 'space', '_schema')
+box.schema.user.revoke('guest', 'create', 'universe')
 ---
 ...
 c:close()
diff --git a/test/box/net.box.test.lua b/test/box/net.box.test.lua
index c34616a..e1636c8 100644
--- a/test/box/net.box.test.lua
+++ b/test/box/net.box.test.lua
@@ -875,8 +875,7 @@ box.session.on_disconnect(nil, on_disconnect)
 -- gh-2666: check that netbox.call is not repeated on schema
 -- change.
 --
-box.schema.user.grant('guest', 'write', 'space', '_space')
-box.schema.user.grant('guest', 'write', 'space', '_schema')
+box.schema.user.grant('guest', 'create', 'universe')
 count = 0
 function create_space(name) count = count + 1 box.schema.create_space(name) return true end
 c = net.connect(box.cfg.listen)
@@ -889,8 +888,7 @@ count
 box.space.test1:drop()
 box.space.test2:drop()
 box.space.test3:drop()
-box.schema.user.revoke('guest', 'write', 'space', '_space')
-box.schema.user.revoke('guest', 'write', 'space', '_schema')
+box.schema.user.revoke('guest', 'create', 'universe')
 c:close()
 
 --
diff --git a/test/box/role.result b/test/box/role.result
index 806cea9..38dc43e 100644
--- a/test/box/role.result
+++ b/test/box/role.result
@@ -671,7 +671,7 @@ box.session.su('admin')
 _ = box.schema.space.create('test')
 ---
 ...
-box.schema.user.grant('john', 'read,write,execute', 'universe')
+box.schema.user.grant('john', 'read', 'universe')
 ---
 ...
 box.session.su('john')
@@ -695,14 +695,33 @@ box.schema.user.grant('grantee', 'public')
 - error: User 'grantee' already has role 'public'
 ...
 --
--- revoking role 'public' is another deal - only the
--- superuser can do that, and even that would be useless,
+-- revoking role 'public' is another deal:
+-- the superuser or creator of user can do that, and even that would be useless,
 -- since one can still re-grant it back to oneself.
 --
 box.schema.user.revoke('grantee', 'public')
 ---
 - error: Revoke access to role 'public' is denied for user 'john'
 ...
+box.session.su("admin")
+---
+...
+box.schema.user.grant("john", "create", 'universe')
+---
+...
+box.session.su('john')
+---
+...
+box.schema.user.create("grantee2")
+---
+...
+-- must be ok
+box.schema.user.revoke('grantee2', 'public')
+---
+...
+box.schema.user.drop('grantee2')
+---
+...
 box.session.su('admin')
 ---
 ...
diff --git a/test/box/role.test.lua b/test/box/role.test.lua
index e97339f..abdd1b1 100644
--- a/test/box/role.test.lua
+++ b/test/box/role.test.lua
@@ -261,7 +261,7 @@ box.schema.user.grant('grantee', 'role')
 --
 box.session.su('admin')
 _ = box.schema.space.create('test')
-box.schema.user.grant('john', 'read,write,execute', 'universe')
+box.schema.user.grant('john', 'read', 'universe')
 box.session.su('john')
 box.schema.user.grant('grantee', 'role')
 box.schema.user.grant('grantee', 'read', 'space', 'test')
@@ -272,11 +272,18 @@ box.schema.user.grant('grantee', 'read', 'space', 'test')
 --
 box.schema.user.grant('grantee', 'public')
 --
--- revoking role 'public' is another deal - only the
--- superuser can do that, and even that would be useless,
+-- revoking role 'public' is another deal:
+-- the superuser or creator of user can do that, and even that would be useless,
 -- since one can still re-grant it back to oneself.
 --
 box.schema.user.revoke('grantee', 'public')
+box.session.su("admin")
+box.schema.user.grant("john", "create", 'universe')
+box.session.su('john')
+box.schema.user.create("grantee2")
+-- must be ok
+box.schema.user.revoke('grantee2', 'public')
+box.schema.user.drop('grantee2')
 
 box.session.su('admin')
 box.schema.user.drop('john')
diff --git a/test/box/sequence.result b/test/box/sequence.result
index f0164b6..56dcb71 100644
--- a/test/box/sequence.result
+++ b/test/box/sequence.result
@@ -1451,7 +1451,7 @@ box.session.su('admin')
 ---
 ...
 -- A user cannot alter sequences created by other users.
-box.schema.user.grant('user', 'read,write', 'universe')
+box.schema.user.grant('user', 'read', 'universe')
 ---
 ...
 box.session.su('user')
@@ -1471,6 +1471,9 @@ box.session.su('admin')
 sq:drop()
 ---
 ...
+box.schema.user.grant('user', 'create', 'universe')
+---
+...
 -- A user can alter/use sequences that he owns.
 box.session.su('user')
 ---
@@ -1522,7 +1525,7 @@ s1 = box.schema.space.create('space1')
 _ = s1:create_index('pk')
 ---
 ...
-box.schema.user.grant('user', 'read,write', 'universe')
+box.schema.user.grant('user', 'read, create', 'universe')
 ---
 ...
 box.session.su('user')
@@ -1536,15 +1539,14 @@ s2 = box.schema.space.create('space2')
 ...
 _ = s2:create_index('pk', {sequence = 'seq1'}) -- error
 ---
-- error: Create access to sequence 'seq1' is denied for user 'user'
 ...
 s1.index.pk:alter({sequence = 'seq1'}) -- error
 ---
-- error: Create access to sequence 'seq1' is denied for user 'user'
+- error: Alter access to space 'space1' is denied for user 'user'
 ...
 box.space._space_sequence:replace{s1.id, sq1.id, false} -- error
 ---
-- error: Create access to sequence 'seq1' is denied for user 'user'
+- error: Alter access to space 'space1' is denied for user 'user'
 ...
 box.space._space_sequence:replace{s1.id, sq2.id, false} -- error
 ---
@@ -1552,7 +1554,7 @@ box.space._space_sequence:replace{s1.id, sq2.id, false} -- error
 ...
 box.space._space_sequence:replace{s2.id, sq1.id, false} -- error
 ---
-- error: Create access to sequence 'seq1' is denied for user 'user'
+- error: Alter access to sequence 'seq1' is denied for user 'user'
 ...
 s2.index.pk:alter({sequence = 'seq2'}) -- ok
 ---
@@ -1563,7 +1565,7 @@ box.session.su('admin')
 -- If the user owns a sequence attached to a space,
 -- it can use it for auto increment, otherwise it
 -- needs privileges.
-box.schema.user.revoke('user', 'read,write', 'universe')
+box.schema.user.revoke('user', 'read,create', 'universe')
 ---
 ...
 box.session.su('user')
@@ -1671,7 +1673,7 @@ s:drop()
 ---
 ...
 -- When a user is dropped, all his sequences are dropped as well.
-box.schema.user.grant('user', 'read,write', 'universe')
+box.schema.user.grant('user', 'create', 'universe')
 ---
 ...
 box.session.su('user')
@@ -1701,10 +1703,10 @@ box.schema.user.create('user1')
 box.schema.user.create('user2')
 ---
 ...
-box.schema.user.grant('user1', 'read,write', 'universe')
+box.schema.user.grant('user1', 'read, create', 'universe')
 ---
 ...
-box.schema.user.grant('user2', 'read,write', 'universe')
+box.schema.user.grant('user2', 'read', 'universe')
 ---
 ...
 box.session.su('user1')
diff --git a/test/box/sequence.test.lua b/test/box/sequence.test.lua
index af3432f..577b3ee 100644
--- a/test/box/sequence.test.lua
+++ b/test/box/sequence.test.lua
@@ -482,12 +482,13 @@ sq:reset() -- error
 box.session.su('admin')
 
 -- A user cannot alter sequences created by other users.
-box.schema.user.grant('user', 'read,write', 'universe')
+box.schema.user.grant('user', 'read', 'universe')
 box.session.su('user')
 sq:alter{step = 2} -- error
 sq:drop() -- error
 box.session.su('admin')
 sq:drop()
+box.schema.user.grant('user', 'create', 'universe')
 
 -- A user can alter/use sequences that he owns.
 box.session.su('user')
@@ -508,7 +509,7 @@ sq:drop()
 sq1 = box.schema.sequence.create('seq1')
 s1 = box.schema.space.create('space1')
 _ = s1:create_index('pk')
-box.schema.user.grant('user', 'read,write', 'universe')
+box.schema.user.grant('user', 'read, create', 'universe')
 box.session.su('user')
 sq2 = box.schema.sequence.create('seq2')
 s2 = box.schema.space.create('space2')
@@ -523,7 +524,7 @@ box.session.su('admin')
 -- If the user owns a sequence attached to a space,
 -- it can use it for auto increment, otherwise it
 -- needs privileges.
-box.schema.user.revoke('user', 'read,write', 'universe')
+box.schema.user.revoke('user', 'read,create', 'universe')
 box.session.su('user')
 s2:insert{nil, 1} -- ok: {1, 1}
 box.session.su('admin')
@@ -559,7 +560,7 @@ box.session.su('admin')
 s:drop()
 
 -- When a user is dropped, all his sequences are dropped as well.
-box.schema.user.grant('user', 'read,write', 'universe')
+box.schema.user.grant('user', 'create', 'universe')
 box.session.su('user')
 _ = box.schema.sequence.create('test1')
 _ = box.schema.sequence.create('test2')
@@ -571,8 +572,8 @@ box.sequence
 -- to a sequence.
 box.schema.user.create('user1')
 box.schema.user.create('user2')
-box.schema.user.grant('user1', 'read,write', 'universe')
-box.schema.user.grant('user2', 'read,write', 'universe')
+box.schema.user.grant('user1', 'read, create', 'universe')
+box.schema.user.grant('user2', 'read', 'universe')
 box.session.su('user1')
 sq = box.schema.sequence.create('test')
 box.session.su('user2')
diff --git a/test/engine/truncate.result b/test/engine/truncate.result
index 3ad400e..33ff70a 100644
--- a/test/engine/truncate.result
+++ b/test/engine/truncate.result
@@ -506,7 +506,7 @@ con = require('net.box').connect(box.cfg.listen)
 ...
 con:eval([[box.space.access_truncate:truncate()]])
 ---
-- error: Write access to space 'access_truncate' is denied for user 'guest'
+- error: Drop access to space 'access_truncate' is denied for user 'guest'
 ...
 con.space.access_truncate:select()
 ---
-- 
2.7.4

      parent reply	other threads:[~2018-05-17 16:15 UTC|newest]

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2018-05-16 12:37 [tarantool-patches] [security 0/2] System spaces access control lists Ilya Markov
2018-05-16 12:37 ` [tarantool-patches] [security 1/2] security: Refactor reads from systems spaces Ilya Markov
2018-05-16 19:22   ` [tarantool-patches] " Konstantin Osipov
2018-05-16 12:37 ` [tarantool-patches] [security 2/2] security: Refactor system space access checks Ilya Markov
2018-05-16 19:27   ` [tarantool-patches] " Konstantin Osipov
2018-05-17 16:15     ` [tarantool-patches] [security 0/2] Access control lists Ilya Markov
2018-05-17 16:15       ` [tarantool-patches] [security 1/2] security: Refactor reads from systems spaces Ilya Markov
2018-05-17 16:15       ` Ilya Markov [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=3c7c9be6d93b22c46681eb8fc78a29fd19005f2f.1526573595.git.imarkov@tarantool.org \
    --to=imarkov@tarantool.org \
    --cc=georgy@tarantool.org \
    --cc=kostja@tarantool.org \
    --cc=tarantool-patches@freelists.org \
    --subject='Re: [tarantool-patches] [security 2/2] security: Refactor system space access checks' \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox