View Javadoc

1   /*
2    * Copyright [2007] [University Corporation for Advanced Internet Development, Inc.]
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package edu.internet2.middleware.shibboleth.common.attribute;
18  
19  import jargs.gnu.CmdLineParser;
20  
21  import java.io.File;
22  import java.io.FileInputStream;
23  import java.io.IOException;
24  import java.io.PrintStream;
25  import java.util.ArrayList;
26  import java.util.List;
27  import java.util.Map;
28  
29  import org.opensaml.Configuration;
30  import org.opensaml.common.SAMLObject;
31  import org.opensaml.saml2.metadata.provider.MetadataProvider;
32  import org.opensaml.saml2.metadata.provider.MetadataProviderException;
33  import org.opensaml.util.resource.FilesystemResource;
34  import org.opensaml.util.resource.Resource;
35  import org.opensaml.util.resource.ResourceException;
36  import org.opensaml.xml.io.Marshaller;
37  import org.opensaml.xml.io.MarshallingException;
38  import org.opensaml.xml.util.XMLHelper;
39  import org.slf4j.Logger;
40  import org.slf4j.LoggerFactory;
41  import org.springframework.context.ApplicationContext;
42  import org.springframework.context.support.GenericApplicationContext;
43  import org.w3c.dom.Element;
44  
45  import ch.qos.logback.classic.LoggerContext;
46  import ch.qos.logback.classic.joran.JoranConfigurator;
47  import ch.qos.logback.core.joran.spi.JoranException;
48  import ch.qos.logback.core.status.ErrorStatus;
49  import ch.qos.logback.core.status.InfoStatus;
50  import ch.qos.logback.core.status.StatusManager;
51  
52  import edu.internet2.middleware.shibboleth.common.attribute.provider.SAML1AttributeAuthority;
53  import edu.internet2.middleware.shibboleth.common.attribute.provider.SAML2AttributeAuthority;
54  import edu.internet2.middleware.shibboleth.common.config.SpringConfigurationUtils;
55  import edu.internet2.middleware.shibboleth.common.profile.provider.BaseSAMLProfileRequestContext;
56  import edu.internet2.middleware.shibboleth.common.relyingparty.RelyingPartyConfiguration;
57  import edu.internet2.middleware.shibboleth.common.relyingparty.provider.SAMLMDRelyingPartyConfigurationManager;
58  
59  /**
60   * A command line tool that allows individuals to invoke an attribute authority and inspect the resultant attribute
61   * statement.
62   * 
63   * This tool expects to retrieve the {@link MetadataProvider} it uses under the bean name SAMLMetadataProvider, a
64   * {@link SAML1AttributeAuthority} under the bean name SAML1AttributeAuthority, and a {@link SAML2AttributeAuthority}
65   * under the bean name SAML2AttributeAuthority.
66   */
67  public class AttributeAuthorityCLI {
68  
69      /** Class logger. */
70      private static Logger log = LoggerFactory.getLogger(AttributeAuthorityCLI.class);
71  
72      /** List of configuration files used with the AACLI. */
73      private static String[] aacliConfigs = { "/internal.xml", "/service.xml", };
74  
75      /** Loaded SAML 1 Attribute Authority. */
76      private static SAML1AttributeAuthority saml1AA;
77  
78      /** Loaded SAML 2 Attribute Authority. */
79      private static SAML2AttributeAuthority saml2AA;
80  
81      /**
82       * Runs this application. Help message prints if no arguments are given or if the "help" argument is given.
83       * 
84       * @param args command line arguments
85       * 
86       * @throws Exception thrown if there is a problem during program execution
87       */
88      public static void main(String[] args) throws Exception {
89          CmdLineParser parser = parseCommandArguments(args);
90          ApplicationContext appCtx = loadConfigurations((String) parser.getOptionValue(CLIParserBuilder.CONFIG_DIR_ARG));
91  
92          saml1AA = (SAML1AttributeAuthority) appCtx.getBean("shibboleth.SAML1AttributeAuthority");
93          saml2AA = (SAML2AttributeAuthority) appCtx.getBean("shibboleth.SAML2AttributeAuthority");
94  
95          SAMLObject attributeStatement;
96          Boolean saml1 = (Boolean) parser.getOptionValue(CLIParserBuilder.SAML1_ARG, Boolean.FALSE);
97          if (saml1.booleanValue()) {
98              attributeStatement = performSAML1AttributeResolution(parser, appCtx);
99          } else {
100             attributeStatement = performSAML2AttributeResolution(parser, appCtx);
101         }
102 
103         printAttributeStatement(attributeStatement);
104     }
105 
106     /**
107      * Parses the command line arguments
108      * 
109      * @param args command line arguments
110      * 
111      * @return parsed command line arguments
112      * 
113      * @throws Exception thrown if the underlying libraries could not be initialized
114      */
115     private static CmdLineParser parseCommandArguments(String[] args) throws Exception {
116         if (args.length < 2) {
117             printHelp(System.out);
118             System.out.flush();
119             System.exit(0);
120         }
121 
122         CmdLineParser parser = CLIParserBuilder.buildParser();
123 
124         try {
125             parser.parse(args);
126         } catch (CmdLineParser.OptionException e) {
127             errorAndExit(e.getMessage(), e);
128         }
129 
130         Boolean helpEnabled = (Boolean) parser.getOptionValue(CLIParserBuilder.HELP_ARG);
131         if (helpEnabled != null) {
132             printHelp(System.out);
133             System.out.flush();
134             System.exit(0);
135         }
136 
137         return parser;
138     }
139 
140     /**
141      * Loads the configuration files into a Spring application context.
142      * 
143      * @param configDir directory containing spring configuration files
144      * 
145      * @return loaded application context
146      * 
147      * @throws IOException throw if there is an error loading the configuration files
148      * @throws ResourceException if there is an error loading the configuration files
149      */
150     private static ApplicationContext loadConfigurations(String configDir) throws IOException, ResourceException {
151         File configDirectory;
152 
153         if (configDir != null) {
154             configDirectory = new File(configDir);
155         } else {
156             configDirectory = new File(System.getenv("IDP_HOME") + "/conf");
157         }
158 
159         if (!configDirectory.exists() || !configDirectory.isDirectory() || !configDirectory.canRead()) {
160             errorAndExit("Configuration directory " + configDir
161                     + " does not exist, is not a directory, or is not readable", null);
162         }
163 
164         loadLoggingConfiguration(configDirectory.getAbsolutePath());
165 
166         List<Resource> configs = new ArrayList<Resource>();
167 
168         File config;
169         for (int i = 0; i < aacliConfigs.length; i++) {
170             config = new File(configDirectory.getPath() + aacliConfigs[i]);
171             if (config.isDirectory() || !config.canRead()) {
172                 errorAndExit("Configuration file " + config.getAbsolutePath() + " is a directory or is not readable",
173                         null);
174             }
175             configs.add(new FilesystemResource(config.getPath()));
176         }
177 
178         GenericApplicationContext gContext = new GenericApplicationContext();
179         SpringConfigurationUtils.populateRegistry(gContext, configs);
180         gContext.refresh();
181         return gContext;
182     }
183 
184     /**
185      * Loads the logging configuration.
186      * 
187      * @param configDir IdP configuration directory
188      */
189     private static void loadLoggingConfiguration(String configDir) {
190         String loggingConfig = configDir + File.separator + "logging.xml";
191 
192         LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
193         StatusManager statusManager = loggerContext.getStatusManager();
194         statusManager.add(new InfoStatus("Loading logging configuration file: " + loggingConfig, null));
195         try {
196             // loggerContext.stop();
197             loggerContext.reset();
198             JoranConfigurator configurator = new JoranConfigurator();
199             configurator.setContext(loggerContext);
200             configurator.doConfigure(new FileInputStream(loggingConfig));
201             loggerContext.start();
202         } catch (JoranException e) {
203             statusManager.add(new ErrorStatus("Error loading logging configuration file: " + configDir, null, e));
204         } catch (IOException e) {
205             statusManager.add(new ErrorStatus("Error loading logging configuration file: " + configDir, null, e));
206         }
207     }
208 
209     /**
210      * Constructs a SAML 1 attribute statement with the retrieved and filtered attributes.
211      * 
212      * @param parser command line arguments
213      * @param appCtx spring application context with loaded attribute authority
214      * 
215      * @return SAML 1 attribute statement
216      */
217     private static SAMLObject performSAML1AttributeResolution(CmdLineParser parser, ApplicationContext appCtx) {
218         BaseSAMLProfileRequestContext requestCtx = buildAttributeRequestContext(parser, appCtx);
219 
220         try {
221             Map<String, BaseAttribute> attributes = saml1AA.getAttributes(requestCtx);
222             return saml1AA.buildAttributeStatement(null, attributes.values());
223         } catch (AttributeRequestException e) {
224             errorAndExit("Error encountered during attribute resolution and filtering", e);
225         }
226 
227         return null;
228     }
229 
230     /**
231      * Constructs a SAML 2 attribute statement with the retrieved and filtered attributes.
232      * 
233      * @param parser command line arguments
234      * @param appCtx spring application context with loaded attribute authority
235      * 
236      * @return SAML 2 attribute statement
237      */
238     private static SAMLObject performSAML2AttributeResolution(CmdLineParser parser, ApplicationContext appCtx) {
239         BaseSAMLProfileRequestContext requestCtx = buildAttributeRequestContext(parser, appCtx);
240 
241         try {
242             Map<String, BaseAttribute> attributes = saml2AA.getAttributes(requestCtx);
243             return saml2AA.buildAttributeStatement(null, attributes.values());
244         } catch (AttributeRequestException e) {
245             errorAndExit("Error encountered during attribute resolution and filtering", e);
246         }
247 
248         return null;
249     }
250 
251     /**
252      * Builds the attribute request context from the command line arguments.
253      * 
254      * @param parser command line argument parser
255      * @param appCtx spring application context
256      * 
257      * @return attribute request context
258      */
259     private static BaseSAMLProfileRequestContext buildAttributeRequestContext(CmdLineParser parser,
260             ApplicationContext appCtx) {
261         BaseSAMLProfileRequestContext requestContext = new BaseSAMLProfileRequestContext();
262 
263         String[] rpConfigManagerNames = appCtx.getBeanNamesForType(SAMLMDRelyingPartyConfigurationManager.class);
264         SAMLMDRelyingPartyConfigurationManager rpConfigManager = (SAMLMDRelyingPartyConfigurationManager) appCtx
265                 .getBean(rpConfigManagerNames[0]);
266 
267         requestContext.setMetadataProvider(rpConfigManager.getMetadataProvider());
268 
269         String requester = (String) parser.getOptionValue(CLIParserBuilder.REQUESTER_ARG);
270         if (requester != null) {
271             requestContext.setRelyingPartyConfiguration(rpConfigManager.getRelyingPartyConfiguration(requester));
272         } else {
273             requester = rpConfigManager.getAnonymousRelyingConfiguration().getRelyingPartyId();
274             requestContext.setRelyingPartyConfiguration(rpConfigManager.getAnonymousRelyingConfiguration());
275         }
276 
277         try {
278             requestContext.setInboundMessageIssuer(requester);
279             requestContext.setPeerEntityId(requester);
280             requestContext.setPeerEntityMetadata(requestContext.getMetadataProvider().getEntityDescriptor(requester));
281         } catch (MetadataProviderException e) {
282             errorAndExit("Unable to query for metadata for requester " + requester, e);
283         }
284 
285         try {
286             String issuer = requestContext.getRelyingPartyConfiguration().getProviderId();
287             requestContext.setOutboundMessageIssuer(issuer);
288             requestContext.setLocalEntityId(issuer);
289             requestContext.setLocalEntityMetadata(requestContext.getMetadataProvider().getEntityDescriptor(issuer));
290         } catch (MetadataProviderException e) {
291             errorAndExit("Unable to query for metadata for issuer " + requester, e);
292         }
293 
294         String principal = (String) parser.getOptionValue(CLIParserBuilder.PRINCIPAL_ARG);
295         requestContext.setPrincipalName(principal);
296 
297         String authnMethod = (String) parser.getOptionValue(CLIParserBuilder.AUTHN_METHOD_ARG);
298         requestContext.setPrincipalAuthenticationMethod(authnMethod);
299 
300         return requestContext;
301     }
302 
303     /**
304      * Prints the given attribute statement to system output.
305      * 
306      * @param attributeStatement attribute statement to print
307      */
308     private static void printAttributeStatement(SAMLObject attributeStatement) {
309         if (attributeStatement == null) {
310             System.out.println("No attribute statement.");
311             return;
312         }
313 
314         Marshaller statementMarshaller = Configuration.getMarshallerFactory().getMarshaller(attributeStatement);
315 
316         try {
317             Element statement = statementMarshaller.marshall(attributeStatement);
318             System.out.println();
319             System.out.println(XMLHelper.prettyPrintXML(statement));
320         } catch (MarshallingException e) {
321             errorAndExit("Unable to marshall attribute statement", e);
322         }
323     }
324 
325     /**
326      * Prints a help message to the given output stream.
327      * 
328      * @param out output to print the help message to
329      */
330     private static void printHelp(PrintStream out) {
331         out.println("Attribute Authority, Command Line Interface");
332         out.println("  This tools provides a command line interface to the Shibboleth Attribute Authority,");
333         out.println("  providing deployers a means to test their attribute resolution and configurations.");
334         out.println();
335         out.println("usage:");
336         out.println("  On Unix systems:       ./aacli.sh <PARAMETERS>");
337         out.println("  On Windows systems:    .\\aacli.bat <PARAMETERS>");
338         out.println();
339         out.println("Required Parameters:");
340         out.println(String.format("  --%-16s %s", CLIParserBuilder.CONFIG_DIR,
341                 "Directory containing attribute authority configuration files"));
342         out.println(String.format("  --%-16s %s", CLIParserBuilder.PRINCIPAL,
343                 "Principal name (user id) of the person whose attributes will be retrieved"));
344 
345         out.println();
346 
347         out.println("Optional Parameters:");
348         out.println(String.format("  --%-16s %s", CLIParserBuilder.HELP, "Print this message"));
349         out.println(String.format("  --%-16s %s", CLIParserBuilder.REQUESTER,
350                 "SAML entity ID of the relying party requesting the attributes. For example, the SPs entity ID.  "
351                         + "If not provided, requester is treated as anonymous."));
352         out.println(String
353                 .format("  --%-16s %s", CLIParserBuilder.AUTHN_METHOD, "Method used to authenticate the user"));
354         out.println(String.format("  --%-16s %s", CLIParserBuilder.SAML1,
355                 "No-value parameter indicating the attribute "
356                         + "authority should answer as if it received a SAML 1 request"));
357 
358         out.println();
359     }
360 
361     /**
362      * Logs, as an error, the error message and exits the program.
363      * 
364      * @param errorMessage error message
365      * @param e exception that caused it
366      */
367     private static void errorAndExit(String errorMessage, Exception e) {
368         if (e == null) {
369             log.error(errorMessage);
370         } else {
371             log.error(errorMessage, e);
372         }
373 
374         System.out.flush();
375         System.exit(1);
376     }
377 
378     /**
379      * Helper class that creates the command line argument parser.
380      */
381     private static class CLIParserBuilder {
382 
383         // Command line arguments
384         public static final String HELP = "help";
385 
386         public static final String CONFIG_DIR = "configDir";
387 
388         public static final String REQUESTER = "requester";
389 
390         public static final String ISSUER = "issuer";
391 
392         public static final String PRINCIPAL = "principal";
393 
394         public static final String AUTHN_METHOD = "authnMethod";
395 
396         public static final String SAML1 = "saml1";
397 
398         // Command line parser arguments
399         public static CmdLineParser.Option HELP_ARG;
400 
401         public static CmdLineParser.Option CONFIG_DIR_ARG;
402 
403         public static CmdLineParser.Option REQUESTER_ARG;
404 
405         // ISSUER arg no longer used
406         public static CmdLineParser.Option ISSUER_ARG;
407 
408         public static CmdLineParser.Option PRINCIPAL_ARG;
409 
410         public static CmdLineParser.Option AUTHN_METHOD_ARG;
411 
412         public static CmdLineParser.Option SAML1_ARG;
413 
414         /**
415          * Create a new command line parser.
416          * 
417          * @return command line parser
418          */
419         public static CmdLineParser buildParser() {
420             CmdLineParser parser = new CmdLineParser();
421 
422             HELP_ARG = parser.addBooleanOption(HELP);
423             CONFIG_DIR_ARG = parser.addStringOption(CONFIG_DIR);
424             REQUESTER_ARG = parser.addStringOption(REQUESTER);
425             ISSUER_ARG = parser.addStringOption(ISSUER);
426             PRINCIPAL_ARG = parser.addStringOption(PRINCIPAL);
427             AUTHN_METHOD_ARG = parser.addStringOption(AUTHN_METHOD);
428             SAML1_ARG = parser.addBooleanOption(SAML1);
429 
430             return parser;
431         }
432     }
433 }