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