diff options
author | Erik Lumme <erik@vaadin.com> | 2017-09-12 13:57:37 +0300 |
---|---|---|
committer | Erik Lumme <erik@vaadin.com> | 2017-09-12 13:57:37 +0300 |
commit | 0c4a4ee6410a69d97036a90e6562981adabb6e1e (patch) | |
tree | 2cf96c0076f3eadd23878ec4ff9ac1e84600f7e0 /documentation | |
parent | 8e541d6aca977b11eafe0463d132a6171600d2e9 (diff) | |
download | vaadin-framework-0c4a4ee6410a69d97036a90e6562981adabb6e1e.tar.gz vaadin-framework-0c4a4ee6410a69d97036a90e6562981adabb6e1e.zip |
Migrate CreatingSecureVaadinApplicationsUsingJEE6
Diffstat (limited to 'documentation')
-rw-r--r-- | documentation/articles/CreatingSecureVaadinApplicationsUsingJEE6.asciidoc | 646 | ||||
-rw-r--r-- | documentation/articles/contents.asciidoc | 1 | ||||
-rw-r--r-- | documentation/articles/img/architecture.png | bin | 0 -> 85721 bytes | |||
-rw-r--r-- | documentation/articles/img/domain.png | bin | 0 -> 26027 bytes | |||
-rw-r--r-- | documentation/articles/img/glassfish_console1.png | bin | 0 -> 203939 bytes | |||
-rw-r--r-- | documentation/articles/img/glassfish_console2.png | bin | 0 -> 155113 bytes | |||
-rw-r--r-- | documentation/articles/img/nbscrshot1.png | bin | 0 -> 252315 bytes | |||
-rw-r--r-- | documentation/articles/img/nbscrshot2.png | bin | 0 -> 122414 bytes |
8 files changed, 647 insertions, 0 deletions
diff --git a/documentation/articles/CreatingSecureVaadinApplicationsUsingJEE6.asciidoc b/documentation/articles/CreatingSecureVaadinApplicationsUsingJEE6.asciidoc new file mode 100644 index 0000000000..cb81e92bd5 --- /dev/null +++ b/documentation/articles/CreatingSecureVaadinApplicationsUsingJEE6.asciidoc @@ -0,0 +1,646 @@ +[[creating-secure-vaadin-applications-using-jee6]] +Creating secure Vaadin applications using JEE6 +---------------------------------------------- + +by http://vaadin.com/petter[Petter Holmström] + +[[introduction]] +Introduction +~~~~~~~~~~~~ + +In this article, we are going to look at how the security features of +JEE6 and GlassFish 3 can be used to create a secure Vaadin application. +You should be familiar with the security features of JEE6. If +not, skim through Part VII of the +http://docs.sun.com/app/docs/doc/820-7627[The Java EE 6 Tutorial, Volume +I] before continuing. + +[[architecture]] +Architecture +^^^^^^^^^^^^ + +The example system we are going to discuss has the following +architecture: + +image:img/architecture.png[System architecture] + +It is a typical JEE web application consisting of several layers. At the +top, there is the client layer, i.e. the users' web browsers. Then comes +the Internet (or any other network for that matter) through which the +data travels between the client layer and the application server. On the +server side, there are the web (or presentation) layer that contains the +user interface - in this case our Vaadin application - and the +enterprise (or application) layer that contains all our business logic +in the form of Enterprise Java Beans (EJB). Finally, the domain layer +contains the system data in the form of persistent entity objects stored +in some relational database - in this case JavaDB - by some Object +Relational Mapping (ORM) solution - in this case EclipseLink 2.0. + +In this article, we are going to secure all the layers, with the +exception of the client layer. This means that once the data has reached +the client it may be stored unencrypted in the browser's memory or +downloaded to a disk, but that is a risk we are willing to take in this +case ;-). + +[[getting-the-example-source-code]] +Getting the Example Source Code +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This article is based around an example application that maintains +employee records. Please note that the example system bears little +resemblance to a similar real-world system in order to keep it simple +and easy to understand. + +The source code can be downloaded +https://vaadin.com/wiki?p_auth=jN6Filxv&p_p_id=36&p_p_lifecycle=1&p_p_state=exclusive&p_p_mode=view&p_p_col_id=row-1&p_p_col_pos=1&p_p_col_count=2&_36_struts_action=%2Fwiki%2Fget_page_attachment&p_r_p_185834411_nodeId=10674&p_r_p_185834411_nodeId=10674&p_r_p_185834411_nodeId=10674&p_r_p_185834411_title=Creating+Secure+Vaadin+Applications+using+JEE6&p_r_p_185834411_title=Creating+Secure+Vaadin+Applications+using+JEE6&_36_fileName=SecureVaadinApplicationDemo.zip[here] +and it has been packaged as a NetBeans project. If you do not have +NetBeans, go to http://www.netbeans.org[the NetBeans web site] and +download the latest version (6.8 at the time of writing). Remember to +select the version that includes GlassFish v3. + +Once NetBeans and GlassFish are installed, you can unzip the source code +archive and open it in NetBeans. The opened project should look +something like this: + +image:img/nbscrshot1.png[Netbeans screenshot 1] + +However, before you can try out the application, you have to create a +new JavaDB for the test data. This can be done from the Services tab +inside NetBeans: + +image:img/nbscrshot2.png[Netbeans screenshot 2] + +When this is done, you should update the _setup/sun-resources.xml_ file +accordingly, so that the correct username/password is used to connect to +the database and the correct JNDI-resources are registered when the +application is deployed. + +[[securing-the-domain-layer]] +Securing the Domain Layer +~~~~~~~~~~~~~~~~~~~~~~~~~ + +In this section we are not going to cover data encryption or Access +Control Lists (ACL), so if that is what you need, you unfortunately have +to look elsewhere for the time being. Instead, we are going to point out +some things that need to be taken into account when designing the domain +model. + +In a simple application where all users have read access to all data and +some users have write access, the domain model can be designed basically +in any way possible. However, if there are groups of users that should +be given read access to only some parts of the data, things get a little +more complicated. + +In the case of the example system written for this article, the system +contains information that is not intended for everyone such as salaries, +competences or individual career development plans (not included in the +example code). The system will be used by different types of users +(roles): + +* A payroll assistant will need read access to the employees' salaries +in order to be able to calculate the paychecks. Write access should be +prohibited. +* A project manager will need read access to the employees' competences +in order to be able to select the right people to his or her team. +However, he or she has nothing to do with how much each employee is +getting paid. +* A director will need full access to the system in order to add new +employees, change salaries, etc. + +To keep our lives simple, we should design the domain model in such a +way that if a part of an object graph is readable by a certain role, +then the entire graph should also be readable by the role: + +image:img/domain.png[Domain model] + +Here, all roles have read access to the `Employee` domain class. Please +note that no associations point out from this class. Therefore, if we +get an instance of `Employee`, we cannot accidentally (or intentionally) +access restricted information by traversing through the object graph. + +If we take a look at the `SalaryInfo` domain class, we note that it has +a unidirectional association to the `Employee` class. Thus, if we get an +instance of `SalaryInfo`, we can also access all the information in the +corresponding `Employee`. However, this is perfectly valid as the +Payroll Assistant role has read access to both domain classes. + +Now it is time to move on to the enterprise layer, where the security +constraints will actually be enforced. + +[[securing-the-enterprise-layer]] +Securing the Enterprise Layer +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Securing the enterprise layer is actually the easiest part, as this is +no different from securing EJBs in an ordinary JEE application. In this +example, role-based security is sufficient so we can use annotations to +secure the EJBs: + +[source,java] +.... +@Stateless +@TransactionManagement +public class EmployeeBean { + // Implementations omitted + + @PersistenceContext + private EntityManager entityManager; + + @RolesAllowed(UserRoles.ROLE_DIRECTOR) + @TransactionAttribute(TransactionAttributeType.REQUIRED) + public void insertEmployee(Employee employee) { + ... + } + + @RolesAllowed(UserRoles.ROLE_DIRECTOR) + @TransactionAttribute(TransactionAttributeType.REQUIRED) + public Employee updateEmployee(Employee employee) { + ... + } + + @RolesAllowed(UserRoles.ROLE_DIRECTOR) + @TransactionAttribute(TransactionAttributeType.REQUIRED) + public void deleteEmployee(Employee employee) { + ... + } + + @RolesAllowed({UserRoles.ROLE_DIRECTOR, + UserRoles.ROLE_PAYROLL_ASSISTANT, + UserRoles.ROLE_PROJECT_MANAGER}) + public Employee getEmployeeByPersonNumber(String personNumber) { + ... + } + + @RolesAllowed({UserRoles.ROLE_DIRECTOR, UserRoles.ROLE_PAYROLL_ASSISTANT}) + public SalaryInfo getSalaryInfo(Employee employee) { + ... + } + + + @RolesAllowed({UserRoles.ROLE_DIRECTOR}) + @TransactionAttribute(TransactionAttributeType.REQUIRED) + public SalaryInfo saveSalaryInfo(SalaryInfo salaryInfo) { + ... + } + + @RolesAllowed({UserRoles.ROLE_DIRECTOR, UserRoles.ROLE_PROJECT_MANAGER}) + public EmployeeCompetences getCompetences(Employee employee) { + ... + } + + @RolesAllowed({UserRoles.ROLE_DIRECTOR, UserRoles.ROLE_PROJECT_MANAGER}) + @TransactionAttribute(TransactionAttributeType.REQUIRED) + public EmployeeCompetences saveCompetences(EmployeeCompetences competences) { + ... + } +} +.... + +The `UserRoles` class is a helper class that defines constants for all +the role names: + +[source,java] +.... +public class UserRoles { + public static final String ROLE_DIRECTOR = "DIRECTOR"; + public static final String ROLE_PAYROLL_ASSISTANT = "PAYROLL_ASSISTANT"; + public static final String ROLE_PROJECT_MANAGER = "PROJECT_MANAGER"; +} +.... + +This is actually all there is to it - the container will take care of +the rest. Note, that there are separate lookup methods for basic +employee information and salary information, and that the methods +require different roles. This is how the security constraints discussed +in the previous section are enforced in practice. + +[[securing-the-web-layer]] +Securing the Web Layer +~~~~~~~~~~~~~~~~~~~~~~ + +As all of the application's data and logic should now be protected +inside the enterprise layer, securing the web layer really comes down to +two basic tasks: handling user authentication and disabling the +restricted parts of your user interface. In the example application, the +user interface has not been restricted in order to make it possible to +test the security of the enterprise layer, e.g. what happens when a +restriction actions is attempted. + +As the Vaadin application runs entirely on the server, this can be done +inside the application in the same manner as in a Swing desktop +application. However, an (arguably) better approach is to rely on +standard JEE web layer security. + +To keep things simple, a Vaadin application should be designed in such a +way that when the application starts, the user is already authenticated +and when the user logs out, the application is closed. In this way, the +JEE container handles the authentication and it is even possible to move +from e.g. form-based authentication to certificate-based authentication +without having to change a single line of code inside the Vaadin +application. + +[[the-vaadin-application-servlet]] +The Vaadin Application Servlet +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Here is the code for the application servlet: + +[source,java] +.... +@WebServlet(urlPatterns={"/ui/*", "/VAADIN/*"}) +public class DemoAppServlet extends AbstractApplicationServlet { + + @Inject Instance<DemoApp> application; + + @Override + protected Class<? extends Application> getApplicationClass() throws + ClassNotFoundException { + return DemoApp.class; + } + + @Override + protected Application getNewApplication(HttpServletRequest request) throws + ServletException { + DemoApp app = application.get(); + Principal principal = request.getUserPrincipal(); + if (principal == null) { + throw new ServletException("Access denied"); + } + + // In this example, a user can be in one role only + if (request.isUserInRole(UserRoles.ROLE_DIRECTOR)) { + app.setUserRole(UserRoles.ROLE_DIRECTOR); + } else if (request.isUserInRole(UserRoles.ROLE_PAYROLL_ASSISTANT)) { + app.setUserRole(UserRoles.ROLE_PAYROLL_ASSISTANT); + } else if (request.isUserInRole(UserRoles.ROLE_PROJECT_MANAGER)) { + app.setUserRole(UserRoles.ROLE_PROJECT_MANAGER); + } else { + throw new ServletException("Access denied"); + } + + app.setUser(principal); + app.setLogoutURL(request.getContextPath() + "/logout.jsp"); + return app; + } +} +.... + +Please note the URL patterns that this servlet handles. The URL for the +Vaadin application will be _$CONTEXT_PATH/ui_. However, the servlet also +has to handle requests to _$CONTEXT_PATH/VAADIN/*_, as the widgetsets +and themes will not load otherwise. + +Next, in the `getNewApplication(..)` method, the user principal is +fetched from the request and passed to the Vaadin application using the +`setUser(..)` method (this is not a requirement, but is useful if the +Vaadin application needs to know the identity of the current user). If +the application will act differently depending on the user's roles, +these have to be passed in as well - in this case using a custom setter +defined in the `DemoApp` class. Finally, the logout URL is set to point +to a custom JSP which we will look at in a moment. + +[[the-deployment-descriptor]] +The Deployment Descriptor +^^^^^^^^^^^^^^^^^^^^^^^^^ + +To make sure the user is authenticated when the Vaadin application is +started, all requests to the Vaadin application should require +authentication. In this example we are going to use form-based +authentication using ordinary JSPs for the login, logout and error +screens, but we could just as well use some other form of authentication +such as certificates. In order to achieve this, we add the following to +the `web.xml` deployment descriptor: + +[source,xml] +.... +<web-app> + ... + <security-constraint> + <display-name>SecureApplicationConstraint</display-name> + <web-resource-collection> + <web-resource-name>Vaadin application</web-resource-name> + <description>The entire Vaadin application is protected</description> + <url-pattern>/ui/*</url-pattern> + </web-resource-collection> + <auth-constraint> + <description>Only valid users are allowed</description> + <role-name>DIRECTOR</role-name> + <role-name>PAYROLL_ASSISTANT</role-name> + <role-name>PROJECT_MANAGER</role-name> + </auth-constraint> + </security-constraint> + <login-config> + <auth-method>FORM</auth-method> + <realm-name>file</realm-name> + <form-login-config> + <form-login-page>/login.jsp</form-login-page> + <form-error-page>/loginError.jsp</form-error-page> + </form-login-config> + </login-config> + <security-role> + <description/> + <role-name>DIRECTOR</role-name> + </security-role> + <security-role> + <description/> + <role-name>PAYROLL_ASSISTANT</role-name> + </security-role> + <security-role> + <description/> + <role-name>PROJECT_MANAGER</role-name> + </security-role> + ... +</web-app> +.... + +Basically, this file tells the container that this web application: + +* uses the roles DIRECTOR, PAYROLL_ASSISTANT and PROJECT_MANAGER, +* requires the user to be in any of these roles when accessing the +Vaadin application, +* requires users to be in the _file_ realm (a built-in realm manageable +from the GlassFish administration console), and +* uses form-based authentication with a JSP for displaying the login +form and another for displaying login errors. + +For more information about configuring security for JEE web +applications, please see the JEE6 documentation. + +[[the-jsps]] +The JSPs +^^^^^^^^ + +Now we are going to write the JSPs that will be used for logging users +in and out. These files are well covered in the JEE6 documentation, so +we are just going to list them here without further commenting. First up +is _login.jsp_: + +[source,html] +.... +<%@page contentType="text/html" pageEncoding="UTF-8"%> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> + <head> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> + <title>Secure Vaadin Application Demo Login</title> + </head> + <body> + <h1>Please login</h1> + <form method="post" action="j_security_check"> + <p> + Username: <input type="text" name="j_username"/> + </p> + <p> + Password: <input type="password" name="j_password"/> + </p> + <p> + <input type="submit" value="Login"/> + </p> + </form> + </body> +</html> +.... + +Then we move on to _loginError.jsp_: + +[source,html] +.... +<%@page contentType="text/html" pageEncoding="UTF-8"%> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> + <head> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> + <title>Secure Vaadin Application Demo Login Failure</title> + </head> + <body> + <h1>Login Failed!</h1> + <p> + Please <a href="login.jsp">try again</a>. + </p> + </body> +</html> +.... + +Coming up next is _logout.jsp_: + +[source,html] +.... +<%@page contentType="text/html" pageEncoding="UTF-8"%> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> + <head> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> + <title>Secure Vaadin Application Demo</title> + </head> + <body> + <h1>You have been logged out</h1> + <p> + <a href="login.jsp">Log in</a> again. + </p> + </body> +</html> +<% + session.invalidate(); +%> +.... + +Please note that this file contains a single line of code at the end +that invalidates the session, effectively logging the user out. + +Finally, an _index.jsp_ file is needed in order to make sure that any +requests to the context path are redirected to the Vaadin application: + +[source,html] +.... +<% + response.sendRedirect("ui/"); +%> +.... + +There! Now the login and logout mechanisms are in place. + +[[securing-the-transport-layer]] +Securing the Transport Layer +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Even though both the web layer and the enterprise layer are now secured, +the data still has to travel across the Internet to reach the client +layer and, as we know, the Internet is full of people with questionable +intentions. Therefore, we need to make sure that the data reaches its +destination undisclosed and unmodified. In other words, we need SSL. + +Provided that the application server has been properly configured to use +SSL (GlassFish v3 should be out of the box, though with a self-signed +certificate), it is very easy to force a web application to use SSL. We +just have to add the following security constraint to the _web.xml_ +file: + +[source,xml] +.... +<security-constraint> + <display-name>SecureChannelConstraint</display-name> + <web-resource-collection> + <web-resource-name>Entire site</web-resource-name> + <description/> + <url-pattern>/*</url-pattern> + </web-resource-collection> + <user-data-constraint> + <description>Require encrypted channel</description> + <transport-guarantee>CONFIDENTIAL</transport-guarantee> + </user-data-constraint> +</security-constraint> +.... + +This will force all requests to the application to go over an encrypted +SSL link. + +[[configuring-glassfish]] +Configuring GlassFish +~~~~~~~~~~~~~~~~~~~~~ + +As we are going to let GlassFish handle the user database, we have to do +some additional configuration before the application can be deployed. +Users created using the GlassFish administration console are assigned to +groups, which in turn can be mapped to application roles. It is possible +to configure GlassFish to automatically map a group name to a role with +the same name, but in this case we are going to define the mapping +manually by adding the following definitions to the _sun-web.xml_ file: + +[source,xml] +.... +<security-role-mapping> + <role-name>DIRECTOR</role-name> + <group-name>Directors</group-name> +</security-role-mapping> +<security-role-mapping> + <role-name>PAYROLL_ASSISTANT</role-name> + <group-name>Payroll Assistants</group-name> +</security-role-mapping> +<security-role-mapping> + <role-name>PROJECT_MANAGER</role-name> + <group-name>Project Managers</group-name> +</security-role-mapping> +.... + +These definitions tell GlassFish that all users that belong to the +_Directors_ group should hold the `DIRECTOR` role, etc. + +The application is now secured. However, in order to try it out we need +to add some users to the _file_ realm using the GlassFish Administration +Console: + +image:img/glassfish_console1.png[Glassfish console 1] + +image:img/glassfish_console2.png[Glassfish console 2] + +Now, we can deploy the application, login with different users and +explore what happens. + +[[adding-auditing]] +Adding Auditing +~~~~~~~~~~~~~~~ + +Although the application is now protected from unauthorized users, it +has not yet been protected from illegal use by authorized users. As the +application deals with sensitive personal information, it should be +possible to see what the users have done with the data while using the +system. + +GlassFish has an auditing system that, when turned on, automatically +records access decisions (such as successful or failed logins). However, +in this case we need some more fine-grained auditing. One way of +accomplishing this is to use CDI and interceptors (go +http://docs.jboss.org/webbeans/reference/1.0.0.PREVIEW1/en-US/html/interceptors.html[here] +for more information). + +We begin by defining the annotation that will be used to annotate the +methods that are to be subject to auditing: + +[source,java] +.... +@InterceptorBinding +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface AuditLog { +} +.... + +Next, we implement the actual interceptor: + +[source,java] +.... +@AuditLog +@Interceptor +public class AuditLogInterceptor { + @Resource + SessionContext sessionContext; + + @EJB + AuditService auditService; + + @AroundInvoke + public Object recordAuditLogEntry(InvocationContext ctx) throws Exception { + Object result = ctx.proceed(); + StringBuilder sb = new StringBuilder(); + sb.append(ctx.getMethod().getName()); + sb.append("("); + for (Object p : ctx.getParameters()) { + sb.append(p); + sb.append(","); + } + sb.append(")"); + String userName = sessionContext.getCallerPrincipal().getName(); + auditService.recordEntry(userName, sb.toString()); + return result; + } +} +.... + +Before we can use the interceptor, we have to activate it by adding the +following to the _beans.xml_ file: + +[source,xml] +.... +<interceptors> + <class>demoapp.security.AuditLogInterceptor</class> +</interceptors> +.... + +Finally, we annotate the enterprise methods that should be subject to +auditing: + +[source,java] +.... +@Stateless +@TransactionManagement +public class EmployeeBean { + ... + + @RolesAllowed(UserRoles.ROLE_DIRECTOR) + @TransactionAttribute(TransactionAttributeType.REQUIRED) + @AuditLog + public void insertEmployee(Employee employee) { + ... + } + + @RolesAllowed(UserRoles.ROLE_DIRECTOR) + @TransactionAttribute(TransactionAttributeType.REQUIRED) + @AuditLog + public Employee updateEmployee(Employee employee) { + ... + } + ... +} +.... + +There! Now, every time a method annotated with `@AuditLog` is +successfully invoked, it will be recorded together with a timestamp and +the name of the user who invoked it. + +[[summary]] +Summary +~~~~~~~ + +In this article, we have discussed how a typical Vaadin/JEE6 application +can be secured. We have secured the enterprise layer using annotations, +secured the web and channel layers by declaring security constraints in +the deployment descriptor and shown how Vaadin can be used together with +form-based authentication. Finally, we have looked at a way of +implementing auditing using interceptors. diff --git a/documentation/articles/contents.asciidoc b/documentation/articles/contents.asciidoc index 4c94702d63..386be81b8e 100644 --- a/documentation/articles/contents.asciidoc +++ b/documentation/articles/contents.asciidoc @@ -55,4 +55,5 @@ are great, too. - link:VaadinSpringTips.asciidoc[Vaadin Spring tips] - link:VaadinCDI.asciidoc[Vaadin CDI] - link:IIInjectionAndScopes.asciidoc[II - Injection and scopes] +- link:CreatingSecureVaadinApplicationsUsingJEE6.asciidoc[Creating secure Vaadin applications using JEE6] - link:CreatingAUIExtension.asciidoc[Creating a UI extension] diff --git a/documentation/articles/img/architecture.png b/documentation/articles/img/architecture.png Binary files differnew file mode 100644 index 0000000000..06780e0040 --- /dev/null +++ b/documentation/articles/img/architecture.png diff --git a/documentation/articles/img/domain.png b/documentation/articles/img/domain.png Binary files differnew file mode 100644 index 0000000000..5cc494d9f5 --- /dev/null +++ b/documentation/articles/img/domain.png diff --git a/documentation/articles/img/glassfish_console1.png b/documentation/articles/img/glassfish_console1.png Binary files differnew file mode 100644 index 0000000000..9ff76819bd --- /dev/null +++ b/documentation/articles/img/glassfish_console1.png diff --git a/documentation/articles/img/glassfish_console2.png b/documentation/articles/img/glassfish_console2.png Binary files differnew file mode 100644 index 0000000000..4efb69c583 --- /dev/null +++ b/documentation/articles/img/glassfish_console2.png diff --git a/documentation/articles/img/nbscrshot1.png b/documentation/articles/img/nbscrshot1.png Binary files differnew file mode 100644 index 0000000000..d8a8f07b50 --- /dev/null +++ b/documentation/articles/img/nbscrshot1.png diff --git a/documentation/articles/img/nbscrshot2.png b/documentation/articles/img/nbscrshot2.png Binary files differnew file mode 100644 index 0000000000..76b99efd50 --- /dev/null +++ b/documentation/articles/img/nbscrshot2.png |