Skip to content

Commit c670535

Browse files
authored
Merge pull request #1126 from Haehnchen/feature/892-security-expression
[Security] support voter attributes in is_granted and has_role security annotation #892
2 parents 5528170 + 46cc910 commit c670535

File tree

6 files changed

+313
-25
lines changed

6 files changed

+313
-25
lines changed

META-INF/plugin.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,7 @@
582582
<GotoCompletionRegistrar implementation="fr.adrienbrault.idea.symfony2plugin.translation.TranslationPlaceholderGotoCompletionRegistrar"/>
583583
<GotoCompletionRegistrar implementation="fr.adrienbrault.idea.symfony2plugin.dic.TaggedParameterGotoCompletionRegistrar"/>
584584
<GotoCompletionRegistrar implementation="fr.adrienbrault.idea.symfony2plugin.templating.RenderParameterGotoCompletionRegistrar"/>
585+
<GotoCompletionRegistrar implementation="fr.adrienbrault.idea.symfony2plugin.security.AnnotationExpressionGotoCompletionRegistrar"/>
585586

586587
<TwigNamespaceExtension implementation="fr.adrienbrault.idea.symfony2plugin.templating.path.JsonFileIndexTwigNamespaces"/>
587588
<TwigNamespaceExtension implementation="fr.adrienbrault.idea.symfony2plugin.templating.path.ConfigAddPathTwigNamespaces"/>
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package fr.adrienbrault.idea.symfony2plugin.security;
2+
3+
import com.intellij.codeInsight.completion.CompletionResultSet;
4+
import com.intellij.patterns.PatternCondition;
5+
import com.intellij.patterns.PlatformPatterns;
6+
import com.intellij.psi.PsiElement;
7+
import com.intellij.util.ProcessingContext;
8+
import com.jetbrains.php.lang.documentation.phpdoc.lexer.PhpDocTokenTypes;
9+
import com.jetbrains.php.lang.documentation.phpdoc.parser.PhpDocElementTypes;
10+
import com.jetbrains.php.lang.documentation.phpdoc.psi.tags.PhpDocTag;
11+
import com.jetbrains.php.lang.psi.elements.StringLiteralExpression;
12+
import de.espend.idea.php.annotation.util.AnnotationUtil;
13+
import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionProvider;
14+
import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionProviderLookupArguments;
15+
import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionRegistrar;
16+
import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionRegistrarParameter;
17+
import fr.adrienbrault.idea.symfony2plugin.security.utils.VoterUtil;
18+
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
19+
import org.jetbrains.annotations.NotNull;
20+
21+
import java.util.Collection;
22+
import java.util.Collections;
23+
import java.util.HashSet;
24+
import java.util.regex.Matcher;
25+
import java.util.regex.Pattern;
26+
27+
/**
28+
* @author Daniel Espendiller <daniel@espendiller.net>
29+
*/
30+
public class AnnotationExpressionGotoCompletionRegistrar implements GotoCompletionRegistrar {
31+
32+
private static final String SECURITY_ANNOTATION = "Sensio\\Bundle\\FrameworkExtraBundle\\Configuration\\Security";
33+
34+
@Override
35+
public void register(@NotNull GotoCompletionRegistrarParameter registrar) {
36+
// "@Security("is_granted('POST_SHOW', post) and has_role('ROLE_ADMIN')")"
37+
registrar.register(
38+
PlatformPatterns.psiElement(PhpDocTokenTypes.DOC_STRING)
39+
.withParent(PlatformPatterns.psiElement(StringLiteralExpression.class)
40+
.withParent(PlatformPatterns.psiElement(PhpDocElementTypes.phpDocAttributeList)
41+
.withParent(PlatformPatterns.psiElement(PhpDocTag.class)
42+
.with(PhpDocInstancePatternCondition.INSTANCE)
43+
)
44+
)
45+
),
46+
MyGotoCompletionProvider::new
47+
);
48+
}
49+
50+
/**
51+
* "@Security("has_role('ROLE_FOOBAR')")"
52+
* "@Security("is_granted('POST_SHOW', post) and has_role('ROLE_ADMIN')")"
53+
*/
54+
private static class MyGotoCompletionProvider extends GotoCompletionProvider {
55+
MyGotoCompletionProvider(@NotNull PsiElement psiElement) {
56+
super(psiElement);
57+
}
58+
59+
@Override
60+
public void getLookupElements(@NotNull GotoCompletionProviderLookupArguments arguments) {
61+
final CompletionResultSet resultSet = arguments.getResultSet();
62+
String blockNamePrefix = resultSet.getPrefixMatcher().getPrefix();
63+
64+
// find caret position:
65+
// - "has_role('"
66+
// - "has_role('YAML_ROLE_"
67+
if(!blockNamePrefix.matches("^.*(has_role|is_granted)\\s*\\(\\s*'[\\w-]*$")) {
68+
return;
69+
}
70+
71+
// clear prefix caret string; for a clean completion independent from inside content
72+
CompletionResultSet myResultSet = resultSet.withPrefixMatcher("");
73+
74+
VoterUtil.LookupElementPairConsumer consumer = new VoterUtil.LookupElementPairConsumer();
75+
VoterUtil.visitAttribute(getProject(), consumer);
76+
myResultSet.addAllElements(consumer.getLookupElements());
77+
}
78+
79+
@NotNull
80+
@Override
81+
public Collection<PsiElement> getPsiTargets(PsiElement element) {
82+
if(getElement().getNode().getElementType() != PhpDocTokenTypes.DOC_STRING) {
83+
return Collections.emptyList();
84+
}
85+
86+
PsiElement parent = getElement().getParent();
87+
if(!(parent instanceof StringLiteralExpression)) {
88+
return Collections.emptyList();
89+
}
90+
91+
String contents = ((StringLiteralExpression) parent).getContents();
92+
93+
Collection<String> roles = new HashSet<>();
94+
for (String regex : new String[]{"is_granted\\s*\\(\\s*['|\"]([^'\"]+)['|\"]\\s*[\\)|,]", "has_role\\s*\\(\\s*['|\"]([^'\"]+)['|\"]\\s*\\)"}) {
95+
Matcher matcher = Pattern.compile(regex).matcher(contents);
96+
while(matcher.find()){
97+
roles.add(matcher.group(1));
98+
}
99+
}
100+
101+
if(roles.size() == 0) {
102+
return Collections.emptyList();
103+
}
104+
105+
Collection<PsiElement> targets = new HashSet<>();
106+
107+
VoterUtil.visitAttribute(getProject(), pair -> {
108+
if(roles.contains(pair.getFirst())) {
109+
targets.add(pair.getSecond());
110+
}
111+
});
112+
113+
return targets;
114+
}
115+
}
116+
117+
/**
118+
* Check if given PhpDocTag is instance of given Annotation class
119+
*/
120+
private static class PhpDocInstancePatternCondition extends PatternCondition<PsiElement> {
121+
private static PhpDocInstancePatternCondition INSTANCE = new PhpDocInstancePatternCondition();
122+
123+
PhpDocInstancePatternCondition() {
124+
super("PhpDoc Annotation Instance");
125+
}
126+
127+
@Override
128+
public boolean accepts(@NotNull PsiElement psiElement, ProcessingContext processingContext) {
129+
return psiElement instanceof PhpDocTag
130+
&& PhpElementsUtil.isEqualClassName(AnnotationUtil.getAnnotationReference((PhpDocTag) psiElement), SECURITY_ANNOTATION);
131+
}
132+
}
133+
}

src/fr/adrienbrault/idea/symfony2plugin/security/VoterGotoCompletionRegistrar.java

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,22 @@
11
package fr.adrienbrault.idea.symfony2plugin.security;
22

33
import com.intellij.codeInsight.lookup.LookupElement;
4-
import com.intellij.codeInsight.lookup.LookupElementBuilder;
54
import com.intellij.patterns.PlatformPatterns;
65
import com.intellij.psi.PsiElement;
7-
import com.intellij.psi.util.PsiTreeUtil;
86
import com.jetbrains.php.lang.PhpLanguage;
9-
import com.jetbrains.php.lang.psi.elements.PhpClass;
107
import com.jetbrains.php.lang.psi.elements.StringLiteralExpression;
11-
import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons;
12-
import fr.adrienbrault.idea.symfony2plugin.templating.TwigPattern;
138
import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionProvider;
149
import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionRegistrar;
1510
import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionRegistrarParameter;
1611
import fr.adrienbrault.idea.symfony2plugin.codeInsight.utils.GotoCompletionUtil;
1712
import fr.adrienbrault.idea.symfony2plugin.security.utils.VoterUtil;
13+
import fr.adrienbrault.idea.symfony2plugin.templating.TwigPattern;
1814
import fr.adrienbrault.idea.symfony2plugin.util.MethodMatcher;
1915
import org.apache.commons.lang.StringUtils;
2016
import org.jetbrains.annotations.NotNull;
2117

22-
import java.util.*;
18+
import java.util.Collection;
19+
import java.util.Collections;
2320

2421
/**
2522
* @author Daniel Espendiller <daniel@espendiller.net>
@@ -84,25 +81,9 @@ private static class MyVisitorGotoCompletionProvider extends GotoCompletionProvi
8481
@NotNull
8582
@Override
8683
public Collection<LookupElement> getLookupElements() {
87-
Collection<LookupElement> lookupElements = new ArrayList<>();
88-
89-
Set<String> elements = new HashSet<>();
90-
91-
VoterUtil.visitAttribute(getProject(), pair -> {
92-
String name = pair.getFirst();
93-
if(!elements.contains(name)) {
94-
LookupElementBuilder lookupElement = LookupElementBuilder.create(name).withIcon(Symfony2Icons.SYMFONY);
95-
PhpClass phpClass = PsiTreeUtil.getParentOfType(pair.getSecond(), PhpClass.class);
96-
if(phpClass != null) {
97-
lookupElement = lookupElement.withTypeText(phpClass.getName(), true);
98-
}
99-
100-
lookupElements.add(lookupElement);
101-
elements.add(name);
102-
}
103-
});
104-
105-
return lookupElements;
84+
VoterUtil.LookupElementPairConsumer consumer = new VoterUtil.LookupElementPairConsumer();
85+
VoterUtil.visitAttribute(getProject(), consumer);
86+
return consumer.getLookupElements();
10687
}
10788

10889
@NotNull

src/fr/adrienbrault/idea/symfony2plugin/security/utils/VoterUtil.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package fr.adrienbrault.idea.symfony2plugin.security.utils;
22

3+
import com.intellij.codeInsight.lookup.LookupElement;
4+
import com.intellij.codeInsight.lookup.LookupElementBuilder;
35
import com.intellij.openapi.project.Project;
46
import com.intellij.openapi.util.Pair;
57
import com.intellij.psi.PsiElement;
@@ -13,6 +15,7 @@
1315
import com.jetbrains.php.lang.lexer.PhpTokenTypes;
1416
import com.jetbrains.php.lang.parser.PhpElementTypes;
1517
import com.jetbrains.php.lang.psi.elements.*;
18+
import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons;
1619
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
1720
import fr.adrienbrault.idea.symfony2plugin.util.PsiElementUtils;
1821
import fr.adrienbrault.idea.symfony2plugin.util.yaml.YamlHelper;
@@ -22,6 +25,8 @@
2225
import org.jetbrains.yaml.YAMLUtil;
2326
import org.jetbrains.yaml.psi.*;
2427

28+
import java.util.ArrayList;
29+
import java.util.Collection;
2530
import java.util.HashSet;
2631
import java.util.Set;
2732
import java.util.function.Consumer;
@@ -213,6 +218,36 @@ public Set<PsiElement> getValues() {
213218
}
214219
}
215220

221+
public static class LookupElementPairConsumer implements Consumer<Pair<String, PsiElement>> {
222+
@NotNull
223+
private final Set<String> elements = new HashSet<>();
224+
225+
@NotNull
226+
public Collection<LookupElement> getLookupElements() {
227+
return lookupElements;
228+
}
229+
230+
@Override
231+
public void accept(Pair<String, PsiElement> pair) {
232+
String name = pair.getFirst();
233+
if (!elements.contains(name)) {
234+
LookupElementBuilder lookupElement = LookupElementBuilder.create(name).withIcon(Symfony2Icons.SYMFONY);
235+
236+
PhpClass phpClass = PsiTreeUtil.getParentOfType(pair.getSecond(), PhpClass.class);
237+
if (phpClass != null) {
238+
lookupElement = lookupElement.withTypeText(phpClass.getName(), true);
239+
}
240+
241+
lookupElements.add(lookupElement);
242+
243+
elements.add(name);
244+
}
245+
}
246+
247+
@NotNull
248+
private final Collection<LookupElement> lookupElements = new ArrayList<>();
249+
}
250+
216251
/**
217252
* Find security roles on Voter implementation and security roles in Yaml
218253
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package fr.adrienbrault.idea.symfony2plugin.tests.security;
2+
3+
import com.intellij.patterns.PlatformPatterns;
4+
import fr.adrienbrault.idea.symfony2plugin.tests.SymfonyLightCodeInsightFixtureTestCase;
5+
6+
import java.io.File;
7+
8+
/**
9+
* @author Daniel Espendiller <daniel@espendiller.net>
10+
*
11+
* @see fr.adrienbrault.idea.symfony2plugin.security.AnnotationExpressionGotoCompletionRegistrar
12+
*/
13+
public class AnnotationExpressionGotoCompletionRegistrarTest extends SymfonyLightCodeInsightFixtureTestCase {
14+
public void setUp() throws Exception {
15+
super.setUp();
16+
17+
myFixture.copyFileToProject("security.yml");
18+
myFixture.copyFileToProject("classes.php");
19+
}
20+
21+
protected String getTestDataPath() {
22+
return new File(this.getClass().getResource("fixtures").getFile()).getAbsolutePath();
23+
}
24+
25+
public void testSecurityAnnotationProvidesCompletion() {
26+
assertCompletionContains(
27+
"test.php",
28+
"<?php\n" +
29+
"use Sensio\\Bundle\\FrameworkExtraBundle\\Configuration\\Security;\n" +
30+
"" +
31+
"/**\n" +
32+
"* @Security(\"has_role('<caret>')\")\n" +
33+
"*/\n" +
34+
"function test() {};\n" +
35+
"",
36+
"YAML_ROLE_USER_FOOBAR"
37+
);
38+
39+
assertCompletionContains(
40+
"test.php",
41+
"<?php\n" +
42+
"use Sensio\\Bundle\\FrameworkExtraBundle\\Configuration\\Security;\n" +
43+
"" +
44+
"/**\n" +
45+
"* @Security(\"has_role('YAML_ROLE_<caret>')\")\n" +
46+
"*/\n" +
47+
"function test() {};\n" +
48+
"",
49+
"YAML_ROLE_USER_FOOBAR"
50+
);
51+
52+
assertCompletionContains(
53+
"test.php",
54+
"<?php\n" +
55+
"use Sensio\\Bundle\\FrameworkExtraBundle\\Configuration\\Security;\n" +
56+
"" +
57+
"/**\n" +
58+
"* @Security(\"is_granted('<caret>')\")\n" +
59+
"*/\n" +
60+
"function test() {};\n" +
61+
"",
62+
"YAML_ROLE_USER_FOOBAR"
63+
);
64+
65+
assertCompletionContains(
66+
"test.php",
67+
"<?php\n" +
68+
"use Sensio\\Bundle\\FrameworkExtraBundle\\Configuration\\Security;\n" +
69+
"" +
70+
"/**\n" +
71+
"* @Security(\"is_granted('<caret>', foo)\")\n" +
72+
"*/\n" +
73+
"function test() {};\n" +
74+
"",
75+
"YAML_ROLE_USER_FOOBAR"
76+
);
77+
}
78+
79+
public void testSecurityAnnotationProvidesRoleNavigation() {
80+
assertNavigationMatch(
81+
"test.php",
82+
"<?php\n" +
83+
"use Sensio\\Bundle\\FrameworkExtraBundle\\Configuration\\Security;\n" +
84+
"/**\n" +
85+
"* @Security(\"has_role('YAML_ROLE<caret>_USER_FOOBAR')\")\n" +
86+
"*/\n" +
87+
"function test() {};\n" +
88+
"",
89+
PlatformPatterns.psiElement()
90+
);
91+
92+
assertNavigationMatch(
93+
"test.php",
94+
"<?php\n" +
95+
"use Sensio\\Bundle\\FrameworkExtraBundle\\Configuration\\Security;\n" +
96+
"/**\n" +
97+
"* @Security(\"has_role ( 'YAML_ROLE<caret>_USER_FOOBAR' ) \")\n" +
98+
"*/\n" +
99+
"function test() {};\n" +
100+
"",
101+
PlatformPatterns.psiElement()
102+
);
103+
104+
assertNavigationMatch(
105+
"test.php",
106+
"<?php\n" +
107+
"use Sensio\\Bundle\\FrameworkExtraBundle\\Configuration\\Security;\n" +
108+
"/**\n" +
109+
"* @Security(\"is_granted('YAML_ROLE<caret>_USER_FOOBAR')\")\n" +
110+
"*/\n" +
111+
"function test() {};\n" +
112+
"",
113+
PlatformPatterns.psiElement()
114+
);
115+
116+
assertNavigationMatch(
117+
"test.php",
118+
"<?php\n" +
119+
"use Sensio\\Bundle\\FrameworkExtraBundle\\Configuration\\Security;\n" +
120+
"/**\n" +
121+
"* @Security(\"is_granted('YAML_ROLE<caret>_USER_FOOBAR', post)\")\n" +
122+
"*/\n" +
123+
"function test() {};\n" +
124+
"",
125+
PlatformPatterns.psiElement()
126+
);
127+
}
128+
}

0 commit comments

Comments
 (0)