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