This commit is contained in:
2020-04-21 18:54:56 +00:00
parent 8c19e087e2
commit 4b4f530914
3 changed files with 38 additions and 24 deletions

View File

@@ -10,6 +10,12 @@ todo: check attachment NOTES property is actually supported
//todo: search tables in schema, I think there is a missing index here, need to look at the search query section again as it was changed several times from the original schema creation //todo: search tables in schema, I think there is a missing index here, need to look at the search query section again as it was changed several times from the original schema creation
todo: log failed
- Download attempts with wrong key
- Add delay for failed download
- authentication attempts (or would this cause a possible DOS?)
todo: some kind of ops specific event log or system that triggers alerts to ops people, I guess notifications would be the word I'm looking for...
- Once that exists a lot of backfilling needs to be done for example attachment files missing notification etc etc etc
todo: can a user be locked out from the server end even though they posess a valid token? todo: can a user be locked out from the server end even though they posess a valid token?
- and prevent download of images etc? - and prevent download of images etc?

View File

@@ -36,7 +36,6 @@ namespace AyaNova.Api.Controllers
[ApiVersion("8.0")] [ApiVersion("8.0")]
[Route("api/v{version:apiVersion}/[controller]")] [Route("api/v{version:apiVersion}/[controller]")]
[Produces("application/json")] [Produces("application/json")]
[Authorize]
public class AttachmentController : ControllerBase public class AttachmentController : ControllerBase
{ {
private readonly AyContext ct; private readonly AyContext ct;
@@ -118,6 +117,7 @@ namespace AyaNova.Api.Controllers
/// ///
/// </summary> /// </summary>
/// <returns>NameValue list of filenames and attachment id's</returns> /// <returns>NameValue list of filenames and attachment id's</returns>
[Authorize]
[HttpPost] [HttpPost]
[DisableFormValueModelBinding] [DisableFormValueModelBinding]
[RequestSizeLimit(10737418241)]//10737418240 = 10gb https://github.com/aspnet/Announcements/issues/267 [RequestSizeLimit(10737418241)]//10737418240 = 10gb https://github.com/aspnet/Announcements/issues/267
@@ -269,6 +269,7 @@ namespace AyaNova.Api.Controllers
/// </summary> /// </summary>
/// <param name="id"></param> /// <param name="id"></param>
/// <returns>Ok</returns> /// <returns>Ok</returns>
[Authorize]
[HttpDelete("{id}")] [HttpDelete("{id}")]
public async Task<IActionResult> DeleteAttachmentAsync([FromRoute] long id) public async Task<IActionResult> DeleteAttachmentAsync([FromRoute] long id)
{ {
@@ -313,11 +314,14 @@ namespace AyaNova.Api.Controllers
/// Download a file attachment /// Download a file attachment
/// </summary> /// </summary>
/// <param name="id"></param> /// <param name="id"></param>
/// <param name="dlkey"></param> /// <param name="t">download token</param>
/// <returns></returns> /// <returns></returns>
[HttpGet("download/{id}")] [HttpGet("download/{id}")]
public async Task<IActionResult> DownloadAsync([FromRoute] long id, [FromQuery] string dlkey) public async Task<IActionResult> DownloadAsync([FromRoute] long id, [FromQuery] string t)
{ {
int nFailedAuthDelay = 3000;//should be just long enough to make brute force a hassle but short enough to not annoy people who just mistyped their creds to login
//NOTE this is the only unauthorized route as it needs to work with wiki url links and relies on the dlkey to work
//copied from Rockfish //copied from Rockfish
//https://dotnetcoretutorials.com/2017/03/12/uploading-files-asp-net-core/ //https://dotnetcoretutorials.com/2017/03/12/uploading-files-asp-net-core/
//https://stackoverflow.com/questions/45763149/asp-net-core-jwt-in-uri-query-parameter/45811270#45811270 //https://stackoverflow.com/questions/45763149/asp-net-core-jwt-in-uri-query-parameter/45811270#45811270
@@ -325,47 +329,57 @@ namespace AyaNova.Api.Controllers
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
if (string.IsNullOrWhiteSpace(dlkey)) //NOTE: this is a potentially dangerous route since it's not Authorized so we need to treat it like Auth route and not leak any
//useful information to bad actors and also ensure a delay to avoid brute force or DOS attacks
if (string.IsNullOrWhiteSpace(t))
{ {
return NotFound(); await Task.Delay(nFailedAuthDelay);//DOS protection
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
} }
//get user by key, if not found then reject //get user by key, if not found then reject
//If user dlkeyexp has not expired then return file //If user dlkeyexp has not expired then return file
var dlkeyUser = await ct.User.SingleOrDefaultAsync(m => m.DlKey == dlkey); var dlkeyUser = await ct.User.SingleOrDefaultAsync(m => m.DlKey == t && m.Active == true);
if (dlkeyUser == null) if (dlkeyUser == null)
{ {
//don't want to leak information so just say not found await Task.Delay(nFailedAuthDelay);//DOS protection
//return BadRequest(new ApiErrorResponse(ApiErrorCode.NOT_AUTHORIZED, "dlkey", "Download token not valid"));
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED)); return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
} }
//Make sure the token provided is for the current user //Make sure the token provided is for the current user
//bugbug: Is this not a bug, there's no way to set this properly is there?
long UserId = UserIdFromContext.Id(HttpContext.Items); long UserId = UserIdFromContext.Id(HttpContext.Items);
if (UserId != dlkeyUser.Id) if (UserId != dlkeyUser.Id)
{ {
// return BadRequest(new ApiErrorResponse(ApiErrorCode.NOT_AUTHORIZED, "dlkey", "Download token not valid")); await Task.Delay(nFailedAuthDelay);//DOS protection
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED)); return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
} }
var utcNow = new DateTimeOffset(DateTime.Now.ToUniversalTime(), TimeSpan.Zero); var utcNow = new DateTimeOffset(DateTime.Now.ToUniversalTime(), TimeSpan.Zero);
if (dlkeyUser.DlKeyExpire < utcNow.DateTime) if (dlkeyUser.DlKeyExpire < utcNow.DateTime)
{ {
// return BadRequest(new ApiErrorResponse(ApiErrorCode.NOT_AUTHORIZED, "dlkey", "Download token has expired"));
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED)); await Task.Delay(nFailedAuthDelay);//DOS protection
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
} }
//Ok, user has a valid download key and it's not expired yet so get the attachment record //Ok, user has a valid download key and it's not expired yet so get the attachment record
var dbObj = await ct.FileAttachment.SingleOrDefaultAsync(m => m.Id == id); var dbObj = await ct.FileAttachment.SingleOrDefaultAsync(m => m.Id == id);
if (dbObj == null) if (dbObj == null)
{ {
await Task.Delay(nFailedAuthDelay);//fishing protection
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND)); return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
} }
//is this allowed? //is this allowed?
if (!Authorized.HasReadFullRole(HttpContext.Items, dbObj.AttachToObjectType)) if (!Authorized.HasReadFullRole(HttpContext.Items, dbObj.AttachToObjectType))
{ {
await Task.Delay(nFailedAuthDelay);//DOS protection
return StatusCode(403, new ApiNotAuthorizedResponse()); return StatusCode(403, new ApiNotAuthorizedResponse());
} }
@@ -376,7 +390,11 @@ namespace AyaNova.Api.Controllers
{ {
//TODO: this should trigger some kind of notification to the ops people //TODO: this should trigger some kind of notification to the ops people
//and a red light on the dashboard //and a red light on the dashboard
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND, null, $"Physical file {dbObj.StoredFileName} not found despite attachment record, this file is missing"));
var errText = $"Physical file {dbObj.StoredFileName} not found despite attachment record, this file is missing";
log.LogError(errText);
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND, null, errText));
} }
//Log //Log

View File

@@ -177,16 +177,6 @@ namespace AyaNova.Api.Controllers
} }
// //If the user is inactive they may not login
// if (!u.Active)
// {
// //This is leaking information, instead just act like bad creds
// //return StatusCode(401, new ApiErrorResponse(ApiErrorCode.NOT_AUTHORIZED, null, "User deactivated"));
// return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
// }
//build the key (JWT set in startup.cs) //build the key (JWT set in startup.cs)
byte[] secretKey = System.Text.Encoding.ASCII.GetBytes(ServerBootConfig.AYANOVA_JWT_SECRET); byte[] secretKey = System.Text.Encoding.ASCII.GetBytes(ServerBootConfig.AYANOVA_JWT_SECRET);