Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/server/command/_command.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ const std = @import("std");

const main = @import("main");
const User = main.server.User;
const permissionLayer = main.server.permissions;

pub const Command = struct {
exec: *const fn (args: []const u8, source: *User) void,
name: []const u8,
description: []const u8,
usage: []const u8,
permissionPath: []const u8,
};

pub var commands: std.StringHashMap(Command) = undefined;
Expand All @@ -21,6 +23,7 @@ pub fn init() void {
.description = @field(commandList, decl.name).description,
.usage = @field(commandList, decl.name).usage,
.exec = &@field(commandList, decl.name).execute,
.permissionPath = "command/" ++ decl.name,
}) catch unreachable;
std.log.debug("Registered command: '/{s}'", .{decl.name});
}
Expand All @@ -34,6 +37,10 @@ pub fn execute(msg: []const u8, source: *User) void {
const end = std.mem.indexOfScalar(u8, msg, ' ') orelse msg.len;
const command = msg[0..end];
if (commands.get(command)) |cmd| {
if (!permissionLayer.hasPermission(source, cmd.permissionPath)) {
source.sendMessage("#ff0000No permission to use Command \"{s}\"", .{command});
return;
}
source.sendMessage("#00ff00Executing Command /{s}", .{msg});
cmd.exec(msg[@min(end + 1, msg.len)..], source);
} else {
Expand Down
173 changes: 173 additions & 0 deletions src/server/permissionLayer.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
const std = @import("std");

const main = @import("main");
const server = main.server;
const User = server.User;
const NeverFailingAllocator = main.heap.NeverFailingAllocator;

pub const ListType = enum {
white,
black,
};

pub const PermissionGroup = struct {
allocator: NeverFailingAllocator,
permissionWhiteList: std.StringHashMapUnmanaged(void) = .{},
permissionBlackList: std.StringHashMapUnmanaged(void) = .{},

pub fn deinit(self: *PermissionGroup) void {
self.permissionWhiteList.deinit(self.allocator.allocator);
self.permissionBlackList.deinit(self.allocator.allocator);
}

pub fn hasPermission(self: *PermissionGroup, permissionPath: []const u8) bool {
var it = std.mem.splitBackwardsScalar(u8, permissionPath, '/');
var current = permissionPath;

while (it.next()) |path| {
if (self.permissionBlackList.contains(current)) return false;
if (self.permissionWhiteList.contains(current)) return true;

const len = current.len -| (path.len + 1);
current = permissionPath[0..len];
}
return false;
}
};

pub var groups: std.StringHashMap(PermissionGroup) = undefined;

pub fn init(allocator: NeverFailingAllocator) void {
groups = .init(allocator.allocator);
}

pub fn deinit() void {
var it = groups.iterator();
while (it.next()) |entry| {
entry.value_ptr.deinit();
}
groups.deinit();
}

pub fn createGroup(name: []const u8, allocator: NeverFailingAllocator) error{Invalid}!void {
if (groups.contains(name)) return error.Invalid;
groups.put(name, .{.allocator = allocator}) catch unreachable;
}

pub fn deleteGroup(name: []const u8) bool {
const users = server.getUserListAndIncreaseRefCount(main.globalAllocator);
for (users) |user| {
_ = user.permissionGroups.remove(name);
}
server.freeUserListAndDecreaseRefCount(main.globalAllocator, users);
if (groups.getPtr(name)) |group| {
group.deinit();
}
return groups.remove(name);
}

pub fn addGroupPermission(name: []const u8, listType: ListType, permissionPath: []const u8) error{Invalid}!void {
if (groups.getPtr(name)) |group| {
switch (listType) {
.white => group.permissionWhiteList.put(group.allocator.allocator, permissionPath, {}) catch unreachable,
.black => group.permissionBlackList.put(group.allocator.allocator, permissionPath, {}) catch unreachable,
}
} else return error.Invalid;
}

pub fn addUserPermission(user: *User, allocator: NeverFailingAllocator, listType: ListType, permissionPath: []const u8) void {
switch (listType) {
.white => user.permissionWhiteList.put(allocator.allocator, permissionPath, {}) catch unreachable,
.black => user.permissionBlackList.put(allocator.allocator, permissionPath, {}) catch unreachable,
}
}

pub fn addUserToGroup(user: *User, allocator: NeverFailingAllocator, name: []const u8) error{Invalid}!void {
if (groups.getPtr(name)) |group| {
user.permissionGroups.put(allocator.allocator, name, group) catch unreachable;
} else {
return error.Invalid;
}
}

pub fn hasPermission(user: *User, permissionPath: []const u8) bool {
var it = std.mem.splitBackwardsScalar(u8, permissionPath, '/');
var current = permissionPath;

while (it.next()) |path| {
if (user.permissionBlackList.contains(current)) return false;
if (user.permissionWhiteList.contains(current)) return true;

const len = current.len -| (path.len + 1);
current = permissionPath[0..len];
}

var groupIt = user.permissionGroups.valueIterator();
while (groupIt.next()) |group| {
if (group.*.hasPermission(permissionPath)) return true;
}

return false;
}

test "GroupWhitePermission" {
init(main.heap.testingAllocator);
defer deinit();

try createGroup("test", main.heap.testingAllocator);
const group = groups.getPtr("test").?;
try addGroupPermission("test", .white, "command/test");

try std.testing.expectEqual(true, group.hasPermission("command/test"));
}

test "GroupBlacklist" {
init(main.heap.testingAllocator);
defer deinit();

try createGroup("test", main.heap.testingAllocator);
const group = groups.getPtr("test").?;
try addGroupPermission("test", .white, "command");
try addGroupPermission("test", .black, "command/test");

try std.testing.expectEqual(false, group.hasPermission("command/test"));
try std.testing.expectEqual(true, group.hasPermission("command"));
}

test "GroupDeepPermission" {
init(main.heap.testingAllocator);
defer deinit();

try createGroup("test", main.heap.testingAllocator);
const group = groups.getPtr("test").?;
try addGroupPermission("test", .white, "server/command/testing/test");

try std.testing.expectEqual(true, group.hasPermission("server/command/testing/test"));
try std.testing.expectEqual(false, group.hasPermission("server/command/testing"));
try std.testing.expectEqual(false, group.hasPermission("server/command"));
try std.testing.expectEqual(false, group.hasPermission("server"));
try std.testing.expectEqual(false, group.hasPermission("server/command/testing/test2"));
}

test "inValidGroupPermission" {
init(main.heap.testingAllocator);
defer deinit();

try createGroup("test", main.heap.testingAllocator);
try std.testing.expectError(error.Invalid, addGroupPermission("root", .white, "command/test"));
}

test "inValidGroupPermissionEmptyGroups" {
init(main.heap.testingAllocator);
defer deinit();

try std.testing.expectError(error.Invalid, addGroupPermission("root", .white, "command/test"));
}

test "inValidGroupCreation" {
init(main.heap.testingAllocator);
defer deinit();

try createGroup("test", main.heap.testingAllocator);
try std.testing.expectError(error.Invalid, createGroup("test", main.heap.testingAllocator));
}
15 changes: 15 additions & 0 deletions src/server/server.zig
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pub const terrain = @import("terrain/terrain.zig");
pub const Entity = @import("Entity.zig");
pub const SimulationChunk = @import("SimulationChunk.zig");
pub const storage = @import("storage.zig");
pub const permissions = @import("permissionLayer.zig");

pub const command = @import("command/_command.zig");

Expand Down Expand Up @@ -129,6 +130,10 @@ pub const User = struct { // MARK: User

inventoryCommands: main.ListUnmanaged([]const u8) = .{},

permissionWhiteList: std.StringHashMapUnmanaged(void) = .{},
permissionBlackList: std.StringHashMapUnmanaged(void) = .{},
permissionGroups: std.StringHashMapUnmanaged(*permissions.PermissionGroup) = .{},

pub fn initAndIncreaseRefCount(manager: *ConnectionManager, ipPort: []const u8) !*User {
const self = main.globalAllocator.create(User);
errdefer main.globalAllocator.destroy(self);
Expand All @@ -147,6 +152,9 @@ pub const User = struct { // MARK: User
main.items.Inventory.ServerSide.disconnectUser(self);
std.debug.assert(self.inventoryClientToServerIdMap.count() == 0); // leak
self.inventoryClientToServerIdMap.deinit();
self.permissionWhiteList.deinit(main.globalAllocator.allocator);
self.permissionBlackList.deinit(main.globalAllocator.allocator);
self.permissionGroups.deinit(main.globalAllocator.allocator);

if (self.inventory != null) {
world.?.savePlayer(self) catch |err| {
Expand Down Expand Up @@ -347,6 +355,7 @@ fn init(name: []const u8, singlePlayerPort: ?u16) void { // MARK: init()
main.heap.allocators.createWorldArena();
std.debug.assert(world == null); // There can only be one world.
command.init();
permissions.init(main.globalAllocator);
users = .init(main.globalAllocator);
userDeinitList = .init(main.globalAllocator, 16);
userConnectList = .init(main.globalAllocator, 16);
Expand Down Expand Up @@ -376,6 +385,11 @@ fn init(name: []const u8, singlePlayerPort: ?u16) void { // MARK: init()
};
defer user.decreaseRefCount();
user.isLocal = true;
// this is to show how the permission system can be used not the supposed setup of a users permission
permissions.addUserPermission(user, main.globalAllocator, .black, "command/spawn");
permissions.createGroup("root", main.globalAllocator) catch unreachable;
permissions.addGroupPermission("root", .white, "command") catch unreachable;
permissions.addUserToGroup(user, main.globalAllocator, "root") catch unreachable;
}
}

Expand All @@ -400,6 +414,7 @@ fn deinit() void {
main.sync.ServerSide.deinit();
main.items.Inventory.ServerSide.deinit();

permissions.deinit();
command.deinit();
main.heap.allocators.destroyWorldArena();
}
Expand Down