From f8c170b29a20ed8424e615fb1a4c3724f8c66296 Mon Sep 17 00:00:00 2001 From: Mysaa Date: Mon, 24 May 2021 00:22:19 +0200 Subject: [PATCH] Premier commit - Inclusion dans le projet git --- .gitignore | 7 + src/com/bernard/math/GraphFunctions.java | 44 +++ src/com/bernard/math/SortFunctions.java | 59 +++ .../math/graph/GraphCycleException.java | 7 + .../permissions/PermissionContext.java | 7 + .../bernard/permissions/StringPermission.java | 247 ++++++++++++ src/com/bernard/permissions/TestMain.java | 126 ++++++ .../permissions/tableured/FileManager.java | 370 ++++++++++++++++++ .../tableured/TPermissionContext.java | 226 +++++++++++ stringPermissionBase.ods | Bin 0 -> 9984 bytes test.ods | Bin 0 -> 16671 bytes 11 files changed, 1093 insertions(+) create mode 100644 .gitignore create mode 100644 src/com/bernard/math/GraphFunctions.java create mode 100644 src/com/bernard/math/SortFunctions.java create mode 100644 src/com/bernard/math/graph/GraphCycleException.java create mode 100644 src/com/bernard/permissions/PermissionContext.java create mode 100644 src/com/bernard/permissions/StringPermission.java create mode 100644 src/com/bernard/permissions/TestMain.java create mode 100644 src/com/bernard/permissions/tableured/FileManager.java create mode 100644 src/com/bernard/permissions/tableured/TPermissionContext.java create mode 100644 stringPermissionBase.ods create mode 100644 test.ods diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..45bbac8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.classpath +.project +.settings/ +bin/ +discordout.ods +testout.ods +testoutout.ods diff --git a/src/com/bernard/math/GraphFunctions.java b/src/com/bernard/math/GraphFunctions.java new file mode 100644 index 0000000..b6ac8db --- /dev/null +++ b/src/com/bernard/math/GraphFunctions.java @@ -0,0 +1,44 @@ +package com.bernard.math; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; + +import com.bernard.math.graph.GraphCycleException; + +public class GraphFunctions { + + public static boolean anyCycle(Map> heritages,Function fonction) { + Set patouché = new HashSet(heritages.keySet()); + Set traitage = new HashSet<>(); + try { + while(!patouché.isEmpty()) { + S el = patouché.stream().findAny().get(); + traitation(el,patouché,traitage,heritages,fonction); + } + }catch (GraphCycleException ex) { + return true; + } + return false; + } + + private static void traitation(S s, Set patouché, Set traitage, Map> heritages,Function fonction) throws GraphCycleException{ + patouché.remove(s); + if(!heritages.containsKey(s)) + return; + traitage.add(s); + for(T heritation : heritages.get(s)) { + S enfant = fonction.apply(heritation); + if(patouché.contains(enfant)) + traitation(enfant,patouché,traitage,heritages,fonction); + if(traitage.contains(enfant)) + throw new GraphCycleException(); + // Sinon, ça ne peut pas faire de cycle: parfait ! + } + traitage.remove(s); + return; + } + +} diff --git a/src/com/bernard/math/SortFunctions.java b/src/com/bernard/math/SortFunctions.java new file mode 100644 index 0000000..c988b5f --- /dev/null +++ b/src/com/bernard/math/SortFunctions.java @@ -0,0 +1,59 @@ +package com.bernard.math; + +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class SortFunctions { + + public static int compare(Set s1,Set s2,Comparator cprt){ + + // Premier ordre: inclusion + if(s1.containsAll(s2)) + return 1; + if(s2.containsAll(s1)) + return -1; + + //TODO: Refaire cette fonction un peu plus proprement … + + // Second ordre: Majoration de chaque terme + List lt1 = s1.stream().sorted(cprt).collect(Collectors.toList()); + List lt2 = s2.stream().sorted(cprt).collect(Collectors.toList()); + + // Si lt1 < lt2 ? + int i=0,j=0; + while(i0) + break; + } + if(i0) + break; + } + if(i { + + public boolean can(Perm p, Permholder ph); + +} diff --git a/src/com/bernard/permissions/StringPermission.java b/src/com/bernard/permissions/StringPermission.java new file mode 100644 index 0000000..56595b7 --- /dev/null +++ b/src/com/bernard/permissions/StringPermission.java @@ -0,0 +1,247 @@ +package com.bernard.permissions; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import com.bernard.math.SortFunctions; + +public class StringPermission implements Comparable{ + + private Nomme[] nommes; + + public StringPermission(Nomme[] nommes) { + this.nommes = nommes; + } + + public static boolean implies(StringPermission s1,StringPermission s2) { + + if(s2==null)return true; + if(s1==null)return false; + if(s2.nommes.length < s1.nommes.length)return false; + + //Bon déjà, ils ont le même nom + + for (int i = 0; i < s1.nommes.length; i++) + if(Nomme.implies(s1.nommes[i], s2.nommes[i])) + return false; + return true; + } + + public boolean implies(StringPermission other) { + return implies(this,other); + } + + + + //Définition des regex, avec des variables les unes dans les autres. Sinon c'est pas possible + public static final String strR = "(([^\"\\\\]|\\\\\\\\|\\\\\")*)\""; // Without first slash + public static final String optionR = "((\""+strR+"|[^=]+)=(\""+strR+"|[^,\\]]+))"; + public static final String optionsR = "((("+optionR+",)*)"+optionR+")"; + public static final String nnameR = "([^.\"\\[]+)"; + public static final String fullNR = "("+nnameR+"(\\["+optionsR+"\\])?)"; + public static final String spR = "(("+fullNR+"\\.)*)("+fullNR+")"; + + public static final Pattern spregex = Pattern.compile(spR); + public static final Pattern optsregex = Pattern.compile(optionsR); + + public static StringPermission parse(String str) { + + Nomme[] nommes = recParse(str, 0); + + return new StringPermission(nommes); + } + + private static Nomme[] recParse(String str,int index) { + + Matcher mtch = spregex.matcher(str); + if(!mtch.matches()) + throw new IllegalArgumentException(); + + String optionstr = mtch.group(27); + + String nom = mtch.group(25); + + Map options = null; + if(optionstr!=null && !optionstr.isEmpty()) { + options = new HashMap<>(); + String roptions = optionstr; + Matcher optMtch; + do { + optMtch = optsregex.matcher(roptions); + optMtch.matches(); + String name = pppstring(optMtch.group(12),optMtch.group(13)); + String value = pppstring(optMtch.group(15),optMtch.group(16)); + options.put(name, value); + roptions = optMtch.group(2); + roptions = roptions.substring(0, Math.max(roptions.length()-1,0)); + }while(!roptions.isEmpty()); + } + + Nomme nomme = new Nomme(nom, options); + String reste = mtch.group(1); + Nomme[] nommes; + if(reste.isEmpty()) { //Non null car l'étoile de Kleene match le groupe vide + //Dernier parse + nommes = new Nomme[index+1]; + nommes[0] = nomme; + }else { + nommes = recParse(reste.substring(0, reste.length()-1),index+1); // Pof + nommes[nommes.length-1-index] = nomme; + } + return nommes; + } + + public static final String pppstring(String raw,String quoted) { + if(quoted!=null) { + return quoted.replaceAll("\\\\\\\\", "\\").replaceAll("\\\\\"", "\""); + }else { + return raw; + } + } + + public static final String escapeIfNecessary(String raw) { + return escapeIfNecessary(raw, true); + } + public static final String escapeIfNecessary(String raw,boolean canEqualSign) { + if(raw.contains("\"") || raw.contains("\\") || (!canEqualSign && raw.contains("="))) + return "\"" + raw.replaceAll("\\\\", "\\\\").replaceAll("\"", "\\\"") + "\""; + else + return raw; + } + + @Override + public int compareTo(StringPermission sp) { + return this.toString().compareTo(sp.toString()); + } + + @Deprecated //TODO Fix this method with a real order (this one doesn't support nomme's options order) + public int compareTo(StringPermission sp,boolean disabled) { + int l1=this.nommes.length,l2=sp.nommes.length; + for (int k = 0; k < Math.min(l1, l2); k++) { + if(this.nommes[k]!=sp.nommes[k]) + return - this.nommes[k].compareTo(sp.nommes[k]);//TODO: Fix why is this minus sign here + } + //Ici, tous les points sont égaux. Donc on trie du plus court au plus long. + return Integer.compare(l1, l2); + } + + + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Arrays.hashCode(nommes); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + StringPermission other = (StringPermission) obj; + if (!Arrays.equals(nommes, other.nommes)) + return false; + return true; + } + + @Override + public String toString() { + return Arrays.stream(nommes).map(Nomme::toString).collect(Collectors.joining(".")); + } + + + public static class Nomme implements Comparable{ + String name; + Map options; + + public Nomme(String name, Map options) { + this.name = name; + this.options = options; + } + + @Override + public String toString() { + return name + ((options!=null)?("[" + options.entrySet().stream() + .map(e->escapeIfNecessary(e.getKey(),false)+"="+escapeIfNecessary(e.getKey())) + .collect(Collectors.joining(",")) + "]"):""); + } + + public static boolean implies(Nomme n1,Nomme n2) { + if(n2==null)return true; + if(n1==null)return false; + if(n2.name.equals(n1.name))return false; + + //Bon déjà, ils ont le même nom + if(n1.options == null)return true; + if(n2.options == null)return false; //car il ne peut y avoir d'options à n1. + if(!n2.options.keySet().containsAll(n1.options.keySet()))return false; + + // Donc Keys(n1) ⊂ Keys(n2) + // Il ne reste qu'à tester tous les nommes , si leur valeur est égale (ou absente -> Impossible car inclusion) + return n1.options.keySet().stream().allMatch(opt -> n1.options.get(opt).equals(n2.options.get(opt))); + } + + public boolean implies(Nomme other) { + return implies(this,other); + } + + @Override + public int compareTo(Nomme n) { + if(!this.name.equals(n.name)) + return this.name.compareTo(n.name); + if(options==null || n.options==null) + return (options==null)?((n.options==null)?0:-1):1; + System.out.println("bitenbois"); + //On ordonne les options + return SortFunctions.compare(options.entrySet(), + n.options.entrySet(), + (e1,e2) -> (e1.getKey()==e2.getKey()) + ?e1.getValue().compareTo(e2.getValue()) + :e1.getKey().compareTo(e2.getKey())); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((options == null) ? 0 : options.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Nomme other = (Nomme) obj; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + if (options == null) { + if (other.options != null) + return false; + } else if (!options.equals(other.options)) + return false; + return true; + } + + } + + + +} diff --git a/src/com/bernard/permissions/TestMain.java b/src/com/bernard/permissions/TestMain.java new file mode 100644 index 0000000..3c79b47 --- /dev/null +++ b/src/com/bernard/permissions/TestMain.java @@ -0,0 +1,126 @@ +package com.bernard.permissions; + +import java.io.File; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.security.auth.login.LoginException; + +import com.bernard.permissions.tableured.FileManager; +import com.bernard.permissions.tableured.TPermissionContext; +import com.bernard.permissions.tableured.TPermissionContext.Inheritance; +import com.bernard.permissions.tableured.TPermissionContext.Val; +import com.sun.star.uno.Exception; + +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.JDABuilder; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.Role; +import net.dv8tion.jda.api.requests.GatewayIntent; + +public class TestMain { + + public static void main(String[] args) throws Exception, LoginException, InterruptedException{ + + testDiscordRead(); + + System.out.println("OK, i'm done !"); + System.exit(0);; + } + + public static void testReadAndWrite() { + try { + TPermissionContext tpc = FileManager.readFromFile(new File("test.ods")); + String[] perms = {"julia","julia.bdd","julia.bdd.read","julia.bdd.write","julia.messages","julia.coucou"}; + String[] holders = {"Admin","Developeur","Client","Étranger","Mysaa","Bernard","Zlopeg","Ferbex","Pbalkany"}; + for (String perm : perms) { + for (String holder : holders) { + System.out.println(holder+" can "+perm+" : "+tpc.can(StringPermission.parse(perm), holder)); + } + System.out.println("========================="); + } + System.out.println(tpc); + System.out.println("Writing back to file."); + + new File("testout.ods").delete(); + FileManager.writeToFile(new File("testout.ods"), tpc); + System.out.println("Re-reading it"); + TPermissionContext newTPC = FileManager.readFromFile(new File("testout.ods")); + System.out.println(tpc); + System.out.println("Equals test: "+tpc.equals(newTPC)); + System.out.println("Writing to another file"); + FileManager.writeToFile(new File("testoutout.ods"), newTPC); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static void testStringPermissions() { + System.out.println(StringPermission.spR); + System.out.println(StringPermission.optionsR); + + String testParse = "pouf[Text=coucou].wow[Quoi=\"Hmm\",bou=2].incroyable.ducul[coucou=genial,super=cool]"; + System.out.println(StringPermission.parse(testParse)); + + } + + public static void testDiscordRead() throws LoginException, InterruptedException, Exception { + JDA jda = JDABuilder.createDefault("",GatewayIntent.GUILD_MEMBERS,GatewayIntent.values()).build(); + jda.awaitReady(); + + Guild g = jda.getGuildById(222947179017404416L); + + Map> specs = new HashMap<>(); + + List roles = g.getRoles(); + + List membres = g.loadMembers().get(); + System.out.println("Membres: "+membres); + + Set defaultGiven = g.getPublicRole().getPermissions().stream() + .map(p -> StringPermission.parse(p.getName())). + collect(Collectors.toSet()); + + Map> inheritance = membres.stream().collect(Collectors.toMap( + m -> "M"+m.getUser().getName()+"#"+m.getUser().getDiscriminator(), + m -> m.getRoles().stream().map(r -> new Inheritance("R"+r.getName(), TPermissionContext.InheritancePolicy.ALL)) + .collect(Collectors.toList()))); + +// for (int i = 1; i < roles.size(); i++) { +// inheritance.put("R"+roles.get(i).getName(), List.of(new Inheritance("R"+roles.get(i-1).getName(),TPermissionContext.InheritancePolicy.ALL))); +// } + + for(Role r : roles) { + for(Permission p : r.getPermissions()) { + StringPermission sp = StringPermission.parse(p.getName()); + if(!specs.containsKey(sp)) + specs.put(sp, new HashMap<>()); + specs.get(sp).put("R"+r.getName(), Val.GRANTED); + } + } +// for(Member m : membres) { +// for(Permission p : m.getPermissions()) { +// StringPermission sp = StringPermission.parse(p.getName()); +// if(!specs.containsKey(sp)) +// specs.put(sp, new HashMap<>()); +// specs.get(sp).put("M"+m.getUser().getName()+"#"+m.getUser().getDiscriminator(), Val.GRANTED); +// } +// } + + + TPermissionContext tpc = new TPermissionContext(defaultGiven, specs, inheritance); + + if(tpc.anyCycle()) + throw new IllegalStateException(); + System.out.println(tpc); + + FileManager.writeToFile(new File("discordout.ods"), tpc); + + } + +} diff --git a/src/com/bernard/permissions/tableured/FileManager.java b/src/com/bernard/permissions/tableured/FileManager.java new file mode 100644 index 0000000..0e38021 --- /dev/null +++ b/src/com/bernard/permissions/tableured/FileManager.java @@ -0,0 +1,370 @@ +package com.bernard.permissions.tableured; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.bernard.permissions.StringPermission; +import com.bernard.permissions.tableured.TPermissionContext.Inheritance; +import com.bernard.permissions.tableured.TPermissionContext.InheritancePolicy; +import com.bernard.permissions.tableured.TPermissionContext.Val; +import com.sun.star.beans.PropertyValue; +import com.sun.star.beans.XPropertySet; +import com.sun.star.comp.helper.Bootstrap; +import com.sun.star.comp.helper.BootstrapException; +import com.sun.star.frame.XComponentLoader; +import com.sun.star.frame.XStorable; +import com.sun.star.lang.XMultiComponentFactory; +import com.sun.star.sheet.XConditionalFormat; +import com.sun.star.sheet.XConditionalFormats; +import com.sun.star.sheet.XSheetCellRange; +import com.sun.star.sheet.XSheetCellRangeContainer; +import com.sun.star.sheet.XSheetCellRanges; +import com.sun.star.sheet.XSpreadsheet; +import com.sun.star.sheet.XSpreadsheetDocument; +import com.sun.star.sheet.XSpreadsheets; +import com.sun.star.table.CellRangeAddress; +import com.sun.star.table.XCell; +import com.sun.star.table.XCellRange; +import com.sun.star.table.XColumnRowRange; +import com.sun.star.uno.Exception; +import com.sun.star.uno.UnoRuntime; +import com.sun.star.uno.XComponentContext; +import com.sun.star.util.XCloseable; +import com.sun.star.util.XMergeable; + +public class FileManager { + + private static XComponentContext xcc_noaccess; + + public static final int MAX_ROW = 3141; + public static final int MAX_COL = 3141; + + public static XComponentContext xcc() { + if (xcc_noaccess == null) { + try { + xcc_noaccess = Bootstrap.bootstrap(); + System.out.println("Connected to a running office ..."); + } catch (BootstrapException e) { + System.err.println("Couldn't connect to office, you won't be able to use "); + } + } + return xcc_noaccess; + } + + //TODO: remove «throws Exception» for more precise exceptions + + public static final TPermissionContext readFromFile(File f) throws Exception { + + XComponentContext xcc = xcc(); + XMultiComponentFactory xmcf; + XSpreadsheetDocument xsd; + + xmcf = xcc.getServiceManager(); + Object object = xmcf.createInstanceWithContext("com.sun.star.frame.Desktop", xcc); + XComponentLoader xComponentLoader = qi(XComponentLoader.class,object); + PropertyValue[] docLoadProperties = { new PropertyValue() }; + docLoadProperties[0].Name = "Hidden"; + docLoadProperties[0].Value = true; + xsd = qi(XSpreadsheetDocument.class, + xComponentLoader.loadComponentFromURL("file://"+f.getAbsolutePath(), "_blank", 0, docLoadProperties)); + + // Variables de sortie: + Set defaultGiven = new HashSet<>(); + Map> specs = new HashMap<>(); + Map> inheritance = new HashMap<>(); + + + String[] sheetNames = xsd.getSheets().getElementNames(); + for (int s = 0; s < sheetNames.length; s++) { + String sheetName = sheetNames[s]; + XSpreadsheet sheet = qi(XSpreadsheet.class,xsd.getSheets().getByName(sheetName)); + + // Si c'est des permissions + if(sheetName.startsWith("Permission")) { + + // Parsing users/groups names + int defaultColIndex; + List holders = new ArrayList<>(); + // PermName,…,…,…,Default + for(defaultColIndex = 1;!sheet.getCellByPosition(defaultColIndex+1, 0).getFormula().equals("#~END~#") && defaultColIndex thisSpec = new HashMap<>(); + + + + for(int hi=0;hi inhz = null; + + for(int col = 1;!sheet.getCellByPosition(col, 0).getFormula().equals("#~END~#") && col <= MAX_COL;col++) { + String rule = sheet.getCellByPosition(col, i).getFormula(); + if(!rule.isBlank()) { + InheritancePolicy ipol = InheritancePolicy.fromChar(rule.charAt(0)); + String motherHolderName = (ipol==InheritancePolicy.ALL)?rule:rule.substring(1); + + if(inhz==null) + inhz = new ArrayList<>(); + inhz.add(new Inheritance(motherHolderName, ipol)); + } + } + if(inhz!=null) + inheritance.put(holder, inhz); + + + } + + + } + } + + System.out.println(defaultGiven); + System.out.println(specs); + System.out.println(inheritance); + + TPermissionContext tpc = new TPermissionContext(defaultGiven, specs, inheritance); + if(tpc.anyCycle()) + throw new IllegalArgumentException("Cycle detected !"); + + return tpc; + } + + public static final void writeToFile(File f,TPermissionContext tpc) throws Exception { + + XComponentContext xcc = xcc(); + XMultiComponentFactory xmcf; + XSpreadsheetDocument xsd; + + xmcf = xcc.getServiceManager(); + + xmcf = xcc.getServiceManager(); + Object object = xmcf.createInstanceWithContext("com.sun.star.frame.Desktop", xcc); + XComponentLoader xcl = qi(XComponentLoader.class,object); + + PropertyValue[] docLoadProperties = { new PropertyValue() }; + docLoadProperties[0].Name = "Hidden"; + docLoadProperties[0].Value = true; + String baseFile = new File("stringPermissionBase.ods").getAbsolutePath(); + + xsd = UnoRuntime.queryInterface( + XSpreadsheetDocument.class, xcl.loadComponentFromURL("file://"+baseFile, "_blank", 0, docLoadProperties )); + + XSpreadsheets sheets = xsd.getSheets(); + + + XSpreadsheet permissionsS = qi(XSpreadsheet.class,sheets.getByName("Permissions")); + XSpreadsheet permissionsCompleteS = qi(XSpreadsheet.class,sheets.getByName("Permissions-complete")); + XSpreadsheet holdersS = qi(XSpreadsheet.class,sheets.getByName("Holders")); + + + + + // ----- Permissions sheet ----- + + writePermissionsSheetOutline(tpc, permissionsS,false); + + + + + //TODO: Ajouter le support pour les holders qui commencent par un + ou un - (avec ++=+, +++=++, ...) + //TODO: Faire un tri qui «garde la structure du graphe d'héritage» + + // ----- Heritance sheet ----- + List enfants = tpc.inheritance.keySet().stream().sorted().collect(Collectors.toList()); + + holdersS.getCellByPosition(0, 0).setFormula("Holder");; + holdersS.getCellByPosition(1, 0).setFormula("Héritages");; + System.out.println(tpc.inheritance); + int maxCol = 0; + for(int i=0;i parents = tpc.inheritance.get(enfants.get(i)); + for (int j = 0; j < parents.size(); j++) { + String toWrite = parents.get(j).policy.toChar()+parents.get(j).holder; + holdersS.getCellByPosition(j+1, i+1).setFormula(toWrite); + } + maxCol = Math.max(maxCol, parents.size()); + } + + // Adding the style on the heritance core. + XSheetCellRange xscr = qi(XSheetCellRange.class,holdersS.getCellRangeByPosition(1,0,maxCol,0)); + System.out.println(xscr); + qi(XMergeable.class,xscr).merge(true); + for(int i=0;i permissions = Stream.concat( + tpc.specs.keySet().stream(), + tpc.defaultGiven.stream() + ).distinct().sorted().collect(Collectors.toList()); + List permSpecHolders = Stream.concat(tpc.inheritance.keySet().stream(),tpc.specs.values().stream().flatMap(m -> m.keySet().stream())).distinct().sorted().collect(Collectors.toList()); + //TODO: Find a way to distinct and sorted at the same time + for (int i = 0; i < permissions.size(); i++) { + XCell cell = permissionsS.getCellByPosition(0,i+1); + cell.setFormula(permissions.get(i).toString()); + qi(XPropertySet.class, cell).setPropertyValue("CellStyle", (i%2==0)?"SpecPermissionEven":"SpecPermissionOdd"); + } + for (int j = 0; j < permSpecHolders.size(); j++) { + XCell cell = permissionsS.getCellByPosition(j+1,0); + cell.setFormula(permSpecHolders.get(j)); + qi(XPropertySet.class, cell).setPropertyValue("CellStyle", (j%2==0)?"SpecHolderEven":"SpecHolderOdd"); + } + + int defaultHolderCol = permSpecHolders.size()+1; + XCell celld = permissionsS.getCellByPosition(defaultHolderCol,0); + celld.setFormula("#~DEFAULT~#"); + qi(XPropertySet.class, celld).setPropertyValue("CellStyle", "SpecHolderDefault"); + + for(Entry> etry : tpc.specs.entrySet()) { + int permIndex = Collections.binarySearch(permissions, etry.getKey()); + for(Entry ett : etry.getValue().entrySet()) { + int hldrIndex = Collections.binarySearch(permSpecHolders, ett.getKey()); + if(ett.getValue()!=Val.UNSPECIFIED) + permissionsS.getCellByPosition(hldrIndex+1,permIndex+1).setFormula(String.valueOf(ett.getValue().toChar())); + } + } + // Writing the DEFAULT column + for(int i = 0;i dg.implies(sp))) + permissionsS.getCellByPosition(defaultHolderCol, i+1).setFormula("."); + else + permissionsS.getCellByPosition(defaultHolderCol, i+1).setFormula("_"); + } + if(writeEverything) { + for(int j = 0;j T qi(Class aType, Object o) { + return UnoRuntime.queryInterface(aType, o); + } + +} diff --git a/src/com/bernard/permissions/tableured/TPermissionContext.java b/src/com/bernard/permissions/tableured/TPermissionContext.java new file mode 100644 index 0000000..92e8002 --- /dev/null +++ b/src/com/bernard/permissions/tableured/TPermissionContext.java @@ -0,0 +1,226 @@ +package com.bernard.permissions.tableured; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.bernard.math.GraphFunctions; +import com.bernard.permissions.PermissionContext; +import com.bernard.permissions.StringPermission; + +public class TPermissionContext implements PermissionContext { + + Set defaultGiven; + + //List permHolder; + //List permissions; + //Val[][] specs; + + Map> specs; // > + + Map> inheritance; // Par ordre chronologique + // Si [0] revoke et [1] grant, alors sera granted + + + + + public TPermissionContext(Set defaultGiven, Map> specs, Map> inheritance) { + this.defaultGiven = defaultGiven; + this.specs = specs; + this.inheritance = inheritance; + } + + @Override + public boolean can(StringPermission p, String ph) { + Val value = isRightGiven(p, ph); + if(value==Val.UNSPECIFIED) { + for(StringPermission perm : defaultGiven) { + if(perm.implies(p)) + return true; + } + return false; + } + return value==Val.GRANTED; + } + + public Val isRightGiven(StringPermission p, String ph) { + + for(StringPermission perm : specs.keySet()) { + if(perm.implies(p) && specs.get(perm).get(ph)==Val.GRANTED) + return Val.GRANTED; + if(p.implies(perm) && specs.get(perm).get(ph)==Val.REVOKED) + return Val.REVOKED; + } + + if(!inheritance.containsKey(ph)) + return Val.UNSPECIFIED; + + // Here, the permission is neither specifically given or revoked to this user. + // Let's check the parents. + List inhz = inheritance.get(ph); + + //Ordonné, car les premiers héritages sont prioritaires. + for(Inheritance ancetre: inhz) { + Val value = isRightGiven(p, ancetre.holder); + if(value == Val.UNSPECIFIED) + continue; // Cet ancetre n'a pas de spécification pour cette permission, on passe. + if(ancetre.policy==InheritancePolicy.ALL + || (ancetre.policy==InheritancePolicy.GRANT_ALL && value==Val.GRANTED) + || (ancetre.policy==InheritancePolicy.REVOKE_ALL && value==Val.REVOKED)) + return value; // != UNSPECIFIED + //Sinon, on ne sait pas. + } + + return Val.UNSPECIFIED; + } + + + public boolean anyCycle() { + return GraphFunctions.anyCycle(inheritance, i -> i.holder); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((defaultGiven == null) ? 0 : defaultGiven.hashCode()); + result = prime * result + ((inheritance == null) ? 0 : inheritance.hashCode()); + result = prime * result + ((specs == null) ? 0 : specs.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj)//FIXME: This doesnt work ... + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + TPermissionContext other = (TPermissionContext) obj; + if (defaultGiven == null) { + if (other.defaultGiven != null) + return false; + } else if (!defaultGiven.equals(other.defaultGiven)) + return false; + if (inheritance == null) { + if (other.inheritance != null) + return false; + } else if (!inheritance.equals(other.inheritance)) + return false; + if (specs == null) { + if (other.specs != null) + return false; + } else if (!specs.equals(other.specs)) + return false; + return true; + } + + @Override + public String toString() { + return "TPermissionContext [defaultGiven=" + defaultGiven + ", specs=" + specs + ", inheritance=" + inheritance + + "]"; + } + + + + + + + public static enum Val { + UNSPECIFIED, + GRANTED, + REVOKED; + + public char toChar() { + switch(this) { + case GRANTED: + return 'o'; + case REVOKED: + return 'x'; + case UNSPECIFIED: + } + return (char)0x0; + } + } + + public static class Inheritance{ + String holder; + InheritancePolicy policy; + + + + public Inheritance(String holder, InheritancePolicy policy) { + this.holder = holder; + this.policy = policy; + } + + public String getHolder() { + return holder; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((holder == null) ? 0 : holder.hashCode()); + result = prime * result + ((policy == null) ? 0 : policy.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Inheritance other = (Inheritance) obj; + if (holder == null) { + if (other.holder != null) + return false; + } else if (!holder.equals(other.holder)) + return false; + if (policy != other.policy) + return false; + return true; + } + + @Override + public String toString() { + return "Inheritance [holder=" + holder + ", policy=" + policy + "]"; + } + + + + } + + public static enum InheritancePolicy{ + GRANT_ALL, + REVOKE_ALL, + ALL; + + public static InheritancePolicy fromChar(char chr) { + switch(chr) { + case '+': + return GRANT_ALL; + case '-': + return REVOKE_ALL; + default: + return ALL; + } + } + public char toChar() { + switch(this) { + case GRANT_ALL: + return '+'; + case REVOKE_ALL: + return '-'; + default: + return (char)0x0; + } + } + } + +} diff --git a/stringPermissionBase.ods b/stringPermissionBase.ods new file mode 100644 index 0000000000000000000000000000000000000000..6e39b38aef56bca9381c2044797954839ea5c3cb GIT binary patch literal 9984 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 ns21G_