View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
19   *
20   */
21  
22  package org.opencastproject.userdirectory.sakai;
23  
24  import org.opencastproject.security.api.Organization;
25  import org.opencastproject.security.api.OrganizationDirectoryService;
26  import org.opencastproject.security.api.RoleProvider;
27  import org.opencastproject.security.api.UserProvider;
28  import org.opencastproject.util.NotFoundException;
29  
30  import org.apache.commons.lang3.StringUtils;
31  import org.osgi.framework.BundleContext;
32  import org.osgi.framework.ServiceRegistration;
33  import org.osgi.service.cm.ConfigurationException;
34  import org.osgi.service.cm.ManagedServiceFactory;
35  import org.osgi.service.component.ComponentContext;
36  import org.osgi.service.component.annotations.Activate;
37  import org.osgi.service.component.annotations.Component;
38  import org.osgi.service.component.annotations.Reference;
39  import org.slf4j.Logger;
40  import org.slf4j.LoggerFactory;
41  
42  import java.lang.management.ManagementFactory;
43  import java.util.Arrays;
44  import java.util.Dictionary;
45  import java.util.HashSet;
46  import java.util.Map;
47  import java.util.Set;
48  import java.util.concurrent.ConcurrentHashMap;
49  
50  import javax.management.MalformedObjectNameException;
51  import javax.management.ObjectName;
52  
53  /**
54   * Sakai implementation of the spring UserDetailsService, taking configuration information from the component context.
55   */
56  @Component(
57      immediate = true,
58      service = ManagedServiceFactory.class,
59      property = {
60          "service.pid=org.opencastproject.userdirectory.sakai",
61          "service.description=Provides Sakai user directory instances"
62      }
63  )
64  public class SakaiUserProviderFactory implements ManagedServiceFactory {
65  
66    /** The logger */
67    protected static final Logger logger = LoggerFactory.getLogger(SakaiUserProviderFactory.class);
68  
69    /** This service factory's PID */
70    public static final String PID = "org.opencastproject.userdirectory.sakai";
71  
72    /** The key to look up the organization identifer in the service configuration properties */
73    private static final String ORGANIZATION_KEY = "org.opencastproject.userdirectory.sakai.org";
74  
75    /** The key to look up the user DN to use for performing searches. */
76    private static final String SAKAI_SEARCH_USER = "org.opencastproject.userdirectory.sakai.user";
77  
78    /** The key to look up the password to use for performing searches */
79    private static final String SEARCH_PASSWORD = "org.opencastproject.userdirectory.sakai.password";
80  
81    /** The key to look up the regular expression used to validate sites */
82    private static final String SITE_PATTERN_KEY = "org.opencastproject.userdirectory.sakai.site.pattern";
83  
84    /** The key to look up the regular expression used to validate users */
85    private static final String USER_PATTERN_KEY = "org.opencastproject.userdirectory.sakai.user.pattern";
86  
87    /** The key to look up the number of user records to cache */
88    private static final String CACHE_SIZE = "org.opencastproject.userdirectory.sakai.cache.size";
89  
90    /** The key to look up the URL of the Sakai instance */
91    private static final String SAKAI_URL_KEY = "org.opencastproject.userdirectory.sakai.url";
92  
93    /** The key to look up the list of Instructor roles on the Sakai instance */
94    private static final String SAKAI_INSTRUCTOR_ROLES_KEY = "org.opencastproject.userdirectory.sakai.instructor.roles";
95  
96    /** The key to look up the number of minutes to cache users */
97    private static final String CACHE_EXPIRATION = "org.opencastproject.userdirectory.sakai.cache.expiration";
98  
99    /** A map of pid to sakai user provider instance */
100   private Map<String, ServiceRegistration> providerRegistrations = new ConcurrentHashMap<String, ServiceRegistration>();
101 
102   /** The OSGI bundle context */
103   protected BundleContext bundleContext = null;
104 
105   /** The organization directory service */
106   private OrganizationDirectoryService orgDirectory;
107 
108   /** OSGi callback for setting the organization directory service. */
109   @Reference
110   public void setOrgDirectory(OrganizationDirectoryService orgDirectory) {
111     this.orgDirectory = orgDirectory;
112   }
113 
114   /**
115    * Callback for the activation of this component
116    *
117    * @param cc
118    *          the component context
119    */
120   @Activate
121   public void activate(ComponentContext cc) {
122     logger.debug("Activate SakaiUserProviderFactory");
123     this.bundleContext = cc.getBundleContext();
124   }
125 
126   /**
127    * {@inheritDoc}
128    *
129    * @see org.osgi.service.cm.ManagedServiceFactory#getName()
130    */
131   @Override
132   public String getName() {
133     return PID;
134   }
135 
136   /**
137    * {@inheritDoc}
138    *
139    * @see org.osgi.service.cm.ManagedServiceFactory#updated(java.lang.String, java.util.Dictionary)
140    */
141   @Override
142   public void updated(String pid, Dictionary properties) throws ConfigurationException {
143 
144     logger.debug("updated SakaiUserProviderFactory");
145 
146     String organization = (String) properties.get(ORGANIZATION_KEY);
147     if (StringUtils.isBlank(organization)) {
148       throw new ConfigurationException(ORGANIZATION_KEY, "is not set");
149     }
150 
151     String url = (String) properties.get(SAKAI_URL_KEY);
152     if (StringUtils.isBlank(url)) {
153       throw new ConfigurationException(SAKAI_URL_KEY, "is not set");
154     }
155 
156     String userDn = (String) properties.get(SAKAI_SEARCH_USER);
157     String password = (String) properties.get(SEARCH_PASSWORD);
158 
159     String sitePattern = (String) properties.get(SITE_PATTERN_KEY);
160     String userPattern = (String) properties.get(USER_PATTERN_KEY);
161 
162     int cacheSize = 1000;
163     try {
164       if (properties.get(CACHE_SIZE) != null) {
165         Integer configuredCacheSize = Integer.parseInt(properties.get(CACHE_SIZE).toString());
166         if (configuredCacheSize != null) {
167           cacheSize = configuredCacheSize.intValue();
168         }
169       }
170     } catch (Exception e) {
171       logger.warn("{} could not be loaded, default value is used: {}", CACHE_SIZE, cacheSize);
172     }
173 
174 
175     int cacheExpiration = 60;
176     try {
177       if (properties.get(CACHE_EXPIRATION) != null) {
178         Integer configuredCacheExpiration = Integer.parseInt(properties.get(CACHE_EXPIRATION).toString());
179         if (configuredCacheExpiration != null) {
180           cacheExpiration = configuredCacheExpiration.intValue();
181         }
182       }
183     } catch (Exception e) {
184       logger.warn("{} could not be loaded, default value is used: {}", CACHE_EXPIRATION, cacheExpiration);
185     }
186 
187     // Instructor roles
188     Set<String> instructorRoles;
189     String instructorRoleList = (String) properties.get(SAKAI_INSTRUCTOR_ROLES_KEY);
190 
191     if (!StringUtils.isEmpty(instructorRoleList)) {
192       String trimmedRoles = StringUtils.trim(instructorRoleList);
193       String[] roles = trimmedRoles.split(",");
194       instructorRoles = new HashSet<String>(Arrays.asList(roles));
195       logger.info("Sakai instructor roles: {}", Arrays.toString(roles));
196     } else {
197       // Default instructor roles
198       instructorRoles = new HashSet<String>();
199       instructorRoles.add("Site owner");
200       instructorRoles.add("Instructor");
201       instructorRoles.add("maintain");
202     }
203 
204     // Now that we have everything we need, go ahead and activate a new provider, removing an old one if necessary
205     ServiceRegistration existingRegistration = providerRegistrations.remove(pid);
206     if (existingRegistration != null) {
207       existingRegistration.unregister();
208     }
209 
210     Organization org;
211     try {
212       org = orgDirectory.getOrganization(organization);
213     } catch (NotFoundException e) {
214       logger.warn("Organization {} not found!", organization);
215       throw new ConfigurationException(ORGANIZATION_KEY, "not found");
216     }
217 
218     logger.debug("creating new SakaiUserProviderInstance for pid=" + pid);
219     SakaiUserProviderInstance provider = new SakaiUserProviderInstance(pid,
220             org, url, userDn, password, sitePattern, userPattern, instructorRoles, cacheSize, cacheExpiration);
221 
222     providerRegistrations.put(pid, bundleContext.registerService(UserProvider.class.getName(), provider, null));
223     providerRegistrations.put(pid, bundleContext.registerService(RoleProvider.class.getName(), provider, null));
224 
225   }
226 
227   /**
228    * {@inheritDoc}
229    *
230    * @see org.osgi.service.cm.ManagedServiceFactory#deleted(java.lang.String)
231    */
232   @Override
233   public void deleted(String pid) {
234     logger.debug("delete SakaiUserProviderInstance for pid=" + pid);
235     ServiceRegistration registration = providerRegistrations.remove(pid);
236     if (registration != null) {
237       registration.unregister();
238       try {
239         ManagementFactory.getPlatformMBeanServer().unregisterMBean(SakaiUserProviderFactory.getObjectName(pid));
240       } catch (Exception e) {
241         logger.warn("Unable to unregister mbean for pid='{}': {}", pid, e.getMessage());
242       }
243     }
244   }
245 
246   /**
247    * Builds a JMX object name for a given PID
248    *
249    * @param pid
250    *          the PID
251    * @return the object name
252    * @throws NullPointerException
253    * @throws MalformedObjectNameException
254    */
255   public static final ObjectName getObjectName(String pid) throws MalformedObjectNameException, NullPointerException {
256     return new ObjectName(pid + ":type=SakaiRequests");
257   }
258 
259 }