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