View Javadoc

1   package org.opensciencegrid.authz.client;
2   
3   /***
4    *
5    *  A base class that abstracts most of the SAML communication
6    *  on the client side
7    * 
8   
9   Author:  Markus Lorch
10  Project: OpenScienceGrid Privilege
11  Date:    2004-11-16
12  
13  changes: 2005-01-04  modified interfaces to opensaml to use stock opensaml 1.0.1
14                       added use of separate osg-opensaml-extension package
15  
16           2005-01-07  moved away from Globus depenencies to new WS client stub
17           2005-01-08  code cleanup, moved general SAML utility functions to saml.SAMLUtil
18           2005-01-18  added system properties for subject.dn and subject.fqan (for testing)
19  
20  
21  status: this implementation currently lacks
22          - extraction of subject DN from gss context 
23          - extraction of FQAN from the gss context (attribute certificate support)
24            --> these parameters are gotten via the system properties subject.dn and subject.fqan
25            --> subject.fqan is optional
26  */
27  
28  import java.net.URL;
29  import java.util.Set;
30  import java.util.Iterator;
31  import java.util.StringTokenizer;
32  import java.util.ArrayList;
33  import java.util.Date;
34  import java.security.cert.X509Certificate;
35  import javax.security.auth.Subject;
36  
37  import org.apache.axis.message.MessageElement;
38  import org.apache.log4j.Category; // logging system
39  import org.globus.axis.gsi.GSIConstants;
40  import org.globus.gsi.gssapi.GlobusGSSCredentialImpl;
41  import org.globus.wsrf.impl.security.authorization.NoAuthorization;
42  import org.globus.wsrf.security.Constants;
43  
44  import org.w3c.dom.Element;
45  
46  import org.opensaml.v1_0_1.XML;
47  import org.opensaml.v1_0_1.QName;
48  import org.opensaml.v1_0_1.SAMLAction;
49  import org.opensaml.v1_0_1.SAMLDecision;
50  import org.opensaml.v1_0_1.SAMLRequest;
51  import org.opensaml.v1_0_1.SAMLSubject;
52  import org.opensaml.v1_0_1.SAMLNameIdentifier;
53  import org.opensaml.v1_0_1.SAMLResponse;
54  import org.opensaml.v1_0_1.SAMLException;
55  import org.opensaml.v1_0_1.SAMLAssertion;
56  import org.opensaml.v1_0_1.SAMLAttribute;
57  import org.opensaml.v1_0_1.SAMLAttributeStatement;
58  import org.opensaml.v1_0_1.SAMLAttributeDesignator;
59  import org.opensaml.v1_0_1.SAMLAuthorizationDecisionQuery;
60  import org.opensaml.v1_0_1.SAMLAuthorizationDecisionStatement;
61  
62  import org.opensciencegrid.authz.saml.ObligatedAuthorizationDecisionStatement;
63  import org.opensciencegrid.authz.saml.XACMLObligation;
64  import org.opensciencegrid.authz.saml.SAMLExtensionInit;
65  import org.opensciencegrid.authz.saml.SAMLUtil;
66  
67  import org.opensciencegrid.authz.common.LocalId;
68  import org.opensciencegrid.authz.common.OSGAuthorizationConstants;
69  
70  import org.opensciencegrid.authz.stubs.AuthorizationServiceLocator;
71  import org.opensciencegrid.authz.stubs.SAMLRequestPortType;
72  import org.opensciencegrid.authz.stubs.SAMLRequestType;
73  import org.opensciencegrid.authz.stubs.SAMLResponseType;
74  
75  //#import org.gridforum.jgss.ExtendedGSSContext;
76  
77  
78  public class SAMLAuthZClientBase {
79  
80  
81      /*** logging category */
82      static Category log = Category.getInstance(SAMLAuthZClientBase.class.getName());
83  
84  
85  
86      /***
87        * create a SAML Subject from a DN String
88        *
89        */
90  
91      public SAMLSubject getSAMLSubjectFromString (String subjectName) throws SAMLException {
92  
93          if(subjectName==null || subjectName.length()==0) {
94              log.error("The subjectName parameter must not be null or empty"); 
95              return null;
96          }
97                   
98          log.debug("Creating SAMLSubject from string: "+subjectName);
99        
100 	String nameQualifier = "";
101 	String format =  "urn:osasis:names:tc:SAML:1.1nameid-format:X509SubjectName";
102 	ArrayList confirmationMethods = null;
103 
104         SAMLNameIdentifier subjectId = new SAMLNameIdentifier(subjectName, 
105  			   				     nameQualifier, 
106 						             format);
107 
108 	confirmationMethods = new ArrayList(1);
109 	confirmationMethods.add("urn:oasis:names:tc:SAML:1.0:am:X509-PKI");
110 	
111         log.debug("Subject name " + subjectName + " nameQualifier " 
112 		     + nameQualifier + " format " + format);
113 
114  
115 	return 	new SAMLSubject(subjectId, confirmationMethods, null, null);
116 
117     } // end getSAMLSubjectFromString
118 
119 
120     /***
121       * create a SAML Subject based on the subject name of the
122       * peer certificate (issuing EEC in case of proxy) subjecta
123       * as extracted from the supplied gss context
124       * (not implemented)
125       * Subject will include certificate path as confirmation method
126       * (not implemented)
127       */
128 
129     public SAMLSubject getSAMLSubjectFromGSS (String gssContext) throws SAMLException {
130 
131         // need to extract identity certificate of peer from GSS context 
132         String subjectName="/CN=Dummy Subject";
133 
134         log.debug("Creating SAMLSubject from peer certificate subject (not implemented): "+subjectName);
135  
136 	String nameQualifier = "";
137 	String format =  "urn:osasis:names:tc:SAML:1.1nameid-format:X509SubjectName";
138 	ArrayList confirmationMethods = null;
139 
140         SAMLNameIdentifier subjectId = new SAMLNameIdentifier(subjectName, 
141  			   				     nameQualifier, 
142 						             format);
143 
144         // here we can add the certificate path as a confirmation method, once we have an
145         // implementation for the certificate path in XML
146         // this will be useful if instead of talking to an identity mapping service we 
147         // talk to a site authorization service like SAZ that re-validates the 
148         // certificate path and checks for certificate revocation
149  
150 	confirmationMethods = new ArrayList(1);
151 	confirmationMethods.add("urn:oasis:names:tc:SAML:1.0:am:X509-PKI");
152 	
153         log.debug("Subject name " + subjectName + " nameQualifier " 
154 		     + nameQualifier + " format " + format);
155 
156  
157 	return 	new SAMLSubject(subjectId, confirmationMethods, null, null);
158 
159     } // end getSAMLSubjectFromString
160 
161     
162     /***
163      * Create an ArrayList with the OSG Mapping Action as the single SAMLAction element
164      */
165 
166     public ArrayList createMappingActions() throws SAMLException{
167 
168 
169         //****** create the SAML Action   *************************************************************
170 	
171 	ArrayList samlActions = new ArrayList();
172 	SAMLAction samlAction = 
173 	   new SAMLAction(OSGAuthorizationConstants.AUTHZ_NS, OSGAuthorizationConstants.ACCESS_AS_LOCAL_ID);
174 	samlActions.add(samlAction);
175 
176         return samlActions;
177 
178      }
179 
180 
181 	
182     /**
183      * Create an ArrayList with a single SAML Attribute Statement (bound to the
184      * specified SAMLSubject) embedded in a single SAML Assertion. The attribute 
185      * statement will hold a single VOMS FQAN attribute. 
186      */
187 
188     public ArrayList createFQANEvidenceFromString(SAMLSubject samlSubject, String fqanIssuer, String fqan) 
189           throws SAMLException, CloneNotSupportedException {
190 
191         ArrayList samlEvidence = null;
192         
193         // ok, now create the FQAN evidence statement and add it to the evidence arraylist
194         if(fqan!=null && fqan.length()!=0 && fqanIssuer!=null && fqanIssuer.length()!=0) {
195 
196           samlEvidence = new ArrayList(1);
197   
198           log.debug("Creating Evidence based on FQAN String: "+fqan+ " from: "+fqanIssuer);
199 
200           ArrayList values = new ArrayList(1);
201           values.add(fqan);
202 
203           SAMLAttribute sa = new SAMLAttribute("FQAN", OSGAuthorizationConstants.AUTHZ_NS, null,0, values);
204 
205           ArrayList attrs = new ArrayList(1);
206           attrs.add(sa);
207 
208           SAMLAttributeStatement stmt = new SAMLAttributeStatement((SAMLSubject)samlSubject.clone(), attrs);
209 
210           ArrayList stmts = new ArrayList(1);
211           stmts.add(stmt);
212 
213           SAMLAssertion ass = new SAMLAssertion(fqanIssuer, new Date(), new Date(), null, null, stmts);
214 
215           samlEvidence.add(ass);
216         } // end if
217         else {
218           log.warn("fqan and fqanIssuer information must be provided for evidence element to be created");
219         }
220       
221 
222       return samlEvidence;
223 
224     } // end createFQAN
225 
226 
227     /***
228      * Create an ArrayList with a single SAML Attribute Statement embedded in
229      * a single SAML Assertion. The attribute statement will hold a single
230      * VOMS FQAN attribute. The FQAN information is extracted from the GSS
231      * context. 
232      * (NOT IMPLEMENTED)
233      */
234 
235     public ArrayList createFQANEvidenceFromGSS(String gssContext) {
236 
237       return null;
238 
239     } // end createFQANEvidenceFromGSS
240 
241        
242 
243 
244    /*** General method to create and submit a SAMLAuthorizationDecisionQuery
245     * to an Authorization Service. Supports standard as well as
246     * obligated authorization decision responses.
247     * Will return the AuthorizationDecisionStatement object
248     * (may be SAMLAuthorizationDecisionStatement or ObligatedAuthorizationDecisionStatement)
249     * returns null or throws exception if no valid response/statement 
250     * was received
251     */
252 
253     public SAMLAuthorizationDecisionStatement queryAuthZService( SAMLSubject samlSubject, 
254 								ArrayList samlEvidence,
255 								ArrayList samlActions,
256 								String requestedServiceName,
257                                                                 URL contact) 
258         throws SAMLException, javax.xml.rpc.ServiceException, java.rmi.RemoteException, java.lang.Exception {
259     
260 
261         //****** initialize SAML Extensions *****
262         SAMLExtensionInit.init();
263 
264 
265         //***** assemble the SAML Authorization Decision Query*******
266 	
267         SAMLAuthorizationDecisionQuery query = null;
268         
269         query = new SAMLAuthorizationDecisionQuery(samlSubject, 
270 						       requestedServiceName,
271 						       samlActions, samlEvidence);
272 
273 
274 	//**** set valid respondWiths ***
275         // these define with what type of statement the identity mapping service may respond to
276         // our service request
277 
278         ArrayList supportedRespondWiths = new ArrayList();
279         supportedRespondWiths.add( OSGAuthorizationConstants.AUTHZDECISIONSTATEMENT);
280         supportedRespondWiths.add( OSGAuthorizationConstants.OBLIGATEDAUTHZDECISIONSTATEMENT);
281 
282         //***** assemble the SAML Request that holds the query and submit request***
283 	SAMLRequest request = null;
284 	request = new SAMLRequest(supportedRespondWiths, query, null, null);
285 
286         SAMLRequestType requestType = new SAMLRequestType();
287 	requestType.set_any(new MessageElement[] {
288 	    new MessageElement((Element)request.toDOM())});
289 
290 	SAMLResponseType responseType = null;
291 	
292         AuthorizationServiceLocator serviceLocator = new AuthorizationServiceLocator();
293         
294         log.debug("Sending SAML query/request to "+contact+" "+request);
295 
296         //***** Send Request
297 	SAMLRequestPortType authzPort = 
298 		(SAMLRequestPortType) serviceLocator.getAuthorizationServicePort(contact);
299         if (getGlobusCredentials() != null) {
300             org.apache.axis.client.Stub stub = (org.apache.axis.client.Stub) authzPort;
301             log.debug("Setting Globus GSI credentials on the WS stub.");
302             stub._setProperty(Constants.GSI_TRANSPORT, Constants.ENCRYPTION);
303             stub._setProperty(GSIConstants.GSI_CREDENTIALS, getGlobusCredentials());
304             stub._setProperty(Constants.AUTHORIZATION, NoAuthorization.getInstance());
305         }
306         
307 	responseType = authzPort.SAMLRequest(requestType);	
308         
309 	
310         //****** Process Response *********************************************************************
311 
312 	if (responseType == null) {
313            // This is an error since atleast a response with no actions
314   	   // should have been returned.
315 	   log.error("Authorization query/request failed - received a null response");
316            return null;
317         }
318 
319 	SAMLResponse response = null;
320         MessageElement[] msgElement = responseType.get_any();
321         response = new SAMLResponse(msgElement[0].getAsDOM());
322 
323 	// Assert that our request id matches the InResponseTo
324 	String inResponseTo = response.getInResponseTo();
325 	if ((inResponseTo == null) || 
326 	    (!inResponseTo.equals(request.getId()))) {
327 	    log.error("Authorization query/request failed - received a bad inResponseTo");
328             return null;
329 	}
330 
331         log.debug("Received response corresponding to our request: "+response);
332 
333 
334         //****** Extract Authorization Decision Statements from Response ******************************
335 
336 	log.debug("Extracting authorization decision statements from response");
337 	
338         // response must have assertions
339 	Iterator assertionsItr = response.getAssertions();
340 	if ((assertionsItr != null) && (assertionsItr.hasNext())) {
341             SAMLAssertion assertion = (SAMLAssertion)assertionsItr.next();
342             
343             // assertions must hold statements
344 	    Iterator stmtsItr = assertion.getStatements();
345 	    if ((stmtsItr != null) && (stmtsItr.hasNext())) {
346 		Object obj = stmtsItr.next();
347                 
348 	        // accepted statements include AuthorizationDecisionStatements 
349                 // and ObligatedAuthorizationDecisionStatements
350      	        if ((obj instanceof ObligatedAuthorizationDecisionStatement) 
351      	            || (obj instanceof SAMLAuthorizationDecisionStatement)  ) {
352 		   log.debug("Received and returning Authorization Decision Statement");
353                    return (SAMLAuthorizationDecisionStatement) obj;
354 		} 
355                 else {
356                   log.error("Received statement was not an Authorization Decison Statement"); 
357 		}
358 	    }
359             else {
360              log.error("Received assertion did not contain a SAML statement"); 
361 	    }
362              
363 	} // end if assertionsItr
364         else {
365          log.error("Received response did not contain a SAML Assertion");
366         }
367 
368       return null;
369 	
370     } // end queryAuthZService
371 
372 
373 
374     /** Specialized method to process a received authorization decision statement
375     * that holds obligations with local user ID qualifications
376     *
377     * If the response is valid, action permitted and obligations present then it
378     * will return a LocaldQualifiers object with the attributes conveyed
379     * via obligations set. If the decision is permit but no obligations present
380     * or only partially present then the LocalID object will have 
381     * some elements be null. If decision is deny, unknown obligations present
382     * or the response is invalid for other reasons then the "null" will be returned.
383     * 
384     * Supported obligations are:
385     * - UserIdObligation with single local user name as string attribute
386     * - GroupIdObligation with single local primary group name as string attribute
387     * - SupplementalGroupIdsObligation with space delimited list of local group names as string attribute
388     * - RootPathObligation with root path as string attribute
389     * - RelativeHomePathObligation with home path relative to root path as string attribute
390     */
391 
392     public LocalId processAuthzStmt(SAMLAuthorizationDecisionStatement stmt, String resource, 
393 				     ArrayList actions, SAMLSubject samlSubject) {
394 	
395         log.debug("Processing Authorization Decision Statement");
396         // localIds will only be allocated if the following decision check succeeds
397         LocalId localIds = null;
398 
399 	
400 	if ((stmt.getDecision().equals(SAMLDecision.PERMIT)) &&
401 	    (stmt.getResource().equals(resource))) {
402 	    log.debug("Authorization decision is Permit and Resource matches");
403 	    
404             if (SAMLUtil.samlSubjectMatch(samlSubject, stmt.getSubject())) {
405 	        log.debug("Response subject name matches request subject name");
406 		// all actions for which query was made shld be in permit response
407                 Iterator actionIterator = stmt.getActions(); 
408                 // create a collection with all returned (permitted) actions
409                 ArrayList decisionActions = new ArrayList();
410 		while (actionIterator.hasNext()) {
411                   SAMLAction a = (SAMLAction) actionIterator.next();
412                   decisionActions.add(a);
413                   log.debug("Authorized action: " + a.getNamespace() + "    " +a.getData());
414                 }
415                 int found=0;
416                 for(int j=0; j<actions.size(); j++) {
417                   found = 0; // reset the found marker for each
418                   // lets see if this requested action is present in any of the
419                   // decision action
420                   for(int i=0;i<decisionActions.size();i++){
421                       if(SAMLUtil.samlActionMatch((SAMLAction)actions.get(j),
422                                          (SAMLAction)decisionActions.get(i))) { // found it
423                         found = 1;
424                         break; // end inner for loop, we found the action
425                       }
426                   }
427                   if(found!=1) break; // didn't find this action, so no reason to continue
428                 }
429  
430                 if(found==1) { 
431                        // while loop ended and all actions where found
432                        // statement is valid, so allocate an (empty) localIdQualifiers objecta
433                        log.debug("Authorized actions include requested actions");
434                        localIds = new LocalId();
435                 } else {
436                   log.warn("Authorization decision did not permit requested actions");
437                 }  
438 
439 	    } else { // subject doesn't match - print debug information on why 
440 	       log.warn("Response subject does not match request subject");
441                log.debug("Request  Subject Name "+samlSubject.getName().getName());
442                log.debug("Response Subject Name "+stmt.getSubject().getName().getName());
443                if(samlSubject.getName().getName().equals(stmt.getSubject().getName().getName()))
444                  log.debug("The name is equal");
445                else
446                  log.debug("The name is not equal");
447                log.debug("Request  Subject Name Qualifier "+samlSubject.getName().getNameQualifier());
448                log.debug("Response Subject Name Qualifier "+stmt.getSubject().getName().getNameQualifier());
449                if(samlSubject.getName().getNameQualifier().equals(stmt.getSubject().getName().getNameQualifier()))
450                  log.debug("The name qualifier is equal");
451                else
452                  log.debug("The name qualifier is not equal");
453                log.debug("Request  Subject Name Format "+samlSubject.getName().getFormat());
454                log.debug("Response Subject Name Format "+stmt.getSubject().getName().getFormat());
455                if(samlSubject.getName().getFormat().equals(stmt.getSubject().getName().getFormat()))
456                  log.debug("The format is equal");
457                else
458                  log.debug("The format is not equal");
459             } // end else subject match
460 	}
461 
462         // localIds has not been allocated, then deny 
463         if(localIds == null) {
464            return null;
465         }
466            
467         // ok, we are have a permit, lets see if we also have obligations
468 
469         if(stmt instanceof ObligatedAuthorizationDecisionStatement) {
470             log.debug("Processing Obligations");
471             
472             Iterator obligationIterator = ((ObligatedAuthorizationDecisionStatement) stmt).getXACMLObligations();
473             
474             while (obligationIterator.hasNext()) {
475               XACMLObligation o = (XACMLObligation) obligationIterator.next();
476               
477               if(o.getObligationId().equals(OSGAuthorizationConstants.USERIDOBLIGATION) 
478                       && o.getFullfillOn().equals("Permit") ) {
479                  log.debug("Found UserId obligation");                      
480                  if(localIds.getUserName() !=null) 
481                     log.warn("Warning multiple UserId Obligations, overriding previous");
482                  if(o.getAttributeId().equals(OSGAuthorizationConstants.USERIDATTRIBUTE)
483                       && o.getDatatype().equals(OSGAuthorizationConstants.STRINGDATATYPE) ) {
484                    localIds.setUserName(o.getValue());
485                  } else {
486                    log.error("Obligation has unexpected attributeId or datatype");    
487                    return null;
488                  }
489               } 
490               else if(o.getObligationId().equals(OSGAuthorizationConstants.GROUPIDOBLIGATION) 
491                       && o.getFullfillOn().equals("Permit") ) {
492                  log.debug("Found GroupId obligation");                      
493                  if(localIds.getGroupName() !=null) 
494                     log.warn("Warning multiple GroupId Obligations, overriding previous");
495                  if(o.getAttributeId().equals(OSGAuthorizationConstants.GROUPIDATTRIBUTE)
496                       && o.getDatatype().equals(OSGAuthorizationConstants.STRINGDATATYPE) ) {
497                    localIds.setGroupName(o.getValue());
498                  } else {
499                    log.error("Obligation has unexpected attributeId or datatype");    
500                    return null;
501                  }
502               } 
503               else if(o.getObligationId().equals(OSGAuthorizationConstants.SUPGROUPIDSOBLIGATION) 
504                       && o.getFullfillOn().equals("Permit") ) {
505                  log.debug("Found SupplementalGroupId obligation");                      
506                  if(localIds.getGroupName() !=null) 
507                     log.warn("Warning multiple SupplementalGroupIds Obligations, overriding previous");
508                  if(o.getAttributeId().equals(OSGAuthorizationConstants.SUPGROUPIDSATTRIBUTE)
509                       && o.getDatatype().equals(OSGAuthorizationConstants.STRINGDATATYPE) ) {
510                    // the Supplemental Group Names are separated by space in one string
511                    localIds.setSupplementalGroupNames(o.getValue().split("//s"));
512                  } else {
513                    log.error("Obligation has unexpected attributeId or datatype");    
514                    return null;
515                  }
516               } 
517               else if(o.getObligationId().equals(OSGAuthorizationConstants.ROOTPATHOBLIGATION) 
518                       && o.getFullfillOn().equals("Permit") ) {
519                  log.debug("Found Root Path obligation");                      
520                  if(localIds.getGroupName() !=null) 
521                     log.warn("Warning multiple Root Path Obligations, overriding previous");
522                  if(o.getAttributeId().equals(OSGAuthorizationConstants.ROOTPATHATTRIBUTE)
523                       && o.getDatatype().equals(OSGAuthorizationConstants.STRINGDATATYPE) ) {
524                    localIds.setRootPath(o.getValue());
525                  } else {
526                    log.error("Obligation has unexpected attributeId or datatype");    
527                    return null;
528                  }
529               } 
530 	      else if(o.getObligationId().equals(OSGAuthorizationConstants.RELHOMEPATHOBLIGATION) 
531                       && o.getFullfillOn().equals("Permit") ) {
532                  log.debug("Found Relative Home Path obligation");                      
533                  if(localIds.getRelativeHomePath() !=null) 
534                     log.warn("Warning multiple Relative Home Path Obligations, overriding previous");
535                  if(o.getAttributeId().equals(OSGAuthorizationConstants.RELHOMEPATHATTRIBUTE)
536                       && o.getDatatype().equals(OSGAuthorizationConstants.STRINGDATATYPE) ) {
537                    localIds.setRelativeHomePath(o.getValue());
538                  } else {
539                    log.error("Obligation has unexpected attributeId or datatype");    
540                    return null;
541                  }
542               } 
543               else  {
544                  log.error("Found unsupported obligation - mapping denied");
545                  return null;
546               }
547                                        
548                    
549                
550             }
551 
552         } // enf if obligations
553 
554         return localIds;
555 
556     } // end processAuthzStmt
557 
558     /***
559      * Holds value of property globusCredentials.
560      */
561     private GlobusGSSCredentialImpl globusCredentials;
562 
563     /***
564      * Getter for property globusCredentials.
565      * @return Value of property globusCredentials.
566      */
567     public GlobusGSSCredentialImpl getGlobusCredentials() {
568         return this.globusCredentials;
569     }
570 
571     /***
572      * Setter for property globusCredentials.
573      * @param globusCredentials New value of property globusCredentials.
574      */
575     public void setGlobusCredentials(GlobusGSSCredentialImpl globusCredentials) {
576         this.globusCredentials = globusCredentials;
577     }
578 
579 } // end class