From 8a5400ae973e1fd7b0859ae55eeaddce350cd9d2 Mon Sep 17 00:00:00 2001 From: Splamy Date: Thu, 15 Jun 2017 09:33:40 +0200 Subject: [PATCH] Rights structure protptype --- TS3AudioBot/MainBot.cs | 5 +- TS3AudioBot/Properties/AssemblyInfo.cs | 9 +- TS3AudioBot/Rights/RightsDecl.cs | 102 +++++++++++ TS3AudioBot/Rights/RightsGroup.cs | 37 ++++ TS3AudioBot/Rights/RightsManager.cs | 241 +++++++++++++++++++++++++ TS3AudioBot/Rights/RightsRule.cs | 127 +++++++++++++ TS3AudioBot/Rights/TomlTools.cs | 118 ++++++++++++ TS3AudioBot/TS3AudioBot.csproj | 18 +- TS3AudioBot/packages.config | 7 +- 9 files changed, 651 insertions(+), 13 deletions(-) create mode 100644 TS3AudioBot/Rights/RightsDecl.cs create mode 100644 TS3AudioBot/Rights/RightsGroup.cs create mode 100644 TS3AudioBot/Rights/RightsManager.cs create mode 100644 TS3AudioBot/Rights/RightsRule.cs create mode 100644 TS3AudioBot/Rights/TomlTools.cs diff --git a/TS3AudioBot/MainBot.cs b/TS3AudioBot/MainBot.cs index fcfdf997..3ef5e906 100644 --- a/TS3AudioBot/MainBot.cs +++ b/TS3AudioBot/MainBot.cs @@ -31,6 +31,7 @@ namespace TS3AudioBot using Sessions; using Web.Api; using Web; + using Rights; using TS3Client; using TS3Client.Messages; @@ -75,6 +76,7 @@ internal static void Main(string[] args) public PlayManager PlayManager { get; private set; } public ITargetManager TargetManager { get; private set; } public ConfigFile ConfigManager { get; private set; } + public RightsManager RightsManager { get; private set; } public bool QuizMode { get; set; } @@ -116,6 +118,7 @@ private bool InitializeBot() var pld = ConfigManager.GetDataStruct("PlaylistManager", true); var yfd = ConfigManager.GetDataStruct("YoutubeFactory", true); var webd = ConfigManager.GetDataStruct("WebData", true); + var rmd = ConfigManager.GetDataStruct("RightsManager", true); mainBotData = ConfigManager.GetDataStruct("MainBot", true); ConfigManager.Close(); @@ -161,6 +164,7 @@ private bool InitializeBot() PluginManager = new PluginManager(this, pmd); PlayManager = new PlayManager(this); WebManager = new WebManager(this, webd); + RightsManager = new RightsManager(rmd); TargetManager = teamspeakClient; Log.Write(Log.Level.Info, "[=========== Initializing Factories ===========]"); @@ -194,7 +198,6 @@ private bool InitializeBot() // Register callback to remove open private sessions, when user disconnects //QueryConnection.OnClientDisconnect += (s, e) => SessionManager.RemoveSession(e.InvokerUid); - Log.Write(Log.Level.Info, "[================= Finalizing =================]"); WebManager.StartServerAsync(); diff --git a/TS3AudioBot/Properties/AssemblyInfo.cs b/TS3AudioBot/Properties/AssemblyInfo.cs index 7cf5f14b..119f1caa 100644 --- a/TS3AudioBot/Properties/AssemblyInfo.cs +++ b/TS3AudioBot/Properties/AssemblyInfo.cs @@ -1,4 +1,4 @@ -using System.Reflection; +using System.Reflection; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following @@ -9,7 +9,7 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("TS3AudioBot")] -[assembly: AssemblyCopyright("Copyright © Splamy 2016")] +[assembly: AssemblyCopyright("Copyright © Splamy 2017")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -31,5 +31,6 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyVersion("0.2.*")] +[assembly: AssemblyFileVersion("0.2.*")] +[assembly: AssemblyInformationalVersion("0.2.0")] diff --git a/TS3AudioBot/Rights/RightsDecl.cs b/TS3AudioBot/Rights/RightsDecl.cs new file mode 100644 index 00000000..19a191a8 --- /dev/null +++ b/TS3AudioBot/Rights/RightsDecl.cs @@ -0,0 +1,102 @@ +// TS3AudioBot - An advanced Musicbot for Teamspeak 3 +// Copyright (C) 2016 TS3AudioBot contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +namespace TS3AudioBot.Rights +{ + using Nett; + using System.Collections.Generic; + using System.Linq; + + internal abstract class RightsDecl + { + public int Id { get; private set; } + public int Level { get; set; } + + public RightsRule Parent { get; set; } + private string[] includeNames; + public RightsGroup[] Includes { get; set; } + + public string[] DeclAdd { get; set; } + public string[] DeclDeny { get; set; } + + public RightsDecl() { } + + public virtual void FillNull() + { + if (includeNames == null) includeNames = new string[0]; + if (DeclAdd == null) DeclAdd = new string[0]; + if (DeclDeny == null) DeclDeny = new string[0]; + } + + public virtual bool ParseKey(string key, TomlObject tomlObj, List rules) + { + switch (key) + { + case "+": + DeclAdd = TomlTools.GetValues(tomlObj); + if (DeclAdd == null) + Log.Write(Log.Level.Error, "<+> Field has invalid data."); + break; + case "-": + DeclDeny = TomlTools.GetValues(tomlObj); + if (DeclDeny == null) + Log.Write(Log.Level.Error, "<-> Field has invalid data."); + break; + case "include": + includeNames = TomlTools.GetValues(tomlObj); + if (includeNames == null) + Log.Write(Log.Level.Error, " Field has invalid data."); + break; + default: return false; + } + return true; + } + + public void ParseChilden(TomlTable tomlObj, List rules) + { + Id = rules.Count; + rules.Add(this); + + foreach (var item in tomlObj) + { + if (!ParseKey(item.Key, item.Value, rules)) + { + Log.Write(Log.Level.Error, "Unrecognized key <{0}>.", item.Key); + } + } + FillNull(); + } + + public abstract RightsGroup ResolveGroup(string groupName); + + public bool ResolveIncludes() + { + bool hasErrors = false; + if (includeNames != null) + { + Includes = includeNames.Select(ResolveGroup).ToArray(); + for (int i = 0; i < includeNames.Length; i++) + if (Includes[i] == null) + { + Log.Write(Log.Level.Error, "Could not find group \"{0}\" to include.", includeNames[i]); + hasErrors = true; + } + includeNames = null; + } + return !hasErrors; + } + } +} diff --git a/TS3AudioBot/Rights/RightsGroup.cs b/TS3AudioBot/Rights/RightsGroup.cs new file mode 100644 index 00000000..0fbb80f2 --- /dev/null +++ b/TS3AudioBot/Rights/RightsGroup.cs @@ -0,0 +1,37 @@ +// TS3AudioBot - An advanced Musicbot for Teamspeak 3 +// Copyright (C) 2016 TS3AudioBot contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +namespace TS3AudioBot.Rights +{ + internal class RightsGroup : RightsDecl + { + public string Name { get; } + + public RightsGroup(string name) + { + Name = name; + } + + public override RightsGroup ResolveGroup(string groupName) + { + if (Name == groupName) + return this; + if (Parent == null) + return null; + return Parent.ResolveGroup(groupName); + } + } +} diff --git a/TS3AudioBot/Rights/RightsManager.cs b/TS3AudioBot/Rights/RightsManager.cs new file mode 100644 index 00000000..e73bbf4a --- /dev/null +++ b/TS3AudioBot/Rights/RightsManager.cs @@ -0,0 +1,241 @@ +// TS3AudioBot - An advanced Musicbot for Teamspeak 3 +// Copyright (C) 2016 TS3AudioBot contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +namespace TS3AudioBot.Rights +{ + using Nett; + using System; + using System.Collections.Generic; + using System.Linq; + using TS3AudioBot.Helper; + + public class RightsManager + { + private const int RuleLevelSize = 2; + + private RightsManagerData rightsManagerData; + private RightsRule[] Rules; + + public RightsManager(RightsManagerData rmd) + { + Log.RegisterLogger("[%T]%L: %M", "", Console.WriteLine); + rightsManagerData = rmd; + RecalculateRights(); + } + + public bool HasRight(InvokerData inv) + { + return true; + } + + // Loading and Parsing + + private TomlTable ReadFile() + { + try + { + return Toml.ReadFile(rightsManagerData.RightsFile); + } + catch (Exception ex) + { + Log.Write(Log.Level.Error, "The rights file could not be parsed: {0}", ex); + return null; + } + } + + private bool RecalculateRights() + { + Rules = new RightsRule[0]; + + var table = ReadFile(); + if (table == null) + return false; + + var declarations = new List(); + var rootRule = new RightsRule(); + rootRule.ParseChilden(table, declarations); + + var rightGroups = declarations.OfType().ToArray(); + var rightRules = declarations.OfType().ToArray(); + + if (!ValidateUniqueGroupNames(rightGroups)) + return false; + + if (!ResolveIncludes(declarations)) + return false; + + if (!CheckCyclicGroupDependencies(rightGroups)) + return false; + + BuildLevel(rootRule); + + LintDeclarations(declarations); + + FlattenGroups(rightGroups); + + FlattenRules(rootRule); + + Rules = rightRules; + return true; + } + + private static bool ValidateUniqueGroupNames(RightsGroup[] groups) + { + bool hasErrors = false; + + foreach (var checkGroup in groups) + { + // check that the name is unique + var parent = checkGroup.Parent; + while (parent != null) + { + foreach (var cmpGroup in parent.Children) + { + if (cmpGroup != checkGroup + && cmpGroup is RightsGroup + && ((RightsGroup)cmpGroup).Name == checkGroup.Name) + { + Log.Write(Log.Level.Error, "Ambiguous group name: {0}", checkGroup.Name); + hasErrors = true; + } + } + parent = parent.Parent; + } + } + + return !hasErrors; + } + + private static bool ResolveIncludes(List declarations) + { + bool hasErrors = false; + + foreach (var decl in declarations) + hasErrors |= !decl.ResolveIncludes(); + + return !hasErrors; + } + + private static bool CheckCyclicGroupDependencies(RightsGroup[] groups) + { + bool hasErrors = false; + + foreach (var checkGroup in groups) + { + var included = new HashSet(); + var remainingIncludes = new Queue(); + remainingIncludes.Enqueue(checkGroup); + + while (remainingIncludes.Any()) + { + var include = remainingIncludes.Dequeue(); + included.Add(include); + foreach (var newInclude in include.Includes) + { + if (newInclude == checkGroup) + { + hasErrors = true; + Log.Write(Log.Level.Error, "Group \"{0}\" has a cyclic include hierachy.", checkGroup.Name); + break; + } + if (!included.Contains(newInclude)) + remainingIncludes.Enqueue(newInclude); + } + } + } + + return !hasErrors; + } + + private static void BuildLevel(RightsDecl decl, int level = 0) + { + decl.Level = level; + if (decl is RightsRule) + foreach (var child in ((RightsRule)decl).Children) + BuildLevel(child, level + RuleLevelSize); + } + + private static void LintDeclarations(List declarations) + { + // TODO + + // check if <+> contains <-> decl + + // top level <-> declaration is useless + + // check if rule has no matcher + + // check for impossible combinations uid + uid, server + server, perm + perm ? + + // check for unused group + } + + private static void MergeGroups(RightsDecl main, params RightsDecl[] merge) + { + // main.+ = (include+ - main-) + main+ + // main.- = main- + foreach (var include in merge) + main.DeclAdd = include.DeclAdd.Except(main.DeclDeny).Concat(main.DeclAdd).Distinct().ToArray(); + } + + private static void FlattenGroups(RightsGroup[] groups) + { + var notReachable = new Queue(groups); + var currentlyReached = new HashSet(groups.Where(x => x.Includes.Length == 0)); + + while (notReachable.Count > 0) + { + var item = notReachable.Dequeue(); + if (currentlyReached.IsSupersetOf(item.Includes)) + { + currentlyReached.Add(item); + + MergeGroups(item, item.Includes); + item.Includes = null; + } + else + { + notReachable.Enqueue(item); + } + } + } + + private static void FlattenRules(RightsRule root) + { + if (root.Parent != null) + MergeGroups(root, root.Parent); + MergeGroups(root, root.Includes); + root.Includes = null; + + foreach (var child in root.Children) + if (child is RightsRule) + FlattenRules((RightsRule)child); + } + + } + + struct DeclLevel + { + public int Level; + public bool Add; + } + + public class RightsManagerData : ConfigData + { + [Info("Path to the config file", "rights.toml")] + public string RightsFile { get; set; } + } +} diff --git a/TS3AudioBot/Rights/RightsRule.cs b/TS3AudioBot/Rights/RightsRule.cs new file mode 100644 index 00000000..c76aef7a --- /dev/null +++ b/TS3AudioBot/Rights/RightsRule.cs @@ -0,0 +1,127 @@ +// TS3AudioBot - An advanced Musicbot for Teamspeak 3 +// Copyright (C) 2016 TS3AudioBot contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +namespace TS3AudioBot.Rights +{ + using Nett; + using System.Collections.Generic; + + internal class RightsRule : RightsDecl + { + public List Children { get; set; } + public Dictionary DeclMap { get; } + + public string[] MatchHost { get; set; } + public string[] MatchClientUid { get; set; } + public ulong[] MatchClientGroupId { get; set; } + public string[] MatchPermission { get; set; } + + public RightsRule() + { + Children = new List(); + DeclMap = new Dictionary(); + } + + public override void FillNull() + { + base.FillNull(); + if (MatchHost == null) MatchHost = new string[0]; + if (MatchClientUid == null) MatchClientUid = new string[0]; + if (MatchClientGroupId == null) MatchClientGroupId = new ulong[0]; + if (MatchPermission == null) MatchPermission = new string[0]; + } + + public override bool ParseKey(string key, TomlObject tomlObj, List rules) + { + if (base.ParseKey(key, tomlObj, rules)) + return true; + + switch (key) + { + case "host": + MatchHost = TomlTools.GetValues(tomlObj); + if (MatchHost == null) + Log.Write(Log.Level.Error, " Field has invalid data."); + break; + case "groupid": + MatchClientGroupId = TomlTools.GetValues(tomlObj); + if (MatchClientGroupId == null) + Log.Write(Log.Level.Error, " Field has invalid data."); + break; + case "useruid": + MatchClientUid = TomlTools.GetValues(tomlObj); + if (MatchClientUid == null) + Log.Write(Log.Level.Error, " Field has invalid data."); + break; + case "perm": + MatchPermission = TomlTools.GetValues(tomlObj); + if (MatchPermission == null) + Log.Write(Log.Level.Error, " Field has invalid data."); + break; + case "rule": + if (tomlObj.TomlType == TomlObjectType.ArrayOfTables) + { + var childTables = (TomlTableArray)tomlObj; + foreach (var childTable in childTables.Items) + { + var rule = new RightsRule(); + Children.Add(rule); + rule.Parent = this; + rule.ParseChilden(childTable, rules); + } + } + else + { + Log.Write(Log.Level.Error, "Misused key with reserved name \"rule\"."); + } + break; + default: + // group + if (key.StartsWith("$")) + { + if (tomlObj.TomlType == TomlObjectType.Table) + { + var childTable = (TomlTable)tomlObj; + var group = new RightsGroup(key); + Children.Add(group); + group.Parent = this; + group.ParseChilden(childTable, rules); + return true; + } + else + { + Log.Write(Log.Level.Error, "Misused key for group declaration: {0}.", key); + } + } + return false; + } + + return true; + } + + public override RightsGroup ResolveGroup(string groupName) + { + foreach (var child in Children) + { + if (child is RightsGroup && ((RightsGroup)child).Name == groupName) + return (RightsGroup)child; + } + if (Parent == null) + return null; + return Parent.ResolveGroup(groupName); + } + } +} diff --git a/TS3AudioBot/Rights/TomlTools.cs b/TS3AudioBot/Rights/TomlTools.cs new file mode 100644 index 00000000..e1f45447 --- /dev/null +++ b/TS3AudioBot/Rights/TomlTools.cs @@ -0,0 +1,118 @@ +// TS3AudioBot - An advanced Musicbot for Teamspeak 3 +// Copyright (C) 2016 TS3AudioBot contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +namespace TS3AudioBot.Rights +{ + using Nett; + using System; + using System.Linq; + using System.Text; + + internal static class TomlTools + { + public static T[] GetValues(TomlObject tomlObj) + { + T retSingleVal; + if (TryGetValue(tomlObj, out retSingleVal)) + return new T[] { retSingleVal }; + else if (tomlObj.TomlType == TomlObjectType.Array) + { + var tomlArray = (TomlArray)tomlObj; + var retArr = new T[tomlArray.Length]; + for (int i = 0; i < tomlArray.Length; i++) + if (!TryGetValue(tomlArray.Items[i], out retArr[i])) + return null; + return retArr; + } + return null; + } + + public static bool TryGetValue(TomlObject tomlObj, out T value) + { + switch (tomlObj.TomlType) + { + case TomlObjectType.Int: + if (typeof(T) == typeof(long)) + { + value = ((TomlValue)tomlObj).Value; + return true; + } + else if (typeof(T) == typeof(ulong) + || typeof(T) == typeof(uint) || typeof(T) == typeof(int) + || typeof(T) == typeof(ushort) || typeof(T) == typeof(short) + || typeof(T) == typeof(byte) || typeof(T) == typeof(sbyte)) + { + try + { + value = (T)Convert.ChangeType(((TomlInt)tomlObj).Value, typeof(T)); + return true; + } + catch (OverflowException) { } + } + break; + case TomlObjectType.Bool: + case TomlObjectType.Float: + case TomlObjectType.String: + case TomlObjectType.DateTime: + case TomlObjectType.TimeSpan: + var tomlValue = tomlObj as TomlValue; + if (tomlValue != null && typeof(T) == tomlValue.Value.GetType()) + { + value = tomlValue.Value; + return true; + } + break; + } + value = default(T); + return false; + } + + private static string ToString(TomlObject obj) + { + var strb = new StringBuilder(); + //strb.Append(" : "); + switch (obj.TomlType) + { + case TomlObjectType.Bool: strb.Append((obj as TomlBool).Value); break; + case TomlObjectType.Int: strb.Append((obj as TomlInt).Value); break; + case TomlObjectType.Float: strb.Append((obj as TomlFloat).Value); break; + case TomlObjectType.String: strb.Append((obj as TomlString).Value); break; + case TomlObjectType.DateTime: strb.Append((obj as TomlDateTime).Value); break; + case TomlObjectType.TimeSpan: strb.Append((obj as TomlTimeSpan).Value); break; + case TomlObjectType.Array: + strb.Append("[ ") + .Append(string.Join(", ", (obj as TomlArray).Items.Select(x => ToString(x)))) + .Append(" ]"); + break; + case TomlObjectType.Table: + var table = (obj as TomlTable); + foreach (var kvp in table) + { + strb.Append(kvp.Key).Append(" : { ").Append(ToString(kvp.Value)).AppendLine(" }"); + } + break; + case TomlObjectType.ArrayOfTables: + strb.Append("[ ") + .Append(string.Join(", ", (obj as TomlTableArray).Items.Select(x => ToString(x)))) + .Append(" ]"); + break; + default: + break; + } + return strb.ToString(); + } + } +} diff --git a/TS3AudioBot/TS3AudioBot.csproj b/TS3AudioBot/TS3AudioBot.csproj index ae013852..57288b40 100644 --- a/TS3AudioBot/TS3AudioBot.csproj +++ b/TS3AudioBot/TS3AudioBot.csproj @@ -68,10 +68,13 @@ True - ..\packages\LiteDB.3.1.0\lib\net35\LiteDB.dll + ..\packages\LiteDB.3.1.1\lib\net35\LiteDB.dll - - ..\packages\PropertyChanged.Fody.2.0.1\lib\netstandard1.3\PropertyChanged.dll + + ..\packages\Nett.0.6.3\lib\Net40\Nett.dll + + + ..\packages\PropertyChanged.Fody.2.1.2\lib\netstandard1.0\PropertyChanged.dll False @@ -98,6 +101,11 @@ + + + + + @@ -218,12 +226,12 @@ - + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - +