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.service.AccessControlService; 006 import de.deepamehta.plugins.workspaces.service.WorkspacesService; 007 008 import de.deepamehta.core.Association; 009 import de.deepamehta.core.AssociationType; 010 import de.deepamehta.core.ChildTopics; 011 import de.deepamehta.core.DeepaMehtaObject; 012 import de.deepamehta.core.RelatedTopic; 013 import de.deepamehta.core.Topic; 014 import de.deepamehta.core.TopicType; 015 import de.deepamehta.core.Type; 016 import de.deepamehta.core.ViewConfiguration; 017 import de.deepamehta.core.model.AssociationModel; 018 import de.deepamehta.core.model.ChildTopicsModel; 019 import de.deepamehta.core.model.SimpleValue; 020 import de.deepamehta.core.model.TopicModel; 021 import de.deepamehta.core.model.TopicRoleModel; 022 import de.deepamehta.core.osgi.PluginActivator; 023 import de.deepamehta.core.service.DeepaMehtaEvent; 024 import de.deepamehta.core.service.EventListener; 025 import de.deepamehta.core.service.Inject; 026 import de.deepamehta.core.service.Transactional; 027 import de.deepamehta.core.service.accesscontrol.AccessControlException; 028 import de.deepamehta.core.service.accesscontrol.Credentials; 029 import de.deepamehta.core.service.accesscontrol.Operation; 030 import de.deepamehta.core.service.accesscontrol.Permissions; 031 import de.deepamehta.core.service.accesscontrol.SharingMode; 032 import de.deepamehta.core.service.event.PostCreateAssociationListener; 033 import de.deepamehta.core.service.event.PostCreateTopicListener; 034 import de.deepamehta.core.service.event.PostUpdateAssociationListener; 035 import de.deepamehta.core.service.event.PostUpdateTopicListener; 036 import de.deepamehta.core.service.event.PreGetAssociationListener; 037 import de.deepamehta.core.service.event.PreGetTopicListener; 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.JavaUtils; 042 043 // ### TODO: hide Jersey internals. Move to JAX-RS 2.0. 044 import com.sun.jersey.spi.container.ContainerRequest; 045 046 import javax.servlet.http.HttpServletRequest; 047 import javax.servlet.http.HttpSession; 048 049 import javax.ws.rs.GET; 050 import javax.ws.rs.PUT; 051 import javax.ws.rs.POST; 052 import javax.ws.rs.DELETE; 053 import javax.ws.rs.Consumes; 054 import javax.ws.rs.Path; 055 import javax.ws.rs.PathParam; 056 import javax.ws.rs.Produces; 057 import javax.ws.rs.WebApplicationException; 058 import javax.ws.rs.core.Context; 059 import javax.ws.rs.core.Response; 060 import javax.ws.rs.core.Response.Status; 061 062 import java.util.Collection; 063 import java.util.Enumeration; 064 import java.util.logging.Logger; 065 066 067 068 @Path("/accesscontrol") 069 @Consumes("application/json") 070 @Produces("application/json") 071 public class AccessControlPlugin extends PluginActivator implements AccessControlService, PreGetTopicListener, 072 PreGetAssociationListener, 073 PostCreateTopicListener, 074 PostCreateAssociationListener, 075 PostUpdateTopicListener, 076 PostUpdateAssociationListener, 077 ServiceRequestFilterListener, 078 ResourceRequestFilterListener { 079 080 // ------------------------------------------------------------------------------------------------------- Constants 081 082 // Security settings 083 private static final boolean READ_REQUIRES_LOGIN = Boolean.parseBoolean( 084 System.getProperty("dm4.security.read_requires_login", "false")); 085 private static final boolean WRITE_REQUIRES_LOGIN = Boolean.parseBoolean( 086 System.getProperty("dm4.security.write_requires_login", "true")); 087 private static final String SUBNET_FILTER = System.getProperty("dm4.security.subnet_filter", "127.0.0.1/32"); 088 // Note: the default values are required in case no config file is in effect. This applies when DM is started 089 // via feature:install from Karaf. The default values must match the values defined in global POM. 090 091 private static final String AUTHENTICATION_REALM = "DeepaMehta"; 092 093 // Associations 094 private static final String MEMBERSHIP_TYPE = "dm4.accesscontrol.membership"; 095 096 // Property URIs 097 private static String PROP_CREATOR = "dm4.accesscontrol.creator"; 098 private static String PROP_OWNER = "dm4.accesscontrol.owner"; 099 private static String PROP_MODIFIER = "dm4.accesscontrol.modifier"; 100 101 // Events 102 private static DeepaMehtaEvent POST_LOGIN_USER = new DeepaMehtaEvent(PostLoginUserListener.class) { 103 @Override 104 public void deliver(EventListener listener, Object... params) { 105 ((PostLoginUserListener) listener).postLoginUser( 106 (String) params[0] 107 ); 108 } 109 }; 110 private static DeepaMehtaEvent POST_LOGOUT_USER = new DeepaMehtaEvent(PostLogoutUserListener.class) { 111 @Override 112 public void deliver(EventListener listener, Object... params) { 113 ((PostLogoutUserListener) listener).postLogoutUser( 114 (String) params[0] 115 ); 116 } 117 }; 118 119 // ---------------------------------------------------------------------------------------------- Instance Variables 120 121 @Inject 122 private WorkspacesService wsService; 123 124 @Context 125 private HttpServletRequest request; 126 127 private Logger logger = Logger.getLogger(getClass().getName()); 128 129 // -------------------------------------------------------------------------------------------------- Public Methods 130 131 132 133 // ******************************************* 134 // *** AccessControlService Implementation *** 135 // ******************************************* 136 137 138 139 // === User Session === 140 141 @POST 142 @Path("/login") 143 @Override 144 public void login() { 145 // Note: the actual login is performed by the request filter. See requestFilter(). 146 } 147 148 @POST 149 @Path("/logout") 150 @Override 151 public void logout() { 152 _logout(request); 153 // 154 // For a "private" DeepaMehta installation: emulate a HTTP logout by forcing the webbrowser to bring up its 155 // login dialog and to forget the former Authorization information. The user is supposed to press "Cancel". 156 // The login dialog can't be used to login again. 157 if (READ_REQUIRES_LOGIN) { 158 throw401Unauthorized(); 159 } 160 } 161 162 // --- 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: this happens if a request method is called outside request scope. 177 // This is the case while system startup. 178 return null; // user is unknown 179 } 180 } 181 182 183 184 // === User Accounts === 185 186 @POST 187 @Path("/user_account") 188 @Transactional 189 @Override 190 public Topic createUserAccount(Credentials cred) { 191 String username = cred.username; 192 logger.info("Creating user account \"" + username + "\""); 193 // 194 // 1) create user account 195 Topic userAccount = dms.createTopic(new TopicModel("dm4.accesscontrol.user_account", new ChildTopicsModel() 196 .put("dm4.accesscontrol.username", username) 197 .put("dm4.accesscontrol.password", cred.password))); 198 ChildTopics childTopics = userAccount.getChildTopics(); 199 Topic usernameTopic = childTopics.getTopic("dm4.accesscontrol.username"); 200 Topic passwordTopic = childTopics.getTopic("dm4.accesscontrol.password"); 201 // 202 // 2) create private workspace 203 Topic privateWorkspace = wsService.createWorkspace(DEFAULT_PRIVATE_WORKSPACE_NAME, null, SharingMode.PRIVATE); 204 setWorkspaceOwner(privateWorkspace, username); 205 // Note: we don't set a particular creator/modifier here as we don't want suggest that the new user's private 206 // workspace has been created by the new user itself. Instead we set the *current* user as the creator/modifier 207 // (via postCreateTopic() listener). In case of the "admin" user account the creator/modifier remain undefined 208 // as it is actually created by the system itself. 209 // 210 // 3) assign user account and password to private workspace 211 // Note: the current user has no READ access to the private workspace just created. 212 // So we must use the privileged assignToWorkspace calls here (instead of using the Workspaces service). 213 long privateWorkspaceId = privateWorkspace.getId(); 214 dms.getAccessControl().assignToWorkspace(userAccount, privateWorkspaceId); 215 dms.getAccessControl().assignToWorkspace(passwordTopic, privateWorkspaceId); 216 // 217 // 4) assign username to "System" workspace 218 Topic systemWorkspace = wsService.getWorkspace(SYSTEM_WORKSPACE_URI); 219 wsService.assignToWorkspace(usernameTopic, systemWorkspace.getId()); 220 // 221 return usernameTopic; 222 } 223 224 @GET 225 @Path("/user/workspace") 226 @Override 227 public Topic getPrivateWorkspace() { 228 String username = getUsername(); 229 if (username == null) { 230 throw new IllegalStateException("No user is logged in"); 231 } 232 // 233 Topic passwordTopic = getPasswordTopic(getUserAccount(getUsernameTopic(username))); 234 Topic workspace = wsService.getAssignedWorkspace(passwordTopic.getId()); 235 if (workspace == null) { 236 throw new RuntimeException("User \"" + username + "\" has no private workspace"); 237 } 238 return workspace; 239 } 240 241 @Override 242 public Topic getUsernameTopic(String username) { 243 return dms.getTopic("dm4.accesscontrol.username", new SimpleValue(username)); 244 } 245 246 247 248 // === Workspaces / Memberships === 249 250 @GET 251 @Path("/workspace/{workspace_id}/owner") 252 @Produces("text/plain") 253 @Override 254 public String getWorkspaceOwner(@PathParam("workspace_id") long workspaceId) { 255 // ### TODO: delegate to Core's AccessControl.getOwner()? 256 return dms.hasProperty(workspaceId, PROP_OWNER) ? (String) dms.getProperty(workspaceId, PROP_OWNER) : null; 257 } 258 259 @Override 260 public void setWorkspaceOwner(Topic workspace, String username) { 261 try { 262 workspace.setProperty(PROP_OWNER, username, true); // addToIndex=true 263 } catch (Exception e) { 264 throw new RuntimeException("Setting the workspace owner of " + info(workspace) + " failed (username=" + 265 username + ")", e); 266 } 267 } 268 269 // --- 270 271 @POST 272 @Path("/user/{username}/workspace/{workspace_id}") 273 @Transactional 274 @Override 275 public void createMembership(@PathParam("username") String username, @PathParam("workspace_id") long workspaceId) { 276 try { 277 dms.createAssociation(new AssociationModel(MEMBERSHIP_TYPE, 278 new TopicRoleModel(getUsernameTopicOrThrow(username).getId(), "dm4.core.default"), 279 new TopicRoleModel(workspaceId, "dm4.core.default") 280 )); 281 } catch (Exception e) { 282 throw new RuntimeException("Creating membership for user \"" + username + "\" and workspace " + 283 workspaceId + " failed", e); 284 } 285 } 286 287 @Override 288 public boolean isMember(String username, long workspaceId) { 289 return dms.getAccessControl().isMember(username, workspaceId); 290 } 291 292 293 294 // === Permissions === 295 296 @GET 297 @Path("/topic/{id}") 298 @Override 299 public Permissions getTopicPermissions(@PathParam("id") long topicId) { 300 return getPermissions(topicId); 301 } 302 303 @GET 304 @Path("/association/{id}") 305 @Override 306 public Permissions getAssociationPermissions(@PathParam("id") long assocId) { 307 return getPermissions(assocId); 308 } 309 310 311 312 // === Object Info === 313 314 @GET 315 @Path("/object/{id}/creator") 316 @Produces("text/plain") 317 @Override 318 public String getCreator(@PathParam("id") long objectId) { 319 return dms.hasProperty(objectId, PROP_CREATOR) ? (String) dms.getProperty(objectId, PROP_CREATOR) : null; 320 } 321 322 @GET 323 @Path("/object/{id}/modifier") 324 @Produces("text/plain") 325 @Override 326 public String getModifier(@PathParam("id") long objectId) { 327 return dms.hasProperty(objectId, PROP_MODIFIER) ? (String) dms.getProperty(objectId, PROP_MODIFIER) : null; 328 } 329 330 331 332 // === Retrieval === 333 334 @GET 335 @Path("/creator/{username}/topics") 336 @Override 337 public Collection<Topic> getTopicsByCreator(@PathParam("username") String username) { 338 return dms.getTopicsByProperty(PROP_CREATOR, username); 339 } 340 341 @GET 342 @Path("/owner/{username}/topics") 343 @Override 344 public Collection<Topic> getTopicsByOwner(@PathParam("username") String username) { 345 return dms.getTopicsByProperty(PROP_OWNER, username); 346 } 347 348 @GET 349 @Path("/creator/{username}/assocs") 350 @Override 351 public Collection<Association> getAssociationsByCreator(@PathParam("username") String username) { 352 return dms.getAssociationsByProperty(PROP_CREATOR, username); 353 } 354 355 @GET 356 @Path("/owner/{username}/assocs") 357 @Override 358 public Collection<Association> getAssociationsByOwner(@PathParam("username") String username) { 359 return dms.getAssociationsByProperty(PROP_OWNER, username); 360 } 361 362 363 364 // **************************** 365 // *** Hook Implementations *** 366 // **************************** 367 368 369 370 @Override 371 public void init() { 372 logger.info("Security settings:" + 373 "\ndm4.security.read_requires_login=" + READ_REQUIRES_LOGIN + 374 "\ndm4.security.write_requires_login=" + WRITE_REQUIRES_LOGIN + 375 "\ndm4.security.subnet_filter=\"" + SUBNET_FILTER + "\""); 376 } 377 378 379 380 // ******************************** 381 // *** Listener Implementations *** 382 // ******************************** 383 384 385 386 @Override 387 public void preGetTopic(long topicId) { 388 checkReadPermission(topicId); 389 } 390 391 @Override 392 public void preGetAssociation(long assocId) { 393 checkReadPermission(assocId); 394 // 395 long[] playerIds = dms.getPlayerIds(assocId); 396 checkReadPermission(playerIds[0]); 397 checkReadPermission(playerIds[1]); 398 } 399 400 // --- 401 402 @Override 403 public void postCreateTopic(Topic topic) { 404 String typeUri = topic.getTypeUri(); 405 if (typeUri.equals("dm4.workspaces.workspace")) { 406 setWorkspaceOwner(topic); 407 } else if (typeUri.equals("dm4.webclient.search")) { 408 assignSearchTopic(topic); 409 } 410 // 411 setCreatorAndModifier(topic); 412 } 413 414 @Override 415 public void postCreateAssociation(Association assoc) { 416 setCreatorAndModifier(assoc); 417 } 418 419 // --- 420 421 // ### TODO: revise/drop this method. Meanwhile a user account is created via dialog. 422 @Override 423 public void postUpdateTopic(Topic topic, TopicModel newModel, TopicModel oldModel) { 424 if (topic.getTypeUri().equals("dm4.accesscontrol.user_account")) { 425 Topic usernameTopic = topic.getChildTopics().getTopic("dm4.accesscontrol.username"); 426 Topic passwordTopic = topic.getChildTopics().getTopic("dm4.accesscontrol.password"); 427 String newUsername = usernameTopic.getSimpleValue().toString(); 428 TopicModel oldUsernameTopic = oldModel.getChildTopicsModel().getTopic("dm4.accesscontrol.username", 429 null); 430 String oldUsername = oldUsernameTopic != null ? oldUsernameTopic.getSimpleValue().toString() : ""; 431 if (!newUsername.equals(oldUsername)) { 432 // 433 if (!oldUsername.equals("")) { 434 throw new RuntimeException("Changing a Username is not supported (tried \"" + oldUsername + 435 "\" -> \"" + newUsername + "\")"); 436 } 437 // 438 logger.info("### Username has changed from \"" + oldUsername + "\" -> \"" + newUsername + 439 "\". Setting \"" + newUsername + "\" as the new owner of 3 topics:\n" + 440 " - User Account topic (ID " + topic.getId() + ")\n" + 441 " - Username topic (ID " + usernameTopic.getId() + ")\n" + 442 " - Password topic (ID " + passwordTopic.getId() + ")"); 443 // ### setOwner(topic, newUsername); 444 // ### setOwner(usernameTopic, newUsername); 445 // ### setOwner(passwordTopic, newUsername); 446 } 447 } 448 // 449 setModifier(topic); 450 } 451 452 @Override 453 public void postUpdateAssociation(Association assoc, AssociationModel oldModel) { 454 if (isMembership(assoc.getModel())) { 455 if (isMembership(oldModel)) { 456 // ### TODO? 457 } else { 458 wsService.assignToWorkspace(assoc, assoc.getTopicByType("dm4.workspaces.workspace").getId()); 459 } 460 } else if (isMembership(oldModel)) { 461 // ### TODO? 462 } 463 // 464 setModifier(assoc); 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 // ------------------------------------------------------------------------------------------------- Private Methods 484 485 private Topic getUserAccount(Topic usernameTopic) { 486 return usernameTopic.getRelatedTopic("dm4.core.composition", "dm4.core.child", "dm4.core.parent", 487 "dm4.accesscontrol.user_account"); 488 } 489 490 private Topic getPasswordTopic(Topic userAccount) { 491 return userAccount.getChildTopics().getTopic("dm4.accesscontrol.password"); 492 } 493 494 private Topic getUsernameTopicOrThrow(String username) { 495 Topic usernameTopic = getUsernameTopic(username); 496 if (usernameTopic == null) { 497 throw new RuntimeException("User \"" + username + "\" does not exist"); 498 } 499 return usernameTopic; 500 } 501 502 private boolean isMembership(AssociationModel assoc) { 503 return assoc.getTypeUri().equals(MEMBERSHIP_TYPE); 504 } 505 506 private void assignSearchTopic(Topic searchTopic) { 507 try { 508 Topic workspace; 509 if (getUsername() != null) { 510 workspace = getPrivateWorkspace(); 511 } else { 512 workspace = wsService.getWorkspace(WorkspacesService.DEEPAMEHTA_WORKSPACE_URI); 513 } 514 wsService.assignToWorkspace(searchTopic, workspace.getId()); 515 } catch (Exception e) { 516 throw new RuntimeException("Assigning search topic to workspace failed", e); 517 } 518 } 519 520 521 522 // === Request Filter === 523 524 private void requestFilter(HttpServletRequest request) { 525 logger.fine("##### " + request.getMethod() + " " + request.getRequestURL() + 526 "\n ##### \"Authorization\"=\"" + request.getHeader("Authorization") + "\"" + 527 "\n ##### " + info(request.getSession(false))); // create=false 528 // 529 checkRequestOrigin(request); // throws WebApplicationException 530 checkAuthorization(request); // throws WebApplicationException 531 } 532 533 // --- 534 535 private void checkRequestOrigin(HttpServletRequest request) { 536 String remoteAddr = request.getRemoteAddr(); 537 boolean allowed = JavaUtils.isInRange(remoteAddr, SUBNET_FILTER); 538 // 539 logger.fine("Remote address=\"" + remoteAddr + "\", dm4.security.subnet_filter=\"" + SUBNET_FILTER + 540 "\" => " + (allowed ? "ALLOWED" : "FORBIDDEN")); 541 // 542 if (!allowed) { 543 throw403Forbidden(); // throws WebApplicationException 544 } 545 } 546 547 private void checkAuthorization(HttpServletRequest request) { 548 boolean authorized; 549 if (request.getSession(false) != null) { // create=false 550 authorized = true; 551 } else { 552 String authHeader = request.getHeader("Authorization"); 553 if (authHeader != null) { 554 // Note: if login fails we are NOT authorized, even if no login is required 555 authorized = tryLogin(new Credentials(authHeader), request); 556 } else { 557 authorized = !isLoginRequired(request); 558 } 559 } 560 // 561 if (!authorized) { 562 throw401Unauthorized(); // throws WebApplicationException 563 } 564 } 565 566 // --- 567 568 private boolean isLoginRequired(HttpServletRequest request) { 569 return request.getMethod().equals("GET") ? READ_REQUIRES_LOGIN : WRITE_REQUIRES_LOGIN; 570 } 571 572 /** 573 * Checks weather the credentials are valid and if so logs the user in. 574 * 575 * @return true if the credentials are valid. 576 */ 577 private boolean tryLogin(Credentials cred, HttpServletRequest request) { 578 String username = cred.username; 579 if (checkCredentials(cred)) { 580 logger.info("##### Logging in as \"" + username + "\" => SUCCESSFUL!"); 581 _login(username, request); 582 return true; 583 } else { 584 logger.info("##### Logging in as \"" + username + "\" => FAILED!"); 585 return false; 586 } 587 } 588 589 private boolean checkCredentials(Credentials cred) { 590 return dms.getAccessControl().checkCredentials(cred); 591 } 592 593 // --- 594 595 private void _login(String username, HttpServletRequest request) { 596 HttpSession session = request.getSession(); 597 session.setAttribute("username", username); 598 logger.info("##### Creating new " + info(session)); 599 // 600 dms.fireEvent(POST_LOGIN_USER, username); 601 } 602 603 private void _logout(HttpServletRequest request) { 604 HttpSession session = request.getSession(false); // create=false 605 String username = username(session); // save username before invalidating 606 logger.info("##### Logging out from " + info(session)); 607 // 608 session.invalidate(); 609 // 610 dms.fireEvent(POST_LOGOUT_USER, username); 611 } 612 613 // --- 614 615 private String username(HttpSession session) { 616 String username = (String) session.getAttribute("username"); 617 if (username == null) { 618 throw new RuntimeException("Session data inconsistency: \"username\" attribute is missing"); 619 } 620 return username; 621 } 622 623 // --- 624 625 private void throw401Unauthorized() { 626 // Note: a non-private DM installation (read_requires_login=false) utilizes DM's login dialog and must suppress 627 // the browser's login dialog. To suppress the browser's login dialog a contrived authentication scheme "xBasic" 628 // is used (see http://loudvchar.blogspot.ca/2010/11/avoiding-browser-popup-for-401.html) 629 String authScheme = READ_REQUIRES_LOGIN ? "Basic" : "xBasic"; 630 throw new WebApplicationException(Response.status(Status.UNAUTHORIZED) 631 .header("WWW-Authenticate", authScheme + " realm=" + AUTHENTICATION_REALM) 632 .header("Content-Type", "text/html") // for text/plain (default) Safari provides no Web Console 633 .entity("You're not authorized. Sorry.") 634 .build()); 635 } 636 637 private void throw403Forbidden() { 638 throw new WebApplicationException(Response.status(Status.FORBIDDEN) 639 .header("Content-Type", "text/html") // for text/plain (default) Safari provides no Web Console 640 .entity("Access is forbidden. Sorry.") 641 .build()); 642 } 643 644 645 646 // === Setup Access Control === 647 648 /** 649 * Sets the logged in user as the creator/modifier of the given object. 650 * <p> 651 * If no user is logged in, nothing is performed. 652 */ 653 private void setCreatorAndModifier(DeepaMehtaObject object) { 654 try { 655 String username = getUsername(); 656 // Note: when no user is logged in we do NOT fallback to the default user for the access control setup. 657 // This would not help in gaining data consistency because the topics/associations created so far 658 // (BEFORE the Access Control plugin is activated) would still have no access control setup. 659 // Note: for types the situation is different. The type-introduction mechanism (see introduceTopicType() 660 // handler above) ensures EVERY type is catched (regardless of plugin activation order). For instances on 661 // the other hand we don't have such a mechanism (and don't want one either). 662 if (username == null) { 663 logger.fine("Setting the creator/modifier of " + info(object) + " ABORTED -- no user is logged in"); 664 return; 665 } 666 // 667 setCreatorAndModifier(object, username); 668 } catch (Exception e) { 669 throw new RuntimeException("Setting the creator/modifier of " + info(object) + " failed", e); 670 } 671 } 672 673 /** 674 * @param username must not be null. 675 */ 676 private void setCreatorAndModifier(DeepaMehtaObject object, String username) { 677 setCreator(object, username); 678 setModifier(object, username); 679 } 680 681 // --- 682 683 /** 684 * Sets the creator of a topic or an association. 685 */ 686 private void setCreator(DeepaMehtaObject object, String username) { 687 try { 688 object.setProperty(PROP_CREATOR, username, true); // addToIndex=true 689 } catch (Exception e) { 690 throw new RuntimeException("Setting the creator of " + info(object) + " failed (username=" + username + ")", 691 e); 692 } 693 } 694 695 // --- 696 697 private void setModifier(DeepaMehtaObject object) { 698 String username = getUsername(); 699 // Note: when a plugin topic is updated there is no user logged in yet. 700 if (username == null) { 701 return; 702 } 703 // 704 setModifier(object, username); 705 } 706 707 private void setModifier(DeepaMehtaObject object, String username) { 708 object.setProperty(PROP_MODIFIER, username, false); // addToIndex=false 709 } 710 711 // --- 712 713 private void setWorkspaceOwner(Topic workspace) { 714 String username = getUsername(); 715 // Note: username is null if the Access Control plugin is activated already 716 // when a 3rd-party plugin creates a workspace at install-time. 717 if (username == null) { 718 return; 719 } 720 // 721 setWorkspaceOwner(workspace, username); 722 } 723 724 725 726 // === Calculate Permissions === 727 728 /** 729 * @param objectId a topic ID, or an association ID 730 */ 731 private void checkReadPermission(long objectId) { 732 if (!inRequestScope()) { 733 logger.fine("### Object " + objectId + " is accessed by \"System\" -- READ permission is granted"); 734 return; 735 } 736 // 737 String username = getUsername(); 738 if (!hasPermission(username, Operation.READ, objectId)) { 739 throw new AccessControlException(userInfo(username) + " has no READ permission for object " + objectId); 740 } 741 } 742 743 /** 744 * @param objectId a topic ID, or an association ID. 745 */ 746 private Permissions getPermissions(long objectId) { 747 return new Permissions().add(Operation.WRITE, hasPermission(getUsername(), Operation.WRITE, objectId)); 748 } 749 750 /** 751 * Checks if a user is permitted to perform an operation on an object (topic or association). 752 * 753 * @param username the logged in user, or <code>null</code> if no user is logged in. 754 * @param objectId a topic ID, or an association ID. 755 * 756 * @return <code>true</code> if permission is granted, <code>false</code> otherwise. 757 */ 758 private boolean hasPermission(String username, Operation operation, long objectId) { 759 return dms.getAccessControl().hasPermission(username, operation, objectId); 760 } 761 762 private boolean inRequestScope() { 763 try { 764 request.getMethod(); 765 return true; 766 } catch (IllegalStateException e) { 767 // Note: this happens if a request method is called outside request scope. 768 // This is the case while system startup. 769 return false; 770 } 771 } 772 773 774 775 // === Logging === 776 777 private String info(DeepaMehtaObject object) { 778 if (object instanceof TopicType) { 779 return "topic type \"" + object.getUri() + "\" (id=" + object.getId() + ")"; 780 } else if (object instanceof AssociationType) { 781 return "association type \"" + object.getUri() + "\" (id=" + object.getId() + ")"; 782 } else if (object instanceof Topic) { 783 return "topic " + object.getId() + " (typeUri=\"" + object.getTypeUri() + "\", uri=\"" + object.getUri() + 784 "\")"; 785 } else if (object instanceof Association) { 786 return "association " + object.getId() + " (typeUri=\"" + object.getTypeUri() + "\")"; 787 } else { 788 throw new RuntimeException("Unexpected object: " + object); 789 } 790 } 791 792 private String userInfo(String username) { 793 return "user " + (username != null ? "\"" + username + "\"" : "<anonymous>"); 794 } 795 796 private String info(HttpSession session) { 797 return "session" + (session != null ? " " + session.getId() + 798 " (username=" + username(session) + ")" : ": null"); 799 } 800 801 private String info(HttpServletRequest request) { 802 StringBuilder info = new StringBuilder(); 803 info.append(" " + request.getMethod() + " " + request.getRequestURI() + "\n"); 804 Enumeration<String> e1 = request.getHeaderNames(); 805 while (e1.hasMoreElements()) { 806 String name = e1.nextElement(); 807 info.append("\n " + name + ":"); 808 Enumeration<String> e2 = request.getHeaders(name); 809 while (e2.hasMoreElements()) { 810 String header = e2.nextElement(); 811 info.append(" " + header); 812 } 813 } 814 return info.toString(); 815 } 816 }