From 4d9e793c7be327a127b6828b64f02b6cb1a01e81 Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 16 Nov 2022 21:01:29 +1030 Subject: [PATCH] Implement storage limits and status information for users and the system --- sample.env | 4 ++++ src/components/db.js | 8 +++++++ src/components/userManager.js | 40 ++++++++++++++++++++++++++++++++--- src/controllers/auth.js | 29 +++++++++++++++++++------ src/controllers/system.js | 28 ++++++++++++++++++++++++ src/controllers/user.js | 31 +++++++++++++++------------ src/routes/public.js | 2 ++ test/server.js | 10 +++++++++ 8 files changed, 129 insertions(+), 23 deletions(-) create mode 100644 src/controllers/system.js diff --git a/sample.env b/sample.env index c98e1ffb..78115778 100644 --- a/sample.env +++ b/sample.env @@ -20,6 +20,10 @@ DB_DB_INFO=verida_db_info GC_PERCENT=0.1 # Verida Private Key as hex string (used to sign responses). Including leading 0x. VDA_PRIVATE_KEY= +# Default maximum number of Megabytes for a storage context +DEFAULT_USER_CONTEXT_LIMIT_MB=10 +# Maximum number of users supported by this node +MAX_USERS=10000 // alpha numeric only DB_PUBLIC_USER=784c2n780c9cn0789 diff --git a/src/components/db.js b/src/components/db.js index 566ffdf6..60e821a0 100644 --- a/src/components/db.js +++ b/src/components/db.js @@ -29,6 +29,14 @@ class Db { return env.DB_PROTOCOL + "://" + env.DB_HOST + ":" + env.DB_PORT; } + // Total number of users in the system + async totalUsers() { + const couch = db.getCouch() + const usersDb = couch.db.use('_users') + const info = await usersDb.info() + return info.doc_count + } + } const db = new Db() diff --git a/src/components/userManager.js b/src/components/userManager.js index bc4abcca..9b56bebb 100644 --- a/src/components/userManager.js +++ b/src/components/userManager.js @@ -1,5 +1,8 @@ import crypto from 'crypto'; import Db from './db.js' +import Utils from './utils.js' +import DbManager from './dbManager.js'; + import dotenv from 'dotenv'; dotenv.config(); @@ -27,16 +30,26 @@ class UserManager { } async create(username, signature) { + const maxUsers = parseInt(process.env.MAX_USERS) + const currentUsers = await Db.totalUsers() + + if (currentUsers >= maxUsers) { + throw new Error('Maximum user limit reached') + } + const couch = Db.getCouch() const password = crypto.createHash('sha256').update(signature).digest("hex") + const storageLimit = process.env.DEFAULT_USER_CONTEXT_LIMIT_MB*1048576 + // Create CouchDB database user matching username and password let userData = { _id: `org.couchdb.user:${username}`, name: username, password: password, type: "user", - roles: [] + roles: [], + storageLimit }; let usersDb = couch.db.use('_users'); @@ -53,8 +66,6 @@ class UserManager { } } - - /** * Ensure we have a public user in the database for accessing public data */ @@ -102,6 +113,29 @@ class UserManager { } } + async getUsage(did, contextName) { + const username = Utils.generateUsername(did, contextName); + const user = await this.getByUsername(username); + const databases = await DbManager.getUserDatabases(did, contextName) + + const result = { + databases: 0, + bytes: 0, + storageLimit: user.storageLimit + } + + for (let d in databases) { + const database = databases[d] + const dbInfo = await DbManager.getUserDatabase(did, contextName, database.databaseName) + result.databases++ + result.bytes += dbInfo.info.sizes.file + } + + const usage = result.bytes / parseInt(result.storageLimit) + result.usagePercent = Number(usage.toFixed(4)) + return result + } + } let userManager = new UserManager(); diff --git a/src/controllers/auth.js b/src/controllers/auth.js index 50477f36..7c93503b 100644 --- a/src/controllers/auth.js +++ b/src/controllers/auth.js @@ -51,13 +51,20 @@ class AuthController { // Create the user if they don't exist if (!user) { - const response = await UserManager.create(username, signature); - if (!response || !response.id) { + try { + const response = await UserManager.create(username, signature); + if (!response || !response.id) { + return res.status(500).send({ + status: "fail", + data: { + "auth": "User does not exist and unable to create" + } + }) + } + } catch (err) { return res.status(500).send({ - status: "fail", - data: { - "auth": "User does not exist and unable to create" - } + status: 'fail', + message: err.message }) } } @@ -84,6 +91,15 @@ class AuthController { async connect(req, res) { const refreshToken = req.body.refreshToken; const contextName = req.body.contextName; + const did = req.body.did + + const userUsage = await UserManager.getUsage(did, contextName) + if (userUsage.usagePercent >= 100) { + return res.status(400).send({ + status: "fail", + message: 'Storage limit reached' + }); + } const accessToken = await AuthManager.generateAccessToken(refreshToken, contextName); @@ -92,7 +108,6 @@ class AuthController { status: "success", accessToken, host: Db.buildHost() // required to know the CouchDB host - // username: removed, don't think it is needed }); } else { diff --git a/src/controllers/system.js b/src/controllers/system.js new file mode 100644 index 00000000..bb33542c --- /dev/null +++ b/src/controllers/system.js @@ -0,0 +1,28 @@ +import db from '../components/db' +import Utils from '../components/utils' +import packageJson from '../../package.json' + +import dotenv from 'dotenv'; +dotenv.config(); + +class SystemController { + + async status(req, res) { + const currentUsers = await db.totalUsers() + + const results = { + maxUsers: parseInt(process.env.MAX_USERS), + currentUsers, + version: packageJson.version + } + + return Utils.signedResponse({ + status: "success", + results + }, res); + } + +} + +const systemController = new SystemController(); +export default systemController; \ No newline at end of file diff --git a/src/controllers/user.js b/src/controllers/user.js index f3c334a4..71ffd82a 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -1,4 +1,5 @@ import DbManager from '../components/dbManager.js'; +import UserManager from '../components/userManager.js'; import Utils from '../components/utils.js'; import Db from '../components/db.js' @@ -37,6 +38,14 @@ class UserController { }); } + const userUsage = await UserManager.getUsage(did, contextName) + if (userUsage.usagePercent >= 100) { + return res.status(400).send({ + status: "fail", + message: 'Storage limit reached' + }); + } + const databaseHash = Utils.generateDatabaseName(did, contextName, databaseName) let success; @@ -148,6 +157,14 @@ class UserController { const databaseHash = Utils.generateDatabaseName(did, contextName, databaseName) try { + const userUsage = await UserManager.getUsage(did, contextName) + if (userUsage.usagePercent >= 100) { + return res.status(400).send({ + status: "fail", + message: 'Storage limit reached' + }); + } + let success = await DbManager.updateDatabase(username, databaseHash, contextName, options); if (success) { await DbManager.saveUserDatabase(did, contextName, databaseName, databaseHash, options.permissions) @@ -245,19 +262,7 @@ class UserController { } try { - const databases = await DbManager.getUserDatabases(did, contextName) - - const result = { - databases: 0, - bytes: 0 - } - - for (let d in databases) { - const database = databases[d] - const dbInfo = await DbManager.getUserDatabase(did, contextName, database.databaseName) - result.databases++ - result.bytes += dbInfo.info.sizes.file - } + const result = await UserManager.getUsage(did, contextName) return Utils.signedResponse({ status: "success", diff --git a/src/routes/public.js b/src/routes/public.js index 2095f811..18d372d9 100644 --- a/src/routes/public.js +++ b/src/routes/public.js @@ -1,11 +1,13 @@ import express from 'express'; import UserController from '../controllers/user.js'; import AuthController from '../controllers/auth.js'; +import SystemController from '../controllers/system.js'; const router = express.Router(); // Specify public endpoints router.get('/auth/public', UserController.getPublic); +router.get('/status', SystemController.status); router.post('/auth/generateAuthJwt', AuthController.generateAuthJwt); router.post('/auth/authenticate', AuthController.authenticate); router.post('/auth/connect', AuthController.connect); diff --git a/test/server.js b/test/server.js index e5404b77..4aadd7e1 100644 --- a/test/server.js +++ b/test/server.js @@ -257,6 +257,8 @@ describe("Server tests", function() { const result = response.data.result assert.equal(result.databases, 2, 'Expected number of databases') assert.ok(result.bytes > 0, 'More than 0 bytes used') + assert.ok(result.usagePercent > 0, 'More than 0 percentage usage') + assert.equal(result.storageLimit, process.env.DEFAULT_USER_CONTEXT_LIMIT_MB*1048576, 'Storage limit is 100Mb') }) // @todo: updates @@ -312,7 +314,15 @@ describe("Server tests", function() { assert.ok(response.data.results.indexOf('DeleteAll_3') >= 0, 'Deleted correct databases (DeleteAll_3)') assert.ok(TestUtils.verifySignature(response), 'Have a valid signature in response') }) + }) + describe("Server info", () => { + it("Status", async () => { + const response = await Axios.get(`${SERVER_URL}/status`); + assert.equal(response.data.results.maxUsers, process.env.MAX_USERS, 'Correct maximum number of users') + assert.ok(response.data.results.currentUsers > 2, 'At least two users') + assert.ok(response.data.results.version && response.data.results.version.length, 'Version specified') + }) }) }) \ No newline at end of file