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.provider.SAMLMDRelyingPartyConfigurationManager;
57  
58  /**
59   * A command line tool that allows individuals to invoke an attribute authority and inspect the resultant attribute
60   * statement.
61   * 
62   * This tool expects to retrieve the {@link MetadataProvider} it uses under the bean name SAMLMetadataProvider, a
63   * {@link SAML1AttributeAuthority} under the bean name SAML1AttributeAuthority, and a {@link SAML2AttributeAuthority}
64   * under the bean name SAML2AttributeAuthority.
65   */
66  public class AttributeAuthorityCLI {
67  
68      /** Class logger. */
69      private static Logger log = LoggerFactory.getLogger(AttributeAuthorityCLI.class);
70  
71      /** List of assumed Spring configuration files used with the AACLI. */
72      private static String[] aacliConfigs = { "internal.xml", "service.xml", };
73  
74      /** Loaded SAML 1 Attribute Authority. */
75      private static SAML1AttributeAuthority saml1AA;
76  
77      /** Loaded SAML 2 Attribute Authority. */
78      private static SAML2AttributeAuthority saml2AA;
79  
80      /**
81       * Runs this application. Help message prints if no arguments are given or if the "help" argument is given.
82       * 
83       * @param args command line arguments
84       * 
85       * @throws Exception thrown if there is a problem during program execution
86       */
87      public static void main(String[] args) throws Exception {
88          CmdLineParser parser = parseCommandArguments(args);
89          ApplicationContext appCtx = loadConfigurations(
90                  (String) parser.getOptionValue(CLIParserBuilder.CONFIG_DIR_ARG),
91                  (String) parser.getOptionValue(CLIParserBuilder.SPRING_EXTS_ARG)
92                  );
93  
94          saml1AA = (SAML1AttributeAuthority) appCtx.getBean("shibboleth.SAML1AttributeAuthority");
95          saml2AA = (SAML2AttributeAuthority) appCtx.getBean("shibboleth.SAML2AttributeAuthority");
96  
97          SAMLObject attributeStatement;
98          Boolean saml1 = (Boolean) parser.getOptionValue(CLIParserBuilder.SAML1_ARG, Boolean.FALSE);
99          if (saml1.booleanValue()) {
100             attributeStatement = performSAML1AttributeResolution(parser, appCtx);
101         } else {
102             attributeStatement = performSAML2AttributeResolution(parser, appCtx);
103         }
104 
105         printAttributeStatement(attributeStatement);
106     }
107 
108     /**
109      * Parses the command line arguments
110      * 
111      * @param args command line arguments
112      * 
113      * @return parsed command line arguments
114      * 
115      * @throws Exception thrown if the underlying libraries could not be initialized
116      */
117     private static CmdLineParser parseCommandArguments(String[] args) throws Exception {
118         if (args.length < 2) {
119             printHelp(System.out);
120             System.out.flush();
121             System.exit(0);
122         }
123 
124         CmdLineParser parser = CLIParserBuilder.buildParser();
125 
126         try {
127             parser.parse(args);
128         } catch (CmdLineParser.OptionException e) {
129             errorAndExit(e.getMessage(), e);
130         }
131 
132         Boolean helpEnabled = (Boolean) parser.getOptionValue(CLIParserBuilder.HELP_ARG);
133         if (helpEnabled != null) {
134             printHelp(System.out);
135             System.out.flush();
136             System.exit(0);
137         }
138 
139         return parser;
140     }
141 
142     /**
143      * Loads the configuration files into a Spring application context.
144      * 
145      * @param configDir directory containing spring configuration files
146      * @param springExts colon-separated list of spring extension files 
147      * 
148      * @return loaded application context
149      * 
150      * @throws IOException throw if there is an error loading the configuration files
151      * @throws ResourceException if there is an error loading the configuration files
152      */
153     private static ApplicationContext loadConfigurations(String configDir, String springExts)
154             throws IOException, ResourceException {
155         File configDirectory;
156 
157         if (configDir != null) {
158             configDirectory = new File(configDir);
159         } else {
160             configDirectory = new File(System.getenv("IDP_HOME") + "/conf");
161         }
162 
163         if (!configDirectory.exists() || !configDirectory.isDirectory() || !configDirectory.canRead()) {
164             errorAndExit("Configuration directory " + configDir
165                     + " does not exist, is not a directory, or is not readable", null);
166         }
167 
168         loadLoggingConfiguration(configDirectory.getAbsolutePath());
169 
170         File config;
171         List<String> configFiles = new ArrayList<String>();
172         List<Resource> configs = new ArrayList<Resource>();
173         
174         // Add built-in files.
175         for (String i : aacliConfigs) {
176             configFiles.add(i);
177         }
178         
179         // Add extensions, if any.
180         if (springExts != null && !springExts.isEmpty()) {
181             String[] extFiles = springExts.split(":");
182             for (String extFile : extFiles) {
183                 configFiles.add(extFile);
184             }
185         }
186        
187         for (String cfile : configFiles) {
188             config = new File(configDirectory.getPath() + File.separator + cfile);
189             if (config.isDirectory() || !config.canRead()) {
190                 errorAndExit("Configuration file " + config.getAbsolutePath() + " is a directory or is not readable",
191                         null);
192             }
193             configs.add(new FilesystemResource(config.getPath()));
194         }
195 
196         GenericApplicationContext gContext = new GenericApplicationContext();
197         SpringConfigurationUtils.populateRegistry(gContext, configs);
198         gContext.refresh();
199         return gContext;
200     }
201 
202     /**
203      * Loads the logging configuration.
204      * 
205      * @param configDir IdP configuration directory
206      */
207     private static void loadLoggingConfiguration(String configDir) {
208         String loggingConfig = configDir + File.separator + "logging.xml";
209 
210         LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
211         StatusManager statusManager = loggerContext.getStatusManager();
212         statusManager.add(new InfoStatus("Loading logging configuration file: " + loggingConfig, null));
213         try {
214             // loggerContext.stop();
215             loggerContext.reset();
216             JoranConfigurator configurator = new JoranConfigurator();
217             configurator.setContext(loggerContext);
218             configurator.doConfigure(new FileInputStream(loggingConfig));
219             loggerContext.start();
220         } catch (JoranException e) {
221             statusManager.add(new ErrorStatus("Error loading logging configuration file: " + configDir, null, e));
222         } catch (IOException e) {
223             statusManager.add(new ErrorStatus("Error loading logging configuration file: " + configDir, null, e));
224         }
225     }
226 
227     /**
228      * Constructs a SAML 1 attribute statement with the retrieved and filtered attributes.
229      * 
230      * @param parser command line arguments
231      * @param appCtx spring application context with loaded attribute authority
232      * 
233      * @return SAML 1 attribute statement
234      */
235     private static SAMLObject performSAML1AttributeResolution(CmdLineParser parser, ApplicationContext appCtx) {
236         BaseSAMLProfileRequestContext requestCtx = buildAttributeRequestContext(parser, appCtx);
237 
238         try {
239             Map<String, BaseAttribute> attributes = saml1AA.getAttributes(requestCtx);
240             return saml1AA.buildAttributeStatement(null, attributes.values());
241         } catch (AttributeRequestException e) {
242             errorAndExit("Error encountered during attribute resolution and filtering", e);
243         }
244 
245         return null;
246     }
247 
248     /**
249      * Constructs a SAML 2 attribute statement with the retrieved and filtered attributes.
250      * 
251      * @param parser command line arguments
252      * @param appCtx spring application context with loaded attribute authority
253      * 
254      * @return SAML 2 attribute statement
255      */
256     private static SAMLObject performSAML2AttributeResolution(CmdLineParser parser, ApplicationContext appCtx) {
257         BaseSAMLProfileRequestContext requestCtx = buildAttributeRequestContext(parser, appCtx);
258 
259         try {
260             Map<String, BaseAttribute> attributes = saml2AA.getAttributes(requestCtx);
261             return saml2AA.buildAttributeStatement(null, attributes.values());
262         } catch (AttributeRequestException e) {
263             errorAndExit("Error encountered during attribute resolution and filtering", e);
264         }
265 
266         return null;
267     }
268 
269     /**
270      * Builds the attribute request context from the command line arguments.
271      * 
272      * @param parser command line argument parser
273      * @param appCtx spring application context
274      * 
275      * @return attribute request context
276      */
277     private static BaseSAMLProfileRequestContext buildAttributeRequestContext(CmdLineParser parser,
278             ApplicationContext appCtx) {
279         BaseSAMLProfileRequestContext requestContext = new BaseSAMLProfileRequestContext();
280 
281         String[] rpConfigManagerNames = appCtx.getBeanNamesForType(SAMLMDRelyingPartyConfigurationManager.class);
282         SAMLMDRelyingPartyConfigurationManager rpConfigManager = (SAMLMDRelyingPartyConfigurationManager) appCtx
283                 .getBean(rpConfigManagerNames[0]);
284 
285         requestContext.setMetadataProvider(rpConfigManager.getMetadataProvider());
286 
287         String requester = (String) parser.getOptionValue(CLIParserBuilder.REQUESTER_ARG);
288         if (requester != null) {
289             requestContext.setRelyingPartyConfiguration(rpConfigManager.getRelyingPartyConfiguration(requester));
290         } else {
291             requester = rpConfigManager.getAnonymousRelyingConfiguration().getRelyingPartyId();
292             requestContext.setRelyingPartyConfiguration(rpConfigManager.getAnonymousRelyingConfiguration());
293         }
294 
295         try {
296             requestContext.setInboundMessageIssuer(requester);
297             requestContext.setPeerEntityId(requester);
298             requestContext.setPeerEntityMetadata(requestContext.getMetadataProvider().getEntityDescriptor(requester));
299         } catch (MetadataProviderException e) {
300             errorAndExit("Unable to query for metadata for requester " + requester, e);
301         }
302 
303         try {
304             String issuer = requestContext.getRelyingPartyConfiguration().getProviderId();
305             requestContext.setOutboundMessageIssuer(issuer);
306             requestContext.setLocalEntityId(issuer);
307             requestContext.setLocalEntityMetadata(requestContext.getMetadataProvider().getEntityDescriptor(issuer));
308         } catch (MetadataProviderException e) {
309             errorAndExit("Unable to query for metadata for issuer " + requester, e);
310         }
311 
312         String principal = (String) parser.getOptionValue(CLIParserBuilder.PRINCIPAL_ARG);
313         requestContext.setPrincipalName(principal);
314 
315         String authnMethod = (String) parser.getOptionValue(CLIParserBuilder.AUTHN_METHOD_ARG);
316         requestContext.setPrincipalAuthenticationMethod(authnMethod);
317 
318         return requestContext;
319     }
320 
321     /**
322      * Prints the given attribute statement to system output.
323      * 
324      * @param attributeStatement attribute statement to print
325      */
326     private static void printAttributeStatement(SAMLObject attributeStatement) {
327         if (attributeStatement == null) {
328             System.out.println("No attribute statement.");
329             return;
330         }
331 
332         Marshaller statementMarshaller = Configuration.getMarshallerFactory().getMarshaller(attributeStatement);
333 
334         try {
335             Element statement = statementMarshaller.marshall(attributeStatement);
336             System.out.println();
337             System.out.println(XMLHelper.prettyPrintXML(statement));
338         } catch (MarshallingException e) {
339             errorAndExit("Unable to marshall attribute statement", e);
340         }
341     }
342 
343     /**
344      * Prints a help message to the given output stream.
345      * 
346      * @param out output to print the help message to
347      */
348     private static void printHelp(PrintStream out) {
349         out.println("Attribute Authority, Command Line Interface");
350         out.println("  This tools provides a command line interface to the Shibboleth Attribute Authority,");
351         out.println("  providing deployers a means to test their attribute resolution and configurations.");
352         out.println();
353         out.println("usage:");
354         out.println("  On Unix systems:       ./aacli.sh <PARAMETERS>");
355         out.println("  On Windows systems:    .\\aacli.bat <PARAMETERS>");
356         out.println();
357         out.println("Required Parameters:");
358         out.println(String.format("  --%-16s %s", CLIParserBuilder.CONFIG_DIR,
359                 "Directory containing attribute authority configuration files"));
360         out.println(String.format("  --%-16s %s", CLIParserBuilder.PRINCIPAL,
361                 "Principal name (user id) of the person whose attributes will be retrieved"));
362 
363         out.println();
364 
365         out.println("Optional Parameters:");
366         out.println(String.format("  --%-16s %s", CLIParserBuilder.HELP, "Print this message"));
367         out.println(String.format("  --%-16s %s", CLIParserBuilder.SPRING_EXTS,
368                 "Colon-delimited list of files containing Spring extension configurations"));
369         out.println(String.format("  --%-16s %s", CLIParserBuilder.REQUESTER,
370                 "SAML entity ID of the relying party requesting the attributes. For example, the SPs entity ID.  "
371                         + "If not provided, requester is treated as anonymous."));
372         out.println(String
373                 .format("  --%-16s %s", CLIParserBuilder.AUTHN_METHOD, "Method used to authenticate the user"));
374         out.println(String.format("  --%-16s %s", CLIParserBuilder.SAML1,
375                 "No-value parameter indicating the attribute "
376                         + "authority should answer as if it received a SAML 1 request"));
377 
378         out.println();
379     }
380 
381     /**
382      * Logs, as an error, the error message and exits the program.
383      * 
384      * @param errorMessage error message
385      * @param e exception that caused it
386      */
387     private static void errorAndExit(String errorMessage, Exception e) {
388         if (e == null) {
389             log.error(errorMessage);
390         } else {
391             log.error(errorMessage, e);
392         }
393 
394         System.out.flush();
395         System.exit(1);
396     }
397 
398     /**
399      * Helper class that creates the command line argument parser.
400      */
401     private static class CLIParserBuilder {
402 
403         // Command line arguments
404         public static final String HELP = "help";
405 
406         public static final String CONFIG_DIR = "configDir";
407         
408         public static final String SPRING_EXTS = "springExts";
409 
410         public static final String REQUESTER = "requester";
411 
412         public static final String ISSUER = "issuer";
413 
414         public static final String PRINCIPAL = "principal";
415 
416         public static final String AUTHN_METHOD = "authnMethod";
417 
418         public static final String SAML1 = "saml1";
419 
420         // Command line parser arguments
421         public static CmdLineParser.Option HELP_ARG;
422 
423         public static CmdLineParser.Option CONFIG_DIR_ARG;
424         
425         public static CmdLineParser.Option SPRING_EXTS_ARG;
426 
427         public static CmdLineParser.Option REQUESTER_ARG;
428 
429         // ISSUER arg no longer used
430         public static CmdLineParser.Option ISSUER_ARG;
431 
432         public static CmdLineParser.Option PRINCIPAL_ARG;
433 
434         public static CmdLineParser.Option AUTHN_METHOD_ARG;
435 
436         public static CmdLineParser.Option SAML1_ARG;
437 
438         /**
439          * Create a new command line parser.
440          * 
441          * @return command line parser
442          */
443         public static CmdLineParser buildParser() {
444             CmdLineParser parser = new CmdLineParser();
445 
446             HELP_ARG = parser.addBooleanOption(HELP);
447             CONFIG_DIR_ARG = parser.addStringOption(CONFIG_DIR);
448             SPRING_EXTS_ARG = parser.addStringOption(SPRING_EXTS);
449             REQUESTER_ARG = parser.addStringOption(REQUESTER);
450             ISSUER_ARG = parser.addStringOption(ISSUER);
451             PRINCIPAL_ARG = parser.addStringOption(PRINCIPAL);
452             AUTHN_METHOD_ARG = parser.addStringOption(AUTHN_METHOD);
453             SAML1_ARG = parser.addBooleanOption(SAML1);
454 
455             return parser;
456         }
457     }
458 }