View Javadoc

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