View Javadoc
1   package org.argeo.cms.internal.auth;
2   
3   import java.security.AccessControlContext;
4   import java.security.AccessController;
5   import java.security.PrivilegedAction;
6   import java.security.PrivilegedExceptionAction;
7   import java.time.ZonedDateTime;
8   import java.util.Collection;
9   import java.util.HashMap;
10  import java.util.HashSet;
11  import java.util.Hashtable;
12  import java.util.LinkedHashSet;
13  import java.util.Locale;
14  import java.util.Map;
15  import java.util.Set;
16  import java.util.UUID;
17  
18  import javax.crypto.SecretKey;
19  import javax.jcr.Repository;
20  import javax.jcr.Session;
21  import javax.naming.InvalidNameException;
22  import javax.naming.ldap.LdapName;
23  import javax.security.auth.Subject;
24  import javax.security.auth.login.LoginContext;
25  import javax.security.auth.login.LoginException;
26  
27  import org.apache.commons.logging.Log;
28  import org.apache.commons.logging.LogFactory;
29  import org.argeo.api.NodeConstants;
30  import org.argeo.api.security.NodeSecurityUtils;
31  import org.argeo.cms.CmsException;
32  import org.argeo.cms.auth.CmsSession;
33  import org.argeo.jcr.JcrUtils;
34  import org.osgi.framework.BundleContext;
35  import org.osgi.framework.FrameworkUtil;
36  import org.osgi.framework.InvalidSyntaxException;
37  import org.osgi.framework.ServiceReference;
38  import org.osgi.framework.ServiceRegistration;
39  import org.osgi.service.useradmin.Authorization;
40  
41  public class CmsSessionImpl implements CmsSession {
42  	private final static BundleContext bc = FrameworkUtil.getBundle(CmsSessionImpl.class).getBundleContext();
43  	private final static Log log = LogFactory.getLog(CmsSessionImpl.class);
44  
45  	// private final Subject initialSubject;
46  	private final AccessControlContext initialContext;
47  	private final UUID uuid;
48  	private final String localSessionId;
49  	private final Authorization authorization;
50  	private final LdapName userDn;
51  	private final boolean anonymous;
52  
53  	private final ZonedDateTime creationTime;
54  	private ZonedDateTime end;
55  	private final Locale locale;
56  
57  	private ServiceRegistration<CmsSession> serviceRegistration;
58  
59  	private Map<String, Session> dataSessions = new HashMap<>();
60  	private Set<String> dataSessionsInUse = new HashSet<>();
61  	private LinkedHashSet<Session> additionalDataSessions = new LinkedHashSet<>();
62  
63  	public CmsSessionImpl(Subject initialSubject, Authorization authorization, Locale locale, String localSessionId) {
64  		this.creationTime = ZonedDateTime.now();
65  		this.locale = locale;
66  		this.initialContext = Subject.doAs(initialSubject, new PrivilegedAction<AccessControlContext>() {
67  
68  			@Override
69  			public AccessControlContext run() {
70  				return AccessController.getContext();
71  			}
72  
73  		});
74  		// this.initialSubject = initialSubject;
75  		this.localSessionId = localSessionId;
76  		this.authorization = authorization;
77  		if (authorization.getName() != null)
78  			try {
79  				this.userDn = new LdapName(authorization.getName());
80  				this.anonymous = false;
81  			} catch (InvalidNameException e) {
82  				throw new CmsException("Invalid user name " + authorization.getName(), e);
83  			}
84  		else {
85  			this.userDn = NodeSecurityUtils.ROLE_ANONYMOUS_NAME;
86  			this.anonymous = true;
87  		}
88  		this.uuid = UUID.randomUUID();
89  		// register as service
90  		Hashtable<String, String> props = new Hashtable<>();
91  		props.put(CmsSession.USER_DN, userDn.toString());
92  		props.put(CmsSession.SESSION_UUID, uuid.toString());
93  		props.put(CmsSession.SESSION_LOCAL_ID, localSessionId);
94  		serviceRegistration = bc.registerService(CmsSession.class, this, props);
95  	}
96  
97  	public void close() {
98  		end = ZonedDateTime.now();
99  		serviceRegistration.unregister();
100 
101 		synchronized (this) {
102 			// TODO check data session in use ?
103 			for (String path : dataSessions.keySet())
104 				JcrUtils.logoutQuietly(dataSessions.get(path));
105 			for (Session session : additionalDataSessions)
106 				JcrUtils.logoutQuietly(session);
107 		}
108 
109 		try {
110 			LoginContext lc;
111 			if (isAnonymous()) {
112 				lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_ANONYMOUS, getSubject());
113 			} else {
114 				lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER, getSubject());
115 			}
116 			lc.logout();
117 		} catch (LoginException e) {
118 			log.warn("Could not logout " + getSubject() + ": " + e);
119 		}
120 		log.debug("Closed " + this);
121 	}
122 
123 	private Subject getSubject() {
124 		return Subject.getSubject(initialContext);
125 	}
126 
127 	public Set<SecretKey> getSecretKeys() {
128 		return getSubject().getPrivateCredentials(SecretKey.class);
129 	}
130 
131 	public Session newDataSession(String cn, String workspace, Repository repository) {
132 		return login(repository, workspace);
133 	}
134 
135 	public synchronized Session getDataSession(String cn, String workspace, Repository repository) {
136 		// FIXME make it more robust
137 		if (workspace == null)
138 			workspace = "main";
139 		String path = cn + '/' + workspace;
140 		if (dataSessionsInUse.contains(path)) {
141 			try {
142 				wait(1000);
143 				if (dataSessionsInUse.contains(path)) {
144 					Session session = login(repository, workspace);
145 					additionalDataSessions.add(session);
146 					if (log.isTraceEnabled())
147 						log.trace("Additional data session " + path + " for " + userDn);
148 					return session;
149 				}
150 			} catch (InterruptedException e) {
151 				// silent
152 			}
153 		}
154 
155 		Session session = null;
156 		if (dataSessions.containsKey(path)) {
157 			session = dataSessions.get(path);
158 		} else {
159 			session = login(repository, workspace);
160 			dataSessions.put(path, session);
161 			if (log.isTraceEnabled())
162 				log.trace("New data session " + path + " for " + userDn);
163 		}
164 		dataSessionsInUse.add(path);
165 		return session;
166 	}
167 
168 	private Session login(Repository repository, String workspace) {
169 		try {
170 			return Subject.doAs(getSubject(), new PrivilegedExceptionAction<Session>() {
171 				@Override
172 				public Session run() throws Exception {
173 					return repository.login(workspace);
174 				}
175 			});
176 		} catch (Exception e) {
177 			throw new CmsException("Cannot log in " + userDn + " to JCR", e);
178 		}
179 	}
180 
181 	public synchronized void releaseDataSession(String cn, Session session) {
182 		if (additionalDataSessions.contains(session)) {
183 			JcrUtils.logoutQuietly(session);
184 			additionalDataSessions.remove(session);
185 			if (log.isTraceEnabled())
186 				log.trace("Remove additional data session " + session);
187 			return;
188 		}
189 		String path = cn + '/' + session.getWorkspace().getName();
190 		if (!dataSessionsInUse.contains(path))
191 			log.warn("Data session " + path + " was not in use for " + userDn);
192 		dataSessionsInUse.remove(path);
193 		Session registeredSession = dataSessions.get(path);
194 		if (session != registeredSession)
195 			log.warn("Data session " + path + " not consistent for " + userDn);
196 		if (log.isTraceEnabled())
197 			log.trace("Released data session " + session + " for " + path);
198 		notifyAll();
199 	}
200 
201 	@Override
202 	public boolean isValid() {
203 		return !isClosed();
204 	}
205 
206 	protected boolean isClosed() {
207 		return getEnd() != null;
208 	}
209 
210 	@Override
211 	public Authorization getAuthorization() {
212 		return authorization;
213 	}
214 
215 	@Override
216 	public UUID getUuid() {
217 		return uuid;
218 	}
219 
220 	@Override
221 	public LdapName getUserDn() {
222 		return userDn;
223 	}
224 
225 	@Override
226 	public String getLocalId() {
227 		return localSessionId;
228 	}
229 
230 	@Override
231 	public boolean isAnonymous() {
232 		return anonymous;
233 	}
234 
235 	@Override
236 	public Locale getLocale() {
237 		return locale;
238 	}
239 
240 	@Override
241 	public ZonedDateTime getCreationTime() {
242 		return creationTime;
243 	}
244 
245 	@Override
246 	public ZonedDateTime getEnd() {
247 		return end;
248 	}
249 
250 	public String toString() {
251 		return "CMS Session " + userDn + " local=" + localSessionId + ", uuid=" + uuid;
252 	}
253 
254 	public static CmsSessionImpl getByLocalId(String localId) {
255 		Collection<ServiceReference<CmsSession>> sr;
256 		try {
257 			sr = bc.getServiceReferences(CmsSession.class, "(" + CmsSession.SESSION_LOCAL_ID + "=" + localId + ")");
258 		} catch (InvalidSyntaxException e) {
259 			throw new CmsException("Cannot get CMS session for id " + localId, e);
260 		}
261 		ServiceReference<CmsSession> cmsSessionRef;
262 		if (sr.size() == 1) {
263 			cmsSessionRef = sr.iterator().next();
264 			return (CmsSessionImpl) bc.getService(cmsSessionRef);
265 		} else if (sr.size() == 0) {
266 			return null;
267 		} else
268 			throw new CmsException(sr.size() + " CMS sessions registered for " + localId);
269 
270 	}
271 
272 	public static CmsSessionImpl getByUuid(Object uuid) {
273 		Collection<ServiceReference<CmsSession>> sr;
274 		try {
275 			sr = bc.getServiceReferences(CmsSession.class, "(" + CmsSession.SESSION_UUID + "=" + uuid + ")");
276 		} catch (InvalidSyntaxException e) {
277 			throw new CmsException("Cannot get CMS session for uuid " + uuid, e);
278 		}
279 		ServiceReference<CmsSession> cmsSessionRef;
280 		if (sr.size() == 1) {
281 			cmsSessionRef = sr.iterator().next();
282 			return (CmsSessionImpl) bc.getService(cmsSessionRef);
283 		} else if (sr.size() == 0) {
284 			return null;
285 		} else
286 			throw new CmsException(sr.size() + " CMS sessions registered for " + uuid);
287 
288 	}
289 
290 	public static void closeInvalidSessions() {
291 		Collection<ServiceReference<CmsSession>> srs;
292 		try {
293 			srs = bc.getServiceReferences(CmsSession.class, null);
294 			for (ServiceReference<CmsSession> sr : srs) {
295 				CmsSession cmsSession = bc.getService(sr);
296 				if (!cmsSession.isValid()) {
297 					((CmsSessionImpl) cmsSession).close();
298 					if (log.isDebugEnabled())
299 						log.debug("Closed expired CMS session " + cmsSession);
300 				}
301 			}
302 		} catch (InvalidSyntaxException e) {
303 			throw new CmsException("Cannot get CMS sessions", e);
304 		}
305 	}
306 }