Add DB storage for enabling / disabling arbitrary RAT types.

Parse new GSUP IE indicating the RAT types employed by the subscriber.
Store allowed RAT types in the database.
Reject LU if used RAT type is not allowed.

So far we have only GERAN-A and UTRAN-Iu, but to be future compatible,
implement an arbitrary length list of RAT types: add DB table subscriber_rat.

Backwards compatibility: if there is no entry in subscriber_rat, all RAT types
shall be allowed. As soon as there is an entry, it can either be false to
forbid a RAT or true to still allow a RAT type.

Depends: I93850710ab55a605bf61b95063a69682a2899bb1 (libosmocore)
Change-Id: I3e399ca8a85421f77a9a15e608413d1507722955
diff --git a/sql/hlr.sql b/sql/hlr.sql
index 9ff9867..3c65cd5 100644
--- a/sql/hlr.sql
+++ b/sql/hlr.sql
@@ -70,8 +70,17 @@
 	ind_bitlen	INTEGER NOT NULL DEFAULT 5	-- nr of index bits at lower SQN end
 );
 
+-- Optional: add subscriber entries to allow or disallow specific RATs (2G or 3G or ...).
+-- If a subscriber has no entry, that means that all RATs are allowed (backwards compat).
+CREATE TABLE subscriber_rat (
+	subscriber_id	INTEGER,		-- subscriber.id
+	rat		TEXT CHECK(rat in ('GERAN-A', 'UTRAN-Iu')) NOT NULL,	-- Radio Access Technology, see enum ran_type
+	allowed		BOOLEAN CHECK(allowed in (0, 1)) NOT NULL DEFAULT 0
+);
+
 CREATE UNIQUE INDEX idx_subscr_imsi ON subscriber (imsi);
+CREATE UNIQUE INDEX idx_subscr_rat_flag ON subscriber_rat (subscriber_id, rat);
 
 -- Set HLR database schema version number
 -- Note: This constant is currently duplicated in src/db.c and must be kept in sync!
-PRAGMA user_version = 1;
+PRAGMA user_version = 2;
diff --git a/src/db.c b/src/db.c
index f7cbace..0a51178 100644
--- a/src/db.c
+++ b/src/db.c
@@ -28,7 +28,7 @@
 #include "db_bootstrap.h"
 
 /* This constant is currently duplicated in sql/hlr.sql and must be kept in sync! */
-#define CURRENT_SCHEMA_VERSION	1
+#define CURRENT_SCHEMA_VERSION	2
 
 #define SEL_COLUMNS \
 	"id," \
@@ -76,6 +76,12 @@
 		" VALUES($subscriber_id, $algo_id_3g, $k, $op, $opc, $ind_bitlen)",
 	[DB_STMT_AUC_3G_DELETE] = "DELETE FROM auc_3g WHERE subscriber_id = $subscriber_id",
 	[DB_STMT_SET_LAST_LU_SEEN] = "UPDATE subscriber SET last_lu_seen = datetime($val, 'unixepoch') WHERE id = $subscriber_id",
+	[DB_STMT_UPD_RAT_FLAG] = "INSERT OR REPLACE INTO subscriber_rat (subscriber_id, rat, allowed)"
+		" VALUES ($subscriber_id, $rat, $allowed)",
+	[DB_STMT_RAT_BY_ID] =
+		"SELECT rat, allowed"
+		" FROM subscriber_rat"
+		" WHERE subscriber_id = $subscriber_id",
 };
 
 static void sql3_error_log_cb(void *arg, int err_code, const char *msg)
@@ -290,6 +296,51 @@
 	return rc;
 }
 
+static int db_upgrade_v2(struct db_context *dbc)
+{
+	int i;
+	const char *stmts[] = {
+		"CREATE TABLE subscriber_rat",
+		"CREATE UNIQUE INDEX idx_subscr_rat_flag",
+		NULL
+	};
+	sqlite3_stmt *stmt = NULL;
+
+	for (i = 0; i < ARRAY_SIZE(stmts); i++) {
+		int rc;
+		int j;
+		const char *stmt_sql = NULL;
+
+		if (stmts[i] != NULL) {
+			for (j = 0; j < ARRAY_SIZE(stmt_bootstrap_sql); j++) {
+				if (strstr(stmt_bootstrap_sql[j], stmts[i])) {
+					/* make sure we have a unique match, hence also not break; here */
+					OSMO_ASSERT(!stmt_sql);
+					stmt_sql = stmt_bootstrap_sql[j];
+				}
+			}
+		} else
+			stmt_sql = "PRAGMA user_version = 2";
+		OSMO_ASSERT(stmt_sql);
+
+		rc = sqlite3_prepare_v2(dbc->db, stmt_sql, -1, &stmt, NULL);
+		if (rc != SQLITE_OK) {
+			LOGP(DDB, LOGL_ERROR, "Unable to prepare SQL statement '%s'\n", stmt_sql);
+			return rc;
+		}
+		rc = sqlite3_step(stmt);
+		db_remove_reset(stmt);
+		sqlite3_finalize(stmt);
+		if (rc != SQLITE_DONE) {
+			LOGP(DDB, LOGL_ERROR, "Unable to update HLR database schema to version 2: '%s'\n",
+			     stmt_sql);
+			return rc;
+		}
+	}
+
+	return SQLITE_DONE;
+}
+
 static int db_get_user_version(struct db_context *dbc)
 {
 	const char *user_version_sql = "PRAGMA user_version";
@@ -390,6 +441,8 @@
 	LOGP(DDB, LOGL_NOTICE, "Database '%s' has HLR DB schema version %d\n", dbc->fname, version);
 
 	if (version < CURRENT_SCHEMA_VERSION && allow_upgrade) {
+		int orig_version = version;
+
 		switch (version) {
 		case 0:
 			rc = db_upgrade_v1(dbc);
@@ -400,21 +453,30 @@
 			}
 			version = 1;
 			/* fall through */
+		case 1:
+			rc = db_upgrade_v2(dbc);
+			if (rc != SQLITE_DONE) {
+				LOGP(DDB, LOGL_ERROR, "Failed to upgrade HLR DB schema to version 2: (rc=%d) %s\n",
+				     rc, sqlite3_errmsg(dbc->db));
+				goto out_free;
+			}
+			version = 2;
+			/* fall through */
 		/* case N: ... */
 		default:
 			break;
 		}
-		LOGP(DDB, LOGL_NOTICE, "Database '%s' has been upgraded to HLR DB schema version %d\n",
-		     dbc->fname, version);
+		LOGP(DDB, LOGL_NOTICE, "Database '%s' has been upgraded from HLR DB schema version %d to %d\n",
+		     dbc->fname, orig_version, version);
 	}
 
 	if (version != CURRENT_SCHEMA_VERSION) {
 		if (version < CURRENT_SCHEMA_VERSION) {
 			LOGP(DDB, LOGL_NOTICE, "HLR DB schema version %d is outdated\n", version);
 			if (!allow_upgrade) {
-				LOGP(DDB, LOGL_ERROR, "Not upgrading HLR database to schema version %d; "
+				LOGP(DDB, LOGL_ERROR, "Not upgrading HLR database from schema version %d to %d; "
 				     "use the --db-upgrade option to allow HLR database upgrades\n",
-				     CURRENT_SCHEMA_VERSION);
+				     version, CURRENT_SCHEMA_VERSION);
 			}
 		} else
 			LOGP(DDB, LOGL_ERROR, "HLR DB schema version %d is unknown\n", version);
diff --git a/src/db.h b/src/db.h
index ae592fb..6fd33ee 100644
--- a/src/db.h
+++ b/src/db.h
@@ -3,6 +3,8 @@
 #include <stdbool.h>
 #include <sqlite3.h>
 
+#include <osmocom/gsm/gsm_utils.h>
+
 struct hlr;
 
 enum stmt_idx {
@@ -26,6 +28,8 @@
 	DB_STMT_AUC_3G_INSERT,
 	DB_STMT_AUC_3G_DELETE,
 	DB_STMT_SET_LAST_LU_SEEN,
+	DB_STMT_UPD_RAT_FLAG,
+	DB_STMT_RAT_BY_ID,
 	_NUM_DB_STMT
 };
 
@@ -85,8 +89,13 @@
 	bool		ms_purged_cs;
 	bool		ms_purged_ps;
 	time_t		last_lu_seen;
+	bool		rat_types[OSMO_RAT_COUNT];
 };
 
+static const struct hlr_subscriber hlr_subscriber_empty = {
+		.rat_types = { true, true, true },
+	};
+
 /* A format string for use with strptime(3). This format string is
  * used to parse the last_lu_seen column stored in the HLR database.
  * See https://sqlite.org/lang_datefunc.html, function datetime(). */
@@ -138,6 +147,10 @@
 
 int hlr_subscr_nam(struct hlr *hlr, struct hlr_subscriber *subscr, bool nam_val, bool is_ps);
 
+int db_subscr_set_rat_type_flag(struct db_context *dbc, int64_t subscr_id, enum osmo_rat_type rat, bool allowed);
+int db_subscr_get_rat_types(struct db_context *dbc, struct hlr_subscriber *subscr);
+int hlr_subscr_rat_flag(struct hlr *hlr, struct hlr_subscriber *subscr, enum osmo_rat_type rat, bool allowed);
+
 /*! Call sqlite3_column_text() and copy result to a char[].
  * \param[out] buf  A char[] used as sizeof() arg(!) and osmo_strlcpy() target.
  * \param[in] stmt  An sqlite3_stmt*.
diff --git a/src/db_hlr.c b/src/db_hlr.c
index c97cd82..f546e46 100644
--- a/src/db_hlr.c
+++ b/src/db_hlr.c
@@ -409,7 +409,7 @@
 	if (!subscr)
 		goto out;
 
-	*subscr = (struct hlr_subscriber){};
+	*subscr = hlr_subscriber_empty;
 
 	/* obtain the various columns */
 	subscr->id = sqlite3_column_int64(stmt, 0);
@@ -444,6 +444,9 @@
 out:
 	db_remove_reset(stmt);
 
+	if (ret == 0)
+		db_subscr_get_rat_types(dbc, subscr);
+
 	switch (ret) {
 	case 0:
 		*err = NULL;
@@ -782,3 +785,119 @@
 	}
 	return 0;
 }
+
+int db_subscr_set_rat_type_flag(struct db_context *dbc, int64_t subscr_id, enum osmo_rat_type rat, bool allowed)
+{
+	int rc;
+	int ret = 0;
+	sqlite3_stmt *stmt = dbc->stmt[DB_STMT_UPD_RAT_FLAG];
+
+	if (!db_bind_int64(stmt, "$subscriber_id", subscr_id))
+		return -EIO;
+
+	OSMO_ASSERT(rat >= 0 && rat < OSMO_RAT_COUNT);
+	if (!db_bind_text(stmt, "$rat", osmo_rat_type_name(rat)))
+		return -EIO;
+
+	if (!db_bind_int(stmt, "$allowed", allowed ? 1 : 0))
+		return -EIO;
+
+	/* execute the statement */
+	rc = sqlite3_step(stmt);
+	if (rc != SQLITE_DONE) {
+		LOGP(DDB, LOGL_ERROR, "%s %s: SQL error: %s\n",
+		     allowed ? "enable" : "disable", osmo_rat_type_name(rat),
+		     sqlite3_errmsg(dbc->db));
+		ret = -EIO;
+		goto out;
+	}
+
+	/* verify execution result */
+	rc = sqlite3_changes(dbc->db);
+	if (!rc) {
+		LOGP(DDB, LOGL_ERROR, "Cannot %s %s: no such subscriber: ID=%" PRIu64 "\n",
+		     allowed ? "enable" : "disable", osmo_rat_type_name(rat),
+		     subscr_id);
+		ret = -ENOENT;
+		goto out;
+	} else if (rc != 1) {
+		LOGP(DDB, LOGL_ERROR, "%s %s: SQL modified %d rows (expected 1)\n",
+		     allowed ? "enable" : "disable", osmo_rat_type_name(rat),
+		     rc);
+		ret = -EIO;
+	}
+
+out:
+	db_remove_reset(stmt);
+	return ret;
+}
+
+int db_subscr_get_rat_types(struct db_context *dbc, struct hlr_subscriber *subscr)
+{
+	int rc;
+	int ret = 0;
+	int i;
+	sqlite3_stmt *stmt = dbc->stmt[DB_STMT_RAT_BY_ID];
+
+	if (!db_bind_int64(stmt, "$subscriber_id", subscr->id))
+		return -EIO;
+
+	for (i = 0; i < OSMO_RAT_COUNT; i++)
+		subscr->rat_types[i] = true;
+
+	/* execute the statement */
+	while (1) {
+		enum osmo_rat_type rat;
+		bool allowed;
+
+		rc = sqlite3_step(stmt);
+
+		if (rc == SQLITE_DONE)
+			break;
+		if (rc != SQLITE_ROW)
+			return -rc;
+
+		rc = get_string_value(osmo_rat_type_names, (const char*)sqlite3_column_text(stmt, 0));
+		if (rc == -EINVAL) {
+			ret = -EINVAL;
+			goto out;
+		}
+		if (rc <= 0 || rc >= OSMO_RAT_COUNT) {
+			ret = -EINVAL;
+			goto out;
+		}
+		rat = rc;
+
+		allowed = sqlite3_column_int(stmt, 1);
+
+		subscr->rat_types[rat] = allowed;
+		LOGP(DAUC, LOGL_DEBUG, "db: imsi='%s' %s %s\n",
+		     subscr->imsi, osmo_rat_type_name(rat), allowed ? "allowed" : "forbidden");
+	}
+
+out:
+	db_remove_reset(stmt);
+	return ret;
+}
+
+int hlr_subscr_rat_flag(struct hlr *hlr, struct hlr_subscriber *subscr, enum osmo_rat_type rat, bool allowed)
+{
+	int rc;
+	OSMO_ASSERT(rat >= 0 && rat < OSMO_RAT_COUNT);
+
+	db_subscr_get_rat_types(hlr->dbc, subscr);
+
+	if (subscr->rat_types[rat] == allowed) {
+		LOGHLR(subscr->imsi, LOGL_DEBUG, "Already has the requested value when asked to %s %s\n",
+		       allowed ? "enable" : "disable", osmo_rat_type_name(rat));
+		return -ENOEXEC;
+	}
+
+	rc = db_subscr_set_rat_type_flag(hlr->dbc, subscr->id, rat, allowed);
+	if (rc)
+		return rc > 0? -rc : rc;
+
+	/* FIXME: If we're disabling, send message to VLR to detach subscriber */
+
+	return 0;
+}
diff --git a/src/hlr.c b/src/hlr.c
index 4873a66..098b934 100644
--- a/src/hlr.c
+++ b/src/hlr.c
@@ -263,6 +263,9 @@
 {
 	struct hlr_subscriber *subscr;
 	struct lu_operation *luop = lu_op_alloc_conn(conn);
+	int i;
+	bool allowed;
+
 	if (!luop) {
 		LOGP(DMAIN, LOGL_ERROR, "LU REQ from conn without addr?\n");
 		return -EINVAL;
@@ -308,6 +311,34 @@
 		return 0;
 	}
 
+	/* Check if any available RAT type is allowed. See 3GPP TS 29.010 3.2 'Routeing area updating' and 3.8 'Location
+	 * update' for the "No Suitable cells in location area" error code. */
+	allowed = false;
+	LOGP(DAUC, LOGL_DEBUG, "LU: IMSI='%s' on %s sent RAT types: %zu\n", subscr->imsi,
+	     gsup->cn_domain == OSMO_GSUP_CN_DOMAIN_CS ? "CS" : "PS", gsup->rat_types_len);
+	for (i = 0; i < gsup->rat_types_len; i++) {
+		enum osmo_rat_type rat = gsup->rat_types[i];
+		if (rat <= 0 || rat >= OSMO_RAT_COUNT) {
+			lu_op_tx_error(luop, GMM_CAUSE_COND_IE_ERR);
+			return 0;
+		}
+		if (luop->subscr.rat_types[rat]) {
+			allowed = true;
+			LOGP(DAUC, LOGL_DEBUG, "LU: IMSI='%s' allowed on %s\n",
+			     subscr->imsi, osmo_rat_type_name(rat));
+		} else {
+			LOGP(DAUC, LOGL_DEBUG, "LU: IMSI='%s' not allowed on %s\n",
+			     subscr->imsi, osmo_rat_type_name(rat));
+		}
+	}
+	if (!allowed && gsup->rat_types_len > 0) {
+		LOGP(DAUC, LOGL_DEBUG, "ISMI='%s' not allowed on %s%s\n",
+		     subscr->imsi, osmo_rat_type_name(gsup->rat_types[0]),
+		     gsup->rat_types_len > 1 ? " (nor on the other available RAT types)" : "");
+		lu_op_tx_error(luop, GMM_CAUSE_NO_SUIT_CELL_IN_LA);
+		return 0;
+	}
+
 	/* TODO: Set subscriber tracing = deactive in VLR/SGSN */
 
 #if 0
diff --git a/src/hlr_vty_subscr.c b/src/hlr_vty_subscr.c
index c2359d4..21296ff 100644
--- a/src/hlr_vty_subscr.c
+++ b/src/hlr_vty_subscr.c
@@ -27,6 +27,7 @@
 #include <osmocom/vty/vty.h>
 #include <osmocom/vty/command.h>
 #include <osmocom/core/utils.h>
+#include <osmocom/gsm/gsm_utils.h>
 
 #include "hlr.h"
 #include "db.h"
@@ -49,6 +50,7 @@
 static void subscr_dump_full_vty(struct vty *vty, struct hlr_subscriber *subscr)
 {
 	int rc;
+	int i;
 	struct osmo_sub_auth_data aud2g;
 	struct osmo_sub_auth_data aud3g;
 
@@ -78,6 +80,12 @@
 		vty_out(vty, "    PS purged%s", VTY_NEWLINE);
 	if (subscr->last_lu_seen)
 		vty_out(vty, "    last LU seen: %s%s", get_datestr(&subscr->last_lu_seen), VTY_NEWLINE);
+	for (i = OSMO_RAT_UNKNOWN + 1; i < ARRAY_SIZE(subscr->rat_types); i++) {
+		vty_out(vty, "    %s: %s%s", osmo_rat_type_name(i), subscr->rat_types[i] ? "allowed" : "forbidden",
+			VTY_NEWLINE);
+	}
+	if (subscr->ms_purged_cs)
+		vty_out(vty, "    CS purged%s", VTY_NEWLINE);
 
 	if (!*subscr->imsi)
 		return;
@@ -506,6 +514,45 @@
 	return CMD_SUCCESS;
 }
 
+DEFUN(subscriber_rat,
+      subscriber_rat_cmd,
+      SUBSCR_UPDATE "rat (geran-a|utran-iu) (allowed|forbidden)",
+      SUBSCR_UPDATE_HELP
+      "Allow or forbid specific Radio Access Types\n"
+      "Set access to GERAN-A\n"
+      "Set access to UTRAN-Iu\n"
+      "Allow access\n"
+      "Forbid access\n")
+{
+	struct hlr_subscriber subscr;
+	const char *id_type = argv[0];
+	const char *id = argv[1];
+	const char *rat_str = argv[2];
+	const char *allowed_forbidden = argv[3];
+	enum osmo_rat_type rat;
+	bool allowed;
+	int rc;
+
+	if (strcmp(rat_str, "geran-a") == 0)
+		rat = OSMO_RAT_GERAN_A;
+	else if (strcmp(rat_str, "utran-iu") == 0)
+		rat = OSMO_RAT_UTRAN_IU;
+
+	allowed = (strcmp(allowed_forbidden, "allowed") == 0);
+
+	if (get_subscr_by_argv(vty, id_type, id, &subscr))
+		return CMD_WARNING;
+
+	rc = hlr_subscr_rat_flag(g_hlr, &subscr, rat, allowed);
+
+	if (rc && rc != -ENOEXEC) {
+		vty_out(vty, "%% Error: cannot set %s to %s%s",
+			osmo_rat_type_name(rat), allowed ? "allowed" : "forbidden", VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	return CMD_SUCCESS;
+}
+
 void hlr_vty_subscriber_init(void)
 {
 	install_element_ve(&subscriber_show_cmd);
@@ -517,4 +564,5 @@
 	install_element(ENABLE_NODE, &subscriber_aud2g_cmd);
 	install_element(ENABLE_NODE, &subscriber_no_aud3g_cmd);
 	install_element(ENABLE_NODE, &subscriber_aud3g_cmd);
+	install_element(ENABLE_NODE, &subscriber_rat_cmd);
 }