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