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.ResourceRequestFilterListener; 047import de.deepamehta.core.service.event.ServiceRequestFilterListener; 048import de.deepamehta.core.util.JavaUtils; 049 050// ### TODO: hide Jersey internals. Move to JAX-RS 2.0. 051import com.sun.jersey.spi.container.ContainerRequest; 052 053import javax.servlet.http.HttpServletRequest; 054import javax.servlet.http.HttpSession; 055 056import javax.ws.rs.GET; 057import javax.ws.rs.PUT; 058import javax.ws.rs.POST; 059import javax.ws.rs.DELETE; 060import javax.ws.rs.Consumes; 061import javax.ws.rs.Path; 062import javax.ws.rs.PathParam; 063import javax.ws.rs.Produces; 064import javax.ws.rs.WebApplicationException; 065import javax.ws.rs.core.Context; 066import javax.ws.rs.core.Response; 067import javax.ws.rs.core.Response.Status; 068 069import java.util.Collection; 070import java.util.Enumeration; 071import java.util.concurrent.Callable; 072import java.util.logging.Logger; 073 074 075 076@Path("/accesscontrol") 077@Consumes("application/json") 078@Produces("application/json") 079public class AccessControlPlugin extends PluginActivator implements AccessControlService, ConfigCustomizer, 080 CheckTopicReadAccessListener, 081 CheckTopicWriteAccessListener, 082 CheckAssociationReadAccessListener, 083 CheckAssociationWriteAccessListener, 084 PreCreateTopicListener, 085 PreUpdateTopicListener, 086 PostCreateTopicListener, 087 PostCreateAssociationListener, 088 PostUpdateTopicListener, 089 PostUpdateAssociationListener, 090 ServiceRequestFilterListener, 091 ResourceRequestFilterListener, 092 CheckDiskQuotaListener { 093 094 // ------------------------------------------------------------------------------------------------------- Constants 095 096 // Security settings 097 private static final String ANONYMOUS_READ_ALLOWED = System.getProperty("dm4.security.anonymous_read_allowed", 098 "ALL"); 099 private static final String ANONYMOUS_WRITE_ALLOWED = System.getProperty("dm4.security.anonymous_write_allowed", 100 "NONE"); 101 private static final GlobalRequestFilter requestFilter = new GlobalRequestFilter(ANONYMOUS_READ_ALLOWED, 102 ANONYMOUS_WRITE_ALLOWED); 103 private static final String SUBNET_FILTER = System.getProperty("dm4.security.subnet_filter", "127.0.0.1/32"); 104 private static final boolean NEW_ACCOUNTS_ARE_ENABLED = Boolean.parseBoolean( 105 System.getProperty("dm4.security.new_accounts_are_enabled", "true")); 106 // Note: the default values are required in case no config file is in effect. This applies when DM is started 107 // via feature:install from Karaf. The default values must match the values defined in project POM. 108 private static final boolean IS_PUBLIC_INSTALLATION = ANONYMOUS_READ_ALLOWED.equals("ALL"); 109 110 private static final String AUTHENTICATION_REALM = "DeepaMehta"; 111 112 // Type URIs 113 private static final String LOGIN_ENABLED_TYPE = "dm4.accesscontrol.login_enabled"; 114 private static final String MEMBERSHIP_TYPE = "dm4.accesscontrol.membership"; 115 116 // Property URIs 117 private static final String PROP_CREATOR = "dm4.accesscontrol.creator"; 118 private static final String PROP_OWNER = "dm4.accesscontrol.owner"; 119 private static final String PROP_MODIFIER = "dm4.accesscontrol.modifier"; 120 121 // Events 122 private static DeepaMehtaEvent POST_LOGIN_USER = new DeepaMehtaEvent(PostLoginUserListener.class) { 123 @Override 124 public void dispatch(EventListener listener, Object... params) { 125 ((PostLoginUserListener) listener).postLoginUser( 126 (String) params[0] 127 ); 128 } 129 }; 130 private static DeepaMehtaEvent POST_LOGOUT_USER = new DeepaMehtaEvent(PostLogoutUserListener.class) { 131 @Override 132 public void dispatch(EventListener listener, Object... params) { 133 ((PostLogoutUserListener) listener).postLogoutUser( 134 (String) params[0] 135 ); 136 } 137 }; 138 139 // ---------------------------------------------------------------------------------------------- Instance Variables 140 141 @Inject 142 private WorkspacesService wsService; 143 144 @Inject 145 private FilesService filesService; 146 147 @Inject 148 private ConfigService configService; 149 150 @Context 151 private HttpServletRequest request; 152 153 private static Logger logger = Logger.getLogger(AccessControlPlugin.class.getName()); 154 155 static { 156 logger.info("Security settings:" + 157 "\n dm4.security.anonymous_read_allowed = " + requestFilter.dumpReadSetting() + 158 "\n dm4.security.anonymous_write_allowed = " + requestFilter.dumpWriteSetting() + 159 "\n dm4.security.subnet_filter = " + SUBNET_FILTER + 160 "\n dm4.security.new_accounts_are_enabled = " + NEW_ACCOUNTS_ARE_ENABLED); 161 } 162 163 // -------------------------------------------------------------------------------------------------- Public Methods 164 165 166 167 // ******************************************* 168 // *** AccessControlService Implementation *** 169 // ******************************************* 170 171 172 173 // === User Session === 174 175 @POST 176 @Path("/login") 177 @Override 178 public void login() { 179 // Note: the actual login is performed by the request filter. See requestFilter(). 180 } 181 182 @POST 183 @Path("/logout") 184 @Override 185 public void logout() { 186 _logout(request); 187 // 188 // For a "private" DeepaMehta installation: emulate a HTTP logout by forcing the webbrowser to bring up its 189 // login dialog and to forget the former Authorization information. The user is supposed to press "Cancel". 190 // The login dialog can't be used to login again. 191 if (!IS_PUBLIC_INSTALLATION) { 192 throw401Unauthorized(true); // showBrowserLoginDialog=true 193 } 194 } 195 196 // --- 197 198 @GET 199 @Path("/user") 200 @Produces("text/plain") 201 @Override 202 public String getUsername() { 203 return dm4.getAccessControl().getUsername(request); 204 } 205 206 @GET 207 @Path("/username") 208 @Override 209 public Topic getUsernameTopic() { 210 return dm4.getAccessControl().getUsernameTopic(request); 211 } 212 213 // --- 214 215 @GET 216 @Path("/user/workspace") 217 @Override 218 public Topic getPrivateWorkspace() { 219 String username = getUsername(); 220 if (username == null) { 221 throw new IllegalStateException("No user is logged in"); 222 } 223 return dm4.getAccessControl().getPrivateWorkspace(username); 224 } 225 226 227 228 // === User Accounts === 229 230 @POST 231 @Path("/user_account") 232 @Transactional 233 @Override 234 public Topic createUserAccount(final Credentials cred) { 235 try { 236 final String username = cred.username; 237 logger.info("Creating user account \"" + username + "\""); 238 // 239 // 1) create user account 240 AccessControl ac = dm4.getAccessControl(); 241 // We suppress standard workspace assignment here as a User Account topic (and its child topics) require 242 // special assignments. See steps 3) and 4) below. 243 Topic userAccount = ac.runWithoutWorkspaceAssignment(new Callable<Topic>() { 244 @Override 245 public Topic call() { 246 return dm4.createTopic(mf.newTopicModel("dm4.accesscontrol.user_account", mf.newChildTopicsModel() 247 .put("dm4.accesscontrol.username", username) 248 .put("dm4.accesscontrol.password", cred.password))); 249 } 250 }); 251 ChildTopics childTopics = userAccount.getChildTopics(); 252 Topic usernameTopic = childTopics.getTopic("dm4.accesscontrol.username"); 253 Topic passwordTopic = childTopics.getTopic("dm4.accesscontrol.password"); 254 // 255 // 2) create private workspace 256 Topic privateWorkspace = wsService.createWorkspace(DEFAULT_PRIVATE_WORKSPACE_NAME, null, 257 SharingMode.PRIVATE); 258 setWorkspaceOwner(privateWorkspace, username); 259 // Note: we don't set a particular creator/modifier here as we don't want suggest that the new user's 260 // private workspace has been created by the new user itself. Instead we set the *current* user as the 261 // creator/modifier (via postCreateTopic() listener). In case of the "admin" user account the creator/ 262 // modifier remain undefined as it is actually created by the system itself. 263 // 264 // 3) assign user account and password to private workspace 265 // Note: the current user has no READ access to the private workspace just created. 266 // So we must use the privileged assignToWorkspace calls here (instead of using the Workspaces service). 267 long privateWorkspaceId = privateWorkspace.getId(); 268 ac.assignToWorkspace(userAccount, privateWorkspaceId); 269 ac.assignToWorkspace(passwordTopic, privateWorkspaceId); 270 // 271 // 4) assign username to "System" workspace 272 // Note: user <anonymous> has no READ access to the System workspace. So we must use privileged calls here. 273 // This is to support the "DM4 Sign-up" 3rd-party plugin. 274 long systemWorkspaceId = ac.getSystemWorkspaceId(); 275 ac.assignToWorkspace(usernameTopic, systemWorkspaceId); 276 // 277 return usernameTopic; 278 } catch (Exception e) { 279 throw new RuntimeException("Creating user account \"" + cred.username + "\" failed", e); 280 } 281 } 282 283 @GET 284 @Path("/username/{username}") 285 @Override 286 public Topic getUsernameTopic(@PathParam("username") String username) { 287 return dm4.getAccessControl().getUsernameTopic(username); 288 } 289 290 291 292 // === Workspaces / Memberships === 293 294 @GET 295 @Path("/workspace/{workspace_id}/owner") 296 @Produces("text/plain") 297 @Override 298 public String getWorkspaceOwner(@PathParam("workspace_id") long workspaceId) { 299 // ### TODO: delegate to Core's AccessControl.getOwner()? 300 return dm4.hasProperty(workspaceId, PROP_OWNER) ? (String) dm4.getProperty(workspaceId, PROP_OWNER) : null; 301 } 302 303 @Override 304 public void setWorkspaceOwner(Topic workspace, String username) { 305 try { 306 workspace.setProperty(PROP_OWNER, username, true); // addToIndex=true 307 } catch (Exception e) { 308 throw new RuntimeException("Setting the workspace owner of " + info(workspace) + " failed (username=" + 309 username + ")", e); 310 } 311 } 312 313 // --- 314 315 @POST 316 @Path("/user/{username}/workspace/{workspace_id}") 317 @Transactional 318 @Override 319 public void createMembership(@PathParam("username") String username, @PathParam("workspace_id") long workspaceId) { 320 try { 321 dm4.createAssociation(mf.newAssociationModel(MEMBERSHIP_TYPE, 322 mf.newTopicRoleModel(getUsernameTopicOrThrow(username).getId(), "dm4.core.default"), 323 mf.newTopicRoleModel(workspaceId, "dm4.core.default") 324 )); 325 } catch (Exception e) { 326 throw new RuntimeException("Creating membership for user \"" + username + "\" and workspace " + 327 workspaceId + " failed", e); 328 } 329 } 330 331 @Override 332 public boolean isMember(String username, long workspaceId) { 333 return dm4.getAccessControl().isMember(username, workspaceId); 334 } 335 336 337 338 // === Permissions === 339 340 @GET 341 @Path("/topic/{id}") 342 @Override 343 public Permissions getTopicPermissions(@PathParam("id") long topicId) { 344 return getPermissions(topicId); 345 } 346 347 @GET 348 @Path("/association/{id}") 349 @Override 350 public Permissions getAssociationPermissions(@PathParam("id") long assocId) { 351 return getPermissions(assocId); 352 } 353 354 355 356 // === Object Info === 357 358 @GET 359 @Path("/object/{id}/creator") 360 @Produces("text/plain") 361 @Override 362 public String getCreator(@PathParam("id") long objectId) { 363 return dm4.getAccessControl().getCreator(objectId); 364 } 365 366 @GET 367 @Path("/object/{id}/modifier") 368 @Produces("text/plain") 369 @Override 370 public String getModifier(@PathParam("id") long objectId) { 371 return dm4.hasProperty(objectId, PROP_MODIFIER) ? (String) dm4.getProperty(objectId, PROP_MODIFIER) : null; 372 } 373 374 375 376 // === Retrieval === 377 378 @GET 379 @Path("/creator/{username}/topics") 380 @Override 381 public Collection<Topic> getTopicsByCreator(@PathParam("username") String username) { 382 return dm4.getTopicsByProperty(PROP_CREATOR, username); 383 } 384 385 @GET 386 @Path("/owner/{username}/topics") 387 @Override 388 public Collection<Topic> getTopicsByOwner(@PathParam("username") String username) { 389 return dm4.getTopicsByProperty(PROP_OWNER, username); 390 } 391 392 @GET 393 @Path("/creator/{username}/assocs") 394 @Override 395 public Collection<Association> getAssociationsByCreator(@PathParam("username") String username) { 396 return dm4.getAssociationsByProperty(PROP_CREATOR, username); 397 } 398 399 @GET 400 @Path("/owner/{username}/assocs") 401 @Override 402 public Collection<Association> getAssociationsByOwner(@PathParam("username") String username) { 403 return dm4.getAssociationsByProperty(PROP_OWNER, username); 404 } 405 406 407 408 // **************************** 409 // *** Hook Implementations *** 410 // **************************** 411 412 413 414 @Override 415 public void preInstall() { 416 configService.registerConfigDefinition(new ConfigDefinition( 417 ConfigTarget.TYPE_INSTANCES, "dm4.accesscontrol.username", 418 mf.newTopicModel(LOGIN_ENABLED_TYPE, new SimpleValue(NEW_ACCOUNTS_ARE_ENABLED)), 419 ConfigModificationRole.ADMIN, this 420 )); 421 } 422 423 @Override 424 public void shutdown() { 425 // Note 1: unregistering is crucial e.g. for redeploying the Access Control plugin. The next register call 426 // (at preInstall() time) would fail as the Config service already holds such a registration. 427 // Note 2: we must check if the Config service is still available. If the Config plugin is redeployed the 428 // Access Control plugin is stopped/started as well but at shutdown() time the Config service is already gone. 429 if (configService != null) { 430 configService.unregisterConfigDefinition(LOGIN_ENABLED_TYPE); 431 } else { 432 logger.warning("Config service is already gone"); 433 } 434 } 435 436 437 438 // **************************************** 439 // *** ConfigCustomizer Implementations *** 440 // **************************************** 441 442 443 444 @Override 445 public TopicModel getConfigValue(Topic topic) { 446 if (!topic.getTypeUri().equals("dm4.accesscontrol.username")) { 447 throw new RuntimeException("Unexpected configurable topic: " + topic); 448 } 449 // the "admin" account must be enabled regardless of the "dm4.security.new_accounts_are_enabled" setting 450 if (topic.getSimpleValue().toString().equals(ADMIN_USERNAME)) { 451 return mf.newTopicModel(LOGIN_ENABLED_TYPE, new SimpleValue(true)); 452 } 453 // don't customize 454 return null; 455 } 456 457 458 459 // ******************************** 460 // *** Listener Implementations *** 461 // ******************************** 462 463 464 465 @Override 466 public void checkTopicReadAccess(long topicId) { 467 checkReadAccess(topicId); 468 } 469 470 @Override 471 public void checkTopicWriteAccess(long topicId) { 472 checkWriteAccess(topicId); 473 } 474 475 // --- 476 477 @Override 478 public void checkAssociationReadAccess(long assocId) { 479 checkReadAccess(assocId); 480 // 481 long[] playerIds = dm4.getPlayerIds(assocId); 482 checkReadAccess(playerIds[0]); 483 checkReadAccess(playerIds[1]); 484 } 485 486 @Override 487 public void checkAssociationWriteAccess(long assocId) { 488 checkWriteAccess(assocId); 489 } 490 491 // --- 492 493 @Override 494 public void preCreateTopic(TopicModel model) { 495 if (model.getTypeUri().equals("dm4.accesscontrol.username")) { 496 String username = model.getSimpleValue().toString(); 497 Topic usernameTopic = getUsernameTopic(username); 498 if (usernameTopic != null) { 499 throw new RuntimeException("Username \"" + username + "\" exists already"); 500 } 501 } 502 } 503 504 @Override 505 public void postCreateTopic(Topic topic) { 506 String typeUri = topic.getTypeUri(); 507 if (typeUri.equals("dm4.workspaces.workspace")) { 508 setWorkspaceOwner(topic); 509 } else if (typeUri.equals("dm4.webclient.search")) { 510 // ### TODO: refactoring. The Access Control module must not know about the Webclient. 511 // Let the Webclient do the workspace assignment instead. 512 assignSearchTopic(topic); 513 } 514 // 515 setCreatorAndModifier(topic); 516 } 517 518 @Override 519 public void postCreateAssociation(Association assoc) { 520 setCreatorAndModifier(assoc); 521 } 522 523 // --- 524 525 @Override 526 public void preUpdateTopic(Topic topic, TopicModel newModel) { 527 if (topic.getTypeUri().equals("dm4.accesscontrol.username")) { 528 SimpleValue newUsername = newModel.getSimpleValue(); 529 String oldUsername = topic.getSimpleValue().toString(); 530 if (newUsername != null && !newUsername.toString().equals(oldUsername)) { 531 throw new RuntimeException("A Username can't be changed (tried \"" + oldUsername + "\" -> \"" + 532 newUsername + "\")"); 533 } 534 } 535 } 536 537 @Override 538 public void postUpdateTopic(Topic topic, TopicModel newModel, TopicModel oldModel) { 539 setModifier(topic); 540 } 541 542 @Override 543 public void postUpdateAssociation(Association assoc, AssociationModel newModel, AssociationModel oldModel) { 544 if (isMembership(assoc.getModel())) { 545 if (isMembership(oldModel)) { 546 // ### TODO? 547 } else { 548 wsService.assignToWorkspace(assoc, assoc.getTopicByType("dm4.workspaces.workspace").getId()); 549 } 550 } else if (isMembership(oldModel)) { 551 // ### TODO? 552 } 553 // 554 setModifier(assoc); 555 } 556 557 // --- 558 559 @Override 560 public void serviceRequestFilter(ContainerRequest containerRequest) { 561 // Note: we pass the injected HttpServletRequest 562 requestFilter(request); 563 } 564 565 @Override 566 public void resourceRequestFilter(HttpServletRequest servletRequest) { 567 // Note: for the resource filter no HttpServletRequest is injected 568 requestFilter(servletRequest); 569 } 570 571 // --- 572 573 @Override 574 public void checkDiskQuota(String username, long fileSize, long diskQuota) { 575 if (diskQuota < 0) { 576 logger.info("### Checking disk quota of " + userInfo(username) + " ABORTED -- disk quota is disabled"); 577 return; 578 } 579 // 580 long occupiedSpace = getOccupiedSpace(username); 581 boolean quotaOK = occupiedSpace + fileSize <= diskQuota; 582 // 583 logger.info("### File size: " + fileSize + " bytes, " + userInfo(username) + " occupies " + occupiedSpace + 584 " bytes, disk quota: " + diskQuota + " bytes => QUOTA " + (quotaOK ? "OK" : "EXCEEDED")); 585 // 586 if (!quotaOK) { 587 throw new RuntimeException("Disk quota of " + userInfo(username) + " exceeded. Disk quota: " + 588 diskQuota + " bytes. Currently occupied: " + occupiedSpace + " bytes."); 589 } 590 } 591 592 593 594 // ------------------------------------------------------------------------------------------------- Private Methods 595 596 private Topic getUsernameTopicOrThrow(String username) { 597 Topic usernameTopic = getUsernameTopic(username); 598 if (usernameTopic == null) { 599 throw new RuntimeException("User \"" + username + "\" does not exist"); 600 } 601 return usernameTopic; 602 } 603 604 private boolean isMembership(AssociationModel assoc) { 605 return assoc.getTypeUri().equals(MEMBERSHIP_TYPE); 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}