Skip to content

Instantly share code, notes, and snippets.

@apetro
Last active March 27, 2019 16:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save apetro/cf1f3392ef12a3cc754f4c21d0447e82 to your computer and use it in GitHub Desktop.
Save apetro/cf1f3392ef12a3cc754f4c21d0447e82 to your computer and use it in GitHub Desktop.
HttpHeaderTester PAGS plugin

HttpHeaderTester

EDIT: YOU PROBABLY DON'T NEED THIS. You can probably just use included StringEqualsTester. Cf. uPortal-user@ thread re.

A new HttpHeaderTester PAGS "tester". In PAGS config, allows writing configuration like this:

<test>
  <attribute-name>ismemberof</attribute-name>
  <tester-class>edu.wisc.my.groups.pags.testers.HttpHeaderTester</tester-class>
  <test-value>uw:domain:my.wisc.edu:my_uw_developers</test-value>
</test>

as a successor to currently available options.

(MyUW's uPortal is evolved from uPortal 4.2.1 and so uses PAGSGroupStoreConfig.xml.)

Backwards compatible

Entirely backwards-compatible in that one opts into using the new HttpHeaderTester by starting to declare it in place of the other testers in PAGS configuration. Don't declare this tester, and it won't be used for anything.

Test

Covered by a new unit test which comprehensively demonstrates the features, behaviors of the new tester.

Documentation

JavaDoc'ed. I didn't XML-escape the XML in the JavaDoc, but it's entirely readable in the source, and anyway someone else can come along and properly escape that if generating formal JavaDoc becomes an issue.

Why

Current options include the RegexTester

<test>
  <attribute-name>ismemberof</attribute-name>
  <tester-class>org.jasig.portal.groups.pags.testers.RegexTester</tester-class>
  <test-value>\S*uw:domain\:my\.wisc\.edu\:my_uw_developers\S*</test-value>
</test>

Writing regular expressions is powerful but difficult. This regular expression will match a group like my_uw_developers_hrs. That's probably not the intent, but that is what that regex will do, and we've already tripped over this in production. It's possible to write tighter matching regular expressions, but that requires skill in writing regular expressions, and is just too much cognitive load to ask of the configurer for the trivial task of mapping in a Manifest (UW-Madison localized grouper; it releases group memberships via a multi-valued ismemberof shib heeader) group.

or the StringContainsTester

<test>
  <attribute-name>ismemberof</attribute-name>
  <tester-class>org.jasig.portal.groups.pags.testers.StringContainsTester</tester-class>
  <test-value>uw:domain:my.wisc.edu:my_uw_developers</test-value>
</test>

which makes it easier and cleaner to write a rule that is incorrect in that same not-sufficiently-tightly-matching way.

HttpHeaderTester is easy to configure (well, for a PAGS tester anyway) and automatically gets right this tight matching characteristic, such that

<test>
  <attribute-name>ismemberof</attribute-name>
  <tester-class>edu.wisc.my.groups.pags.testers.HttpHeaderTester</tester-class>
  <test-value>uw:domain:my.wisc.edu:my_uw_developers</test-value>
</test>

will match its intended Manifest group uw:domain:my.wisc.edu:my_uw_developers but will not match uw:domain:my.wisc.edu:my_uw_developers_sis.

That is, HttpHeaderTester makes it easy to implement the matching rule that the configurer almost certainly intends when matching against HTTP headers.

What if the HTTP header is parsed to multiple values upstream

In some alternate universe, technology upstream of this tester would parse the HTTP headers into IPerson attributes, and would translate multi-value HTTP headers to multi-valued IPerson attributes. That's arguably the right thing for that architectural layer to do -- it's an abstraction violation for the PAGS tester tier to be relying upon implementation details of how HTTP headers are encoded or even that an attribute came from an HTTP header at all. By the time it gets to PAGS it should be in the abstraction of person attributes, not of HTTP headers with their semicolon delimiters.

If that enhancement did come along, this tester would work fine unchanged. It supports multiple String values for the IPerson attribute through its superclass StringTester supporting this. HttpHeaderTester splits attribute values on semicolons, and is perfectly happy having no semicolon to split on, as demonstrated by unit test.

License

Apache2.

/**
* Licensed to Apereo under one or more contributor license
* agreements. See the NOTICE file distributed with this work
* for additional information regarding copyright ownership.
* Apereo licenses this file to you 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 the following location:
*
* 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 edu.wisc.my.groups.pags.testers;
import org.jasig.portal.groups.pags.testers.StringTester;
/**
* HttpHeaderTester is a PAGS tester suited for testing whether user attributes derived from HTTP
* headers contain a test value as a header value. HTTP multi-valued headers are
* semicolon-delimited. This tester splits the attribute value on semicolon, trims the resulting
* values, and then checks whether the set of those values contains the test value.
*
* Example configuration:
*
* <test>
* <attribute-name>isMemberOf</attribute-name>
* <tester-class>edu.wisc.my.groups.pags.testers.HttpHeaderTester</tester-class>
* <test-value>uw:domain:my.wisc.edu:myuw_administrators_superusers</test-value>
* </test>
*
* Arguably, this tester should not have to exist. Relying upon HTTP header encoding implementation
* details, or even on that an attribute came from an HTTP header at all, in a PAGS tester is an
* abstraction violation. By the time it gets to the Person Attribute Group Store it should be a
* person attribute and so the architectural tier that translates HTTP headers to person attributes
* should be translating multi-valued HTTP headers to multi-valued IPerson attributes.
*
* If that upstream support were added, this PAGS tester would continue working unchanged. Supports
* String[] IPerson attributes via subclassing StringTester.
*
*/
public class HttpHeaderTester extends StringTester {
public HttpHeaderTester(String attribute, String test) {
super(attribute, test);
}
@Override
public boolean test(String attributeValue) {
if (null == attributeValue) {
return false;
}
String[] splitAttributeValue = attributeValue.split(";");
for (String headerValue : splitAttributeValue) {
if (this.testValue.equals(headerValue.trim())) {
return true;
}
}
return false;
}
}
/**
* Licensed to Apereo under one or more contributor license
* agreements. See the NOTICE file distributed with this work
* for additional information regarding copyright ownership.
* Apereo licenses this file to you 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 the following location:
*
* 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 edu.wisc.my.groups.pags.testers;
import static org.junit.Assert.*;
import org.junit.Test;
public class HttpHeaderTesterTest {
/**
* Test that this PAGS tester returns true when the sought value is somewhere in the
* semicolon-delimited header value.
*/
@Test
public void testMatchAmidstMultipleAttributeValues() {
HttpHeaderTester tester =
new HttpHeaderTester("isMemberOf",
"uw:domain:my.wisc.edu:my_uw_administrators");
// sampled from a real isMemberOf
String isMemberOfValue = "uw:domain:ohr.wisc.edu:trems;uw:domain:ADI_STAR_TimeEntry:star_users;uw:domain:office365.wisc.edu:eligible_to_activate_office_365;uw:domain:my.wisc.edu:audiences:mfa_eligible;uw:domain:my.wisc.edu:audiences:wiscit_client_widget_users;uw:domain:my.wisc.edu:my_uw_developers;uw:domain:office365.wisc.edu:licensed_for_o365;uw:domain:my.wisc.edu:my_uw_administrators;uw:domain:office365.wisc.edu:msol_license_eligible;uw:domain:my.wisc.edu:myuw_administrators_superusers;uw:domain:my.wisc.edu:message_audiences:myuw_access_to_uw-madison_qualtrics;uw:domain:my.wisc.edu:audiences:all_starfish_roles;uw:domain:office365.wisc.edu:license_change_2016;uw:domain:ohr.wisc.edu:myuw pmdp;uw:domain:my.wisc.edu:audiences:authorized_for_qualtrics;uw:domain:tools.advising.wisc.edu:advising_gateway_access;uw:domain:registrar.wisc.edu:emergency_info_admin;uw:domain:apps.mumaa.doit.wisc.edu:lumen_access;uw:domain:my.wisc.edu:my_uw_hr_officers";
assertTrue(tester.test(isMemberOfValue));
// test case where the sought value is first
assertTrue(tester.test("uw:domain:my.wisc.edu:my_uw_administrators;uw:domain:apps.mumaa.doit.wisc.edu:lumen_access;uw:domain:my.wisc.edu:my_uw_hr_officers"));
// test case where the sought value is last
assertTrue(tester.test("uw:domain:apps.mumaa.doit.wisc.edu:lumen_access;uw:domain:my.wisc.edu:my_uw_hr_officers;uw:domain:my.wisc.edu:my_uw_administrators"));
// test case where the value is last and there's an end semicolon delimiter
// (This doesn't seem to happen in practice,
// but the tester should nonetheless get it right if it would happen.)
assertTrue(tester.test("uw:domain:apps.mumaa.doit.wisc.edu:lumen_access;uw:domain:my.wisc.edu:my_uw_hr_officers;uw:domain:my.wisc.edu:my_uw_administrators;"));
}
/**
* Test that header values that are strings that include the sought value (have a prefix or
* suffix) do NOT match. This is one of the trip hazards in the PAGS testers that came before this
* tester that this tester is intended to eliminate.
*/
@Test
public void testStringsIncludingTheSoughtValueDoNotMatch() {
HttpHeaderTester tester =
new HttpHeaderTester("isMemberOf",
"uw:domain:my.wisc.edu:my_uw_administrators");
assertFalse(tester.test("uw:domain:my.wisc.edu:my_uw_administrators_hrs"));
assertFalse(tester.test("uw:domain:my.wisc.edu:my_uw_administrators_sis;uw:domain:my.wisc.edu:my_uw_administrators_hrs"));
}
/**
* Test that when the PAGS tester encounters an attribute value that exactly matches the value
* it is looking for, it returns true.
*/
@Test
public void testSingleMatchingValue() {
HttpHeaderTester tester = new HttpHeaderTester("handedness", "left");
assertTrue(tester.test("left"));
}
/**
* Test that when the PAGS tester encounters an attribute value that exactly matches but for
* untrimmed whitespace, it returns true.
*/
@Test
public void testSingleUntrimmedMatchingValue() {
HttpHeaderTester tester = new HttpHeaderTester("phoneNumber", "203-915-7333");
assertTrue(tester.test(" 203-915-7333 "));
}
/**
* Test that when the PAGS tester encounters an attribute value that does not match the value
* it is looking for, it returns false.
*/
@Test
public void testSingleNotMatchingValue() {
HttpHeaderTester tester = new HttpHeaderTester("preferredName", "Doug");
assertFalse(tester.test("John"));
}
/**
* Test that when the PAGS tester encounters a null attribute value (should never happen), it
* returns false.
*/
@Test
public void testNullEvaluatesFalse() {
HttpHeaderTester tester = new HttpHeaderTester("eyeColor", "gray");
String nullString = null;
assertFalse(tester.test(nullString));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment