From 46cc9102262c943f7e95bbd2cfb331b454c714d5 Mon Sep 17 00:00:00 2001 From: Daniel Espendiller Date: Sat, 23 Dec 2017 16:58:35 +0100 Subject: [PATCH] [Security] support voter attributes in is_granted and has_role security annotation #892 --- META-INF/plugin.xml | 1 + ...tionExpressionGotoCompletionRegistrar.java | 133 ++++++++++++++++++ .../VoterGotoCompletionRegistrar.java | 31 +--- .../security/utils/VoterUtil.java | 35 +++++ ...ExpressionGotoCompletionRegistrarTest.java | 128 +++++++++++++++++ .../tests/security/fixtures/classes.php | 10 ++ 6 files changed, 313 insertions(+), 25 deletions(-) create mode 100644 src/fr/adrienbrault/idea/symfony2plugin/security/AnnotationExpressionGotoCompletionRegistrar.java create mode 100644 tests/fr/adrienbrault/idea/symfony2plugin/tests/security/AnnotationExpressionGotoCompletionRegistrarTest.java diff --git a/META-INF/plugin.xml b/META-INF/plugin.xml index 1821a3692..8cee03086 100644 --- a/META-INF/plugin.xml +++ b/META-INF/plugin.xml @@ -582,6 +582,7 @@ + diff --git a/src/fr/adrienbrault/idea/symfony2plugin/security/AnnotationExpressionGotoCompletionRegistrar.java b/src/fr/adrienbrault/idea/symfony2plugin/security/AnnotationExpressionGotoCompletionRegistrar.java new file mode 100644 index 000000000..92156aa7d --- /dev/null +++ b/src/fr/adrienbrault/idea/symfony2plugin/security/AnnotationExpressionGotoCompletionRegistrar.java @@ -0,0 +1,133 @@ +package fr.adrienbrault.idea.symfony2plugin.security; + +import com.intellij.codeInsight.completion.CompletionResultSet; +import com.intellij.patterns.PatternCondition; +import com.intellij.patterns.PlatformPatterns; +import com.intellij.psi.PsiElement; +import com.intellij.util.ProcessingContext; +import com.jetbrains.php.lang.documentation.phpdoc.lexer.PhpDocTokenTypes; +import com.jetbrains.php.lang.documentation.phpdoc.parser.PhpDocElementTypes; +import com.jetbrains.php.lang.documentation.phpdoc.psi.tags.PhpDocTag; +import com.jetbrains.php.lang.psi.elements.StringLiteralExpression; +import de.espend.idea.php.annotation.util.AnnotationUtil; +import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionProvider; +import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionProviderLookupArguments; +import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionRegistrar; +import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionRegistrarParameter; +import fr.adrienbrault.idea.symfony2plugin.security.utils.VoterUtil; +import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author Daniel Espendiller + */ +public class AnnotationExpressionGotoCompletionRegistrar implements GotoCompletionRegistrar { + + private static final String SECURITY_ANNOTATION = "Sensio\\Bundle\\FrameworkExtraBundle\\Configuration\\Security"; + + @Override + public void register(@NotNull GotoCompletionRegistrarParameter registrar) { + // "@Security("is_granted('POST_SHOW', post) and has_role('ROLE_ADMIN')")" + registrar.register( + PlatformPatterns.psiElement(PhpDocTokenTypes.DOC_STRING) + .withParent(PlatformPatterns.psiElement(StringLiteralExpression.class) + .withParent(PlatformPatterns.psiElement(PhpDocElementTypes.phpDocAttributeList) + .withParent(PlatformPatterns.psiElement(PhpDocTag.class) + .with(PhpDocInstancePatternCondition.INSTANCE) + ) + ) + ), + MyGotoCompletionProvider::new + ); + } + + /** + * "@Security("has_role('ROLE_FOOBAR')")" + * "@Security("is_granted('POST_SHOW', post) and has_role('ROLE_ADMIN')")" + */ + private static class MyGotoCompletionProvider extends GotoCompletionProvider { + MyGotoCompletionProvider(@NotNull PsiElement psiElement) { + super(psiElement); + } + + @Override + public void getLookupElements(@NotNull GotoCompletionProviderLookupArguments arguments) { + final CompletionResultSet resultSet = arguments.getResultSet(); + String blockNamePrefix = resultSet.getPrefixMatcher().getPrefix(); + + // find caret position: + // - "has_role('" + // - "has_role('YAML_ROLE_" + if(!blockNamePrefix.matches("^.*(has_role|is_granted)\\s*\\(\\s*'[\\w-]*$")) { + return; + } + + // clear prefix caret string; for a clean completion independent from inside content + CompletionResultSet myResultSet = resultSet.withPrefixMatcher(""); + + VoterUtil.LookupElementPairConsumer consumer = new VoterUtil.LookupElementPairConsumer(); + VoterUtil.visitAttribute(getProject(), consumer); + myResultSet.addAllElements(consumer.getLookupElements()); + } + + @NotNull + @Override + public Collection getPsiTargets(PsiElement element) { + if(getElement().getNode().getElementType() != PhpDocTokenTypes.DOC_STRING) { + return Collections.emptyList(); + } + + PsiElement parent = getElement().getParent(); + if(!(parent instanceof StringLiteralExpression)) { + return Collections.emptyList(); + } + + String contents = ((StringLiteralExpression) parent).getContents(); + + Collection roles = new HashSet<>(); + for (String regex : new String[]{"is_granted\\s*\\(\\s*['|\"]([^'\"]+)['|\"]\\s*[\\)|,]", "has_role\\s*\\(\\s*['|\"]([^'\"]+)['|\"]\\s*\\)"}) { + Matcher matcher = Pattern.compile(regex).matcher(contents); + while(matcher.find()){ + roles.add(matcher.group(1)); + } + } + + if(roles.size() == 0) { + return Collections.emptyList(); + } + + Collection targets = new HashSet<>(); + + VoterUtil.visitAttribute(getProject(), pair -> { + if(roles.contains(pair.getFirst())) { + targets.add(pair.getSecond()); + } + }); + + return targets; + } + } + + /** + * Check if given PhpDocTag is instance of given Annotation class + */ + private static class PhpDocInstancePatternCondition extends PatternCondition { + private static PhpDocInstancePatternCondition INSTANCE = new PhpDocInstancePatternCondition(); + + PhpDocInstancePatternCondition() { + super("PhpDoc Annotation Instance"); + } + + @Override + public boolean accepts(@NotNull PsiElement psiElement, ProcessingContext processingContext) { + return psiElement instanceof PhpDocTag + && PhpElementsUtil.isEqualClassName(AnnotationUtil.getAnnotationReference((PhpDocTag) psiElement), SECURITY_ANNOTATION); + } + } +} diff --git a/src/fr/adrienbrault/idea/symfony2plugin/security/VoterGotoCompletionRegistrar.java b/src/fr/adrienbrault/idea/symfony2plugin/security/VoterGotoCompletionRegistrar.java index f9ff822f1..00a3f60ec 100644 --- a/src/fr/adrienbrault/idea/symfony2plugin/security/VoterGotoCompletionRegistrar.java +++ b/src/fr/adrienbrault/idea/symfony2plugin/security/VoterGotoCompletionRegistrar.java @@ -1,25 +1,22 @@ package fr.adrienbrault.idea.symfony2plugin.security; import com.intellij.codeInsight.lookup.LookupElement; -import com.intellij.codeInsight.lookup.LookupElementBuilder; import com.intellij.patterns.PlatformPatterns; import com.intellij.psi.PsiElement; -import com.intellij.psi.util.PsiTreeUtil; import com.jetbrains.php.lang.PhpLanguage; -import com.jetbrains.php.lang.psi.elements.PhpClass; import com.jetbrains.php.lang.psi.elements.StringLiteralExpression; -import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons; -import fr.adrienbrault.idea.symfony2plugin.templating.TwigPattern; import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionProvider; import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionRegistrar; import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionRegistrarParameter; import fr.adrienbrault.idea.symfony2plugin.codeInsight.utils.GotoCompletionUtil; import fr.adrienbrault.idea.symfony2plugin.security.utils.VoterUtil; +import fr.adrienbrault.idea.symfony2plugin.templating.TwigPattern; import fr.adrienbrault.idea.symfony2plugin.util.MethodMatcher; import org.apache.commons.lang.StringUtils; import org.jetbrains.annotations.NotNull; -import java.util.*; +import java.util.Collection; +import java.util.Collections; /** * @author Daniel Espendiller @@ -84,25 +81,9 @@ private static class MyVisitorGotoCompletionProvider extends GotoCompletionProvi @NotNull @Override public Collection getLookupElements() { - Collection lookupElements = new ArrayList<>(); - - Set elements = new HashSet<>(); - - VoterUtil.visitAttribute(getProject(), pair -> { - String name = pair.getFirst(); - if(!elements.contains(name)) { - LookupElementBuilder lookupElement = LookupElementBuilder.create(name).withIcon(Symfony2Icons.SYMFONY); - PhpClass phpClass = PsiTreeUtil.getParentOfType(pair.getSecond(), PhpClass.class); - if(phpClass != null) { - lookupElement = lookupElement.withTypeText(phpClass.getName(), true); - } - - lookupElements.add(lookupElement); - elements.add(name); - } - }); - - return lookupElements; + VoterUtil.LookupElementPairConsumer consumer = new VoterUtil.LookupElementPairConsumer(); + VoterUtil.visitAttribute(getProject(), consumer); + return consumer.getLookupElements(); } @NotNull diff --git a/src/fr/adrienbrault/idea/symfony2plugin/security/utils/VoterUtil.java b/src/fr/adrienbrault/idea/symfony2plugin/security/utils/VoterUtil.java index df06e441a..aa21ea9e6 100644 --- a/src/fr/adrienbrault/idea/symfony2plugin/security/utils/VoterUtil.java +++ b/src/fr/adrienbrault/idea/symfony2plugin/security/utils/VoterUtil.java @@ -1,5 +1,7 @@ package fr.adrienbrault.idea.symfony2plugin.security.utils; +import com.intellij.codeInsight.lookup.LookupElement; +import com.intellij.codeInsight.lookup.LookupElementBuilder; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Pair; import com.intellij.psi.PsiElement; @@ -13,6 +15,7 @@ import com.jetbrains.php.lang.lexer.PhpTokenTypes; import com.jetbrains.php.lang.parser.PhpElementTypes; import com.jetbrains.php.lang.psi.elements.*; +import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons; import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil; import fr.adrienbrault.idea.symfony2plugin.util.PsiElementUtils; import fr.adrienbrault.idea.symfony2plugin.util.yaml.YamlHelper; @@ -22,6 +25,8 @@ import org.jetbrains.yaml.YAMLUtil; import org.jetbrains.yaml.psi.*; +import java.util.ArrayList; +import java.util.Collection; import java.util.HashSet; import java.util.Set; import java.util.function.Consumer; @@ -213,6 +218,36 @@ public Set getValues() { } } + public static class LookupElementPairConsumer implements Consumer> { + @NotNull + private final Set elements = new HashSet<>(); + + @NotNull + public Collection getLookupElements() { + return lookupElements; + } + + @Override + public void accept(Pair pair) { + String name = pair.getFirst(); + if (!elements.contains(name)) { + LookupElementBuilder lookupElement = LookupElementBuilder.create(name).withIcon(Symfony2Icons.SYMFONY); + + PhpClass phpClass = PsiTreeUtil.getParentOfType(pair.getSecond(), PhpClass.class); + if (phpClass != null) { + lookupElement = lookupElement.withTypeText(phpClass.getName(), true); + } + + lookupElements.add(lookupElement); + + elements.add(name); + } + } + + @NotNull + private final Collection lookupElements = new ArrayList<>(); + } + /** * Find security roles on Voter implementation and security roles in Yaml */ diff --git a/tests/fr/adrienbrault/idea/symfony2plugin/tests/security/AnnotationExpressionGotoCompletionRegistrarTest.java b/tests/fr/adrienbrault/idea/symfony2plugin/tests/security/AnnotationExpressionGotoCompletionRegistrarTest.java new file mode 100644 index 000000000..9210e10f7 --- /dev/null +++ b/tests/fr/adrienbrault/idea/symfony2plugin/tests/security/AnnotationExpressionGotoCompletionRegistrarTest.java @@ -0,0 +1,128 @@ +package fr.adrienbrault.idea.symfony2plugin.tests.security; + +import com.intellij.patterns.PlatformPatterns; +import fr.adrienbrault.idea.symfony2plugin.tests.SymfonyLightCodeInsightFixtureTestCase; + +import java.io.File; + +/** + * @author Daniel Espendiller + * + * @see fr.adrienbrault.idea.symfony2plugin.security.AnnotationExpressionGotoCompletionRegistrar + */ +public class AnnotationExpressionGotoCompletionRegistrarTest extends SymfonyLightCodeInsightFixtureTestCase { + public void setUp() throws Exception { + super.setUp(); + + myFixture.copyFileToProject("security.yml"); + myFixture.copyFileToProject("classes.php"); + } + + protected String getTestDataPath() { + return new File(this.getClass().getResource("fixtures").getFile()).getAbsolutePath(); + } + + public void testSecurityAnnotationProvidesCompletion() { + assertCompletionContains( + "test.php", + "')\")\n" + + "*/\n" + + "function test() {};\n" + + "", + "YAML_ROLE_USER_FOOBAR" + ); + + assertCompletionContains( + "test.php", + "')\")\n" + + "*/\n" + + "function test() {};\n" + + "", + "YAML_ROLE_USER_FOOBAR" + ); + + assertCompletionContains( + "test.php", + "')\")\n" + + "*/\n" + + "function test() {};\n" + + "", + "YAML_ROLE_USER_FOOBAR" + ); + + assertCompletionContains( + "test.php", + "', foo)\")\n" + + "*/\n" + + "function test() {};\n" + + "", + "YAML_ROLE_USER_FOOBAR" + ); + } + + public void testSecurityAnnotationProvidesRoleNavigation() { + assertNavigationMatch( + "test.php", + "_USER_FOOBAR')\")\n" + + "*/\n" + + "function test() {};\n" + + "", + PlatformPatterns.psiElement() + ); + + assertNavigationMatch( + "test.php", + "_USER_FOOBAR' ) \")\n" + + "*/\n" + + "function test() {};\n" + + "", + PlatformPatterns.psiElement() + ); + + assertNavigationMatch( + "test.php", + "_USER_FOOBAR')\")\n" + + "*/\n" + + "function test() {};\n" + + "", + PlatformPatterns.psiElement() + ); + + assertNavigationMatch( + "test.php", + "_USER_FOOBAR', post)\")\n" + + "*/\n" + + "function test() {};\n" + + "", + PlatformPatterns.psiElement() + ); + } +} diff --git a/tests/fr/adrienbrault/idea/symfony2plugin/tests/security/fixtures/classes.php b/tests/fr/adrienbrault/idea/symfony2plugin/tests/security/fixtures/classes.php index a5045fd91..f150d10eb 100644 --- a/tests/fr/adrienbrault/idea/symfony2plugin/tests/security/fixtures/classes.php +++ b/tests/fr/adrienbrault/idea/symfony2plugin/tests/security/fixtures/classes.php @@ -6,4 +6,14 @@ interface AuthorizationCheckerInterface { public function isGranted($attributes, $object = null); } +} + +/** + * @Annotation + */ +namespace Sensio\Bundle\FrameworkExtraBundle\Configuration +{ + class Security + { + } } \ No newline at end of file