001 package de.deepamehta.plugins.accesscontrol;
002
003 import de.deepamehta.plugins.accesscontrol.event.PostLoginUserListener;
004 import de.deepamehta.plugins.accesscontrol.event.PostLogoutUserListener;
005 import de.deepamehta.plugins.accesscontrol.model.AccessControlList;
006 import de.deepamehta.plugins.accesscontrol.model.ACLEntry;
007 import de.deepamehta.plugins.accesscontrol.model.Credentials;
008 import de.deepamehta.plugins.accesscontrol.model.Operation;
009 import de.deepamehta.plugins.accesscontrol.model.Permissions;
010 import de.deepamehta.plugins.accesscontrol.model.UserRole;
011 import de.deepamehta.plugins.accesscontrol.service.AccessControlService;
012 import de.deepamehta.plugins.workspaces.service.WorkspacesService;
013
014 import de.deepamehta.core.Association;
015 import de.deepamehta.core.AssociationType;
016 import de.deepamehta.core.DeepaMehtaObject;
017 import de.deepamehta.core.RelatedTopic;
018 import de.deepamehta.core.Topic;
019 import de.deepamehta.core.TopicType;
020 import de.deepamehta.core.Type;
021 import de.deepamehta.core.ViewConfiguration;
022 import de.deepamehta.core.model.ChildTopicsModel;
023 import de.deepamehta.core.model.SimpleValue;
024 import de.deepamehta.core.model.TopicModel;
025 import de.deepamehta.core.osgi.PluginActivator;
026 import de.deepamehta.core.service.DeepaMehtaEvent;
027 import de.deepamehta.core.service.EventListener;
028 import de.deepamehta.core.service.Inject;
029 import de.deepamehta.core.service.Transactional;
030 import de.deepamehta.core.service.event.AllPluginsActiveListener;
031 import de.deepamehta.core.service.event.IntroduceTopicTypeListener;
032 import de.deepamehta.core.service.event.IntroduceAssociationTypeListener;
033 import de.deepamehta.core.service.event.PostCreateAssociationListener;
034 import de.deepamehta.core.service.event.PostCreateTopicListener;
035 import de.deepamehta.core.service.event.PostUpdateTopicListener;
036 import de.deepamehta.core.service.event.PreSendAssociationTypeListener;
037 import de.deepamehta.core.service.event.PreSendTopicTypeListener;
038 import de.deepamehta.core.service.event.ResourceRequestFilterListener;
039 import de.deepamehta.core.service.event.ServiceRequestFilterListener;
040 import de.deepamehta.core.storage.spi.DeepaMehtaTransaction;
041 import de.deepamehta.core.util.DeepaMehtaUtils;
042 import de.deepamehta.core.util.JavaUtils;
043
044 import org.codehaus.jettison.json.JSONObject;
045
046 // ### TODO: hide Jersey internals. Move to JAX-RS 2.0.
047 import com.sun.jersey.spi.container.ContainerRequest;
048
049 import javax.servlet.http.HttpServletRequest;
050 import javax.servlet.http.HttpServletResponse;
051 import javax.servlet.http.HttpSession;
052
053 import javax.ws.rs.GET;
054 import javax.ws.rs.PUT;
055 import javax.ws.rs.POST;
056 import javax.ws.rs.DELETE;
057 import javax.ws.rs.Consumes;
058 import javax.ws.rs.Path;
059 import javax.ws.rs.PathParam;
060 import javax.ws.rs.Produces;
061 import javax.ws.rs.WebApplicationException;
062 import javax.ws.rs.core.Context;
063 import javax.ws.rs.core.Response;
064 import javax.ws.rs.core.Response.Status;
065
066 import java.util.Collection;
067 import java.util.Enumeration;
068 import java.util.List;
069 import java.util.logging.Logger;
070
071
072
073 @Path("/accesscontrol")
074 @Consumes("application/json")
075 @Produces("application/json")
076 public class AccessControlPlugin extends PluginActivator implements AccessControlService, AllPluginsActiveListener,
077 PostCreateTopicListener,
078 PostCreateAssociationListener,
079 PostUpdateTopicListener,
080 IntroduceTopicTypeListener,
081 IntroduceAssociationTypeListener,
082 ServiceRequestFilterListener,
083 ResourceRequestFilterListener,
084 PreSendTopicTypeListener,
085 PreSendAssociationTypeListener {
086
087 // ------------------------------------------------------------------------------------------------------- Constants
088
089 // Security settings
090 private static final boolean READ_REQUIRES_LOGIN = Boolean.getBoolean("dm4.security.read_requires_login");
091 private static final boolean WRITE_REQUIRES_LOGIN = Boolean.getBoolean("dm4.security.write_requires_login");
092 private static final String SUBNET_FILTER = System.getProperty("dm4.security.subnet_filter");
093
094 private static final String AUTHENTICATION_REALM = "DeepaMehta";
095
096 // Default user account
097 private static final String DEFAULT_USERNAME = "admin";
098 private static final String DEFAULT_PASSWORD = "";
099
100 // Default ACLs
101 private static final AccessControlList DEFAULT_INSTANCE_ACL = new AccessControlList(
102 new ACLEntry(Operation.WRITE, UserRole.CREATOR, UserRole.OWNER, UserRole.MEMBER)
103 );
104 private static final AccessControlList DEFAULT_TYPE_ACL = new AccessControlList(
105 new ACLEntry(Operation.WRITE, UserRole.CREATOR, UserRole.OWNER, UserRole.MEMBER),
106 new ACLEntry(Operation.CREATE, UserRole.CREATOR, UserRole.OWNER, UserRole.MEMBER)
107 );
108 //
109 private static final AccessControlList DEFAULT_USER_ACCOUNT_ACL = new AccessControlList(
110 new ACLEntry(Operation.WRITE, UserRole.CREATOR, UserRole.OWNER)
111 );
112
113 // Property names
114 private static String URI_CREATOR = "dm4.accesscontrol.creator";
115 private static String URI_OWNER = "dm4.accesscontrol.owner";
116 private static String URI_ACL = "dm4.accesscontrol.acl";
117
118 // Events
119 private static DeepaMehtaEvent POST_LOGIN_USER = new DeepaMehtaEvent(PostLoginUserListener.class) {
120 @Override
121 public void deliver(EventListener listener, Object... params) {
122 ((PostLoginUserListener) listener).postLoginUser(
123 (String) params[0]
124 );
125 }
126 };
127 private static DeepaMehtaEvent POST_LOGOUT_USER = new DeepaMehtaEvent(PostLogoutUserListener.class) {
128 @Override
129 public void deliver(EventListener listener, Object... params) {
130 ((PostLogoutUserListener) listener).postLogoutUser(
131 (String) params[0]
132 );
133 }
134 };
135
136 // ---------------------------------------------------------------------------------------------- Instance Variables
137
138 @Inject
139 private WorkspacesService wsService;
140
141 @Context
142 private HttpServletRequest request;
143
144 private Logger logger = Logger.getLogger(getClass().getName());
145
146 // -------------------------------------------------------------------------------------------------- Public Methods
147
148
149
150 // *******************************************
151 // *** AccessControlService Implementation ***
152 // *******************************************
153
154
155
156 // === Session ===
157
158 @POST
159 @Path("/login")
160 @Override
161 public void login() {
162 // Note: the actual login is performed by the request filter. See requestFilter().
163 }
164
165 @POST
166 @Path("/logout")
167 @Override
168 public void logout() {
169 _logout(request);
170 //
171 // For a "private" DeepaMehta installation: emulate a HTTP logout by forcing the webbrowser to bring up its
172 // login dialog and to forget the former Authorization information. The user is supposed to press "Cancel".
173 // The login dialog can't be used to login again.
174 if (READ_REQUIRES_LOGIN) {
175 throw401Unauthorized();
176 }
177 }
178
179
180
181 // === User ===
182
183 @GET
184 @Path("/user")
185 @Produces("text/plain")
186 @Override
187 public String getUsername() {
188 try {
189 HttpSession session = request.getSession(false); // create=false
190 if (session == null) {
191 return null;
192 }
193 return username(session);
194 } catch (IllegalStateException e) {
195 // Note: if not invoked through network no request (and thus no session) is available.
196 // This happens e.g. while starting up.
197 return null; // user is unknown
198 }
199 }
200
201 @Override
202 public Topic getUsername(String username) {
203 return dms.getTopic("dm4.accesscontrol.username", new SimpleValue(username));
204 }
205
206
207
208 // === Permissions ===
209
210 @GET
211 @Path("/topic/{id}")
212 @Override
213 public Permissions getTopicPermissions(@PathParam("id") long topicId) {
214 return getPermissions(dms.getTopic(topicId));
215 }
216
217 @GET
218 @Path("/association/{id}")
219 @Override
220 public Permissions getAssociationPermissions(@PathParam("id") long assocId) {
221 return getPermissions(dms.getAssociation(assocId));
222 }
223
224
225
226 // === Creator ===
227
228 @Override
229 public String getCreator(DeepaMehtaObject object) {
230 return object.hasProperty(URI_CREATOR) ? (String) object.getProperty(URI_CREATOR) : null;
231 }
232
233 @Override
234 public void setCreator(DeepaMehtaObject object, String username) {
235 try {
236 object.setProperty(URI_CREATOR, username, true); // addToIndex=true
237 } catch (Exception e) {
238 throw new RuntimeException("Setting the creator of " + info(object) + " failed (username=" + username + ")",
239 e);
240 }
241 }
242
243
244
245 // === Owner ===
246
247 @Override
248 public String getOwner(DeepaMehtaObject object) {
249 return object.hasProperty(URI_OWNER) ? (String) object.getProperty(URI_OWNER) : null;
250 }
251
252 @Override
253 public void setOwner(DeepaMehtaObject object, String username) {
254 try {
255 object.setProperty(URI_OWNER, username, true); // addToIndex=true
256 } catch (Exception e) {
257 throw new RuntimeException("Setting the owner of " + info(object) + " failed (username=" + username + ")",
258 e);
259 }
260 }
261
262
263
264 // === Access Control List ===
265
266 @Override
267 public AccessControlList getACL(DeepaMehtaObject object) {
268 try {
269 if (object.hasProperty(URI_ACL)) {
270 return new AccessControlList(new JSONObject((String) object.getProperty(URI_ACL)));
271 } else {
272 return new AccessControlList();
273 }
274 } catch (Exception e) {
275 throw new RuntimeException("Fetching the ACL of " + info(object) + " failed", e);
276 }
277 }
278
279 @Override
280 public void setACL(DeepaMehtaObject object, AccessControlList acl) {
281 try {
282 object.setProperty(URI_ACL, acl.toJSON().toString(), false); // addToIndex=false
283 } catch (Exception e) {
284 throw new RuntimeException("Setting the ACL of " + info(object) + " failed", e);
285 }
286 }
287
288
289
290 // === Workspaces ===
291
292 @POST
293 @Path("/user/{username}/workspace/{workspace_id}")
294 @Transactional
295 @Override
296 public void joinWorkspace(@PathParam("username") String username, @PathParam("workspace_id") long workspaceId) {
297 joinWorkspace(getUsername(username), workspaceId);
298 }
299
300 @Override
301 public void joinWorkspace(Topic username, long workspaceId) {
302 try {
303 wsService.assignToWorkspace(username, workspaceId);
304 } catch (Exception e) {
305 throw new RuntimeException("Joining user " + username + " to workspace " + workspaceId + " failed", e);
306 }
307 }
308
309
310
311 // === Retrieval ===
312
313 @GET
314 @Path("/creator/{username}/topics")
315 @Override
316 public Collection<Topic> getTopicsByCreator(@PathParam("username") String username) {
317 return dms.getTopicsByProperty(URI_CREATOR, username);
318 }
319
320 @GET
321 @Path("/owner/{username}/topics")
322 @Override
323 public Collection<Topic> getTopicsByOwner(@PathParam("username") String username) {
324 return dms.getTopicsByProperty(URI_OWNER, username);
325 }
326
327 @GET
328 @Path("/creator/{username}/assocs")
329 @Override
330 public Collection<Association> getAssociationsByCreator(@PathParam("username") String username) {
331 return dms.getAssociationsByProperty(URI_CREATOR, username);
332 }
333
334 @GET
335 @Path("/owner/{username}/assocs")
336 @Override
337 public Collection<Association> getAssociationsByOwner(@PathParam("username") String username) {
338 return dms.getAssociationsByProperty(URI_OWNER, username);
339 }
340
341
342
343 // ****************************
344 // *** Hook Implementations ***
345 // ****************************
346
347
348
349 @Override
350 public void postInstall() {
351 logger.info("Creating \"admin\" user account");
352 Topic adminAccount = createUserAccount(new Credentials(DEFAULT_USERNAME, DEFAULT_PASSWORD));
353 // Note 1: the admin account needs to be setup for access control itself.
354 // At post-install time our listeners are not yet registered. So we must setup manually here.
355 // Note 2: at post-install time there is no user session. So we call setupAccessControl() directly
356 // instead of (the higher-level) setupUserAccountAccessControl().
357 setupAccessControl(adminAccount, DEFAULT_USER_ACCOUNT_ACL, DEFAULT_USERNAME);
358 // ### TODO: setup access control for the admin account's Username and Password topics.
359 // However, they are not strictly required for the moment.
360 }
361
362 @Override
363 public void init() {
364 logger.info("Security settings:" +
365 "\n dm4.security.read_requires_login=" + READ_REQUIRES_LOGIN +
366 "\n dm4.security.write_requires_login=" + WRITE_REQUIRES_LOGIN +
367 "\n dm4.security.subnet_filter=\""+ SUBNET_FILTER + "\"");
368 }
369
370
371
372 // ********************************
373 // *** Listener Implementations ***
374 // ********************************
375
376
377
378 /**
379 * Setup access control for the default user and the default topicmap.
380 * 1) assign default user to default workspace
381 * 2) assign default topicmap to default workspace
382 * 3) setup access control for default topicmap
383 */
384 @Override
385 public void allPluginsActive() {
386 DeepaMehtaTransaction tx = dms.beginTx();
387 try {
388 // 1) assign default user to default workspace
389 Topic defaultUser = fetchDefaultUser();
390 assignToDefaultWorkspace(defaultUser, "default user (\"admin\")");
391 //
392 Topic defaultTopicmap = fetchDefaultTopicmap();
393 if (defaultTopicmap != null) {
394 // 2) assign default topicmap to default workspace
395 assignToDefaultWorkspace(defaultTopicmap, "default topicmap (\"untitled\")");
396 // 3) setup access control for default topicmap
397 setupAccessControlForDefaultTopicmap(defaultTopicmap);
398 }
399 //
400 tx.success();
401 } catch (Exception e) {
402 logger.warning("ROLLBACK! (" + this + ")");
403 throw new RuntimeException("Setting up " + this + " failed", e);
404 } finally {
405 tx.finish();
406 }
407 }
408
409 // ---
410
411 @Override
412 public void postCreateTopic(Topic topic) {
413 if (isUserAccount(topic)) {
414 setupUserAccountAccessControl(topic);
415 } else {
416 setupDefaultAccessControl(topic);
417 }
418 //
419 // when a workspace is created its creator joins automatically
420 joinIfWorkspace(topic);
421 }
422
423 @Override
424 public void postCreateAssociation(Association assoc) {
425 setupDefaultAccessControl(assoc);
426 }
427
428 // ---
429
430 @Override
431 public void postUpdateTopic(Topic topic, TopicModel newModel, TopicModel oldModel) {
432 if (topic.getTypeUri().equals("dm4.accesscontrol.user_account")) {
433 Topic usernameTopic = topic.getChildTopics().getTopic("dm4.accesscontrol.username");
434 Topic passwordTopic = topic.getChildTopics().getTopic("dm4.accesscontrol.password");
435 String newUsername = usernameTopic.getSimpleValue().toString();
436 TopicModel oldUsernameTopic = oldModel.getChildTopicsModel().getTopic("dm4.accesscontrol.username",
437 null);
438 String oldUsername = oldUsernameTopic != null ? oldUsernameTopic.getSimpleValue().toString() : "";
439 if (!newUsername.equals(oldUsername)) {
440 //
441 if (!oldUsername.equals("")) {
442 throw new RuntimeException("Changing a Username is not supported (tried \"" + oldUsername +
443 "\" -> \"" + newUsername + "\")");
444 }
445 //
446 logger.info("### Username has changed from \"" + oldUsername + "\" -> \"" + newUsername +
447 "\". Setting \"" + newUsername + "\" as the new owner of 3 topics:\n" +
448 " - User Account topic (ID " + topic.getId() + ")\n" +
449 " - Username topic (ID " + usernameTopic.getId() + ")\n" +
450 " - Password topic (ID " + passwordTopic.getId() + ")");
451 setOwner(topic, newUsername);
452 setOwner(usernameTopic, newUsername);
453 setOwner(passwordTopic, newUsername);
454 }
455 }
456 }
457
458 // ---
459
460 @Override
461 public void introduceTopicType(TopicType topicType) {
462 setupDefaultAccessControl(topicType);
463 }
464
465 @Override
466 public void introduceAssociationType(AssociationType assocType) {
467 setupDefaultAccessControl(assocType);
468 }
469
470 // ---
471
472 @Override
473 public void serviceRequestFilter(ContainerRequest containerRequest) {
474 // Note: we pass the injected HttpServletRequest
475 requestFilter(request);
476 }
477
478 @Override
479 public void resourceRequestFilter(HttpServletRequest servletRequest) {
480 // Note: for the resource filter no HttpServletRequest is injected
481 requestFilter(servletRequest);
482 }
483
484 // ---
485
486 // ### TODO: make the types cachable (like topics/associations). That is, don't deliver the permissions along
487 // with the types (don't use the preSend{}Type hooks). Instead let the client request the permissions separately.
488
489 @Override
490 public void preSendTopicType(TopicType topicType) {
491 // Note: the permissions for "Meta Meta Type" must be set manually.
492 // This type doesn't exist in DB. Fetching its ACL entries would fail.
493 if (topicType.getUri().equals("dm4.core.meta_meta_type")) {
494 enrichWithPermissions(topicType, createPermissions(false, false)); // write=false, create=false
495 return;
496 }
497 //
498 enrichWithPermissions(topicType, getPermissions(topicType));
499 }
500
501 @Override
502 public void preSendAssociationType(AssociationType assocType) {
503 enrichWithPermissions(assocType, getPermissions(assocType));
504 }
505
506
507
508 // ------------------------------------------------------------------------------------------------- Private Methods
509
510 private Topic createUserAccount(Credentials cred) {
511 return dms.createTopic(new TopicModel("dm4.accesscontrol.user_account", new ChildTopicsModel()
512 .put("dm4.accesscontrol.username", cred.username)
513 .put("dm4.accesscontrol.password", cred.password)));
514 }
515
516 private boolean isUserAccount(Topic topic) {
517 String typeUri = topic.getTypeUri();
518 return typeUri.equals("dm4.accesscontrol.user_account")
519 || typeUri.equals("dm4.accesscontrol.username")
520 || typeUri.equals("dm4.accesscontrol.password");
521 }
522
523 /**
524 * Fetches the default user ("admin").
525 *
526 * @throws RuntimeException If the default user doesn't exist.
527 *
528 * @return The default user (a Topic of type "Username" / <code>dm4.accesscontrol.username</code>).
529 */
530 private Topic fetchDefaultUser() {
531 return getUsernameOrThrow(DEFAULT_USERNAME);
532 }
533
534 private Topic getUsernameOrThrow(String username) {
535 Topic usernameTopic = getUsername(username);
536 if (usernameTopic == null) {
537 throw new RuntimeException("User \"" + username + "\" does not exist");
538 }
539 return usernameTopic;
540 }
541
542 private void joinIfWorkspace(Topic topic) {
543 if (topic.getTypeUri().equals("dm4.workspaces.workspace")) {
544 String username = getUsername();
545 // Note: when the default workspace is created there is no user logged in yet.
546 // The default user is assigned to the default workspace later on (see allPluginsActive()).
547 if (username != null) {
548 joinWorkspace(username, topic.getId());
549 }
550 }
551 }
552
553
554
555 // === All Plugins Activated ===
556
557 private void assignToDefaultWorkspace(Topic topic, String info) {
558 String operation = "### Assigning the " + info + " to the default workspace (\"DeepaMehta\")";
559 try {
560 // abort if already assigned
561 List<RelatedTopic> workspaces = wsService.getAssignedWorkspaces(topic);
562 if (workspaces.size() != 0) {
563 logger.info("### Assigning the " + info + " to a workspace ABORTED -- " +
564 "already assigned (" + DeepaMehtaUtils.topicNames(workspaces) + ")");
565 return;
566 }
567 //
568 logger.info(operation);
569 Topic defaultWorkspace = wsService.getDefaultWorkspace();
570 wsService.assignToWorkspace(topic, defaultWorkspace.getId());
571 } catch (Exception e) {
572 throw new RuntimeException(operation + " failed", e);
573 }
574 }
575
576 private void setupAccessControlForDefaultTopicmap(Topic defaultTopicmap) {
577 String operation = "### Setup access control for the default topicmap (\"untitled\")";
578 try {
579 // Note: we only check for creator assignment.
580 // If an object has a creator assignment it is expected to have an ACL entry as well.
581 if (getCreator(defaultTopicmap) != null) {
582 logger.info(operation + " ABORTED -- already setup");
583 return;
584 }
585 //
586 logger.info(operation);
587 setupAccessControl(defaultTopicmap, DEFAULT_INSTANCE_ACL, DEFAULT_USERNAME);
588 } catch (Exception e) {
589 throw new RuntimeException(operation + " failed", e);
590 }
591 }
592
593 private Topic fetchDefaultTopicmap() {
594 // Note: the Access Control plugin does not DEPEND on the Topicmaps plugin but is designed to work TOGETHER
595 // with the Topicmaps plugin.
596 // Currently the Access Control plugin needs to know some Topicmaps internals e.g. the URI of the default
597 // topicmap. ### TODO: make "optional plugin dependencies" an explicit concept. Plugins must be able to ask
598 // the core weather a certain plugin is installed (regardles weather it is activated already) and would wait
599 // for its service only if installed.
600 return dms.getTopic("uri", new SimpleValue("dm4.topicmaps.default_topicmap"));
601 }
602
603
604
605 // === Request Filter ===
606
607 private void requestFilter(HttpServletRequest request) {
608 logger.fine("##### " + request.getMethod() + " " + request.getRequestURL() +
609 "\n ##### \"Authorization\"=\"" + request.getHeader("Authorization") + "\"" +
610 "\n ##### " + info(request.getSession(false))); // create=false
611 //
612 checkRequestOrigin(request); // throws WebApplicationException
613 checkAuthorization(request); // throws WebApplicationException
614 }
615
616 // ---
617
618 private void checkRequestOrigin(HttpServletRequest request) {
619 String remoteAddr = request.getRemoteAddr();
620 boolean allowed = JavaUtils.isInRange(remoteAddr, SUBNET_FILTER);
621 //
622 logger.fine("Remote address=\"" + remoteAddr + "\", dm4.security.subnet_filter=\"" + SUBNET_FILTER +
623 "\" => " + (allowed ? "ALLOWED" : "FORBIDDEN"));
624 //
625 if (!allowed) {
626 throw403Forbidden(); // throws WebApplicationException
627 }
628 }
629
630 private void checkAuthorization(HttpServletRequest request) {
631 boolean authorized;
632 if (request.getSession(false) != null) { // create=false
633 authorized = true;
634 } else {
635 String authHeader = request.getHeader("Authorization");
636 if (authHeader != null) {
637 // Note: if login fails we are NOT authorized, even if no login is required
638 authorized = tryLogin(new Credentials(authHeader), request);
639 } else {
640 authorized = !isLoginRequired(request);
641 }
642 }
643 //
644 if (!authorized) {
645 throw401Unauthorized(); // throws WebApplicationException
646 }
647 }
648
649 // ---
650
651 private boolean isLoginRequired(HttpServletRequest request) {
652 return request.getMethod().equals("GET") ? READ_REQUIRES_LOGIN : WRITE_REQUIRES_LOGIN;
653 }
654
655 /**
656 * Checks weather the credentials are valid and if so logs the user in.
657 *
658 * @return true if the credentials are valid.
659 */
660 private boolean tryLogin(Credentials cred, HttpServletRequest request) {
661 String username = cred.username;
662 if (checkCredentials(cred)) {
663 logger.info("##### Logging in as \"" + username + "\" => SUCCESSFUL!");
664 _login(username, request);
665 return true;
666 } else {
667 logger.info("##### Logging in as \"" + username + "\" => FAILED!");
668 return false;
669 }
670 }
671
672 private boolean checkCredentials(Credentials cred) {
673 Topic username = getUsername(cred.username);
674 if (username == null) {
675 return false;
676 }
677 return matches(username, cred.password);
678 }
679
680 // ---
681
682 private void _login(String username, HttpServletRequest request) {
683 HttpSession session = request.getSession();
684 session.setAttribute("username", username);
685 logger.info("##### Creating new " + info(session));
686 //
687 dms.fireEvent(POST_LOGIN_USER, username);
688 }
689
690 private void _logout(HttpServletRequest request) {
691 HttpSession session = request.getSession(false); // create=false
692 String username = username(session); // save username before invalidating
693 logger.info("##### Logging out from " + info(session));
694 //
695 session.invalidate();
696 //
697 dms.fireEvent(POST_LOGOUT_USER, username);
698 }
699
700 // ---
701
702 /**
703 * Prerequisite: username is not <code>null</code>.
704 *
705 * @param password The encrypted password.
706 */
707 private boolean matches(Topic username, String password) {
708 return password(fetchUserAccount(username)).equals(password);
709 }
710
711 /**
712 * Prerequisite: username is not <code>null</code>.
713 */
714 private Topic fetchUserAccount(Topic username) {
715 Topic userAccount = username.getRelatedTopic("dm4.core.composition", "dm4.core.child", "dm4.core.parent",
716 "dm4.accesscontrol.user_account");
717 if (userAccount == null) {
718 throw new RuntimeException("Data inconsistency: there is no User Account topic for username \"" +
719 username.getSimpleValue() + "\" (username=" + username + ")");
720 }
721 return userAccount;
722 }
723
724 // ---
725
726 private String username(HttpSession session) {
727 String username = (String) session.getAttribute("username");
728 if (username == null) {
729 throw new RuntimeException("Session data inconsistency: \"username\" attribute is missing");
730 }
731 return username;
732 }
733
734 /**
735 * @return The encryted password of the specified User Account.
736 */
737 private String password(Topic userAccount) {
738 return userAccount.getChildTopics().getString("dm4.accesscontrol.password");
739 }
740
741 // ---
742
743 private void throw401Unauthorized() {
744 // Note: a non-private DM installation (read_requires_login=false) utilizes DM's login dialog and must suppress
745 // the browser's login dialog. To suppress the browser's login dialog a contrived authentication scheme "xBasic"
746 // is used (see http://loudvchar.blogspot.ca/2010/11/avoiding-browser-popup-for-401.html)
747 String authScheme = READ_REQUIRES_LOGIN ? "Basic" : "xBasic";
748 throw new WebApplicationException(Response.status(Status.UNAUTHORIZED)
749 .header("WWW-Authenticate", authScheme + " realm=" + AUTHENTICATION_REALM)
750 .header("Content-Type", "text/html") // for text/plain (default) Safari provides no Web Console
751 .entity("You're not authorized. Sorry.")
752 .build());
753 }
754
755 private void throw403Forbidden() {
756 throw new WebApplicationException(Response.status(Status.FORBIDDEN)
757 .header("Content-Type", "text/html") // for text/plain (default) Safari provides no Web Console
758 .entity("Access is forbidden. Sorry.")
759 .build());
760 }
761
762
763
764 // === Create ACL Entries ===
765
766 /**
767 * Sets the logged in user as the creator and the owner of the specified object
768 * and creates a default access control entry for it.
769 *
770 * If no user is logged in, nothing is performed.
771 */
772 private void setupDefaultAccessControl(DeepaMehtaObject object) {
773 setupAccessControl(object, DEFAULT_INSTANCE_ACL);
774 }
775
776 private void setupDefaultAccessControl(Type type) {
777 try {
778 String username = getUsername();
779 //
780 if (username == null) {
781 username = DEFAULT_USERNAME;
782 setupViewConfigAccessControl(type.getViewConfig());
783 }
784 //
785 setupAccessControl(type, DEFAULT_TYPE_ACL, username);
786 } catch (Exception e) {
787 throw new RuntimeException("Setting up access control for " + info(type) + " failed (" + type + ")", e);
788 }
789 }
790
791 // ---
792
793 private void setupUserAccountAccessControl(Topic topic) {
794 setupAccessControl(topic, DEFAULT_USER_ACCOUNT_ACL);
795 }
796
797 private void setupViewConfigAccessControl(ViewConfiguration viewConfig) {
798 for (Topic configTopic : viewConfig.getConfigTopics()) {
799 setupAccessControl(configTopic, DEFAULT_INSTANCE_ACL, DEFAULT_USERNAME);
800 }
801 }
802
803 // ---
804
805 private void setupAccessControl(DeepaMehtaObject object, AccessControlList acl) {
806 try {
807 String username = getUsername();
808 // Note: when no user is logged in we do NOT fallback to the default user for the access control setup.
809 // This would not help in gaining data consistency because the topics/associations created so far
810 // (BEFORE the Access Control plugin is activated) would still have no access control setup.
811 // Note: for types the situation is different. The type-introduction mechanism (see introduceTopicType()
812 // handler above) ensures EVERY type is catched (regardless of plugin activation order). For instances on
813 // the other hand we don't have such a mechanism (and don't want one either).
814 if (username == null) {
815 logger.fine("Setting up access control for " + info(object) + " ABORTED -- no user is logged in");
816 return;
817 }
818 //
819 setupAccessControl(object, acl, username);
820 } catch (Exception e) {
821 throw new RuntimeException("Setting up access control for " + info(object) + " failed (" + object + ")", e);
822 }
823 }
824
825 /**
826 * @param username must not be null.
827 */
828 private void setupAccessControl(DeepaMehtaObject object, AccessControlList acl, String username) {
829 setCreator(object, username);
830 setOwner(object, username);
831 setACL(object, acl);
832 }
833
834
835
836 // === Determine Permissions ===
837
838 private Permissions getPermissions(DeepaMehtaObject object) {
839 return createPermissions(hasPermission(getUsername(), Operation.WRITE, object));
840 }
841
842 private Permissions getPermissions(Type type) {
843 String username = getUsername();
844 return createPermissions(hasPermission(username, Operation.WRITE, type),
845 hasPermission(username, Operation.CREATE, type));
846 }
847
848 // ---
849
850 /**
851 * Checks if a user is allowed to perform an operation on an object (topic or association).
852 * If so, <code>true</code> is returned.
853 *
854 * @param username the logged in user (a Topic of type "Username" / <code>dm4.accesscontrol.username</code>),
855 * or <code>null</code> if no user is logged in.
856 */
857 private boolean hasPermission(String username, Operation operation, DeepaMehtaObject object) {
858 try {
859 logger.fine("Determining permission for " + userInfo(username) + " to " + operation + " " + info(object));
860 UserRole[] userRoles = getACL(object).getUserRoles(operation);
861 for (UserRole userRole : userRoles) {
862 logger.fine("There is an ACL entry for user role " + userRole);
863 if (userOccupiesRole(username, userRole, object)) {
864 logger.fine("=> ALLOWED");
865 return true;
866 }
867 }
868 logger.fine("=> DENIED");
869 return false;
870 } catch (Exception e) {
871 throw new RuntimeException("Determining permission for " + info(object) + " failed (" +
872 userInfo(username) + ", operation=" + operation + ")", e);
873 }
874 }
875
876 /**
877 * Checks if a user occupies a role with regard to the specified object.
878 * If so, <code>true</code> is returned.
879 *
880 * @param username the logged in user (a Topic of type "Username" / <code>dm4.accesscontrol.username</code>),
881 * or <code>null</code> if no user is logged in.
882 */
883 private boolean userOccupiesRole(String username, UserRole userRole, DeepaMehtaObject object) {
884 switch (userRole) {
885 case EVERYONE:
886 return true;
887 case USER:
888 return username != null;
889 case MEMBER:
890 return username != null && userIsMember(username, object);
891 case OWNER:
892 return username != null && userIsOwner(username, object);
893 case CREATOR:
894 return username != null && userIsCreator(username, object);
895 default:
896 throw new RuntimeException(userRole + " is an unsupported user role");
897 }
898 }
899
900 // ---
901
902 /**
903 * Checks if a user is a member of any workspace the object is assigned to.
904 * If so, <code>true</code> is returned.
905 *
906 * Prerequisite: a user is logged in (<code>username</code> is not <code>null</code>).
907 *
908 * @param username a Topic of type "Username" (<code>dm4.accesscontrol.username</code>). ### FIXDOC
909 * @param object the object in question.
910 */
911 private boolean userIsMember(String username, DeepaMehtaObject object) {
912 Topic usernameTopic = getUsernameOrThrow(username);
913 List<RelatedTopic> workspaces = wsService.getAssignedWorkspaces(object);
914 logger.fine(info(object) + " is assigned to " + workspaces.size() + " workspaces");
915 for (RelatedTopic workspace : workspaces) {
916 if (wsService.isAssignedToWorkspace(usernameTopic, workspace.getId())) {
917 logger.fine(userInfo(username) + " IS member of workspace " + workspace);
918 return true;
919 } else {
920 logger.fine(userInfo(username) + " is NOT member of workspace " + workspace);
921 }
922 }
923 return false;
924 }
925
926 /**
927 * Checks if a user is the owner of the object.
928 * If so, <code>true</code> is returned.
929 *
930 * Prerequisite: a user is logged in (<code>username</code> is not <code>null</code>).
931 *
932 * @param username a Topic of type "Username" (<code>dm4.accesscontrol.username</code>). ### FIXDOC
933 */
934 private boolean userIsOwner(String username, DeepaMehtaObject object) {
935 String owner = getOwner(object);
936 logger.fine("The owner is " + userInfo(owner));
937 return owner != null && owner.equals(username);
938 }
939
940 /**
941 * Checks if a user is the creator of the object.
942 * If so, <code>true</code> is returned.
943 *
944 * Prerequisite: a user is logged in (<code>username</code> is not <code>null</code>).
945 *
946 * @param username a Topic of type "Username" (<code>dm4.accesscontrol.username</code>). ### FIXDOC
947 */
948 private boolean userIsCreator(String username, DeepaMehtaObject object) {
949 String creator = getCreator(object);
950 logger.fine("The creator is " + userInfo(creator));
951 return creator != null && creator.equals(username);
952 }
953
954 // ---
955
956 private void enrichWithPermissions(Type type, Permissions permissions) {
957 // Note: we must extend/override possibly existing permissions.
958 // Consider a type update: directive UPDATE_TOPIC_TYPE is followed by UPDATE_TOPIC, both on the same object.
959 ChildTopicsModel typePermissions = permissions(type);
960 typePermissions.put(Operation.WRITE.uri, permissions.get(Operation.WRITE.uri));
961 typePermissions.put(Operation.CREATE.uri, permissions.get(Operation.CREATE.uri));
962 }
963
964 private ChildTopicsModel permissions(DeepaMehtaObject object) {
965 // Note 1: "dm4.accesscontrol.permissions" is a contrived URI. There is no such type definition.
966 // Permissions are for transfer only, recalculated for each request, not stored in DB.
967 // Note 2: The permissions topic exists only in the object's model (see note below).
968 // There is no corresponding topic in the attached composite value. So we must query the model here.
969 // (object.getChildTopics().getTopic(...) would not work)
970 TopicModel permissionsTopic = object.getChildTopics().getModel()
971 .getTopic("dm4.accesscontrol.permissions", null);
972 ChildTopicsModel permissions;
973 if (permissionsTopic != null) {
974 permissions = permissionsTopic.getChildTopicsModel();
975 } else {
976 permissions = new ChildTopicsModel();
977 // Note: we put the permissions topic directly in the model here (instead of the attached composite value).
978 // The "permissions" topic is for transfer only. It must not be stored in the DB (as it would when putting
979 // it in the attached composite value).
980 object.getChildTopics().getModel().put("dm4.accesscontrol.permissions", permissions);
981 }
982 return permissions;
983 }
984
985 // ---
986
987 private Permissions createPermissions(boolean write) {
988 return new Permissions().add(Operation.WRITE, write);
989 }
990
991 private Permissions createPermissions(boolean write, boolean create) {
992 return createPermissions(write).add(Operation.CREATE, create);
993 }
994
995
996
997 // === Logging ===
998
999 private String info(DeepaMehtaObject object) {
1000 if (object instanceof TopicType) {
1001 return "topic type \"" + object.getUri() + "\" (id=" + object.getId() + ")";
1002 } else if (object instanceof AssociationType) {
1003 return "association type \"" + object.getUri() + "\" (id=" + object.getId() + ")";
1004 } else if (object instanceof Topic) {
1005 return "topic " + object.getId() + " (typeUri=\"" + object.getTypeUri() + "\", uri=\"" + object.getUri() +
1006 "\")";
1007 } else if (object instanceof Association) {
1008 return "association " + object.getId() + " (typeUri=\"" + object.getTypeUri() + "\")";
1009 } else {
1010 throw new RuntimeException("Unexpected object: " + object);
1011 }
1012 }
1013
1014 private String userInfo(String username) {
1015 return "user " + (username != null ? "\"" + username + "\"" : "<anonymous>");
1016 }
1017
1018 private String info(HttpSession session) {
1019 return "session" + (session != null ? " " + session.getId() +
1020 " (username=" + username(session) + ")" : ": null");
1021 }
1022
1023 private String info(HttpServletRequest request) {
1024 StringBuilder info = new StringBuilder();
1025 info.append(" " + request.getMethod() + " " + request.getRequestURI() + "\n");
1026 Enumeration<String> e1 = request.getHeaderNames();
1027 while (e1.hasMoreElements()) {
1028 String name = e1.nextElement();
1029 info.append("\n " + name + ":");
1030 Enumeration<String> e2 = request.getHeaders(name);
1031 while (e2.hasMoreElements()) {
1032 String header = e2.nextElement();
1033 info.append(" " + header);
1034 }
1035 }
1036 return info.toString();
1037 }
1038 }