mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 23:18:44 -07:00
Refactor room role permissions: enhance role checks for saving and overlaying data; introduce editable role data for RRO rooms
This commit is contained in:
@@ -1079,7 +1079,9 @@ public class RoomsController(
|
|||||||
// the all-perms default blob until their first save replaces
|
// the all-perms default blob until their first save replaces
|
||||||
// it via StorageController; the rec center / paintball / etc.
|
// it via StorageController; the rec center / paintball / etc.
|
||||||
// serve "" so the persistence flow short-circuits.
|
// serve "" so the persistence flow short-circuits.
|
||||||
var customisable = room.IsDormRoom || room.CreatorPlayerId != 1;
|
var roleList = roles ?? Array.Empty<RoomRoleEntity>();
|
||||||
|
var editableRro = room.IsAGRoom && roleList.Any(r => r.Accepted);
|
||||||
|
var customisable = room.IsDormRoom || room.CreatorPlayerId != 1 || editableRro;
|
||||||
var dataBlobName = !string.IsNullOrEmpty(room.CurrentDataBlobName)
|
var dataBlobName = !string.IsNullOrEmpty(room.CurrentDataBlobName)
|
||||||
? room.CurrentDataBlobName
|
? room.CurrentDataBlobName
|
||||||
: customisable ? $"room_{room.Id}_v1.dat" : "";
|
: customisable ? $"room_{room.Id}_v1.dat" : "";
|
||||||
@@ -1153,7 +1155,6 @@ public class RoomsController(
|
|||||||
// Additional grants come from RoomRoleEntity rows when supplied;
|
// Additional grants come from RoomRoleEntity rows when supplied;
|
||||||
// callers without role data (clone/myrooms/etc.) ship just the
|
// callers without role data (clone/myrooms/etc.) ship just the
|
||||||
// implicit creator.
|
// implicit creator.
|
||||||
var roleList = roles ?? Array.Empty<RoomRoleEntity>();
|
|
||||||
int[] PlayersIn(int role, bool accepted) => roleList
|
int[] PlayersIn(int role, bool accepted) => roleList
|
||||||
.Where(r => r.Role == role && r.Accepted == accepted)
|
.Where(r => r.Role == role && r.Accepted == accepted)
|
||||||
.Select(r => (int)r.PlayerId)
|
.Select(r => (int)r.PlayerId)
|
||||||
@@ -1194,7 +1195,9 @@ public class RoomsController(
|
|||||||
string? overrideDataBlobName = null,
|
string? overrideDataBlobName = null,
|
||||||
IReadOnlyList<RoomRoleEntity>? roles = null)
|
IReadOnlyList<RoomRoleEntity>? roles = null)
|
||||||
{
|
{
|
||||||
var customisable = room.IsDormRoom || room.CreatorPlayerId != 1;
|
var roleList = roles ?? Array.Empty<RoomRoleEntity>();
|
||||||
|
var editableRro = room.IsAGRoom && roleList.Any(r => r.Accepted);
|
||||||
|
var customisable = room.IsDormRoom || room.CreatorPlayerId != 1 || editableRro;
|
||||||
var dataBlobName = !string.IsNullOrEmpty(room.CurrentDataBlobName)
|
var dataBlobName = !string.IsNullOrEmpty(room.CurrentDataBlobName)
|
||||||
? room.CurrentDataBlobName
|
? room.CurrentDataBlobName
|
||||||
: customisable ? $"room_{room.Id}_v1.dat" : "";
|
: customisable ? $"room_{room.Id}_v1.dat" : "";
|
||||||
@@ -1233,7 +1236,6 @@ public class RoomsController(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
var roleList = roles ?? Array.Empty<RoomRoleEntity>();
|
|
||||||
static int WireRole(int role) => role switch
|
static int WireRole(int role) => role switch
|
||||||
{
|
{
|
||||||
0 => 30, // CoOwner
|
0 => 30, // CoOwner
|
||||||
|
|||||||
@@ -299,10 +299,17 @@ public class RoomsModerationController(DorkNetDbContext db) : ControllerBase
|
|||||||
return Ok(new { success = true, error = "" });
|
return Ok(new { success = true, error = "" });
|
||||||
|
|
||||||
var room = await db.Rooms.FirstOrDefaultAsync(r => r.Id == roomId);
|
var room = await db.Rooms.FirstOrDefaultAsync(r => r.Id == roomId);
|
||||||
int role = (room?.CreatorPlayerId == playerId) ? 30 : 0;
|
var acceptedRoles = await db.RoomRoles
|
||||||
|
.Where(r => r.RoomId == roomId && r.PlayerId == playerId && r.Accepted)
|
||||||
|
.Select(r => r.Role)
|
||||||
|
.ToListAsync();
|
||||||
|
int role = (room?.CreatorPlayerId == playerId)
|
||||||
|
? 30
|
||||||
|
: acceptedRoles.Select(RoomRoleRank).DefaultIfEmpty(0).Max();
|
||||||
long permissions = role switch
|
long permissions = role switch
|
||||||
{
|
{
|
||||||
30 => -1L,
|
30 => -1L,
|
||||||
|
20 => 0x0FFFFFFFL,
|
||||||
10 => 0x0FFFFFFFL,
|
10 => 0x0FFFFFFFL,
|
||||||
5 => 0x000000FFL,
|
5 => 0x000000FFL,
|
||||||
_ => 0x0000000FL,
|
_ => 0x0000000FL,
|
||||||
|
|||||||
@@ -356,7 +356,7 @@ public class CdnController(
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
s3Bytes = roomDataBlob.OverlayAllPermsRoleData(s3Bytes);
|
s3Bytes = roomDataBlob.OverlayRroEditableRoleData(s3Bytes);
|
||||||
overlaidRoomRoleData = true;
|
overlaidRoomRoleData = true;
|
||||||
logger.LogInformation("[cdn] overlaid RRO room-role permissions host={Host} file={File} bytes={Bytes}",
|
logger.LogInformation("[cdn] overlaid RRO room-role permissions host={Host} file={File} bytes={Bytes}",
|
||||||
Request.Host.Host, fileName, s3Bytes.Length);
|
Request.Host.Host, fileName, s3Bytes.Length);
|
||||||
@@ -393,6 +393,16 @@ public class CdnController(
|
|||||||
return new FileContentResult(TransparentPng, "image/png");
|
return new FileContentResult(TransparentPng, "image/png");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (await ShouldOverlayRroRoomRoleDataAsync(fileName))
|
||||||
|
{
|
||||||
|
logger.LogInformation("[cdn] MISS host={Host} file={File} -> RRO editable role blob",
|
||||||
|
Request.Host.Host, fileName);
|
||||||
|
var rroBlob = roomDataBlob.GetRroEditableBlob();
|
||||||
|
Response.Headers.CacheControl = "public, max-age=60";
|
||||||
|
signatures.AddContentSignature(Response, rroBlob);
|
||||||
|
return new FileContentResult(rroBlob, "application/octet-stream");
|
||||||
|
}
|
||||||
|
|
||||||
logger.LogInformation("[cdn] MISS host={Host} file={File} -> default blob",
|
logger.LogInformation("[cdn] MISS host={Host} file={File} -> default blob",
|
||||||
Request.Host.Host, fileName);
|
Request.Host.Host, fileName);
|
||||||
var fallbackBlob = roomDataBlob.GetDefaultBlob();
|
var fallbackBlob = roomDataBlob.GetDefaultBlob();
|
||||||
@@ -405,13 +415,24 @@ public class CdnController(
|
|||||||
if (string.IsNullOrWhiteSpace(fileName)) return false;
|
if (string.IsNullOrWhiteSpace(fileName)) return false;
|
||||||
|
|
||||||
if (await db.Rooms.AsNoTracking().AnyAsync(r =>
|
if (await db.Rooms.AsNoTracking().AnyAsync(r =>
|
||||||
r.IsAGRoom && r.CurrentDataBlobName == fileName))
|
r.IsAGRoom &&
|
||||||
|
r.CurrentDataBlobName == fileName &&
|
||||||
|
db.RoomRoles.Any(rr => rr.RoomId == r.Id && rr.Accepted)))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (fileName.StartsWith("room_", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
fileName.EndsWith("_v1.dat", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
long.TryParse(fileName[5..^7], out var roomId) &&
|
||||||
|
await db.Rooms.AsNoTracking().AnyAsync(r => r.Id == roomId && r.IsAGRoom) &&
|
||||||
|
await db.RoomRoles.AsNoTracking().AnyAsync(r => r.RoomId == roomId && r.Accepted))
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
return await (
|
return await (
|
||||||
from scene in db.RoomScenes.AsNoTracking()
|
from scene in db.RoomScenes.AsNoTracking()
|
||||||
join room in db.Rooms.AsNoTracking() on scene.RoomId equals room.Id
|
join room in db.Rooms.AsNoTracking() on scene.RoomId equals room.Id
|
||||||
where room.IsAGRoom && scene.DataBlobName == fileName
|
where room.IsAGRoom &&
|
||||||
|
scene.DataBlobName == fileName &&
|
||||||
|
db.RoomRoles.Any(rr => rr.RoomId == room.Id && rr.Accepted)
|
||||||
select scene.Id
|
select scene.Id
|
||||||
).AnyAsync();
|
).AnyAsync();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,10 +28,9 @@ namespace DorkNet.Server.Controllers.Storage;
|
|||||||
/// the room.</item>
|
/// the room.</item>
|
||||||
/// </list>
|
/// </list>
|
||||||
///
|
///
|
||||||
/// Permission rule: only the room's creator (or the dorm owner for a
|
/// Permission rule: the room's creator, accepted co-owners, and admins
|
||||||
/// dorm save) can persist a new blob. Saves from other players in the
|
/// can persist a new blob. Dorm saves are always scoped to the local
|
||||||
/// same room are silently rejected with 403 — they can build with the
|
/// player's own dorm row.
|
||||||
/// Maker Pen but their changes won't outlive the session.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
@@ -174,12 +173,14 @@ public class StorageController(
|
|||||||
if (room.IsDormRoom)
|
if (room.IsDormRoom)
|
||||||
return await UploadDormSaveAsync(playerId, bytes, references, room.Id);
|
return await UploadDormSaveAsync(playerId, bytes, references, room.Id);
|
||||||
|
|
||||||
// Permission: only the room owner can save. (Future Phase 5
|
var canSave = room.CreatorPlayerId == playerId
|
||||||
// expansion: co-owners + admins. For now, strict owner-only —
|
|| await db.Players.AnyAsync(p => p.Id == playerId && p.IsAdmin)
|
||||||
// matches the 2020 official server's "RoomRolePermissions
|
|| await db.RoomRoles.AnyAsync(r =>
|
||||||
// .CanUseMakerPen" behaviour, which rejects upload at the wire
|
r.RoomId == room.Id &&
|
||||||
// for non-owners.)
|
r.PlayerId == playerId &&
|
||||||
if (room.CreatorPlayerId != playerId)
|
r.Accepted &&
|
||||||
|
r.Role == 0);
|
||||||
|
if (!canSave)
|
||||||
{
|
{
|
||||||
logger.LogWarning(
|
logger.LogWarning(
|
||||||
"[storage] player {PlayerId} tried to save room {RoomId} (creator={CreatorId})",
|
"[storage] player {PlayerId} tried to save room {RoomId} (creator={CreatorId})",
|
||||||
|
|||||||
@@ -31,8 +31,11 @@ public class RoomDataBlobService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly byte[] _allPermsBlob = BuildAllPermsBlob();
|
private readonly byte[] _allPermsBlob = BuildAllPermsBlob();
|
||||||
private readonly RoomRoleCollectionData _allPermsRoleData = BuildAllPermsRoleData();
|
private readonly RoomRoleCollectionData _allPermsRoleData = BuildAllPermsRoleData();
|
||||||
|
private readonly byte[] _rroEditableBlob = BuildRroEditableBlob();
|
||||||
|
private readonly RoomRoleCollectionData _rroEditableRoleData = BuildRroEditableRoleData();
|
||||||
|
|
||||||
public byte[] GetDefaultBlob() => _allPermsBlob;
|
public byte[] GetDefaultBlob() => _allPermsBlob;
|
||||||
|
public byte[] GetRroEditableBlob() => _rroEditableBlob;
|
||||||
|
|
||||||
public byte[] OverlayAllPermsRoleData(byte[] existingBlob)
|
public byte[] OverlayAllPermsRoleData(byte[] existingBlob)
|
||||||
{
|
{
|
||||||
@@ -41,6 +44,13 @@ public class RoomDataBlobService
|
|||||||
return msg.ToByteArray();
|
return msg.ToByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public byte[] OverlayRroEditableRoleData(byte[] existingBlob)
|
||||||
|
{
|
||||||
|
var msg = PersistedRoomData.Parser.ParseFrom(existingBlob);
|
||||||
|
msg.RoomRoleData = _rroEditableRoleData.Clone();
|
||||||
|
return msg.ToByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
private static byte[] BuildAllPermsBlob()
|
private static byte[] BuildAllPermsBlob()
|
||||||
{
|
{
|
||||||
var msg = new PersistedRoomData
|
var msg = new PersistedRoomData
|
||||||
@@ -52,6 +62,35 @@ public class RoomDataBlobService
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static RoomRoleCollectionData BuildAllPermsRoleData()
|
private static RoomRoleCollectionData BuildAllPermsRoleData()
|
||||||
|
{
|
||||||
|
var collection = new RoomRoleCollectionData();
|
||||||
|
collection.RoomRoles.Add(BuildPermissiveRole(0, 100, "Creator"));
|
||||||
|
|
||||||
|
return collection;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] BuildRroEditableBlob()
|
||||||
|
{
|
||||||
|
var msg = new PersistedRoomData
|
||||||
|
{
|
||||||
|
RoomRoleData = BuildRroEditableRoleData(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return msg.ToByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RoomRoleCollectionData BuildRroEditableRoleData()
|
||||||
|
{
|
||||||
|
var collection = new RoomRoleCollectionData();
|
||||||
|
collection.RoomRoles.Add(BuildPermissiveRole(2_097_152, 100, "Creator")); // AG_CREATOR
|
||||||
|
collection.RoomRoles.Add(BuildPermissiveRole(4_194_304, 90, "Co-owner")); // AG_COOWNER
|
||||||
|
collection.RoomRoles.Add(BuildPermissiveRole(8_388_608, 80, "Host")); // AG_HOST
|
||||||
|
collection.RoomRoles.Add(BuildPermissiveRole(16_777_216, 70, "Moderator")); // AG_MODERATOR
|
||||||
|
|
||||||
|
return collection;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PlayerRoomRoleData BuildPermissiveRole(int roleId, int rank, string name)
|
||||||
{
|
{
|
||||||
// OverridableBoolData(overrides=true, inner_value=true) — used for
|
// OverridableBoolData(overrides=true, inner_value=true) — used for
|
||||||
// every Can* field on the role.
|
// every Can* field on the role.
|
||||||
@@ -61,27 +100,24 @@ public class RoomDataBlobService
|
|||||||
InnerValue = true,
|
InnerValue = true,
|
||||||
};
|
};
|
||||||
|
|
||||||
var role = new PlayerRoomRoleData
|
return new PlayerRoomRoleData
|
||||||
{
|
{
|
||||||
// Identity fields — these are deprecated in the 2026 schema
|
// Identity fields — these are deprecated in the 2026 schema
|
||||||
// but the 2020 client still reads them. role_id=0 = AG_EVERYONE
|
// but the 2020 client still reads them.
|
||||||
// which the client treats as "applies to all players in an AG
|
DEPRECATEDRoleId = roleId,
|
||||||
// room"; combined with all permissions overridden true, every
|
DEPRECATEDRoleRank = rank,
|
||||||
// player slot inherits maker pen.
|
|
||||||
DEPRECATEDRoleId = 0,
|
|
||||||
DEPRECATEDRoleRank = 100,
|
|
||||||
DEPRECATEDIsRoleActive = true,
|
DEPRECATEDIsRoleActive = true,
|
||||||
DEPRECATEDIsAgRole = true,
|
DEPRECATEDIsAgRole = true,
|
||||||
RoleName = "Creator",
|
RoleName = name,
|
||||||
RoleVersion = 1,
|
RoleVersion = 1,
|
||||||
RoleGuid = Guid.NewGuid().ToString(),
|
RoleGuid = StableRoleGuid(roleId),
|
||||||
|
|
||||||
// Display name override — keeps the watch's role-list UI from
|
// Display name override — keeps the watch's role-list UI from
|
||||||
// showing a blank label.
|
// showing a blank label.
|
||||||
Name = new OverridableStringData
|
Name = new OverridableStringData
|
||||||
{
|
{
|
||||||
Overrides = true,
|
Overrides = true,
|
||||||
InnerValue = "Creator",
|
InnerValue = name,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Permission grants. Each Overridable*Data(overrides=true,
|
// Permission grants. Each Overridable*Data(overrides=true,
|
||||||
@@ -115,10 +151,42 @@ public class RoomDataBlobService
|
|||||||
InnerValue = 0,
|
InnerValue = 0,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
var collection = new RoomRoleCollectionData();
|
private static string StableRoleGuid(int roleId) =>
|
||||||
collection.RoomRoles.Add(role);
|
GuidUtility.Create(GuidUtility.UrlNamespace, $"dorknet-room-role:{roleId}").ToString();
|
||||||
|
}
|
||||||
|
|
||||||
return collection;
|
internal static class GuidUtility
|
||||||
|
{
|
||||||
|
public static readonly Guid UrlNamespace = new("6ba7b811-9dad-11d1-80b4-00c04fd430c8");
|
||||||
|
|
||||||
|
public static Guid Create(Guid namespaceId, string name)
|
||||||
|
{
|
||||||
|
var namespaceBytes = namespaceId.ToByteArray();
|
||||||
|
SwapByteOrder(namespaceBytes);
|
||||||
|
|
||||||
|
var nameBytes = System.Text.Encoding.UTF8.GetBytes(name);
|
||||||
|
var data = new byte[namespaceBytes.Length + nameBytes.Length];
|
||||||
|
Buffer.BlockCopy(namespaceBytes, 0, data, 0, namespaceBytes.Length);
|
||||||
|
Buffer.BlockCopy(nameBytes, 0, data, namespaceBytes.Length, nameBytes.Length);
|
||||||
|
|
||||||
|
var hash = System.Security.Cryptography.SHA1.HashData(data);
|
||||||
|
var newGuid = new byte[16];
|
||||||
|
Array.Copy(hash, 0, newGuid, 0, 16);
|
||||||
|
|
||||||
|
newGuid[6] = (byte)((newGuid[6] & 0x0F) | 0x50);
|
||||||
|
newGuid[8] = (byte)((newGuid[8] & 0x3F) | 0x80);
|
||||||
|
|
||||||
|
SwapByteOrder(newGuid);
|
||||||
|
return new Guid(newGuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SwapByteOrder(byte[] guid)
|
||||||
|
{
|
||||||
|
(guid[0], guid[3]) = (guid[3], guid[0]);
|
||||||
|
(guid[1], guid[2]) = (guid[2], guid[1]);
|
||||||
|
(guid[4], guid[5]) = (guid[5], guid[4]);
|
||||||
|
(guid[6], guid[7]) = (guid[7], guid[6]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user