From 6bdeeaa164fbe125fc9e771c1846386299a0cc26 Mon Sep 17 00:00:00 2001 From: NickOvt Date: Thu, 4 Jan 2024 13:22:04 +0200 Subject: [PATCH] feat(upload): ZMS-111 (#584) --- lib/api/auth.js | 263 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 217 insertions(+), 46 deletions(-) diff --git a/lib/api/auth.js b/lib/api/auth.js index 6e8c0f0d..08571960 100644 --- a/lib/api/auth.js +++ b/lib/api/auth.js @@ -6,22 +6,53 @@ const ObjectId = require('mongodb').ObjectId; const tools = require('../tools'); const roles = require('../roles'); const { nextPageCursorSchema, previousPageCursorSchema, pageNrSchema, sessSchema, sessIPSchema, booleanSchema, usernameSchema } = require('../schemas'); +const { successRes } = require('../schemas/response/general-schemas'); +const { userId } = require('../schemas/request/general-schemas'); module.exports = (db, server, userHandler) => { server.post( - '/preauth', + { + path: '/preauth', + summary: 'Pre-auth check', + description: 'Check if an username exists and can be used for authentication', + tags: ['Authentication'], + validationObjs: { + requestBody: { + username: Joi.alternatives() + .try(usernameSchema, Joi.string().email({ tlds: false })) + .required() + .description('Username or E-mail address'), + + scope: Joi.string().default('master').description('Required scope. One of master, imap, smtp, pop3'), + + sess: sessSchema, + ip: sessIPSchema + }, + queryParams: {}, + pathParams: {}, + response: { + 200: { + description: 'Success', + model: Joi.object({ + success: successRes, + id: userId, + username: Joi.string().required().description('Username of authenticated User'), + scope: Joi.string().required().description('The scope this authentication is valid for'), + require2fa: Joi.array().items(Joi.string()).required().description('List of enabled 2FA mechanisms') + }) + } + } + } + }, tools.responseWrapper(async (req, res) => { res.charSet('utf-8'); - const schema = Joi.object().keys({ - username: Joi.alternatives() - .try(usernameSchema, Joi.string().email({ tlds: false })) - .required(), + const { requestBody, pathParams, queryParams } = req.route.spec.validationObjs; - scope: Joi.string().default('master'), - - sess: sessSchema, - ip: sessIPSchema + const schema = Joi.object({ + ...requestBody, + ...pathParams, + ...queryParams }); const result = schema.validate(req.params, { @@ -88,28 +119,65 @@ module.exports = (db, server, userHandler) => { ); server.post( - '/authenticate', + { + path: '/authenticate', + summary: 'Authenticate a User', + tags: ['Authentication'], + validationObjs: { + requestBody: { + username: Joi.alternatives() + .try(usernameSchema, Joi.string().email({ tlds: false })) + .required() + .description('Username or E-mail address'), + password: Joi.string().max(256).required().description('Password'), + + protocol: Joi.string().default('API').description('Application identifier for security logs'), + scope: Joi.string() + .default('master') + // token can be true only if scope is master + .when('token', { is: true, then: Joi.valid('master') }) + .description('Required scope. One of master, imap, smtp, pop3'), + + appId: Joi.string().empty('').uri().description('Optional appId which is the URL of the app'), + + token: booleanSchema + .default(false) + .description( + 'If true then generates a temporary access token that is valid for this user. Only available if scope is "master". When using user tokens then you can replace user ID in URLs with "me".' + ), + + sess: sessSchema, + ip: sessIPSchema + }, + queryParams: {}, + pathParams: {}, + response: { + 200: { + description: 'Success', + model: Joi.object({ + success: successRes, + id: userId, + username: Joi.string().required().description('Username of authenticated User'), + scope: Joi.string().required().description('The scope this authentication is valid for'), + require2fa: Joi.array().items(Joi.string()).required().description('List of enabled 2FA mechanisms'), + requirePasswordChange: booleanSchema.required().description('Indicates if account hassword has been reset and should be replaced'), + token: Joi.string().description( + 'If access token was requested then this is the value to use as access token when making API requests on behalf of logged in user.' + ) + }) + } + } + } + }, tools.responseWrapper(async (req, res) => { res.charSet('utf-8'); - const schema = Joi.object().keys({ - username: Joi.alternatives() - .try(usernameSchema, Joi.string().email({ tlds: false })) - .required(), - password: Joi.string().max(256).required(), - - protocol: Joi.string().default('API'), - scope: Joi.string() - .default('master') - // token can be true only if scope is master - .when('token', { is: true, then: Joi.valid('master') }), - - appId: Joi.string().empty('').uri(), + const { requestBody, pathParams, queryParams } = req.route.spec.validationObjs; - token: booleanSchema.default(false), - - sess: sessSchema, - ip: sessIPSchema + const schema = Joi.object({ + ...requestBody, + ...pathParams, + ...queryParams }); const result = schema.validate(req.params, { @@ -203,7 +271,23 @@ module.exports = (db, server, userHandler) => { ); server.del( - '/authenticate', + { + path: '/authenticate', + summary: 'Invalidate authentication token', + description: 'This method invalidates currently used authentication token. If token is not provided then nothing happens', + tags: ['Authentication'], + validationObjs: { + requestBody: {}, + pathParams: {}, + queryParams: {}, + response: { + 200: { + description: 'Success', + model: Joi.object({ success: successRes }) + } + } + } + }, tools.responseWrapper(async (req, res) => { res.charSet('utf-8'); @@ -223,21 +307,71 @@ module.exports = (db, server, userHandler) => { ); server.get( - { name: 'authlog', path: '/users/:user/authlog' }, + { + name: 'authlog', + path: '/users/:user/authlog', + summary: 'List authentication Events', + tags: ['Authentication'], + validationObjs: { + requestBody: {}, + pathParams: { user: userId }, + queryParams: { + action: Joi.string().trim().lowercase().empty('').max(100).description('Limit listing only to values with specific action value'), + limit: Joi.number().default(20).min(1).max(250).description('How many records to return'), + next: nextPageCursorSchema, + previous: previousPageCursorSchema, + page: pageNrSchema, + filterip: sessIPSchema.description('Limit listing only to values with specific IP address'), + + sess: sessSchema, + ip: sessIPSchema + }, + response: { + 200: { + description: 'Success', + model: Joi.object({ + success: successRes, + action: Joi.string().required().description('Limit listing only to values with specific action value'), + total: Joi.number().required().description('How many results were found'), + page: Joi.number().required().description('Current page number. Derived from page query argument'), + previousCursor: previousPageCursorSchema, + nextCursor: nextPageCursorSchema, + results: Joi.array() + .items( + Joi.object({ + id: Joi.string().required().description('ID of the event'), + action: Joi.string().required().description('Action identifier'), + result: Joi.string().required().description('Did the action succeed'), + sess: sessSchema, + ip: sessIPSchema, + created: Joi.date().required().description('Datestring of the Event time'), + protocol: Joi.string().description('Protocol that the authentication was made from'), + requiredScope: Joi.string().description('Scope of the auth'), + last: Joi.date().required().description('Date of the last update of data'), + events: Joi.number().required().description('Number of times same auth log has accured'), + source: Joi.string().description('Source of auth. Example: `master` if password auth was used'), + expires: Joi.date() + .required() + .description( + 'After this date the given auth log document will not be updated and instead a new one will be created' + ) + }).$_setFlag('objectName', 'GetAuthlogResult') + ) + .required() + }) + } + } + } + }, tools.responseWrapper(async (req, res) => { res.charSet('utf-8'); - const schema = Joi.object().keys({ - user: Joi.string().hex().lowercase().length(24).required(), - action: Joi.string().trim().lowercase().empty('').max(100), - limit: Joi.number().default(20).min(1).max(250), - next: nextPageCursorSchema, - previous: previousPageCursorSchema, - page: pageNrSchema, - filterip: sessIPSchema, - - sess: sessSchema, - ip: sessIPSchema + const { requestBody, pathParams, queryParams } = req.route.spec.validationObjs; + + const schema = Joi.object({ + ...requestBody, + ...pathParams, + ...queryParams }); const result = schema.validate(req.params, { @@ -359,15 +493,52 @@ module.exports = (db, server, userHandler) => { ); server.get( - '/users/:user/authlog/:event', + { + path: '/users/:user/authlog/:event', + summary: 'Request Event information', + tags: ['Authentication'], + validationObjs: { + requestBody: {}, + queryParams: { + sess: sessSchema, + ip: sessIPSchema + }, + pathParams: { + user: userId, + event: Joi.string().hex().lowercase().length(24).required().description('ID of the Event') + }, + response: { + 200: { + description: 'Success', + model: Joi.object({ + id: Joi.string().required().description('ID of the event'), + action: Joi.string().required().description('Action identifier'), + result: Joi.string().required().description('Did the action succeed'), + sess: sessSchema, + ip: sessIPSchema, + created: Joi.date().required().description('Datestring of the Event time'), + protocol: Joi.string().description('Protocol that the authentication was made from'), + requiredScope: Joi.string().description('Scope of the auth'), + last: Joi.date().required().description('Date of the last update of Event'), + events: Joi.number().required().description('Number of times same auth Event has accured'), + source: Joi.string().description('Source of auth. Example: `master` if password auth was used'), + expires: Joi.date() + .required() + .description('After this date the given auth Event will not be updated and instead a new one will be created') + }) + } + } + } + }, tools.responseWrapper(async (req, res) => { res.charSet('utf-8'); - const schema = Joi.object().keys({ - user: Joi.string().hex().lowercase().length(24).required(), - event: Joi.string().hex().lowercase().length(24).required(), - sess: sessSchema, - ip: sessIPSchema + const { requestBody, pathParams, queryParams } = req.route.spec.validationObjs; + + const schema = Joi.object({ + ...requestBody, + ...pathParams, + ...queryParams }); const result = schema.validate(req.params, {