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