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