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