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