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