Skip to content

Commit

Permalink
New mechanism for gettings user visible spaces (#52)
Browse files Browse the repository at this point in the history
* Add flat permissions contract and extend methods for use this permissions
* Use new functional for search spaces with permissions
* Remove caching all space list
  • Loading branch information
AMEST authored Sep 22, 2024
1 parent 8be618a commit fc49bd5
Show file tree
Hide file tree
Showing 10 changed files with 188 additions and 130 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Mimisbrunnr.Wiki.Contracts;
using Skidbladnir.Modules;
using Skidbladnir.Repository.Abstractions;

namespace Mimisbrunnr.Storage.MongoDb.Migrations
{
public class SpacesFlatPermissionMigrationModule : BackgroundModule
{
private const int BatchSize = 100;

public override async Task ExecuteAsync(IServiceProvider provider, CancellationToken cancellationToken = default)
{
var logger = provider.GetService<ILogger<SpacesFlatPermissionMigrationModule>>();
var spaceRepository = provider.GetService<IRepository<Space>>();

logger.LogInformation("Permissions migration started");

var page = 0;
var spaces = await GetBatch(spaceRepository, page++);
while (spaces.Length > 0)
{
foreach( var space in spaces){
var previousCount = space.PermissionsFlat == null ? 0 : space.PermissionsFlat.Length;
space.UpdatePermissions();
if(previousCount != space.PermissionsFlat.Length)
await spaceRepository.Update(space);
}
spaces = await GetBatch(spaceRepository, page++);
}
logger.LogInformation("Permissions migration completed");

}

private static async Task<Space[]> GetBatch(IRepository<Space> repository, int page)
{
return await repository.GetAll().Take(BatchSize).Skip(BatchSize * page).ToArrayAsync();
}

}
}
12 changes: 11 additions & 1 deletion src/Mimisbrunnr.Storage.MongoDb/MongoDbStoreModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ namespace Mimisbrunnr.Storage.MongoDb;

public class MongoDbStoreModule : RunnableModule
{
public override Type[] DependsModules => new[] { typeof(PersonalSpaceAvatarMigrationModule) };
public override Type[] DependsModules => [
typeof(PersonalSpaceAvatarMigrationModule),
typeof(SpacesFlatPermissionMigrationModule)
];

public override void Configure(IServiceCollection services)
{
// Register conventions
Expand Down Expand Up @@ -153,6 +157,12 @@ private static async Task CreateSpaceIndexes(IMongoDbContext mongoContext)
Background = true
}));

var visibleSpaces = Builders<Space>.IndexKeys.Ascending(x => x.PermissionsFlat).Ascending(x => x.Type);
await collection.Indexes.CreateOneAsync(new CreateIndexModel<Space>(visibleSpaces, new CreateIndexOptions()
{
Background = true
}));

var nameSpaceFullTextSearch = Builders<Space>.IndexKeys.Text(x => x.Name);
await collection.Indexes.CreateOneAsync(new CreateIndexModel<Space>(nameSpaceFullTextSearch, new CreateIndexOptions()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ namespace Mimisbrunnr.Web.Host.Services.CacheDecorators;

internal class SpaceManagerCacheDecorator : ISpaceManager
{
private const string SpacesCacheKey = "space_cache";
private readonly TimeSpan _maxCacheTime = TimeSpan.FromDays(1);
private readonly TimeSpan _slidingCacheTime = TimeSpan.FromHours(8);
private readonly ISpaceManager _inner;
Expand Down Expand Up @@ -48,21 +47,19 @@ public Task<Space> FindPersonalSpace(UserInfo user)
return _inner.FindPersonalSpace(user);
}

public async Task<Space[]> GetAll()
public Task<Space[]> GetAll(int? take = null, int? skip = null)
{
var cached = await _cache.GetAsync<Space[]>(SpacesCacheKey);
if (cached is not null)
return cached;
return _inner.GetAll(take, skip);
}

cached = await _inner.GetAll();
public Task<Space[]> GetAllWithPermissions(UserInfo user = null, string[] userGroups = null, int? take = null, int? skip = null)
{
return _inner.GetAllWithPermissions(user, userGroups, take, skip);
}

await _cache.SetAsync(SpacesCacheKey,
cached, new DistributedCacheEntryOptions()
{
AbsoluteExpirationRelativeToNow = _maxCacheTime,
SlidingExpiration = _slidingCacheTime
});
return cached;
public Task<Space[]> GetPublicSpaces(int? take = null, int? skip = null)
{
return _inner.GetPublicSpaces(take, skip);
}

public async Task<Space> GetById(string id)
Expand Down Expand Up @@ -131,15 +128,13 @@ private async Task AddSpaceToCache(Space space)
AbsoluteExpirationRelativeToNow = _maxCacheTime,
SlidingExpiration = _slidingCacheTime
});
await _cache.RemoveAsync(SpacesCacheKey);
}

private async Task DeleteSpaceFromCache(Space space)
{
if (space is null) return;
await _cache.RemoveAsync(GetSpaceCacheKey(space.Key));
await _cache.RemoveAsync(GetSpaceCacheKeyById(space.Id));
await _cache.RemoveAsync(SpacesCacheKey);
}

private static string GetSpaceCacheKey(string spaceKey) => $"space_cache_key_{spaceKey}";
Expand Down
2 changes: 1 addition & 1 deletion src/Mimisbrunnr.Web/Search/SearchService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public async Task<IEnumerable<PageModel>> SearchPages(string text, UserInfo sear

var pages = await _pageSearcher.Search(text);
var userSpaces = await _spaceDisplayService.FindUserVisibleSpaces(searchBy);
var userSpacesId = userSpaces.Select(x => x.Id);
var userSpacesId = userSpaces.Select(x => x.Id).ToArray();
return pages.Where(x => userSpacesId.Contains(x.SpaceId)).Select(x => x.ToModel(userSpaces.First(s => s.Id == x.SpaceId).Key));
}

Expand Down
14 changes: 7 additions & 7 deletions src/Mimisbrunnr.Web/Services/ISpaceDisplayService.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using Mimisbrunnr.Wiki.Contracts;

namespace Mimisbrunnr.Web.Services;

public interface ISpaceDisplayService
{
Task<IEnumerable<Space>> FindUserVisibleSpaces(UserInfo userInfo);
using Mimisbrunnr.Wiki.Contracts;

namespace Mimisbrunnr.Web.Services;

public interface ISpaceDisplayService
{
Task<IEnumerable<Space>> FindUserVisibleSpaces(UserInfo userInfo, int? take = null, int? skip = null);
}
2 changes: 1 addition & 1 deletion src/Mimisbrunnr.Web/Wiki/ISpaceService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace Mimisbrunnr.Web.Wiki;

public interface ISpaceService
{
Task<SpaceModel[]> GetAll(UserInfo requestedBy);
Task<SpaceModel[]> GetAll(UserInfo requestedBy, int? take = null, int? skip = null);

Task<SpaceModel> GetByKey(string key, UserInfo requestedBy);

Expand Down
75 changes: 7 additions & 68 deletions src/Mimisbrunnr.Web/Wiki/SpaceService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,13 @@ internal class SpaceService : ISpaceService, ISpaceDisplayService
private readonly ISpaceManager _spaceManager;
private readonly IUserManager _userManager;
private readonly IUserGroupManager _userGroupManager;
private readonly IDistributedCache _distributedCache;
private readonly IApplicationConfigurationManager _applicationConfigurationManager;
private readonly ILogger<SpaceService> _logger;

public SpaceService(IPermissionService permissionService,
ISpaceManager spaceManager,
IUserManager userManager,
IUserGroupManager userGroupManager,
IDistributedCache distributedCache,
IApplicationConfigurationManager applicationConfigurationManager,
ILogger<SpaceService> logger
)
Expand All @@ -34,23 +32,14 @@ ILogger<SpaceService> logger
_spaceManager = spaceManager;
_userManager = userManager;
_userGroupManager = userGroupManager;
_distributedCache = distributedCache;
_applicationConfigurationManager = applicationConfigurationManager;
_logger = logger;
}

public async Task<SpaceModel[]> GetAll(UserInfo requestedBy)
public async Task<SpaceModel[]> GetAll(UserInfo requestedBy, int? take = null, int? skip = null)
{
await _permissionService.EnsureAnonymousAllowed(requestedBy);
var spaces = await _spaceManager.GetAll();
if (requestedBy is null)
return spaces.Where(x => x.Type == SpaceType.Public).Select(x => x.ToModel()).ToArray();

var user = await _userManager.GetByEmail(requestedBy.Email);
if (user.Role == UserRole.Admin)
return spaces.Select(x => x.ToModel()).ToArray();

var visibleSpaces = await FindUserVisibleSpaces(requestedBy);
var visibleSpaces = await FindUserVisibleSpaces(requestedBy, take, skip);
return visibleSpaces.Select(x => x.ToModel()).ToArray();
}

Expand Down Expand Up @@ -106,7 +95,6 @@ public async Task<SpacePermissionModel> AddPermission(string key, SpacePermissio
EnsureSpaceExists(space);

await _spaceManager.AddPermission(space, model.ToEntity());
await ClearUserVisibleSpacesAfterChangingPermissions(model);

return model;
}
Expand All @@ -120,7 +108,6 @@ public async Task UpdatePermission(string key, SpacePermissionModel model, UserI
throw new SpaceNotFoundException();

await _spaceManager.UpdatePermission(space, model.ToEntity());
await ClearUserVisibleSpacesAfterChangingPermissions(model);
}

public async Task RemovePermission(string key, SpacePermissionModel model, UserInfo removedBy)
Expand All @@ -131,7 +118,6 @@ public async Task RemovePermission(string key, SpacePermissionModel model, UserI
EnsureSpaceExists(space);

await _spaceManager.RemovePermission(space, model.ToEntity());
await ClearUserVisibleSpacesAfterChangingPermissions(model);
}

public async Task<SpaceModel> Create(SpaceCreateModel model, UserInfo createdBy)
Expand Down Expand Up @@ -240,61 +226,17 @@ public async Task Remove(string key, UserInfo removedBy)
_logger.LogInformation("User `{User}` remove space `{SpaceKey}`", removedBy.Email, key);
}

public async Task<IEnumerable<Space>> FindUserVisibleSpaces(UserInfo userInfo)
public async Task<IEnumerable<Space>> FindUserVisibleSpaces(UserInfo userInfo, int? take = null, int? skip = null)
{
var cacheKey = CreateUserVisibleSpacesCacheKey(userInfo?.Email ?? "Anonymous");
IEnumerable<Space> spaces = await _distributedCache.GetAsync<Space[]>(cacheKey);
if (spaces is not null) return spaces;

spaces = await _spaceManager.GetAll();
if (userInfo == null)
{
spaces = spaces.Where(x => x.Type == SpaceType.Public);
await AddVisibleSpacesToCache(cacheKey, spaces);
return spaces;
}
if (userInfo is null)
return await _spaceManager.GetPublicSpaces(take, skip);

var user = await _userManager.GetByEmail(userInfo.Email);
if (user.Role == UserRole.Admin)
{
await AddVisibleSpacesToCache(cacheKey, spaces);
return spaces;
}
return await _spaceManager.GetAll(take, skip);

var userGroups = await _userGroupManager.GetUserGroups(user);
spaces = spaces.Where(x =>
x.Type == SpaceType.Public || FindPermission(x.Permissions.ToArray(), userInfo, userGroups) != null);
await AddVisibleSpacesToCache(cacheKey, spaces);
return spaces;
}

private async Task ClearUserVisibleSpacesAfterChangingPermissions(SpacePermissionModel permissionModel)
{
if (permissionModel?.User is not null)
{
await _distributedCache.RemoveAsync(CreateUserVisibleSpacesCacheKey(permissionModel.User.Email));
return;
}

var group = await _userGroupManager.FindByName(permissionModel?.Group.Name);
if (group is null) throw new GroupNotFoundException();

var usersInGroup = await _userGroupManager.GetUsersInGroup(group);
var clearTasks = new List<Task>();
foreach (var user in usersInGroup)
clearTasks.Add(_distributedCache.RemoveAsync(CreateUserVisibleSpacesCacheKey(user.Email)));

await Task.WhenAll(clearTasks);
}

private Task AddVisibleSpacesToCache(string cacheKey, IEnumerable<Space> spaces)
{
return _distributedCache.SetAsync(cacheKey, spaces,
new DistributedCacheEntryOptions()
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(2)
}
);
return await _spaceManager.GetAllWithPermissions(userInfo, userGroups.Select(x => x.Name).ToArray(), take, skip);
}

private static void EnsureIsNotAnonymous(UserInfo userInfo)
Expand Down Expand Up @@ -324,7 +266,4 @@ private static Permission FindPermission(Permission[] permissions, UserInfo user
x.Group != null && groups.Any(g => g.Name.Equals(x.Group.Name)));
return userPermission ?? groupPermission;
}

private static string CreateUserVisibleSpacesCacheKey(string email) =>
$"user_visible_spaces_cache_email_{email.ToLower()}";
}
12 changes: 12 additions & 0 deletions src/Mimisbrunnr.Wiki/Contracts/Space.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public Space(string id, string key, string name, string description, string home
Status = status;
Permissions = permissions;
}

public string Id { get; set; }

public string Key { get; internal set; }
Expand All @@ -39,5 +40,16 @@ public Space(string id, string key, string name, string description, string home

public SpaceStatus Status { get; internal set; }

public string[] PermissionsFlat { get; private set; }

public IEnumerable<Permission> Permissions { get; internal set; }

public void UpdatePermissions()
{
var flatUsers = Permissions.Where(x => x.User != null)
.Select(x => $"user_{x.User.Email?.ToLower()}");
var flatGroup = Permissions.Where(x => x.Group != null)
.Select(x => $"group_{x.Group.Name?.ToLower()}");
PermissionsFlat = flatUsers.Concat(flatGroup).ToArray();
}
}
Loading

0 comments on commit fc49bd5

Please sign in to comment.