Move all doc generating tasks into docs package

This commit is contained in:
ljacqu
2016-10-08 14:58:06 +02:00
parent dd9312f581
commit a8d5b19807
11 changed files with 13 additions and 15 deletions
+3 -3
View File
@@ -1,9 +1,9 @@
package tools.docs;
import com.google.common.collect.ImmutableSet;
import tools.commands.CommandPageCreater;
import tools.hashmethods.HashAlgorithmsDescriptionTask;
import tools.permissions.PermissionsListWriter;
import tools.docs.commands.CommandPageCreater;
import tools.docs.hashmethods.HashAlgorithmsDescriptionTask;
import tools.docs.permissions.PermissionsListWriter;
import tools.utils.AutoToolTask;
import tools.utils.ToolTask;
@@ -0,0 +1,79 @@
package tools.docs.commands;
import fr.xephi.authme.command.CommandArgumentDescription;
import fr.xephi.authme.command.CommandDescription;
import fr.xephi.authme.command.CommandInitializer;
import fr.xephi.authme.command.CommandUtils;
import fr.xephi.authme.permission.PermissionNode;
import tools.utils.AutoToolTask;
import tools.utils.FileUtils;
import tools.utils.TagValue.NestedTagValue;
import tools.utils.TagValueHolder;
import tools.utils.ToolsConstants;
import java.util.Collection;
import java.util.Scanner;
import java.util.Set;
public class CommandPageCreater implements AutoToolTask {
private static final String OUTPUT_FILE = ToolsConstants.DOCS_FOLDER + "commands.md";
@Override
public String getTaskName() {
return "createCommandPage";
}
@Override
public void execute(Scanner scanner) {
executeDefault();
}
@Override
public void executeDefault() {
CommandInitializer commandInitializer = new CommandInitializer();
final Set<CommandDescription> baseCommands = commandInitializer.getCommands();
NestedTagValue commandTags = new NestedTagValue();
addCommandsInfo(commandTags, baseCommands);
FileUtils.generateFileFromTemplate(
ToolsConstants.TOOLS_SOURCE_ROOT + "docs/commands/commands.tpl.md",
OUTPUT_FILE,
TagValueHolder.create().put("commands", commandTags));
System.out.println("Wrote to '" + OUTPUT_FILE + "' with " + baseCommands.size() + " base commands.");
}
private static void addCommandsInfo(NestedTagValue commandTags, Collection<CommandDescription> commands) {
for (CommandDescription command : commands) {
TagValueHolder tags = TagValueHolder.create()
.put("command", CommandUtils.constructCommandPath(command))
.put("description", command.getDetailedDescription())
.put("arguments", formatArguments(command.getArguments()))
.put("permissions", formatPermissions(command.getPermission()));
commandTags.add(tags);
if (!command.getChildren().isEmpty()) {
addCommandsInfo(commandTags, command.getChildren());
}
}
}
private static String formatPermissions(PermissionNode permission) {
if (permission == null) {
return "";
} else {
return permission.getNode();
}
}
private static String formatArguments(Iterable<CommandArgumentDescription> arguments) {
StringBuilder result = new StringBuilder();
for (CommandArgumentDescription argument : arguments) {
String argumentName = argument.isOptional()
? "[" + argument.getName() + "]"
: "&lt;" + argument.getName() + ">";
result.append(" ").append(argumentName);
}
return result.toString();
}
}
@@ -0,0 +1,13 @@
<!-- {gen_warning} -->
<!-- File auto-generated on {gen_date}. See commands/commands.tpl.md -->
## AuthMe Commands
You can use the following commands to use the features of AuthMe. Mandatory arguments are marked with `< >`
brackets; optional arguments are enclosed in square brackets (`[ ]`).
[#commands]
- **{command}**{arguments}: {description}[permissions]
<br />Requires `{permissions}`[/permissions]
[/#commands]
{gen_footer}
@@ -0,0 +1,159 @@
package tools.docs.hashmethods;
import ch.jalu.injector.Injector;
import ch.jalu.injector.InjectorBuilder;
import com.github.authme.configme.properties.Property;
import com.google.common.collect.ImmutableSet;
import fr.xephi.authme.security.HashAlgorithm;
import fr.xephi.authme.security.crypts.EncryptionMethod;
import fr.xephi.authme.security.crypts.HexSaltedMethod;
import fr.xephi.authme.security.crypts.description.AsciiRestricted;
import fr.xephi.authme.security.crypts.description.HasSalt;
import fr.xephi.authme.security.crypts.description.Recommendation;
import fr.xephi.authme.settings.Settings;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import java.lang.annotation.Annotation;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Gathers information on {@link EncryptionMethod} implementations based on
* the annotations in {@link fr.xephi.authme.security.crypts.description}.
*/
public class EncryptionMethodInfoGatherer {
private final static Set<Class<? extends Annotation>> RELEVANT_ANNOTATIONS =
ImmutableSet.of(HasSalt.class, Recommendation.class, AsciiRestricted.class);
private static Injector injector = createInitializer();
private Map<HashAlgorithm, MethodDescription> descriptions;
public EncryptionMethodInfoGatherer() {
descriptions = new LinkedHashMap<>();
constructDescriptions();
}
public Map<HashAlgorithm, MethodDescription> getDescriptions() {
return descriptions;
}
private void constructDescriptions() {
for (HashAlgorithm algorithm : HashAlgorithm.values()) {
if (!HashAlgorithm.CUSTOM.equals(algorithm) && !algorithm.getClazz().isAnnotationPresent(Deprecated.class)) {
MethodDescription description = createDescription(algorithm);
descriptions.put(algorithm, description);
}
}
}
private static MethodDescription createDescription(HashAlgorithm algorithm) {
Class<? extends EncryptionMethod> clazz = algorithm.getClazz();
EncryptionMethod method = injector.newInstance(clazz);
if (method == null) {
throw new NullPointerException("Method for '" + algorithm + "' is null");
}
MethodDescription description = new MethodDescription(clazz);
description.setHashLength(method.computeHash("test", "user").getHash().length());
description.setHasSeparateSalt(method.hasSeparateSalt());
Map<Class<?>, Annotation> annotationMap = gatherAnnotations(clazz);
if (annotationMap.containsKey(HasSalt.class)) {
setSaltInformation(description, returnTyped(annotationMap, HasSalt.class), method);
}
if (annotationMap.containsKey(Recommendation.class)) {
description.setUsage(returnTyped(annotationMap, Recommendation.class).value());
}
if (annotationMap.containsKey(AsciiRestricted.class)) {
description.setAsciiRestricted(true);
}
return description;
}
private static Map<Class<?>, Annotation> gatherAnnotations(Class<?> methodClass) {
// Note ljacqu 20151231: The map could be Map<Class<? extends Annotation>, Annotation> and it has the constraint
// that for a key Class<T>, the value is of type T. We write a simple "Class<?>" for brevity.
Map<Class<?>, Annotation> collection = new HashMap<>();
Class<?> currentMethodClass = methodClass;
while (currentMethodClass != null) {
getRelevantAnnotations(currentMethodClass, collection);
currentMethodClass = getSuperClass(currentMethodClass);
}
return collection;
}
// Parameters could be Class<? extends EncryptionMethod>; Map<Class<? extends Annotation>, Annotation>
// but the constraint doesn't have any technical relevance, so just clutters the code
private static void getRelevantAnnotations(Class<?> methodClass, Map<Class<?>, Annotation> collection) {
for (Annotation annotation : methodClass.getAnnotations()) {
if (RELEVANT_ANNOTATIONS.contains(annotation.annotationType())
&& !collection.containsKey(annotation.annotationType())) {
collection.put(annotation.annotationType(), annotation);
}
}
}
/**
* Returns the super class of the given encryption method if it is also of EncryptionMethod type.
* (Anything beyond EncryptionMethod is not of interest.)
*/
private static Class<?> getSuperClass(Class<?> methodClass) {
Class<?> zuper = methodClass.getSuperclass();
if (EncryptionMethod.class.isAssignableFrom(zuper)) {
return zuper;
}
return null;
}
/**
* Set the salt information for the given encryption method and the found {@link HasSalt} annotation.
* Also gets the salt length from {@link HexSaltedMethod#getSaltLength()} for such instances.
*
* @param description The description to update
* @param hasSalt The associated HasSalt annotation
* @param method The encryption method
*/
private static void setSaltInformation(MethodDescription description, HasSalt hasSalt, EncryptionMethod method) {
description.setSaltType(hasSalt.value());
if (hasSalt.length() != 0) {
description.setSaltLength(hasSalt.length());
} else if (method instanceof HexSaltedMethod) {
int saltLength = ((HexSaltedMethod) method).getSaltLength();
description.setSaltLength(saltLength);
}
}
// Convenience method for retrieving an annotation in a typed fashion.
// We know implicitly that the key of the map always corresponds to the type of the value
private static <T> T returnTyped(Map<Class<?>, Annotation> map, Class<T> key) {
return key.cast(map.get(key));
}
@SuppressWarnings("unchecked")
private static Injector createInitializer() {
Settings settings = mock(Settings.class);
// Return the default value for any property
when(settings.getProperty(any(Property.class))).thenAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
Property<?> property = (Property<?>) invocation.getArguments()[0];
return property.getDefaultValue();
}
});
// By passing some bogus "package" to the constructor, the injector will throw if it needs to
// instantiate any dependency other than what we provide.
Injector injector = new InjectorBuilder().addDefaultHandlers("fr.xephi.authme.security.crypts").create();
injector.register(Settings.class, settings);
return injector;
}
}
@@ -0,0 +1,95 @@
package tools.docs.hashmethods;
import fr.xephi.authme.security.HashAlgorithm;
import tools.utils.AutoToolTask;
import tools.utils.FileUtils;
import tools.utils.TagValue.NestedTagValue;
import tools.utils.TagValueHolder;
import tools.utils.ToolsConstants;
import java.util.Map;
import java.util.Scanner;
/**
* Task for generating the markdown page describing the AuthMe hash algorithms.
*
* @see fr.xephi.authme.security.HashAlgorithm
*/
public class HashAlgorithmsDescriptionTask implements AutoToolTask {
private static final String CUR_FOLDER = ToolsConstants.TOOLS_SOURCE_ROOT + "docs/hashmethods/";
private static final String OUTPUT_FILE = ToolsConstants.DOCS_FOLDER + "hash_algorithms.md";
@Override
public void execute(Scanner scanner) {
executeDefault();
}
@Override
public void executeDefault() {
// Gather info and construct a row for each method
EncryptionMethodInfoGatherer infoGatherer = new EncryptionMethodInfoGatherer();
Map<HashAlgorithm, MethodDescription> descriptions = infoGatherer.getDescriptions();
final NestedTagValue methodRows = constructMethodRows(descriptions);
// Write to the docs file
TagValueHolder tags = TagValueHolder.create().put("algorithms", methodRows);
FileUtils.generateFileFromTemplate(CUR_FOLDER + "hash_algorithms.tpl.md", OUTPUT_FILE, tags);
}
private static NestedTagValue constructMethodRows(Map<HashAlgorithm, MethodDescription> descriptions) {
NestedTagValue methodTags = new NestedTagValue();
for (Map.Entry<HashAlgorithm, MethodDescription> entry : descriptions.entrySet()) {
MethodDescription description = entry.getValue();
TagValueHolder tags = TagValueHolder.create()
.put("name", asString(entry.getKey()))
.put("recommendation", asString(description.getUsage()))
.put("hash_length", asString(description.getHashLength()))
.put("ascii_restricted", asString(description.isAsciiRestricted()))
.put("salt_type", asString(description.getSaltType()))
.put("salt_length", asString(description.getSaltLength()))
.put("separate_salt", asString(description.hasSeparateSalt()));
methodTags.add(tags);
}
return methodTags;
}
@Override
public String getTaskName() {
return "describeHashAlgos";
}
// ----
// String representations
// ----
private static String asString(boolean value) {
return value ? "Y" : "";
}
private static String asString(int value) {
return String.valueOf(value);
}
private static String asString(Integer value) {
if (value == null) {
return "";
}
return String.valueOf(value);
}
private static String asString(HashAlgorithm value) {
return value.toString();
}
private static <E extends Enum<E>> String asString(E value) {
if (value == null) {
return "";
}
// Get the enum name and replace something like "DO_NOT_USE" to "Do not use"
String enumName = value.toString().replace("_", " ");
return enumName.length() > 2
? enumName.substring(0, 1) + enumName.substring(1).toLowerCase()
: enumName;
}
}
@@ -0,0 +1,85 @@
package tools.docs.hashmethods;
import fr.xephi.authme.security.crypts.EncryptionMethod;
import fr.xephi.authme.security.crypts.description.SaltType;
import fr.xephi.authme.security.crypts.description.Usage;
/**
* Description of a {@link EncryptionMethod}.
*/
public class MethodDescription {
/** The implementation class the description belongs to. */
private final Class<? extends EncryptionMethod> method;
/** The type of the salt that is used. */
private SaltType saltType;
/** The length of the salt for SaltType.TEXT salts. */
private Integer saltLength;
/** The usage recommendation. */
private Usage usage;
/** Whether or not the encryption method is restricted to ASCII characters for proper functioning. */
private boolean asciiRestricted;
/** Whether or not the encryption method requires its salt stored separately. */
private boolean hasSeparateSalt;
/** The length of the hash output, based on a test hash (i.e. assumes same length for all hashes.) */
private int hashLength;
public MethodDescription(Class<? extends EncryptionMethod> method) {
this.method = method;
}
// Trivial getters and setters
public Class<? extends EncryptionMethod> getMethod() {
return method;
}
public SaltType getSaltType() {
return saltType;
}
public void setSaltType(SaltType saltType) {
this.saltType = saltType;
}
public Integer getSaltLength() {
return saltLength;
}
public void setSaltLength(int saltLength) {
this.saltLength = saltLength;
}
public Usage getUsage() {
return usage;
}
public void setUsage(Usage usage) {
this.usage = usage;
}
public boolean isAsciiRestricted() {
return asciiRestricted;
}
public void setAsciiRestricted(boolean asciiRestricted) {
this.asciiRestricted = asciiRestricted;
}
public boolean hasSeparateSalt() {
return hasSeparateSalt;
}
public void setHasSeparateSalt(boolean hasSeparateSalt) {
this.hasSeparateSalt = hasSeparateSalt;
}
public int getHashLength() {
return hashLength;
}
public void setHashLength(int hashLength) {
this.hashLength = hashLength;
}
}
@@ -0,0 +1,57 @@
<!-- {gen_warning} -->
<!-- File auto-generated on {gen_date}. See hashmethods/hash_algorithms.tpl.md -->
## Hash Algorithms
AuthMe supports the following hash algorithms for storing your passwords safely.
Algorithm | Recommendation | Hash length | ASCII | | Salt type | Length | Separate?
--------- | -------------- | ----------- | ----- | --- | --------- | ------ | ---------
[#algorithms]
{name} | {recommendation} | {hash_length} | {ascii_restricted} | | {salt_type} | {salt_length} | {separate_salt}
[/#algorithms]CUSTOM | | | | | | | |
<!-- {gen_warning} -->
### Columns
#### Algorithm
The algorithm is the hashing algorithm used to store passwords with. Default is SHA256 and is recommended.
You can change the hashing algorithm in the config.yml: under `security`, locate `passwordHash`.
#### Recommendation
The recommendation lists our usage recommendation in terms of how secure it is (not how _well_ the algorithm works!).
- Recommended: The hash algorithm appears to be cryptographically secure and is one we recommend.
- Acceptable: There are safer algorithms that can be chosen but using the algorithm is generally OK.
- Do not use: Hash algorithm isn't sufficiently secure. Use only if required to hook into another system.
- Does not work: The algorithm does not work properly; do not use.
#### Hash Length
The length of the hashes the algorithm produces. Note that the hash length is not (primarily) indicative of
whether an algorithm is secure or not.
#### ASCII
If denoted with a **y**, means that the algorithm is restricted to ASCII characters only, i.e. it will simply ignore
"special characters" such as `ÿ` or `Â`. Note that we do not recommend the use of "special characters" in passwords.
#### Salt Columns
Before hashing, a _salt_ may be appended to the password to make the hash more secure. The following columns describe
the salt the algorithm uses.
<!-- {gen_warning} -->
##### Salt Type
We do not recommend the usage
of any algorithm that doesn't use a randomly generated text as salt. This "salt type" column indicates what type of
salt the algorithm uses:
- Text: randomly generated text (see also the following column, "Length")
- Username: the salt is constructed from the username (bad)
- None: the algorithm uses no salt (bad)
##### Length
If applicable (salt type is "Text"), indicates the length of the generated salt. The longer the better.
If this column is empty when the salt type is "Text", it typically means the salt length can be defined in config.yml.
##### Separate
If denoted with a **y**, it means that the salt is stored in a separate column in the database. This is neither good
or bad.
{gen_footer}
@@ -0,0 +1,88 @@
package tools.docs.permissions;
import fr.xephi.authme.ClassCollector;
import fr.xephi.authme.permission.PermissionNode;
import tools.utils.FileUtils;
import tools.utils.ToolsConstants;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Gatherer to generate up-to-date lists of the AuthMe permission nodes.
*/
public class PermissionNodesGatherer {
/**
* Regular expression that should match the JavaDoc comment above an enum, <i>including</i>
* the name of the enum value. The first group (i.e. {@code \\1}) should be the JavaDoc description;
* the second group should contain the enum value.
*/
private static final Pattern JAVADOC_WITH_ENUM_PATTERN = Pattern.compile(
"/\\*\\*\\s+\\*" // Match starting '/**' and the '*' on the next line
+ "(.*?)\\s+\\*/" // Capture everything until we encounter '*/'
+ "\\s+([A-Z_]+)\\("); // Match the enum name (e.g. 'LOGIN'), until before the first '('
/**
* Return a sorted collection of all permission nodes, including its JavaDoc description.
*
* @return Ordered map whose keys are the permission nodes and the values the associated JavaDoc
*/
public <T extends Enum<T> & PermissionNode> Map<String, String> gatherNodesWithJavaDoc() {
Map<String, String> result = new TreeMap<>();
result.put("authme.admin.*", "Give access to all admin commands.");
result.put("authme.player.*", "Permission to use all player (non-admin) commands.");
result.put("authme.player.email", "Grants all email permissions.");
new ClassCollector(ToolsConstants.MAIN_SOURCE_ROOT, "")
.collectClasses(PermissionNode.class)
.stream()
.filter(Class::isEnum)
.forEach(clz -> addDescriptionsForClass((Class<T>) clz, result));
return result;
}
private <T extends Enum<T> & PermissionNode> void addDescriptionsForClass(Class<T> clazz,
Map<String, String> descriptions) {
String classSource = getSourceForClass(clazz);
Map<String, String> sourceDescriptions = extractJavaDocFromSource(classSource);
for (T perm : EnumSet.allOf(clazz)) {
String description = sourceDescriptions.get(perm.name());
if (description == null) {
System.out.println("Note: Could not retrieve description for "
+ clazz.getSimpleName() + "#" + perm.name());
description = "";
}
descriptions.put(perm.getNode(), description.trim());
}
}
private static Map<String, String> extractJavaDocFromSource(String source) {
Map<String, String> allMatches = new HashMap<>();
Matcher matcher = JAVADOC_WITH_ENUM_PATTERN.matcher(source);
while (matcher.find()) {
String description = matcher.group(1);
String enumValue = matcher.group(2);
allMatches.put(enumValue, description);
}
return allMatches;
}
/**
* Return the Java source code for the given implementation of {@link PermissionNode}.
*
* @param clazz The clazz to the get the source for
* @param <T> The concrete type
* @return Source code of the file
*/
private static <T extends Enum<T> & PermissionNode> String getSourceForClass(Class<T> clazz) {
String classFile = ToolsConstants.MAIN_SOURCE_ROOT + clazz.getName().replace(".", "/") + ".java";
return FileUtils.readFromFile(classFile);
}
}
@@ -0,0 +1,56 @@
package tools.docs.permissions;
import tools.utils.AutoToolTask;
import tools.utils.FileUtils;
import tools.utils.TagValue.NestedTagValue;
import tools.utils.TagValueHolder;
import tools.utils.ToolsConstants;
import java.util.Map;
import java.util.Scanner;
/**
* Task responsible for formatting a permissions node list and
* for writing it to a file if desired.
*/
public class PermissionsListWriter implements AutoToolTask {
private static final String TEMPLATE_FILE = ToolsConstants.TOOLS_SOURCE_ROOT + "docs/permissions/permission_nodes.tpl.md";
private static final String PERMISSIONS_OUTPUT_FILE = ToolsConstants.DOCS_FOLDER + "permission_nodes.md";
@Override
public String getTaskName() {
return "writePermissionsList";
}
@Override
public void execute(Scanner scanner) {
generateAndWriteFile();
}
@Override
public void executeDefault() {
generateAndWriteFile();
}
private static void generateAndWriteFile() {
final NestedTagValue permissionsTagValue = generatePermissionsList();
TagValueHolder tags = TagValueHolder.create().put("nodes", permissionsTagValue);
FileUtils.generateFileFromTemplate(TEMPLATE_FILE, PERMISSIONS_OUTPUT_FILE, tags);
System.out.println("Wrote to '" + PERMISSIONS_OUTPUT_FILE + "'");
System.out.println("Before committing, please verify the output!");
}
private static NestedTagValue generatePermissionsList() {
PermissionNodesGatherer gatherer = new PermissionNodesGatherer();
Map<String, String> permissions = gatherer.gatherNodesWithJavaDoc();
NestedTagValue permissionTags = new NestedTagValue();
for (Map.Entry<String, String> entry : permissions.entrySet()) {
permissionTags.add(TagValueHolder.create()
.put("node", entry.getKey())
.put("description", entry.getValue()));
}
return permissionTags;
}
}
@@ -0,0 +1,11 @@
<!-- {gen_warning} -->
<!-- File auto-generated on {gen_date}. See permissions/permission_nodes.tpl.md -->
## AuthMe Permission Nodes
The following are the permission nodes that are currently supported by the latest dev builds.
[#nodes]
- **{node}** {description}
[/#nodes]
{gen_footer}