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