From 0c0abea3ada05ee4ccea89c8d85ddf7c79685538 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Mon, 26 Mar 2018 10:31:03 -0600 Subject: [PATCH] XsdDocumentedTests groovy->java Groovy has more extensive support for Xml parsing via XmlSlurper. To replace it, this conversion also introduces a SAX wrapper, NicerXmlParser, and a companion Node wrapper, NicerNode, that allowed for less modification of the converted tests. Issue: gh-4939 --- .../security/config/doc/Attribute.groovy | 33 -- .../security/config/doc/Element.groovy | 90 ------ .../config/doc/SpringSecurityXsdParser.groovy | 177 ----------- .../config/doc/XsdDocumentedTests.groovy | 222 ------------- .../security/config/doc/Attribute.java | 66 ++++ .../security/config/doc/Element.java | 169 ++++++++++ .../security/config/doc/NicerNode.java | 73 +++++ .../security/config/doc/NicerXmlParser.java | 51 +++ .../security/config/doc/NicerXmlSupport.java | 48 +++ .../config/doc/SpringSecurityXsdParser.java | 211 +++++++++++++ .../config/doc/XsdDocumentedTests.java | 293 ++++++++++++++++++ .../http/SecurityFiltersAssertions.java | 40 +++ 12 files changed, 951 insertions(+), 522 deletions(-) delete mode 100644 config/src/test/groovy/org/springframework/security/config/doc/Attribute.groovy delete mode 100644 config/src/test/groovy/org/springframework/security/config/doc/Element.groovy delete mode 100644 config/src/test/groovy/org/springframework/security/config/doc/SpringSecurityXsdParser.groovy delete mode 100644 config/src/test/groovy/org/springframework/security/config/doc/XsdDocumentedTests.groovy create mode 100644 config/src/test/java/org/springframework/security/config/doc/Attribute.java create mode 100644 config/src/test/java/org/springframework/security/config/doc/Element.java create mode 100644 config/src/test/java/org/springframework/security/config/doc/NicerNode.java create mode 100644 config/src/test/java/org/springframework/security/config/doc/NicerXmlParser.java create mode 100644 config/src/test/java/org/springframework/security/config/doc/NicerXmlSupport.java create mode 100644 config/src/test/java/org/springframework/security/config/doc/SpringSecurityXsdParser.java create mode 100644 config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java create mode 100644 config/src/test/java/org/springframework/security/config/http/SecurityFiltersAssertions.java diff --git a/config/src/test/groovy/org/springframework/security/config/doc/Attribute.groovy b/config/src/test/groovy/org/springframework/security/config/doc/Attribute.groovy deleted file mode 100644 index a419d45bf7..0000000000 --- a/config/src/test/groovy/org/springframework/security/config/doc/Attribute.groovy +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2002-2011 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package org.springframework.security.config.doc - -/** - * Represents a Spring Security XSD Attribute. It is created when parsing the current xsd to compare to the documented appendix. - * - * @author Rob Winch - * @see SpringSecurityXsdParser - * @see XsdDocumentedSpec - */ -class Attribute { - def name - def desc - def elmt - - def getId() { - return "${elmt.id}-${name}".toString() - } -} diff --git a/config/src/test/groovy/org/springframework/security/config/doc/Element.groovy b/config/src/test/groovy/org/springframework/security/config/doc/Element.groovy deleted file mode 100644 index b7d877f809..0000000000 --- a/config/src/test/groovy/org/springframework/security/config/doc/Element.groovy +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2002-2011 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package org.springframework.security.config.doc - -/** -* Represents a Spring Security XSD Element. It is created when parsing the current xsd to compare to the documented appendix. -* -* @author Rob Winch -* @see SpringSecurityXsdParser -* @see XsdDocumentedSpec -*/ -class Element { - def name - def desc - def attrs - /** - * Contains the elements that extend this element (i.e. any-user-service contains ldap-user-service) - */ - def subGrps = [] - def childElmts = [:] - def parentElmts = [:] - - def getId() { - return "nsa-${name}".toString() - } - - /** - * Gets all the ids related to this Element including attributes, parent elements, and child elements. - * - *

- * The expected ids to be found are documented below. - *

- * @return - */ - def getIds() { - def ids = [id] - childElmts.values()*.ids.each { ids.addAll it } - attrs*.id.each { ids.add it } - if(childElmts) { - ids.add id+'-children' - } - if(attrs) { - ids.add id+'-attributes' - } - if(parentElmts) { - ids.add id+'-parents' - } - ids - } - - def getAllChildElmts() { - def result = [:] - childElmts.values()*.subGrps*.each { elmt -> result.put(elmt.name,elmt) } - result + childElmts - } - - def getAllParentElmts() { - def result = [:] - parentElmts.values()*.subGrps*.each { elmt -> result.put(elmt.name,elmt) } - result + parentElmts - } -} diff --git a/config/src/test/groovy/org/springframework/security/config/doc/SpringSecurityXsdParser.groovy b/config/src/test/groovy/org/springframework/security/config/doc/SpringSecurityXsdParser.groovy deleted file mode 100644 index cb25182675..0000000000 --- a/config/src/test/groovy/org/springframework/security/config/doc/SpringSecurityXsdParser.groovy +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright 2002-2011 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package org.springframework.security.config.doc - -import groovy.xml.Namespace - -/** - * Parses the Spring Security Xsd Document - * - * @author Rob Winch - */ -class SpringSecurityXsdParser { - private def rootElement - - private def xs = new Namespace("http://www.w3.org/2001/XMLSchema", 'xs') - private def attrElmts = [] as Set - private def elementNameToElement = [:] as Map - - /** - * Returns a map of the element name to the {@link Element}. - * @return - */ - Map parse() { - elements(rootElement) - elementNameToElement - } - - /** - * Creates a Map of the name to an Element object of all the children of element. - * - * @param element - * @return - */ - private def elements(element) { - def elementNameToElement = [:] as Map - element.children().each { c-> - if(c.name() == 'element') { - def e = elmt(c) - elementNameToElement.put(e.name,e) - } else { - elementNameToElement.putAll(elements(c)) - } - } - elementNameToElement - } - - /** - * Any children that are attribute will be returned as an Attribute object. - * @param element - * @return a collection of Attribute objects that are children of element. - */ - private def attrs(element) { - def r = [] - element.children().each { c-> - if(c.name() == 'attribute') { - r.add(attr(c)) - }else if(c.name() == 'element') { - }else { - r.addAll(attrs(c)) - } - } - r - } - - /** - * Any children will be searched for an attributeGroup, each of it's children will be returned as an Attribute - * @param element - * @return - */ - private def attrgrps(element) { - def r = [] - element.children().each { c-> - if(c.name() == 'element') { - }else if (c.name() == 'attributeGroup') { - if(c.attributes().get('name')) { - r.addAll(attrgrp(c)) - } else { - def n = c.attributes().get('ref').split(':')[1] - def attrGrp = findNode(element,n) - r.addAll(attrgrp(attrGrp)) - } - } else { - r.addAll(attrgrps(c)) - } - } - r - } - - private def findNode(c,name) { - def root = c - while(root.name() != 'schema') { - root = root.parent() - } - def result = root.breadthFirst().find { child-> name == child.@name?.text() } - assert result?.@name?.text() == name - result - } - - /** - * Processes an individual attributeGroup by obtaining all the attributes and then looking for more attributeGroup elements and prcessing them. - * @param e - * @return all the attributes for a specific attributeGroup and any child attributeGroups - */ - private def attrgrp(e) { - def attrs = attrs(e) - attrs.addAll(attrgrps(e)) - attrs - } - - /** - * Obtains the description for a specific element - * @param element - * @return - */ - private def desc(element) { - return element['annotation']['documentation'] - } - - /** - * Given an element creates an attribute from it. - * @param n - * @return - */ - private def attr(n) { - new Attribute(desc: desc(n), name: n.@name.text()) - } - - /** - * Given an element creates an Element out of it by collecting all its attributes and child elements. - * - * @param n - * @return - */ - private def elmt(n) { - def name = n.@ref.text() - if(name) { - name = name.split(':')[1] - n = findNode(n,name) - } else { - name = n.@name.text() - } - if(elementNameToElement.containsKey(name)) { - return elementNameToElement.get(name) - } - attrElmts.add(name) - def e = new Element() - e.name = n.@name.text() - e.desc = desc(n) - e.childElmts = elements(n) - e.attrs = attrs(n) - e.attrs.addAll(attrgrps(n)) - e.attrs*.elmt = e - e.childElmts.values()*.each { it.parentElmts.put(e.name,e) } - - def subGrpName = n.@substitutionGroup.text() - if(subGrpName) { - def subGrp = elmt(findNode(n,subGrpName.split(":")[1])) - subGrp.subGrps.add(e) - } - - elementNameToElement.put(name,e) - e - } -} diff --git a/config/src/test/groovy/org/springframework/security/config/doc/XsdDocumentedTests.groovy b/config/src/test/groovy/org/springframework/security/config/doc/XsdDocumentedTests.groovy deleted file mode 100644 index eea0757a1c..0000000000 --- a/config/src/test/groovy/org/springframework/security/config/doc/XsdDocumentedTests.groovy +++ /dev/null @@ -1,222 +0,0 @@ -/* - * Copyright 2011-2016 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package org.springframework.security.config.doc - -import groovy.util.slurpersupport.GPathResult -import spock.lang.* - -import org.springframework.security.config.http.SecurityFilters - -/** - * Tests to ensure that the xsd is properly documented. - * - * @author Rob Winch - */ -class XsdDocumentedTests extends Specification { - - def ignoredIds = [ - 'nsa-any-user-service', - 'nsa-any-user-service-parents', - 'nsa-authentication', - 'nsa-websocket-security', - 'nsa-ldap', - 'nsa-method-security', - 'nsa-web' - ] - @Shared def reference = new File('../docs/manual/src/docs/asciidoc/_includes/appendix/namespace.adoc') - - @Shared File schema31xDocument = new File('src/main/resources/org/springframework/security/config/spring-security-3.1.xsd') - @Shared File schemaDocument = new File('src/main/resources/org/springframework/security/config/spring-security-5.0.xsd') - @Shared Map elementNameToElement - @Shared GPathResult schemaRootElement - - def setupSpec() { - schemaRootElement = new XmlSlurper().parse(schemaDocument) - elementNameToElement = new SpringSecurityXsdParser(rootElement: schemaRootElement).parse() - } - - def cleanupSpec() { - reference = null - schema31xDocument = null - schemaDocument = null - elementNameToElement = null - schemaRootElement = null - } - - def 'SEC-2139: named-security-filter are all defined and ordered properly'() { - setup: - def expectedFilters = (EnumSet.allOf(SecurityFilters) as List).sort { it.order } - when: - def nsf = schemaRootElement.simpleType.find { it.@name == 'named-security-filter' } - def nsfValues = nsf.children().children().collect { c -> - Enum.valueOf(SecurityFilters, c.@value.toString()) - } - then: - expectedFilters == nsfValues - } - - def 'SEC-2139: 3.1.x named-security-filter are all defined and ordered properly'() { - setup: - def expectedFilters = [ - "FIRST", - "CHANNEL_FILTER", - "SECURITY_CONTEXT_FILTER", - "CONCURRENT_SESSION_FILTER", - "LOGOUT_FILTER", - "X509_FILTER", - "PRE_AUTH_FILTER", - "CAS_FILTER", - "FORM_LOGIN_FILTER", - "OPENID_FILTER", - "LOGIN_PAGE_FILTER", - "DIGEST_AUTH_FILTER", - "BASIC_AUTH_FILTER", - "REQUEST_CACHE_FILTER", - "SERVLET_API_SUPPORT_FILTER", - "JAAS_API_SUPPORT_FILTER", - "REMEMBER_ME_FILTER", - "ANONYMOUS_FILTER", - "SESSION_MANAGEMENT_FILTER", - "EXCEPTION_TRANSLATION_FILTER", - "FILTER_SECURITY_INTERCEPTOR", - "SWITCH_USER_FILTER", - "LAST" - ].collect { - Enum.valueOf(SecurityFilters, it) - } - def schema31xRootElement = new XmlSlurper().parse(schema31xDocument) - when: - def nsf = schema31xRootElement.simpleType.find { it.@name == 'named-security-filter' } - def nsfValues = nsf.children().children().collect { c -> - Enum.valueOf(SecurityFilters, c.@value.toString()) - } - then: - expectedFilters == nsfValues - } - - /** - * This will check to ensure that the expected number of xsd documents are found to ensure that we are validating - * against the current xsd document. If this test fails, all that is needed is to update the schemaDocument - * and the expected size for this test. - * @return - */ - def 'the latest schema is being validated'() { - when: 'all the schemas are found' - def schemas = schemaDocument.getParentFile().list().findAll { it.endsWith('.xsd') } - then: 'the count is equal to 12, if not then schemaDocument needs updated' - schemas.size() == 12 - } - - /** - * This uses a naming convention for the ids of the appendix to ensure that the entire appendix is documented. - * The naming convention for the ids is documented in {@link Element#getIds()}. - * @return - */ - def 'the entire schema is included in the appendix documentation'() { - setup: 'get all the documented ids and the expected ids' - def documentedIds = [] - reference.eachLine { line -> - if(line.matches("\\[\\[(nsa-.*)\\]\\]")) { - documentedIds.add(line.substring(2,line.length() - 2)) - } - } - when: 'the schema is compared to the appendix documentation' - def expectedIds = [] as Set - elementNameToElement*.value*.ids*.each { expectedIds.addAll it } - documentedIds.removeAll ignoredIds - expectedIds.removeAll ignoredIds - def undocumentedIds = (expectedIds - documentedIds) - def shouldNotBeDocumented = (documentedIds - expectedIds) - then: 'all the elements and attributes are documented' - shouldNotBeDocumented.empty - undocumentedIds.empty - } - - /** - * This test ensures that any element that has children or parents contains a section that has links pointing to that - * documentation. - * @return - */ - def 'validate parents and children are linked in the appendix documentation'() { - when: "get all the links for each element's children and parents" - def docAttrNameToChildren = [:] - def docAttrNameToParents = [:] - - def currentDocAttrNameToElmt - def docAttrName - - reference.eachLine { line -> - if(line.matches('^\\[\\[.*\\]\\]$')) { - def id = line.substring(2,line.length() - 2) - if(id.endsWith("-children")) { - docAttrName = id.substring(0,id.length() - 9) - currentDocAttrNameToElmt = docAttrNameToChildren - } else if(id.endsWith("-parents")) { - docAttrName = id.substring(0,id.length() - 8) - currentDocAttrNameToElmt = docAttrNameToParents - } else if(docAttrName && !id.startsWith(docAttrName)) { - currentDocAttrNameToElmt = null - docAttrName = null - } - } - - if(docAttrName) { - def expression = '^\\* <<(nsa-.*),.*>>$' - if(line.matches(expression)) { - String elmtId = line.replaceAll(expression, '$1') - currentDocAttrNameToElmt.get(docAttrName, []).add(elmtId) - } - } - } - - def schemaAttrNameToParents = [:] - def schemaAttrNameToChildren = [:] - elementNameToElement.each { entry -> - def key = 'nsa-'+entry.key - if(ignoredIds.contains(key)) { - return - } - def parentIds = entry.value.allParentElmts.values()*.id.findAll { !ignoredIds.contains(it) }.sort() - if(parentIds) { - schemaAttrNameToParents.put(key,parentIds) - } - def childIds = entry.value.allChildElmts.values()*.id.findAll { !ignoredIds.contains(it) }.sort() - if(childIds) { - schemaAttrNameToChildren.put(key,childIds) - } - } - then: "the expected parents and children are all documented" - schemaAttrNameToChildren.sort() == docAttrNameToChildren.sort() - schemaAttrNameToParents.sort() == docAttrNameToParents.sort() - } - - /** - * This test checks each xsd element and ensures there is documentation for it. - * @return - */ - def 'entire xsd is documented'() { - when: "validate that the entire xsd contains documentation" - def notDocElmtIds = elementNameToElement.values().findAll { - !it.desc.text() && !ignoredIds.contains(it.id) - }*.id.sort().join("\n") - def notDocAttrIds = elementNameToElement.values()*.attrs.flatten().findAll { - !it.desc.text() && !ignoredIds.contains(it.id) - }*.id.sort().join("\n") - then: "all the elements and attributes have some documentation" - !notDocElmtIds - !notDocAttrIds - } -} diff --git a/config/src/test/java/org/springframework/security/config/doc/Attribute.java b/config/src/test/java/org/springframework/security/config/doc/Attribute.java new file mode 100644 index 0000000000..57b1b90deb --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/doc/Attribute.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.config.doc; + +/** + * Represents a Spring Security XSD Attribute. It is created when parsing the current xsd to compare to the documented appendix. + * + * @author Rob Winch + * @author Josh Cummings + * + * @see SpringSecurityXsdParser + * @see XsdDocumentedTests + */ +public class Attribute { + private String name; + + private String desc; + + private Element elmt; + + public Attribute(String desc, String name) { + this.desc = desc; + this.name = name; + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDesc() { + return this.desc; + } + + public void setDesc(String desc) { + this.desc = desc; + } + + public Element getElmt() { + return this.elmt; + } + + public void setElmt(Element elmt) { + this.elmt = elmt; + } + + public String getId() { + return String.format("%s-%s", this.elmt.getId(), this.name); + } +} diff --git a/config/src/test/java/org/springframework/security/config/doc/Element.java b/config/src/test/java/org/springframework/security/config/doc/Element.java new file mode 100644 index 0000000000..fcb2fc2adf --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/doc/Element.java @@ -0,0 +1,169 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.config.doc; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +/** + * Represents a Spring Security XSD Element. It is created when parsing + * the current xsd to compare to the documented appendix. + * + * @author Rob Winch + * @author Josh Cummings + * + * @see SpringSecurityXsdParser + * @see XsdDocumentedTests +*/ +public class Element { + private String name; + private String desc; + private Collection attrs = new ArrayList<>(); + + /** + * Contains the elements that extend this element (i.e. any-user-service contains ldap-user-service) + */ + private Collection subGrps = new ArrayList<>(); + private Map childElmts = new HashMap<>(); + private Map parentElmts = new HashMap<>(); + + public String getId() { + return String.format("nsa-%s", this.name); + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDesc() { + return this.desc; + } + + public void setDesc(String desc) { + this.desc = desc; + } + + public Collection getAttrs() { + return this.attrs; + } + + public void setAttrs(Collection attrs) { + this.attrs = attrs; + } + + public Collection getSubGrps() { + return this.subGrps; + } + + public void setSubGrps(Collection subGrps) { + this.subGrps = subGrps; + } + + public Map getChildElmts() { + return this.childElmts; + } + + public void setChildElmts(Map childElmts) { + this.childElmts = childElmts; + } + + public Map getParentElmts() { + return this.parentElmts; + } + + public void setParentElmts(Map parentElmts) { + this.parentElmts = parentElmts; + } + + /** + * Gets all the ids related to this Element including attributes, parent elements, and child elements. + * + *

+ * The expected ids to be found are documented below. + *

    + *
  • Elements - any xml element will have the nsa-<element>. For example the http element will have the id + * nsa-http
  • + *
  • Parent Section - Any element with a parent other than beans will have a section named + * nsa-<element>-parents. For example, authentication-provider would have a section id of + * nsa-authentication-provider-parents. The section would then contain a list of links pointing to the + * documentation for each parent element.
  • + *
  • Attributes Section - Any element with attributes will have a section with the id + * nsa-<element>-attributes. For example the http element would require a section with the id + * http-attributes.
  • + *
  • Attribute - Each attribute of an element would have an id of nsa-<element>-<attributeName>. For + * example the attribute create-session for the http attribute would have the id http-create-session.
  • + *
  • Child Section - Any element with a child element will have a section named nsa-<element>-children. + * For example, authentication-provider would have a section id of nsa-authentication-provider-children. The + * section would then contain a list of links pointing to the documentation for each child element.
  • + *
+ * @return + */ + public Collection getIds() { + Collection ids = new ArrayList<>(); + ids.add(getId()); + + this.childElmts.values() + .forEach(elmt -> ids.add(elmt.getId())); + + this.attrs.forEach(attr -> ids.add(attr.getId())); + + if ( !this.childElmts.isEmpty() ) { + ids.add(getId() + "-children"); + } + + if ( !this.attrs.isEmpty() ) { + ids.add(getId() + "-attributes"); + } + + if ( !this.parentElmts.isEmpty() ) { + ids.add(getId() + "-parents"); + } + + return ids; + } + + public Map getAllChildElmts() { + Map result = new HashMap<>(); + + this.childElmts.values() + .forEach(elmt -> + elmt.subGrps.forEach( + subElmt -> result.put(subElmt.name, subElmt))); + + result.putAll(this.childElmts); + + return result; + } + + public Map getAllParentElmts() { + Map result = new HashMap<>(); + + this.parentElmts.values() + .forEach(elmt -> + elmt.subGrps.forEach( + subElmt -> result.put(subElmt.name, subElmt))); + + result.putAll(this.parentElmts); + + return result; + } +} diff --git a/config/src/test/java/org/springframework/security/config/doc/NicerNode.java b/config/src/test/java/org/springframework/security/config/doc/NicerNode.java new file mode 100644 index 0000000000..da6883e95d --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/doc/NicerNode.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.config.doc; + +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.util.Optional; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +/** + * @author Josh Cummings + */ +public class NicerNode { + private final Node node; + + public NicerNode(Node node) { + this.node = node; + } + + public String simpleName() { + String[] parts = this.node.getNodeName().split(":"); + return parts[parts.length-1]; + } + + public String text() { + return this.node.getTextContent(); + } + + public Stream children() { + NodeList children = this.node.getChildNodes(); + + return IntStream.range(0, children.getLength()) + .mapToObj(children::item) + .map(NicerNode::new); + } + + public Optional child(String name) { + return this.children() + .filter(child -> name.equals(child.simpleName())) + .findFirst(); + } + + public Optional parent() { + return Optional.ofNullable(this.node.getParentNode()) + .map(parent -> new NicerNode(parent)); + } + + public String attribute(String name) { + return Optional.ofNullable(this.node.getAttributes()) + .map(attrs -> attrs.getNamedItem(name)) + .map(attr -> attr.getTextContent()) + .orElse(null); + } + + public Node node() { + return this.node; + } +} diff --git a/config/src/test/java/org/springframework/security/config/doc/NicerXmlParser.java b/config/src/test/java/org/springframework/security/config/doc/NicerXmlParser.java new file mode 100644 index 0000000000..eece67afc9 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/doc/NicerXmlParser.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.config.doc; + +import org.xml.sax.SAXException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.io.IOException; +import java.io.InputStream; + +/** + * @author Josh Cummings + */ +public class NicerXmlParser implements AutoCloseable { + private InputStream xml; + + public NicerXmlParser(InputStream xml) { + this.xml = xml; + } + + public NicerNode parse() { + try { + DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder dBuilder = dbFactory.newDocumentBuilder(); + + return new NicerNode(dBuilder.parse(this.xml)); + } catch ( IOException | ParserConfigurationException | SAXException e ) { + throw new IllegalStateException(e); + } + } + + @Override + public void close() throws IOException { + this.xml.close(); + } +} diff --git a/config/src/test/java/org/springframework/security/config/doc/NicerXmlSupport.java b/config/src/test/java/org/springframework/security/config/doc/NicerXmlSupport.java new file mode 100644 index 0000000000..af05fe188c --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/doc/NicerXmlSupport.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.config.doc; + +import org.springframework.core.io.ClassPathResource; + +import java.io.IOException; +import java.util.Map; + +/** + * Support for ensuring preparing the givens in {@link XsdDocumentedTests} + * + * @author Josh Cummings + */ +public class NicerXmlSupport { + private NicerXmlParser parser; + + public NicerNode parse(String location) throws IOException { + ClassPathResource resource = new ClassPathResource(location); + this.parser = new NicerXmlParser(resource.getInputStream()); + + return this.parser.parse(); + } + + public Map elementsByElementName(String location) throws IOException { + NicerNode node = parse(location); + return new SpringSecurityXsdParser(node).parse(); + } + + public void close() throws IOException { + if ( this.parser != null ) { + this.parser.close(); + } + } +} diff --git a/config/src/test/java/org/springframework/security/config/doc/SpringSecurityXsdParser.java b/config/src/test/java/org/springframework/security/config/doc/SpringSecurityXsdParser.java new file mode 100644 index 0000000000..0e3dd667de --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/doc/SpringSecurityXsdParser.java @@ -0,0 +1,211 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.config.doc; + +import org.springframework.util.StringUtils; + +import java.util.*; +import java.util.stream.Stream; + +/** + * Parses the Spring Security Xsd Document + * + * @author Rob Winch + * @author Josh Cummings + */ +public class SpringSecurityXsdParser { + private NicerNode rootElement; + + private Set attrElmts = new LinkedHashSet<>(); + private Map elementNameToElement = new HashMap<>(); + + public SpringSecurityXsdParser(NicerNode rootElement) { + this.rootElement = rootElement; + } + + /** + * Returns a map of the element name to the {@link Element}. + * + * @return + */ + public Map parse() { + elements(this.rootElement); + return this.elementNameToElement; + } + + /** + * Creates a Map of the name to an Element object of all the children of element. + * + * @param node + * @return + */ + private Map elements(NicerNode node) { + Map elementNameToElement = new HashMap<>(); + + node.children().forEach(child -> { + if ("element".equals(child.simpleName())) { + Element e = elmt(child); + elementNameToElement.put(e.getName(), e); + } else { + elementNameToElement.putAll(elements(child)); + } + }); + + return elementNameToElement; + } + + /** + * Any children that are attribute will be returned as an Attribute object. + * + * @param element + * @return a collection of Attribute objects that are children of element. + */ + private Collection attrs(NicerNode element) { + Collection attrs = new ArrayList<>(); + element.children().forEach(c -> { + String name = c.simpleName(); + if ("attribute".equals(name)) { + attrs.add(attr(c)); + } else if ("element".equals(name)) { + } else { + attrs.addAll(attrs(c)); + } + }); + + return attrs; + } + + /** + * Any children will be searched for an attributeGroup, each of its children will be returned as an Attribute + * + * @param element + * @return + */ + private Collection attrgrps(NicerNode element) { + Collection attrgrp = new ArrayList<>(); + + element.children().forEach(c -> { + if ("element".equals(c.simpleName())) { + + } else if ("attributeGroup".equals(c.simpleName())) { + if (c.attribute("name") != null) { + attrgrp.addAll(attrgrp(c)); + } else { + String name = c.attribute("ref").split(":")[1]; + NicerNode attrGrp = findNode(element, name); + attrgrp.addAll(attrgrp(attrGrp)); + } + } else { + attrgrp.addAll(attrgrps(c)); + } + }); + + return attrgrp; + } + + private NicerNode findNode(NicerNode c, String name) { + NicerNode root = c; + while (!"schema".equals(root.simpleName())) { + root = root.parent().get(); + } + + return expand(root) + .filter(node -> name.equals(node.attribute("name"))) + .findFirst().orElseThrow(IllegalArgumentException::new); + } + + private Stream expand(NicerNode root) { + return Stream.concat( + Stream.of(root), + root.children().flatMap(this::expand)); + } + + /** + * Processes an individual attributeGroup by obtaining all the attributes and then looking for more attributeGroup elements and prcessing them. + * + * @param e + * @return all the attributes for a specific attributeGroup and any child attributeGroups + */ + private Collection attrgrp(NicerNode e) { + Collection attrs = attrs(e); + attrs.addAll(attrgrps(e)); + return attrs; + } + + /** + * Obtains the description for a specific element + * + * @param element + * @return + */ + private String desc(NicerNode element) { + return element.child("annotation") + .flatMap(annotation -> annotation.child("documentation")) + .map(documentation -> documentation.text()) + .orElse(null); + } + + /** + * Given an element creates an attribute from it. + * + * @param n + * @return + */ + private Attribute attr(NicerNode n) { + return new Attribute(desc(n), n.attribute("name")); + } + + /** + * Given an element creates an Element out of it by collecting all its attributes and child elements. + * + * @param n + * @return + */ + private Element elmt(NicerNode n) { + String name = n.attribute("ref"); + if (StringUtils.isEmpty(name)) { + name = n.attribute("name"); + } else { + name = name.split(":")[1]; + n = findNode(n, name); + } + + if (this.elementNameToElement.containsKey(name)) { + return this.elementNameToElement.get(name); + } + this.attrElmts.add(name); + + Element e = new Element(); + e.setName(n.attribute("name")); + e.setDesc(desc(n)); + e.setChildElmts(elements(n)); + e.setAttrs(attrs(n)); + e.getAttrs().addAll(attrgrps(n)); + e.getAttrs().forEach(attr -> attr.setElmt(e)); + e.getChildElmts().values().forEach(element -> + element.getParentElmts().put(e.getName(), e)); + + String subGrpName = n.attribute("substitutionGroup"); + if (!StringUtils.isEmpty(subGrpName)) { + Element subGrp = elmt(findNode(n, subGrpName.split(":")[1])); + subGrp.getSubGrps().add(e); + } + + this.elementNameToElement.put(name, e); + + return e; + } +} diff --git a/config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java b/config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java new file mode 100644 index 0000000000..8f711c94f8 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java @@ -0,0 +1,293 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.config.doc; + +import org.apache.commons.lang.StringUtils; +import org.junit.After; +import org.junit.Test; +import org.springframework.core.io.ClassPathResource; +import org.springframework.security.config.http.SecurityFiltersAssertions; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests to ensure that the xsd is properly documented. + * + * @author Rob Winch + * @author Josh Cummings + */ +public class XsdDocumentedTests { + + Collection ignoredIds = Arrays.asList( + "nsa-any-user-service", + "nsa-any-user-service-parents", + "nsa-authentication", + "nsa-websocket-security", + "nsa-ldap", + "nsa-method-security", + "nsa-web"); + + String referenceLocation = "../docs/manual/src/docs/asciidoc/_includes/appendix/namespace.adoc"; + + String schema31xDocumentLocation = "org/springframework/security/config/spring-security-3.1.xsd"; + String schemaDocumentLocation = "org/springframework/security/config/spring-security-5.0.xsd"; + + NicerXmlSupport xml = new NicerXmlSupport(); + + @After + public void close() throws IOException { + this.xml.close(); + } + + @Test + public void parseWhenLatestXsdThenAllNamedSecurityFiltersAreDefinedAndOrderedProperly() + throws IOException { + NicerNode root = this.xml.parse(this.schemaDocumentLocation); + + List nodes = + root.child("schema") + .map(NicerNode::children) + .orElse(Stream.empty()) + .filter(node -> + "simpleType".equals(node.simpleName()) && + "named-security-filter".equals(node.attribute("name"))) + .flatMap(NicerNode::children) + .flatMap(NicerNode::children) + .map(node -> node.attribute("value")) + .filter(StringUtils::isNotEmpty) + .collect(Collectors.toList()); + + SecurityFiltersAssertions.assertEquals(nodes); + } + + @Test + public void parseWhen31XsdThenAllNamedSecurityFiltersAreDefinedAndOrderedProperly() + throws IOException { + + List expected = Arrays.asList( + "FIRST", + "CHANNEL_FILTER", + "SECURITY_CONTEXT_FILTER", + "CONCURRENT_SESSION_FILTER", + "LOGOUT_FILTER", + "X509_FILTER", + "PRE_AUTH_FILTER", + "CAS_FILTER", + "FORM_LOGIN_FILTER", + "OPENID_FILTER", + "LOGIN_PAGE_FILTER", + "DIGEST_AUTH_FILTER", + "BASIC_AUTH_FILTER", + "REQUEST_CACHE_FILTER", + "SERVLET_API_SUPPORT_FILTER", + "JAAS_API_SUPPORT_FILTER", + "REMEMBER_ME_FILTER", + "ANONYMOUS_FILTER", + "SESSION_MANAGEMENT_FILTER", + "EXCEPTION_TRANSLATION_FILTER", + "FILTER_SECURITY_INTERCEPTOR", + "SWITCH_USER_FILTER", + "LAST" + ); + + NicerNode root = this.xml.parse(this.schema31xDocumentLocation); + + List nodes = + root.child("schema") + .map(NicerNode::children) + .orElse(Stream.empty()) + .filter(node -> + "simpleType".equals(node.simpleName()) && + "named-security-filter".equals(node.attribute("name"))) + .flatMap(NicerNode::children) + .flatMap(NicerNode::children) + .map(node -> node.attribute("value")) + .filter(StringUtils::isNotEmpty) + .collect(Collectors.toList()); + + assertThat(nodes).isEqualTo(expected); + } + + /** + * This will check to ensure that the expected number of xsd documents are found to ensure that we are validating + * against the current xsd document. If this test fails, all that is needed is to update the schemaDocument + * and the expected size for this test. + * @return + */ + @Test + public void sizeWhenReadingFilesystemThenIsCorrectNumberOfSchemaFiles() + throws IOException { + + ClassPathResource resource = new ClassPathResource(this.schemaDocumentLocation); + + String[] schemas = resource.getFile().getParentFile().list((dir, name) -> name.endsWith(".xsd")); + + assertThat(schemas.length).isEqualTo(12) + .withFailMessage("the count is equal to 12, if not then schemaDocument needs updating"); + } + + /** + * This uses a naming convention for the ids of the appendix to ensure that the entire appendix is documented. + * The naming convention for the ids is documented in {@link Element#getIds()}. + * @return + */ + @Test + public void countReferencesWhenReviewingDocumentationThenEntireSchemaIsIncluded() + throws IOException { + + Map elementsByElementName = + this.xml.elementsByElementName(this.schemaDocumentLocation); + + List documentIds = + Files.lines(Paths.get(this.referenceLocation)) + .filter(line -> line.matches("\\[\\[(nsa-.*)\\]\\]")) + .map(line -> line.substring(2, line.length() - 2)) + .collect(Collectors.toList()); + + Set expectedIds = + elementsByElementName.values().stream() + .flatMap(element -> element.getIds().stream()) + .collect(Collectors.toSet()); + + documentIds.removeAll(this.ignoredIds); + expectedIds.removeAll(this.ignoredIds); + + assertThat(documentIds).containsAll(expectedIds); + assertThat(expectedIds).containsAll(documentIds); + } + + /** + * This test ensures that any element that has children or parents contains a section that has links pointing to that + * documentation. + * @return + */ + @Test + public void countLinksWhenReviewingDocumentationThenParentsAndChildrenAreCorrectlyLinked() + throws IOException { + + Map> docAttrNameToChildren = new HashMap<>(); + Map> docAttrNameToParents = new HashMap<>(); + + String docAttrName = null; + Map> currentDocAttrNameToElmt = null; + + List lines = Files.readAllLines(Paths.get(this.referenceLocation)); + for ( String line : lines ) { + if(line.matches("^\\[\\[.*\\]\\]$")) { + String id = line.substring(2, line.length() - 2); + + if(id.endsWith("-children")) { + docAttrName = id.substring(0, id.length() - 9); + currentDocAttrNameToElmt = docAttrNameToChildren; + } else if(id.endsWith("-parents")) { + docAttrName = id.substring(0, id.length() - 8); + currentDocAttrNameToElmt = docAttrNameToParents; + } else if(docAttrName != null && !id.startsWith(docAttrName)) { + currentDocAttrNameToElmt = null; + docAttrName = null; + } + } + + if(docAttrName != null && currentDocAttrNameToElmt != null) { + String expression = "^\\* <<(nsa-.*),.*>>$"; + if(line.matches(expression)) { + String elmtId = line.replaceAll(expression, "$1"); + currentDocAttrNameToElmt + .computeIfAbsent(docAttrName, key -> new ArrayList<>()) + .add(elmtId); + } + } + } + + Map elementNameToElement = this.xml.elementsByElementName(this.schemaDocumentLocation); + + Map> schemaAttrNameToChildren = new HashMap<>(); + Map> schemaAttrNameToParents = new HashMap<>(); + + elementNameToElement.entrySet().stream() + .forEach(entry -> { + String key = "nsa-" + entry.getKey(); + if (this.ignoredIds.contains(key) ) { + return; + } + + List parentIds = + entry.getValue().getAllParentElmts().values().stream() + .filter(element -> !this.ignoredIds.contains(element.getId())) + .map(element -> element.getId()) + .sorted() + .collect(Collectors.toList()); + if ( !parentIds.isEmpty() ) { + schemaAttrNameToParents.put(key, parentIds); + } + + List childIds = + entry.getValue().getAllChildElmts().values().stream() + .filter(element -> !this.ignoredIds.contains(element.getId())) + .map(element -> element.getId()) + .sorted() + .collect(Collectors.toList()); + if ( !childIds.isEmpty() ) { + schemaAttrNameToChildren.put(key, childIds); + } + }); + + assertThat(docAttrNameToChildren).isEqualTo(schemaAttrNameToChildren); + assertThat(docAttrNameToParents).isEqualTo(schemaAttrNameToParents); + } + + + /** + * This test checks each xsd element and ensures there is documentation for it. + * @return + */ + @Test + public void countWhenReviewingDocumentationThenAllElementsDocumented() + throws IOException { + + Map elementNameToElement = + this.xml.elementsByElementName(this.schemaDocumentLocation); + + String notDocElmtIds = + elementNameToElement.values().stream() + .filter(element -> + StringUtils.isEmpty(element.getDesc()) && + !this.ignoredIds.contains(element.getId())) + .map(element -> element.getId()) + .sorted() + .collect(Collectors.joining("\n")); + + String notDocAttrIds = + elementNameToElement.values().stream() + .flatMap(element -> element.getAttrs().stream()) + .filter(element -> + StringUtils.isEmpty(element.getDesc()) && + !this.ignoredIds.contains(element.getId())) + .map(element -> element.getId()) + .sorted() + .collect(Collectors.joining("\n")); + + assertThat(notDocElmtIds).isEmpty(); + assertThat(notDocAttrIds).isEmpty(); + } +} diff --git a/config/src/test/java/org/springframework/security/config/http/SecurityFiltersAssertions.java b/config/src/test/java/org/springframework/security/config/http/SecurityFiltersAssertions.java new file mode 100644 index 0000000000..1d0effd36a --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/http/SecurityFiltersAssertions.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.config.http; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Assertions for tests that rely on confirming behavior of the package-private SecurityFilters enum + * + * @author Josh Cummings + */ +public class SecurityFiltersAssertions { + private static Collection ordered = Arrays.asList(SecurityFilters.values()); + + public static void assertEquals(List filters) { + List expected = ordered.stream() + .map(SecurityFilters::name) + .collect(Collectors.toList()); + + assertThat(filters).isEqualTo(expected); + } +}