001package de.deepamehta.accesscontrol; 002 003import de.deepamehta.accesscontrol.event.PostLoginUserListener; 004import de.deepamehta.accesscontrol.event.PostLogoutUserListener; 005 006import de.deepamehta.config.ConfigCustomizer; 007import de.deepamehta.config.ConfigDefinition; 008import de.deepamehta.config.ConfigModificationRole; 009import de.deepamehta.config.ConfigService; 010import de.deepamehta.config.ConfigTarget; 011import de.deepamehta.files.FilesService; 012import de.deepamehta.files.event.CheckDiskQuotaListener; 013import de.deepamehta.workspaces.WorkspacesService; 014 015import de.deepamehta.core.Association; 016import de.deepamehta.core.AssociationType; 017import de.deepamehta.core.DeepaMehtaObject; 018import de.deepamehta.core.Topic; 019import de.deepamehta.core.TopicType; 020import de.deepamehta.core.model.AssociationModel; 021import de.deepamehta.core.model.SimpleValue; 022import de.deepamehta.core.model.TopicModel; 023import de.deepamehta.core.osgi.PluginActivator; 024import de.deepamehta.core.service.DeepaMehtaEvent; 025import de.deepamehta.core.service.EventListener; 026import de.deepamehta.core.service.Inject; 027import de.deepamehta.core.service.Transactional; 028import de.deepamehta.core.service.accesscontrol.AccessControl; 029import de.deepamehta.core.service.accesscontrol.AccessControlException; 030import de.deepamehta.core.service.accesscontrol.Credentials; 031import de.deepamehta.core.service.accesscontrol.Operation; 032import de.deepamehta.core.service.accesscontrol.Permissions; 033import de.deepamehta.core.service.accesscontrol.SharingMode; 034import de.deepamehta.core.service.event.CheckAssociationReadAccessListener; 035import de.deepamehta.core.service.event.CheckAssociationWriteAccessListener; 036import de.deepamehta.core.service.event.CheckTopicReadAccessListener; 037import de.deepamehta.core.service.event.CheckTopicWriteAccessListener; 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.PreUpdateTopicListener; 044import de.deepamehta.core.service.event.ServiceRequestFilterListener; 045import de.deepamehta.core.service.event.StaticResourceFilterListener; 046import de.deepamehta.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("dm4.security.anonymous_read_allowed", 100 "ALL"); 101 private static final String ANONYMOUS_WRITE_ALLOWED = System.getProperty("dm4.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("dm4.security.subnet_filter", "127.0.0.1/32"); 106 private static final boolean NEW_ACCOUNTS_ARE_ENABLED = Boolean.parseBoolean( 107 System.getProperty("dm4.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 = "DeepaMehta"; 113 114 // Type URIs 115 private static final String LOGIN_ENABLED_TYPE = "dm4.accesscontrol.login_enabled"; 116 private static final String MEMBERSHIP_TYPE = "dm4.accesscontrol.membership"; 117 118 // Property URIs 119 private static final String PROP_CREATOR = "dm4.accesscontrol.creator"; 120 private static final String PROP_OWNER = "dm4.accesscontrol.owner"; 121 private static final String PROP_MODIFIER = "dm4.accesscontrol.modifier"; 122 123 // Events 124 private static DeepaMehtaEvent POST_LOGIN_USER = new DeepaMehtaEvent(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 DeepaMehtaEvent POST_LOGOUT_USER = new DeepaMehtaEvent(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 dm4.security.anonymous_read_allowed = " + accessFilter.dumpReadSetting() + 162 "\n dm4.security.anonymous_write_allowed = " + accessFilter.dumpWriteSetting() + 163 "\n dm4.security.subnet_filter = " + SUBNET_FILTER + 164 "\n dm4.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" DeepaMehta 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 dm4.getAccessControl().getUsername(request); 208 } 209 210 @GET 211 @Path("/username") 212 @Override 213 public Topic getUsernameTopic() { 214 return dm4.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 dm4.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 = dm4.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 dm4.createTopic(mf.newTopicModel("dm4.accesscontrol.user_account", mf.newChildTopicsModel() 254 .putRef("dm4.accesscontrol.username", usernameTopic.getId()) 255 .put("dm4.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("dm4.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 = dm4.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 dm4.createTopic(mf.newTopicModel("dm4.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 dm4.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 dm4.hasProperty(workspaceId, PROP_OWNER) ? (String) dm4.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 = dm4.createAssociation(mf.newAssociationModel(MEMBERSHIP_TYPE, 347 mf.newTopicRoleModel(getUsernameTopicOrThrow(username).getId(), "dm4.core.default"), 348 mf.newTopicRoleModel(workspaceId, "dm4.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 dm4.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 dm4.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 dm4.hasProperty(objectId, PROP_MODIFIER) ? (String) dm4.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 dm4.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 dm4.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 dm4.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 dm4.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, "dm4.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("dm4.accesscontrol.username")) { 497 throw new RuntimeException("Unexpected configurable topic: " + topic); 498 } 499 // the "admin" account must be enabled regardless of the "dm4.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 = dm4.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("dm4.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("dm4.workspaces.workspace")) { 558 setWorkspaceOwner(topic); 559 } else if (typeUri.equals("dm4.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("dm4.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("dm4.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.DEEPAMEHTA_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 : dm4.getTopicsByType("dm4.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 // 692 checkRequestOrigin(request); // throws WebApplicationException 403 Forbidden 693 checkAuthorization(request); // throws WebApplicationException 401 Unauthorized 694 } 695 696 // --- 697 698 private void checkRequestOrigin(HttpServletRequest request) { 699 String remoteAddr = request.getRemoteAddr(); 700 boolean allowed = JavaUtils.isInRange(remoteAddr, SUBNET_FILTER); 701 // 702 logger.fine("Remote address=\"" + remoteAddr + "\", dm4.security.subnet_filter=\"" + SUBNET_FILTER + 703 "\" => " + (allowed ? "ALLOWED" : "FORBIDDEN")); 704 // 705 if (!allowed) { 706 throw403Forbidden(); // throws WebApplicationException 707 } 708 } 709 710 private void checkAuthorization(HttpServletRequest request) { 711 if (request.getSession(false) != null) { // create=false 712 return; // authorized already 713 } 714 // 715 boolean authorized; 716 String authHeader = request.getHeader("Authorization"); 717 if (authHeader != null) { 718 Credentials cred = new Credentials(authHeader); 719 AuthorizationMethod am = getAuthorizationMethod(cred); 720 // Note: if login fails we are NOT authorized, even if no login is required 721 authorized = tryLogin(cred, am, request); 722 } else { 723 authorized = accessFilter.isAnonymousAccessAllowed(request); 724 } 725 if (!authorized) { 726 // Note: a non-public DM installation (anonymous_read_allowed != "ALL") utilizes the browser's login dialog. 727 // (In contrast a public DM installation utilizes DM's login dialog and must suppress the browser's login 728 // dialog.) 729 throw401Unauthorized(!IS_PUBLIC_INSTALLATION); // throws WebApplicationException 730 } 731 } 732 733 private AuthorizationMethod getAuthorizationMethod(Credentials cred) { 734 AuthorizationMethod am = null; 735 if (!cred.methodName.equals("Basic")) { 736 logger.info("authMethodName: \"" + cred.methodName + "\""); 737 am = getAuthorizationMethod(cred.methodName); 738 } 739 return am; 740 } 741 742 private AuthorizationMethod getAuthorizationMethod(String name) { 743 AuthorizationMethod am = authorizationMethods.get(name); 744 if (am == null) { 745 throw new RuntimeException("Authorization method \"" + name + "\" is not registered"); 746 } 747 return am; 748 } 749 750 // --- 751 752 /** 753 * Checks weather the credentials are valid and if the user account is enabled, and if both checks are positive 754 * logs the user in. 755 * 756 * @return true if the user has logged in. 757 */ 758 private boolean tryLogin(Credentials cred, AuthorizationMethod am, HttpServletRequest request) { 759 String username = cred.username; 760 Topic usernameTopic = checkCredentials(cred, am); 761 if (usernameTopic != null && getLoginEnabled(usernameTopic)) { 762 logger.info("##### Logging in as \"" + username + "\" => SUCCESSFUL!"); 763 _login(username, request); 764 return true; 765 } else { 766 logger.info("##### Logging in as \"" + username + "\" => FAILED!"); 767 return false; 768 } 769 } 770 771 private Topic checkCredentials(Credentials cred, AuthorizationMethod am) { 772 if (am == null) { 773 return dm4.getAccessControl().checkCredentials(cred); 774 } else { 775 return am.checkCredentials(cred); 776 } 777 } 778 779 private boolean getLoginEnabled(Topic usernameTopic) { 780 Topic loginEnabled = dm4.getAccessControl().getConfigTopic(LOGIN_ENABLED_TYPE, usernameTopic.getId()); 781 return loginEnabled.getSimpleValue().booleanValue(); 782 } 783 784 // --- 785 786 private void _login(String username, HttpServletRequest request) { 787 HttpSession session = request.getSession(); 788 session.setAttribute("username", username); 789 logger.info("##### Creating new " + info(session)); 790 // 791 dm4.fireEvent(POST_LOGIN_USER, username); 792 } 793 794 private void _logout(HttpServletRequest request) { 795 HttpSession session = request.getSession(false); // create=false 796 String username = username(session); // save username before invalidating 797 logger.info("##### Logging out from " + info(session)); 798 // 799 session.invalidate(); 800 // 801 dm4.fireEvent(POST_LOGOUT_USER, username); 802 } 803 804 // --- 805 806 private String username(HttpSession session) { 807 return dm4.getAccessControl().username(session); 808 } 809 810 // --- 811 812 private void throw401Unauthorized(boolean showBrowserLoginDialog) { 813 // Note: to suppress the browser's login dialog a contrived authentication scheme "xBasic" 814 // is used (see http://loudvchar.blogspot.ca/2010/11/avoiding-browser-popup-for-401.html) 815 String authScheme = showBrowserLoginDialog ? "Basic" : "xBasic"; 816 throw new WebApplicationException(Response.status(Status.UNAUTHORIZED) 817 .header("WWW-Authenticate", authScheme + " realm=" + AUTHENTICATION_REALM) 818 .header("Content-Type", "text/html") // for text/plain (default) Safari provides no Web Console 819 .entity("You're not authorized. Sorry.") 820 .build()); 821 } 822 823 private void throw403Forbidden() { 824 throw new WebApplicationException(Response.status(Status.FORBIDDEN) 825 .header("Content-Type", "text/html") // for text/plain (default) Safari provides no Web Console 826 .entity("Access is forbidden. Sorry.") 827 .build()); 828 } 829 830 831 832 // === Setup Access Control === 833 834 /** 835 * Sets the logged in user as the creator/modifier of the given object. 836 * <p> 837 * If no user is logged in, nothing is performed. 838 */ 839 private void setCreatorAndModifier(DeepaMehtaObject object) { 840 try { 841 String username = getUsername(); 842 // Note: when no user is logged in we do NOT fallback to the default user for the access control setup. 843 // This would not help in gaining data consistency because the topics/associations created so far 844 // (BEFORE the Access Control plugin is activated) would still have no access control setup. 845 // Note: for types the situation is different. The type-introduction mechanism (see introduceTopicType() 846 // handler above) ensures EVERY type is catched (regardless of plugin activation order). For instances on 847 // the other hand we don't have such a mechanism (and don't want one either). 848 if (username == null) { 849 logger.fine("Setting the creator/modifier of " + info(object) + " SKIPPED -- no user is logged in"); 850 return; 851 } 852 // 853 setCreatorAndModifier(object, username); 854 } catch (Exception e) { 855 throw new RuntimeException("Setting the creator/modifier of " + info(object) + " failed", e); 856 } 857 } 858 859 /** 860 * @param username must not be null. 861 */ 862 private void setCreatorAndModifier(DeepaMehtaObject object, String username) { 863 setCreator(object, username); 864 setModifier(object, username); 865 } 866 867 // --- 868 869 /** 870 * Sets the creator of a topic or an association. 871 */ 872 private void setCreator(DeepaMehtaObject object, String username) { 873 try { 874 object.setProperty(PROP_CREATOR, username, true); // addToIndex=true 875 } catch (Exception e) { 876 throw new RuntimeException("Setting the creator of " + info(object) + " failed (username=" + username + ")", 877 e); 878 } 879 } 880 881 // --- 882 883 private void setModifier(DeepaMehtaObject object) { 884 String username = getUsername(); 885 // Note: when a plugin topic is updated there is no user logged in yet. 886 if (username == null) { 887 return; 888 } 889 // 890 setModifier(object, username); 891 } 892 893 private void setModifier(DeepaMehtaObject object, String username) { 894 object.setProperty(PROP_MODIFIER, username, false); // addToIndex=false 895 } 896 897 // --- 898 899 private void setWorkspaceOwner(Topic workspace) { 900 String username = getUsername(); 901 // Note: username is null if the Access Control plugin is activated already 902 // when a 3rd-party plugin creates a workspace at install-time. 903 if (username == null) { 904 return; 905 } 906 // 907 setWorkspaceOwner(workspace, username); 908 } 909 910 911 912 // === Calculate Permissions === 913 914 /** 915 * @param objectId a topic ID, or an association ID 916 */ 917 private void checkReadAccess(long objectId) { 918 checkAccess(Operation.READ, objectId); 919 } 920 921 /** 922 * @param objectId a topic ID, or an association ID 923 */ 924 private void checkWriteAccess(long objectId) { 925 checkAccess(Operation.WRITE, objectId); 926 } 927 928 // --- 929 930 /** 931 * @param objectId a topic ID, or an association ID 932 */ 933 private void checkAccess(Operation operation, long objectId) { 934 if (!inRequestScope()) { 935 logger.fine("### Object " + objectId + " is accessed by \"System\" -- " + operation + 936 " permission is granted"); 937 return; 938 } 939 // 940 String username = getUsername(); 941 if (!hasPermission(username, operation, objectId)) { 942 throw new AccessControlException(userInfo(username) + " has no " + operation + " permission for object " + 943 objectId); 944 } 945 } 946 947 /** 948 * @param objectId a topic ID, or an association ID. 949 */ 950 private Permissions getPermissions(long objectId) { 951 return new Permissions().add(Operation.WRITE, hasPermission(getUsername(), Operation.WRITE, objectId)); 952 } 953 954 /** 955 * Checks if a user is permitted to perform an operation on an object (topic or association). 956 * 957 * @param username the logged in user, or <code>null</code> if no user is logged in. 958 * @param objectId a topic ID, or an association ID. 959 * 960 * @return <code>true</code> if permission is granted, <code>false</code> otherwise. 961 */ 962 private boolean hasPermission(String username, Operation operation, long objectId) { 963 return dm4.getAccessControl().hasPermission(username, operation, objectId); 964 } 965 966 private boolean inRequestScope() { 967 try { 968 request.getMethod(); 969 return true; 970 } catch (IllegalStateException e) { 971 // Note: this happens if a request method is called outside request scope. 972 // This is the case while system startup. 973 return false; 974 } catch (NullPointerException e) { 975 // While system startup request might be null. 976 // Jersey might not have injected the proxy object yet. 977 return false; 978 } 979 } 980 981 982 983 // === Logging === 984 985 private String info(DeepaMehtaObject object) { 986 if (object instanceof TopicType) { 987 return "topic type \"" + object.getUri() + "\" (id=" + object.getId() + ")"; 988 } else if (object instanceof AssociationType) { 989 return "association type \"" + object.getUri() + "\" (id=" + object.getId() + ")"; 990 } else if (object instanceof Topic) { 991 return "topic " + object.getId() + " (typeUri=\"" + object.getTypeUri() + "\", uri=\"" + object.getUri() + 992 "\")"; 993 } else if (object instanceof Association) { 994 return "association " + object.getId() + " (typeUri=\"" + object.getTypeUri() + "\")"; 995 } else { 996 throw new RuntimeException("Unexpected object: " + object); 997 } 998 } 999 1000 private String userInfo(String username) { 1001 return "user " + (username != null ? "\"" + username + "\"" : "<anonymous>"); 1002 } 1003 1004 private String info(HttpSession session) { 1005 return "session" + (session != null ? " " + session.getId() + 1006 " (username=" + username(session) + ")" : ": null"); 1007 } 1008 1009 private String info(HttpServletRequest request) { 1010 StringBuilder info = new StringBuilder(); 1011 info.append(" " + request.getMethod() + " " + request.getRequestURI() + "\n"); 1012 Enumeration<String> e1 = request.getHeaderNames(); 1013 while (e1.hasMoreElements()) { 1014 String name = e1.nextElement(); 1015 info.append("\n " + name + ":"); 1016 Enumeration<String> e2 = request.getHeaders(name); 1017 while (e2.hasMoreElements()) { 1018 String header = e2.nextElement(); 1019 info.append(" " + header); 1020 } 1021 } 1022 return info.toString(); 1023 } 1024}