001 package de.deepamehta.plugins.accesscontrol;
002
003 import de.deepamehta.plugins.accesscontrol.event.PostLoginUserListener;
004 import de.deepamehta.plugins.accesscontrol.event.PostLogoutUserListener;
005 import de.deepamehta.plugins.accesscontrol.model.AccessControlList;
006 import de.deepamehta.plugins.accesscontrol.model.ACLEntry;
007 import de.deepamehta.plugins.accesscontrol.model.Credentials;
008 import de.deepamehta.plugins.accesscontrol.model.Operation;
009 import de.deepamehta.plugins.accesscontrol.model.Permissions;
010 import de.deepamehta.plugins.accesscontrol.model.UserRole;
011 import de.deepamehta.plugins.accesscontrol.service.AccessControlService;
012 import de.deepamehta.plugins.workspaces.service.WorkspacesService;
013
014 import de.deepamehta.core.Association;
015 import de.deepamehta.core.AssociationType;
016 import de.deepamehta.core.DeepaMehtaObject;
017 import de.deepamehta.core.RelatedTopic;
018 import de.deepamehta.core.Topic;
019 import de.deepamehta.core.TopicType;
020 import de.deepamehta.core.Type;
021 import de.deepamehta.core.ViewConfiguration;
022 import de.deepamehta.core.model.ChildTopicsModel;
023 import de.deepamehta.core.model.SimpleValue;
024 import de.deepamehta.core.model.TopicModel;
025 import de.deepamehta.core.osgi.PluginActivator;
026 import de.deepamehta.core.service.DeepaMehtaEvent;
027 import de.deepamehta.core.service.EventListener;
028 import de.deepamehta.core.service.Inject;
029 import de.deepamehta.core.service.Transactional;
030 import de.deepamehta.core.service.event.AllPluginsActiveListener;
031 import de.deepamehta.core.service.event.IntroduceTopicTypeListener;
032 import de.deepamehta.core.service.event.IntroduceAssociationTypeListener;
033 import de.deepamehta.core.service.event.PostCreateAssociationListener;
034 import de.deepamehta.core.service.event.PostCreateTopicListener;
035 import de.deepamehta.core.service.event.PostUpdateTopicListener;
036 import de.deepamehta.core.service.event.PreSendAssociationTypeListener;
037 import de.deepamehta.core.service.event.PreSendTopicTypeListener;
038 import de.deepamehta.core.service.event.ResourceRequestFilterListener;
039 import de.deepamehta.core.service.event.ServiceRequestFilterListener;
040 import de.deepamehta.core.storage.spi.DeepaMehtaTransaction;
041 import de.deepamehta.core.util.DeepaMehtaUtils;
042 import de.deepamehta.core.util.JavaUtils;
043
044 import org.codehaus.jettison.json.JSONObject;
045
046 // ### TODO: hide Jersey internals. Move to JAX-RS 2.0.
047 import com.sun.jersey.spi.container.ContainerRequest;
048
049 import javax.servlet.http.HttpServletRequest;
050 import javax.servlet.http.HttpServletResponse;
051 import javax.servlet.http.HttpSession;
052
053 import javax.ws.rs.GET;
054 import javax.ws.rs.PUT;
055 import javax.ws.rs.POST;
056 import javax.ws.rs.DELETE;
057 import javax.ws.rs.Consumes;
058 import javax.ws.rs.Path;
059 import javax.ws.rs.PathParam;
060 import javax.ws.rs.Produces;
061 import javax.ws.rs.WebApplicationException;
062 import javax.ws.rs.core.Context;
063 import javax.ws.rs.core.Response;
064 import javax.ws.rs.core.Response.Status;
065
066 import java.util.Collection;
067 import java.util.Enumeration;
068 import java.util.List;
069 import java.util.logging.Logger;
070
071
072
073 @Path("/accesscontrol")
074 @Consumes("application/json")
075 @Produces("application/json")
076 public class AccessControlPlugin extends PluginActivator implements AccessControlService, AllPluginsActiveListener,
077 PostCreateTopicListener,
078 PostCreateAssociationListener,
079 PostUpdateTopicListener,
080 IntroduceTopicTypeListener,
081 IntroduceAssociationTypeListener,
082 ServiceRequestFilterListener,
083 ResourceRequestFilterListener,
084 PreSendTopicTypeListener,
085 PreSendAssociationTypeListener {
086
087 // ------------------------------------------------------------------------------------------------------- Constants
088
089 // Security settings
090 private static final boolean READ_REQUIRES_LOGIN = Boolean.parseBoolean(
091 System.getProperty("dm4.security.read_requires_login", "false"));
092 private static final boolean WRITE_REQUIRES_LOGIN = Boolean.parseBoolean(
093 System.getProperty("dm4.security.write_requires_login", "true"));
094 private static final String SUBNET_FILTER = System.getProperty("dm4.security.subnet_filter", "127.0.0.1/32");
095 // Note: the default values are required in case no config file is in effect. This applies when DM is started
096 // via feature:install from Karaf. The default values must match the values defined in global POM.
097
098 private static final String AUTHENTICATION_REALM = "DeepaMehta";
099
100 // Default user account
101 private static final String DEFAULT_USERNAME = "admin";
102 private static final String DEFAULT_PASSWORD = "";
103
104 // Default ACLs
105 private static final AccessControlList DEFAULT_INSTANCE_ACL = new AccessControlList(
106 new ACLEntry(Operation.WRITE, UserRole.CREATOR, UserRole.OWNER, UserRole.MEMBER)
107 );
108 private static final AccessControlList DEFAULT_TYPE_ACL = new AccessControlList(
109 new ACLEntry(Operation.WRITE, UserRole.CREATOR, UserRole.OWNER, UserRole.MEMBER),
110 new ACLEntry(Operation.CREATE, UserRole.CREATOR, UserRole.OWNER, UserRole.MEMBER)
111 );
112 //
113 private static final AccessControlList DEFAULT_USER_ACCOUNT_ACL = new AccessControlList(
114 new ACLEntry(Operation.WRITE, UserRole.CREATOR, UserRole.OWNER)
115 );
116
117 // Property names
118 private static String URI_CREATOR = "dm4.accesscontrol.creator";
119 private static String URI_OWNER = "dm4.accesscontrol.owner";
120 private static String URI_ACL = "dm4.accesscontrol.acl";
121
122 // Events
123 private static DeepaMehtaEvent POST_LOGIN_USER = new DeepaMehtaEvent(PostLoginUserListener.class) {
124 @Override
125 public void deliver(EventListener listener, Object... params) {
126 ((PostLoginUserListener) listener).postLoginUser(
127 (String) params[0]
128 );
129 }
130 };
131 private static DeepaMehtaEvent POST_LOGOUT_USER = new DeepaMehtaEvent(PostLogoutUserListener.class) {
132 @Override
133 public void deliver(EventListener listener, Object... params) {
134 ((PostLogoutUserListener) listener).postLogoutUser(
135 (String) params[0]
136 );
137 }
138 };
139
140 // ---------------------------------------------------------------------------------------------- Instance Variables
141
142 @Inject
143 private WorkspacesService wsService;
144
145 @Context
146 private HttpServletRequest request;
147
148 private Logger logger = Logger.getLogger(getClass().getName());
149
150 // -------------------------------------------------------------------------------------------------- Public Methods
151
152
153
154 // *******************************************
155 // *** AccessControlService Implementation ***
156 // *******************************************
157
158
159
160 // === Session ===
161
162 @POST
163 @Path("/login")
164 @Override
165 public void login() {
166 // Note: the actual login is performed by the request filter. See requestFilter().
167 }
168
169 @POST
170 @Path("/logout")
171 @Override
172 public void logout() {
173 _logout(request);
174 //
175 // For a "private" DeepaMehta installation: emulate a HTTP logout by forcing the webbrowser to bring up its
176 // login dialog and to forget the former Authorization information. The user is supposed to press "Cancel".
177 // The login dialog can't be used to login again.
178 if (READ_REQUIRES_LOGIN) {
179 throw401Unauthorized();
180 }
181 }
182
183
184
185 // === User ===
186
187 @GET
188 @Path("/user")
189 @Produces("text/plain")
190 @Override
191 public String getUsername() {
192 try {
193 HttpSession session = request.getSession(false); // create=false
194 if (session == null) {
195 return null;
196 }
197 return username(session);
198 } catch (IllegalStateException e) {
199 // Note: if not invoked through network no request (and thus no session) is available.
200 // This happens e.g. while starting up.
201 return null; // user is unknown
202 }
203 }
204
205 @Override
206 public Topic getUsername(String username) {
207 return dms.getTopic("dm4.accesscontrol.username", new SimpleValue(username));
208 }
209
210
211
212 // === Permissions ===
213
214 @GET
215 @Path("/topic/{id}")
216 @Override
217 public Permissions getTopicPermissions(@PathParam("id") long topicId) {
218 return getPermissions(dms.getTopic(topicId));
219 }
220
221 @GET
222 @Path("/association/{id}")
223 @Override
224 public Permissions getAssociationPermissions(@PathParam("id") long assocId) {
225 return getPermissions(dms.getAssociation(assocId));
226 }
227
228
229
230 // === Creator ===
231
232 @Override
233 public String getCreator(DeepaMehtaObject object) {
234 return object.hasProperty(URI_CREATOR) ? (String) object.getProperty(URI_CREATOR) : null;
235 }
236
237 @Override
238 public void setCreator(DeepaMehtaObject object, String username) {
239 try {
240 object.setProperty(URI_CREATOR, username, true); // addToIndex=true
241 } catch (Exception e) {
242 throw new RuntimeException("Setting the creator of " + info(object) + " failed (username=" + username + ")",
243 e);
244 }
245 }
246
247
248
249 // === Owner ===
250
251 @Override
252 public String getOwner(DeepaMehtaObject object) {
253 return object.hasProperty(URI_OWNER) ? (String) object.getProperty(URI_OWNER) : null;
254 }
255
256 @Override
257 public void setOwner(DeepaMehtaObject object, String username) {
258 try {
259 object.setProperty(URI_OWNER, username, true); // addToIndex=true
260 } catch (Exception e) {
261 throw new RuntimeException("Setting the owner of " + info(object) + " failed (username=" + username + ")",
262 e);
263 }
264 }
265
266
267
268 // === Access Control List ===
269
270 @Override
271 public AccessControlList getACL(DeepaMehtaObject object) {
272 try {
273 if (object.hasProperty(URI_ACL)) {
274 return new AccessControlList(new JSONObject((String) object.getProperty(URI_ACL)));
275 } else {
276 return new AccessControlList();
277 }
278 } catch (Exception e) {
279 throw new RuntimeException("Fetching the ACL of " + info(object) + " failed", e);
280 }
281 }
282
283 @Override
284 public void setACL(DeepaMehtaObject object, AccessControlList acl) {
285 try {
286 object.setProperty(URI_ACL, acl.toJSON().toString(), false); // addToIndex=false
287 } catch (Exception e) {
288 throw new RuntimeException("Setting the ACL of " + info(object) + " failed", e);
289 }
290 }
291
292
293
294 // === Workspaces ===
295
296 @POST
297 @Path("/user/{username}/workspace/{workspace_id}")
298 @Transactional
299 @Override
300 public void joinWorkspace(@PathParam("username") String username, @PathParam("workspace_id") long workspaceId) {
301 joinWorkspace(getUsername(username), workspaceId);
302 }
303
304 @Override
305 public void joinWorkspace(Topic username, long workspaceId) {
306 try {
307 wsService.assignToWorkspace(username, workspaceId);
308 } catch (Exception e) {
309 throw new RuntimeException("Joining user " + username + " to workspace " + workspaceId + " failed", e);
310 }
311 }
312
313
314
315 // === Retrieval ===
316
317 @GET
318 @Path("/creator/{username}/topics")
319 @Override
320 public Collection<Topic> getTopicsByCreator(@PathParam("username") String username) {
321 return dms.getTopicsByProperty(URI_CREATOR, username);
322 }
323
324 @GET
325 @Path("/owner/{username}/topics")
326 @Override
327 public Collection<Topic> getTopicsByOwner(@PathParam("username") String username) {
328 return dms.getTopicsByProperty(URI_OWNER, username);
329 }
330
331 @GET
332 @Path("/creator/{username}/assocs")
333 @Override
334 public Collection<Association> getAssociationsByCreator(@PathParam("username") String username) {
335 return dms.getAssociationsByProperty(URI_CREATOR, username);
336 }
337
338 @GET
339 @Path("/owner/{username}/assocs")
340 @Override
341 public Collection<Association> getAssociationsByOwner(@PathParam("username") String username) {
342 return dms.getAssociationsByProperty(URI_OWNER, username);
343 }
344
345
346
347 // ****************************
348 // *** Hook Implementations ***
349 // ****************************
350
351
352
353 @Override
354 public void postInstall() {
355 logger.info("Creating \"admin\" user account");
356 Topic adminAccount = createUserAccount(new Credentials(DEFAULT_USERNAME, DEFAULT_PASSWORD));
357 // Note 1: the admin account needs to be setup for access control itself.
358 // At post-install time our listeners are not yet registered. So we must setup manually here.
359 // Note 2: at post-install time there is no user session. So we call setupAccessControl() directly
360 // instead of (the higher-level) setupUserAccountAccessControl().
361 setupAccessControl(adminAccount, DEFAULT_USER_ACCOUNT_ACL, DEFAULT_USERNAME);
362 // ### TODO: setup access control for the admin account's Username and Password topics.
363 // However, they are not strictly required for the moment.
364 }
365
366 @Override
367 public void init() {
368 logger.info("Security settings:" +
369 "\ndm4.security.read_requires_login=" + READ_REQUIRES_LOGIN +
370 "\ndm4.security.write_requires_login=" + WRITE_REQUIRES_LOGIN +
371 "\ndm4.security.subnet_filter=\"" + SUBNET_FILTER + "\"");
372 }
373
374
375
376 // ********************************
377 // *** Listener Implementations ***
378 // ********************************
379
380
381
382 /**
383 * Setup access control for the default user and the default topicmap.
384 * 1) assign default user to default workspace
385 * 2) assign default topicmap to default workspace
386 * 3) setup access control for default topicmap
387 */
388 @Override
389 public void allPluginsActive() {
390 DeepaMehtaTransaction tx = dms.beginTx();
391 try {
392 // 1) assign default user to default workspace
393 Topic defaultUser = fetchDefaultUser();
394 assignToDefaultWorkspace(defaultUser, "default user (\"admin\")");
395 //
396 Topic defaultTopicmap = fetchDefaultTopicmap();
397 if (defaultTopicmap != null) {
398 // 2) assign default topicmap to default workspace
399 assignToDefaultWorkspace(defaultTopicmap, "default topicmap (\"untitled\")");
400 // 3) setup access control for default topicmap
401 setupAccessControlForDefaultTopicmap(defaultTopicmap);
402 }
403 //
404 tx.success();
405 } catch (Exception e) {
406 logger.warning("ROLLBACK! (" + this + ")");
407 throw new RuntimeException("Setting up " + this + " failed", e);
408 } finally {
409 tx.finish();
410 }
411 }
412
413 // ---
414
415 @Override
416 public void postCreateTopic(Topic topic) {
417 if (isUserAccount(topic)) {
418 setupUserAccountAccessControl(topic);
419 } else {
420 setupDefaultAccessControl(topic);
421 }
422 //
423 // when a workspace is created its creator joins automatically
424 joinIfWorkspace(topic);
425 }
426
427 @Override
428 public void postCreateAssociation(Association assoc) {
429 setupDefaultAccessControl(assoc);
430 }
431
432 // ---
433
434 @Override
435 public void postUpdateTopic(Topic topic, TopicModel newModel, TopicModel oldModel) {
436 if (topic.getTypeUri().equals("dm4.accesscontrol.user_account")) {
437 Topic usernameTopic = topic.getChildTopics().getTopic("dm4.accesscontrol.username");
438 Topic passwordTopic = topic.getChildTopics().getTopic("dm4.accesscontrol.password");
439 String newUsername = usernameTopic.getSimpleValue().toString();
440 TopicModel oldUsernameTopic = oldModel.getChildTopicsModel().getTopic("dm4.accesscontrol.username",
441 null);
442 String oldUsername = oldUsernameTopic != null ? oldUsernameTopic.getSimpleValue().toString() : "";
443 if (!newUsername.equals(oldUsername)) {
444 //
445 if (!oldUsername.equals("")) {
446 throw new RuntimeException("Changing a Username is not supported (tried \"" + oldUsername +
447 "\" -> \"" + newUsername + "\")");
448 }
449 //
450 logger.info("### Username has changed from \"" + oldUsername + "\" -> \"" + newUsername +
451 "\". Setting \"" + newUsername + "\" as the new owner of 3 topics:\n" +
452 " - User Account topic (ID " + topic.getId() + ")\n" +
453 " - Username topic (ID " + usernameTopic.getId() + ")\n" +
454 " - Password topic (ID " + passwordTopic.getId() + ")");
455 setOwner(topic, newUsername);
456 setOwner(usernameTopic, newUsername);
457 setOwner(passwordTopic, newUsername);
458 }
459 }
460 }
461
462 // ---
463
464 @Override
465 public void introduceTopicType(TopicType topicType) {
466 setupDefaultAccessControl(topicType);
467 }
468
469 @Override
470 public void introduceAssociationType(AssociationType assocType) {
471 setupDefaultAccessControl(assocType);
472 }
473
474 // ---
475
476 @Override
477 public void serviceRequestFilter(ContainerRequest containerRequest) {
478 // Note: we pass the injected HttpServletRequest
479 requestFilter(request);
480 }
481
482 @Override
483 public void resourceRequestFilter(HttpServletRequest servletRequest) {
484 // Note: for the resource filter no HttpServletRequest is injected
485 requestFilter(servletRequest);
486 }
487
488 // ---
489
490 // ### TODO: make the types cachable (like topics/associations). That is, don't deliver the permissions along
491 // with the types (don't use the preSend{}Type hooks). Instead let the client request the permissions separately.
492
493 @Override
494 public void preSendTopicType(TopicType topicType) {
495 // Note: the permissions for "Meta Meta Type" must be set manually.
496 // This type doesn't exist in DB. Fetching its ACL entries would fail.
497 if (topicType.getUri().equals("dm4.core.meta_meta_type")) {
498 enrichWithPermissions(topicType, createPermissions(false, false)); // write=false, create=false
499 return;
500 }
501 //
502 enrichWithPermissions(topicType, getPermissions(topicType));
503 }
504
505 @Override
506 public void preSendAssociationType(AssociationType assocType) {
507 enrichWithPermissions(assocType, getPermissions(assocType));
508 }
509
510
511
512 // ------------------------------------------------------------------------------------------------- Private Methods
513
514 private Topic createUserAccount(Credentials cred) {
515 return dms.createTopic(new TopicModel("dm4.accesscontrol.user_account", new ChildTopicsModel()
516 .put("dm4.accesscontrol.username", cred.username)
517 .put("dm4.accesscontrol.password", cred.password)));
518 }
519
520 private boolean isUserAccount(Topic topic) {
521 String typeUri = topic.getTypeUri();
522 return typeUri.equals("dm4.accesscontrol.user_account")
523 || typeUri.equals("dm4.accesscontrol.username")
524 || typeUri.equals("dm4.accesscontrol.password");
525 }
526
527 /**
528 * Fetches the default user ("admin").
529 *
530 * @throws RuntimeException If the default user doesn't exist.
531 *
532 * @return The default user (a Topic of type "Username" / <code>dm4.accesscontrol.username</code>).
533 */
534 private Topic fetchDefaultUser() {
535 return getUsernameOrThrow(DEFAULT_USERNAME);
536 }
537
538 private Topic getUsernameOrThrow(String username) {
539 Topic usernameTopic = getUsername(username);
540 if (usernameTopic == null) {
541 throw new RuntimeException("User \"" + username + "\" does not exist");
542 }
543 return usernameTopic;
544 }
545
546 private void joinIfWorkspace(Topic topic) {
547 if (topic.getTypeUri().equals("dm4.workspaces.workspace")) {
548 String username = getUsername();
549 // Note: when the default workspace is created there is no user logged in yet.
550 // The default user is assigned to the default workspace later on (see allPluginsActive()).
551 if (username != null) {
552 joinWorkspace(username, topic.getId());
553 }
554 }
555 }
556
557
558
559 // === All Plugins Activated ===
560
561 private void assignToDefaultWorkspace(Topic topic, String info) {
562 String operation = "### Assigning the " + info + " to the default workspace (\"DeepaMehta\")";
563 try {
564 // abort if already assigned
565 List<RelatedTopic> workspaces = wsService.getAssignedWorkspaces(topic);
566 if (workspaces.size() != 0) {
567 logger.info("### Assigning the " + info + " to a workspace ABORTED -- " +
568 "already assigned (" + DeepaMehtaUtils.topicNames(workspaces) + ")");
569 return;
570 }
571 //
572 logger.info(operation);
573 Topic defaultWorkspace = wsService.getDefaultWorkspace();
574 wsService.assignToWorkspace(topic, defaultWorkspace.getId());
575 } catch (Exception e) {
576 throw new RuntimeException(operation + " failed", e);
577 }
578 }
579
580 private void setupAccessControlForDefaultTopicmap(Topic defaultTopicmap) {
581 String operation = "### Setup access control for the default topicmap (\"untitled\")";
582 try {
583 // Note: we only check for creator assignment.
584 // If an object has a creator assignment it is expected to have an ACL entry as well.
585 if (getCreator(defaultTopicmap) != null) {
586 logger.info(operation + " ABORTED -- already setup");
587 return;
588 }
589 //
590 logger.info(operation);
591 setupAccessControl(defaultTopicmap, DEFAULT_INSTANCE_ACL, DEFAULT_USERNAME);
592 } catch (Exception e) {
593 throw new RuntimeException(operation + " failed", e);
594 }
595 }
596
597 private Topic fetchDefaultTopicmap() {
598 // Note: the Access Control plugin does not DEPEND on the Topicmaps plugin but is designed to work TOGETHER
599 // with the Topicmaps plugin.
600 // Currently the Access Control plugin needs to know some Topicmaps internals e.g. the URI of the default
601 // topicmap. ### TODO: make "optional plugin dependencies" an explicit concept. Plugins must be able to ask
602 // the core weather a certain plugin is installed (regardles weather it is activated already) and would wait
603 // for its service only if installed.
604 return dms.getTopic("uri", new SimpleValue("dm4.topicmaps.default_topicmap"));
605 }
606
607
608
609 // === Request Filter ===
610
611 private void requestFilter(HttpServletRequest request) {
612 logger.fine("##### " + request.getMethod() + " " + request.getRequestURL() +
613 "\n ##### \"Authorization\"=\"" + request.getHeader("Authorization") + "\"" +
614 "\n ##### " + info(request.getSession(false))); // create=false
615 //
616 checkRequestOrigin(request); // throws WebApplicationException
617 checkAuthorization(request); // throws WebApplicationException
618 }
619
620 // ---
621
622 private void checkRequestOrigin(HttpServletRequest request) {
623 String remoteAddr = request.getRemoteAddr();
624 boolean allowed = JavaUtils.isInRange(remoteAddr, SUBNET_FILTER);
625 //
626 logger.fine("Remote address=\"" + remoteAddr + "\", dm4.security.subnet_filter=\"" + SUBNET_FILTER +
627 "\" => " + (allowed ? "ALLOWED" : "FORBIDDEN"));
628 //
629 if (!allowed) {
630 throw403Forbidden(); // throws WebApplicationException
631 }
632 }
633
634 private void checkAuthorization(HttpServletRequest request) {
635 boolean authorized;
636 if (request.getSession(false) != null) { // create=false
637 authorized = true;
638 } else {
639 String authHeader = request.getHeader("Authorization");
640 if (authHeader != null) {
641 // Note: if login fails we are NOT authorized, even if no login is required
642 authorized = tryLogin(new Credentials(authHeader), request);
643 } else {
644 authorized = !isLoginRequired(request);
645 }
646 }
647 //
648 if (!authorized) {
649 throw401Unauthorized(); // throws WebApplicationException
650 }
651 }
652
653 // ---
654
655 private boolean isLoginRequired(HttpServletRequest request) {
656 return request.getMethod().equals("GET") ? READ_REQUIRES_LOGIN : WRITE_REQUIRES_LOGIN;
657 }
658
659 /**
660 * Checks weather the credentials are valid and if so logs the user in.
661 *
662 * @return true if the credentials are valid.
663 */
664 private boolean tryLogin(Credentials cred, HttpServletRequest request) {
665 String username = cred.username;
666 if (checkCredentials(cred)) {
667 logger.info("##### Logging in as \"" + username + "\" => SUCCESSFUL!");
668 _login(username, request);
669 return true;
670 } else {
671 logger.info("##### Logging in as \"" + username + "\" => FAILED!");
672 return false;
673 }
674 }
675
676 private boolean checkCredentials(Credentials cred) {
677 Topic username = getUsername(cred.username);
678 if (username == null) {
679 return false;
680 }
681 return matches(username, cred.password);
682 }
683
684 // ---
685
686 private void _login(String username, HttpServletRequest request) {
687 HttpSession session = request.getSession();
688 session.setAttribute("username", username);
689 logger.info("##### Creating new " + info(session));
690 //
691 dms.fireEvent(POST_LOGIN_USER, username);
692 }
693
694 private void _logout(HttpServletRequest request) {
695 HttpSession session = request.getSession(false); // create=false
696 String username = username(session); // save username before invalidating
697 logger.info("##### Logging out from " + info(session));
698 //
699 session.invalidate();
700 //
701 dms.fireEvent(POST_LOGOUT_USER, username);
702 }
703
704 // ---
705
706 /**
707 * Prerequisite: username is not <code>null</code>.
708 *
709 * @param password The encrypted password.
710 */
711 private boolean matches(Topic username, String password) {
712 return password(fetchUserAccount(username)).equals(password);
713 }
714
715 /**
716 * Prerequisite: username is not <code>null</code>.
717 */
718 private Topic fetchUserAccount(Topic username) {
719 Topic userAccount = username.getRelatedTopic("dm4.core.composition", "dm4.core.child", "dm4.core.parent",
720 "dm4.accesscontrol.user_account");
721 if (userAccount == null) {
722 throw new RuntimeException("Data inconsistency: there is no User Account topic for username \"" +
723 username.getSimpleValue() + "\" (username=" + username + ")");
724 }
725 return userAccount;
726 }
727
728 // ---
729
730 private String username(HttpSession session) {
731 String username = (String) session.getAttribute("username");
732 if (username == null) {
733 throw new RuntimeException("Session data inconsistency: \"username\" attribute is missing");
734 }
735 return username;
736 }
737
738 /**
739 * @return The encryted password of the specified User Account.
740 */
741 private String password(Topic userAccount) {
742 return userAccount.getChildTopics().getString("dm4.accesscontrol.password");
743 }
744
745 // ---
746
747 private void throw401Unauthorized() {
748 // Note: a non-private DM installation (read_requires_login=false) utilizes DM's login dialog and must suppress
749 // the browser's login dialog. To suppress the browser's login dialog a contrived authentication scheme "xBasic"
750 // is used (see http://loudvchar.blogspot.ca/2010/11/avoiding-browser-popup-for-401.html)
751 String authScheme = READ_REQUIRES_LOGIN ? "Basic" : "xBasic";
752 throw new WebApplicationException(Response.status(Status.UNAUTHORIZED)
753 .header("WWW-Authenticate", authScheme + " realm=" + AUTHENTICATION_REALM)
754 .header("Content-Type", "text/html") // for text/plain (default) Safari provides no Web Console
755 .entity("You're not authorized. Sorry.")
756 .build());
757 }
758
759 private void throw403Forbidden() {
760 throw new WebApplicationException(Response.status(Status.FORBIDDEN)
761 .header("Content-Type", "text/html") // for text/plain (default) Safari provides no Web Console
762 .entity("Access is forbidden. Sorry.")
763 .build());
764 }
765
766
767
768 // === Create ACL Entries ===
769
770 /**
771 * Sets the logged in user as the creator and the owner of the specified object
772 * and creates a default access control entry for it.
773 *
774 * If no user is logged in, nothing is performed.
775 */
776 private void setupDefaultAccessControl(DeepaMehtaObject object) {
777 setupAccessControl(object, DEFAULT_INSTANCE_ACL);
778 }
779
780 private void setupDefaultAccessControl(Type type) {
781 try {
782 String username = getUsername();
783 //
784 if (username == null) {
785 username = DEFAULT_USERNAME;
786 setupViewConfigAccessControl(type.getViewConfig());
787 }
788 //
789 setupAccessControl(type, DEFAULT_TYPE_ACL, username);
790 } catch (Exception e) {
791 throw new RuntimeException("Setting up access control for " + info(type) + " failed (" + type + ")", e);
792 }
793 }
794
795 // ---
796
797 private void setupUserAccountAccessControl(Topic topic) {
798 setupAccessControl(topic, DEFAULT_USER_ACCOUNT_ACL);
799 }
800
801 private void setupViewConfigAccessControl(ViewConfiguration viewConfig) {
802 for (Topic configTopic : viewConfig.getConfigTopics()) {
803 setupAccessControl(configTopic, DEFAULT_INSTANCE_ACL, DEFAULT_USERNAME);
804 }
805 }
806
807 // ---
808
809 private void setupAccessControl(DeepaMehtaObject object, AccessControlList acl) {
810 try {
811 String username = getUsername();
812 // Note: when no user is logged in we do NOT fallback to the default user for the access control setup.
813 // This would not help in gaining data consistency because the topics/associations created so far
814 // (BEFORE the Access Control plugin is activated) would still have no access control setup.
815 // Note: for types the situation is different. The type-introduction mechanism (see introduceTopicType()
816 // handler above) ensures EVERY type is catched (regardless of plugin activation order). For instances on
817 // the other hand we don't have such a mechanism (and don't want one either).
818 if (username == null) {
819 logger.fine("Setting up access control for " + info(object) + " ABORTED -- no user is logged in");
820 return;
821 }
822 //
823 setupAccessControl(object, acl, username);
824 } catch (Exception e) {
825 throw new RuntimeException("Setting up access control for " + info(object) + " failed (" + object + ")", e);
826 }
827 }
828
829 /**
830 * @param username must not be null.
831 */
832 private void setupAccessControl(DeepaMehtaObject object, AccessControlList acl, String username) {
833 setCreator(object, username);
834 setOwner(object, username);
835 setACL(object, acl);
836 }
837
838
839
840 // === Determine Permissions ===
841
842 private Permissions getPermissions(DeepaMehtaObject object) {
843 return createPermissions(hasPermission(getUsername(), Operation.WRITE, object));
844 }
845
846 private Permissions getPermissions(Type type) {
847 String username = getUsername();
848 return createPermissions(hasPermission(username, Operation.WRITE, type),
849 hasPermission(username, Operation.CREATE, type));
850 }
851
852 // ---
853
854 /**
855 * Checks if a user is allowed to perform an operation on an object (topic or association).
856 * If so, <code>true</code> is returned.
857 *
858 * @param username the logged in user (a Topic of type "Username" / <code>dm4.accesscontrol.username</code>),
859 * or <code>null</code> if no user is logged in.
860 */
861 private boolean hasPermission(String username, Operation operation, DeepaMehtaObject object) {
862 try {
863 logger.fine("Determining permission for " + userInfo(username) + " to " + operation + " " + info(object));
864 UserRole[] userRoles = getACL(object).getUserRoles(operation);
865 for (UserRole userRole : userRoles) {
866 logger.fine("There is an ACL entry for user role " + userRole);
867 if (userOccupiesRole(username, userRole, object)) {
868 logger.fine("=> ALLOWED");
869 return true;
870 }
871 }
872 logger.fine("=> DENIED");
873 return false;
874 } catch (Exception e) {
875 throw new RuntimeException("Determining permission for " + info(object) + " failed (" +
876 userInfo(username) + ", operation=" + operation + ")", e);
877 }
878 }
879
880 /**
881 * Checks if a user occupies a role with regard to the specified object.
882 * If so, <code>true</code> is returned.
883 *
884 * @param username the logged in user (a Topic of type "Username" / <code>dm4.accesscontrol.username</code>),
885 * or <code>null</code> if no user is logged in.
886 */
887 private boolean userOccupiesRole(String username, UserRole userRole, DeepaMehtaObject object) {
888 switch (userRole) {
889 case EVERYONE:
890 return true;
891 case USER:
892 return username != null;
893 case MEMBER:
894 return username != null && userIsMember(username, object);
895 case OWNER:
896 return username != null && userIsOwner(username, object);
897 case CREATOR:
898 return username != null && userIsCreator(username, object);
899 default:
900 throw new RuntimeException(userRole + " is an unsupported user role");
901 }
902 }
903
904 // ---
905
906 /**
907 * Checks if a user is a member of any workspace the object is assigned to.
908 * If so, <code>true</code> is returned.
909 *
910 * Prerequisite: a user is logged in (<code>username</code> is not <code>null</code>).
911 *
912 * @param username a Topic of type "Username" (<code>dm4.accesscontrol.username</code>). ### FIXDOC
913 * @param object the object in question.
914 */
915 private boolean userIsMember(String username, DeepaMehtaObject object) {
916 Topic usernameTopic = getUsernameOrThrow(username);
917 List<RelatedTopic> workspaces = wsService.getAssignedWorkspaces(object);
918 logger.fine(info(object) + " is assigned to " + workspaces.size() + " workspaces");
919 for (RelatedTopic workspace : workspaces) {
920 if (wsService.isAssignedToWorkspace(usernameTopic, workspace.getId())) {
921 logger.fine(userInfo(username) + " IS member of workspace " + workspace);
922 return true;
923 } else {
924 logger.fine(userInfo(username) + " is NOT member of workspace " + workspace);
925 }
926 }
927 return false;
928 }
929
930 /**
931 * Checks if a user is the owner of the object.
932 * If so, <code>true</code> is returned.
933 *
934 * Prerequisite: a user is logged in (<code>username</code> is not <code>null</code>).
935 *
936 * @param username a Topic of type "Username" (<code>dm4.accesscontrol.username</code>). ### FIXDOC
937 */
938 private boolean userIsOwner(String username, DeepaMehtaObject object) {
939 String owner = getOwner(object);
940 logger.fine("The owner is " + userInfo(owner));
941 return owner != null && owner.equals(username);
942 }
943
944 /**
945 * Checks if a user is the creator of the object.
946 * If so, <code>true</code> is returned.
947 *
948 * Prerequisite: a user is logged in (<code>username</code> is not <code>null</code>).
949 *
950 * @param username a Topic of type "Username" (<code>dm4.accesscontrol.username</code>). ### FIXDOC
951 */
952 private boolean userIsCreator(String username, DeepaMehtaObject object) {
953 String creator = getCreator(object);
954 logger.fine("The creator is " + userInfo(creator));
955 return creator != null && creator.equals(username);
956 }
957
958 // ---
959
960 private void enrichWithPermissions(Type type, Permissions permissions) {
961 // Note: we must extend/override possibly existing permissions.
962 // Consider a type update: directive UPDATE_TOPIC_TYPE is followed by UPDATE_TOPIC, both on the same object.
963 ChildTopicsModel typePermissions = permissions(type);
964 typePermissions.put(Operation.WRITE.uri, permissions.get(Operation.WRITE.uri));
965 typePermissions.put(Operation.CREATE.uri, permissions.get(Operation.CREATE.uri));
966 }
967
968 private ChildTopicsModel permissions(DeepaMehtaObject object) {
969 // Note 1: "dm4.accesscontrol.permissions" is a contrived URI. There is no such type definition.
970 // Permissions are for transfer only, recalculated for each request, not stored in DB.
971 // Note 2: The permissions topic exists only in the object's model (see note below).
972 // There is no corresponding topic in the attached composite value. So we must query the model here.
973 // (object.getChildTopics().getTopic(...) would not work)
974 TopicModel permissionsTopic = object.getChildTopics().getModel()
975 .getTopic("dm4.accesscontrol.permissions", null);
976 ChildTopicsModel permissions;
977 if (permissionsTopic != null) {
978 permissions = permissionsTopic.getChildTopicsModel();
979 } else {
980 permissions = new ChildTopicsModel();
981 // Note: we put the permissions topic directly in the model here (instead of the attached composite value).
982 // The "permissions" topic is for transfer only. It must not be stored in the DB (as it would when putting
983 // it in the attached composite value).
984 object.getChildTopics().getModel().put("dm4.accesscontrol.permissions", permissions);
985 }
986 return permissions;
987 }
988
989 // ---
990
991 private Permissions createPermissions(boolean write) {
992 return new Permissions().add(Operation.WRITE, write);
993 }
994
995 private Permissions createPermissions(boolean write, boolean create) {
996 return createPermissions(write).add(Operation.CREATE, create);
997 }
998
999
1000
1001 // === Logging ===
1002
1003 private String info(DeepaMehtaObject object) {
1004 if (object instanceof TopicType) {
1005 return "topic type \"" + object.getUri() + "\" (id=" + object.getId() + ")";
1006 } else if (object instanceof AssociationType) {
1007 return "association type \"" + object.getUri() + "\" (id=" + object.getId() + ")";
1008 } else if (object instanceof Topic) {
1009 return "topic " + object.getId() + " (typeUri=\"" + object.getTypeUri() + "\", uri=\"" + object.getUri() +
1010 "\")";
1011 } else if (object instanceof Association) {
1012 return "association " + object.getId() + " (typeUri=\"" + object.getTypeUri() + "\")";
1013 } else {
1014 throw new RuntimeException("Unexpected object: " + object);
1015 }
1016 }
1017
1018 private String userInfo(String username) {
1019 return "user " + (username != null ? "\"" + username + "\"" : "<anonymous>");
1020 }
1021
1022 private String info(HttpSession session) {
1023 return "session" + (session != null ? " " + session.getId() +
1024 " (username=" + username(session) + ")" : ": null");
1025 }
1026
1027 private String info(HttpServletRequest request) {
1028 StringBuilder info = new StringBuilder();
1029 info.append(" " + request.getMethod() + " " + request.getRequestURI() + "\n");
1030 Enumeration<String> e1 = request.getHeaderNames();
1031 while (e1.hasMoreElements()) {
1032 String name = e1.nextElement();
1033 info.append("\n " + name + ":");
1034 Enumeration<String> e2 = request.getHeaders(name);
1035 while (e2.hasMoreElements()) {
1036 String header = e2.nextElement();
1037 info.append(" " + header);
1038 }
1039 }
1040 return info.toString();
1041 }
1042 }