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 }