This is an automated email from the ASF dual-hosted git repository. kdoran pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/nifi-registry.git
The following commit(s) were added to refs/heads/master by this push: new 461219c NIFIREG-212 Separating proxy into Read, Write, and Delete so some proxies can be set to read-only 461219c is described below commit 461219c7d980533830947ab37b8fd6eecb1432c7 Author: Bryan Bende <bbe...@apache.org> AuthorDate: Fri May 31 10:26:41 2019 -0400 NIFIREG-212 Separating proxy into Read, Write, and Delete so some proxies can be set to read-only This closes #194. Signed-off-by: Kevin Doran <kdo...@apache.org> --- .../src/main/asciidoc/administration-guide.adoc | 12 ++++- .../file/FileAccessPolicyProvider.java | 8 ++- .../x509/X509AuthenticationRequestDetails.java | 40 +++++++++++++++ .../x509/X509IdentityAuthenticationProvider.java | 60 +++++++++++++++++++--- .../authentication/x509/X509IdentityProvider.java | 5 +- .../apache/nifi/registry/web/api/SecureFileIT.java | 2 +- .../nifi/registry/web/api/SecureKerberosIT.java | 2 +- .../apache/nifi/registry/web/api/SecureLdapIT.java | 4 +- .../web/api/SecureNiFiRegistryClientIT.java | 2 +- .../manage-group/nf-registry-manage-group.html | 22 +++++++- .../manage-user/nf-registry-manage-user.html | 22 +++++++- .../main/webapp/services/nf-registry.service.js | 2 +- 12 files changed, 159 insertions(+), 22 deletions(-) diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/administration-guide.adoc b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/administration-guide.adoc index 2193734..75c6f15 100644 --- a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/administration-guide.adoc +++ b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/administration-guide.adoc @@ -686,10 +686,18 @@ Special privilege policies govern the following system level authorizations: | Allows users to delete policies | `resource="/policies" action="D"` -| Can Proxy Requests -| Allows users to proxy requests +| Can Proxy Requests (Read) +| Allows users to proxy read requests (GET) +| `resource="/proxy" action="R"` + +| Can Proxy Requests (Write) +| Allows users to proxy write requests (POST, PUT, PATCH) | `resource="/proxy" action="W"` +| Can Proxy Requests (Delete) +| Allows users to proxy delete requests (DELETE) +| `resource="/proxy" action="D"` + | View Swagger | Allows users to access the self-hosted Swagger UI | `resource="/swagger" action="R"` diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/FileAccessPolicyProvider.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/FileAccessPolicyProvider.java index c3434c4..5eb1874 100644 --- a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/FileAccessPolicyProvider.java +++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/FileAccessPolicyProvider.java @@ -131,14 +131,18 @@ public class FileAccessPolicyProvider implements ConfigurableAccessPolicyProvide new ResourceActionPair("/swagger", READ_CODE), new ResourceActionPair("/swagger", WRITE_CODE), new ResourceActionPair("/swagger", DELETE_CODE), - new ResourceActionPair("/proxy", WRITE_CODE) + new ResourceActionPair("/proxy", READ_CODE), + new ResourceActionPair("/proxy", WRITE_CODE), + new ResourceActionPair("/proxy", DELETE_CODE) }; /* TODO - move this somewhere into nifi-registry-security-framework so it can be applied to any ConfigurableAccessPolicyProvider * (and also gets us away from requiring magic strings here) */ private static final ResourceActionPair[] NIFI_ACCESS_POLICIES = { new ResourceActionPair("/buckets", READ_CODE), - new ResourceActionPair("/proxy", WRITE_CODE) + new ResourceActionPair("/proxy", READ_CODE), + new ResourceActionPair("/proxy", WRITE_CODE), + new ResourceActionPair("/proxy", DELETE_CODE) }; static final String PROP_NIFI_IDENTITY_PREFIX = "NiFi Identity "; diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509AuthenticationRequestDetails.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509AuthenticationRequestDetails.java new file mode 100644 index 0000000..aa24cd6 --- /dev/null +++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509AuthenticationRequestDetails.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.registry.web.security.authentication.x509; + +import java.util.Objects; + +public class X509AuthenticationRequestDetails { + + private final String proxiedEntitiesChain; + + private final String httpMethod; + + public X509AuthenticationRequestDetails(final String proxiedEntitiesChain, final String httpMethod) { + this.proxiedEntitiesChain = proxiedEntitiesChain; + this.httpMethod = Objects.requireNonNull(httpMethod); + } + + public String getProxiedEntitiesChain() { + return proxiedEntitiesChain; + } + + public String getHttpMethod() { + return httpMethod; + } + +} diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityAuthenticationProvider.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityAuthenticationProvider.java index aefdd5b..9d724ac 100644 --- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityAuthenticationProvider.java +++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityAuthenticationProvider.java @@ -35,6 +35,9 @@ import org.apache.nifi.registry.security.authorization.user.NiFiUserDetails; import org.apache.nifi.registry.security.authorization.user.StandardNiFiUser; import org.apache.nifi.registry.security.util.ProxiedEntitiesUtils; import org.apache.nifi.registry.web.security.authentication.exception.UntrustedProxyException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpMethod; import java.util.List; import java.util.ListIterator; @@ -42,6 +45,8 @@ import java.util.Set; public class X509IdentityAuthenticationProvider extends IdentityAuthenticationProvider { + private static final Logger LOGGER = LoggerFactory.getLogger(X509IdentityAuthenticationProvider.class); + private static final Authorizable PROXY_AUTHORIZABLE = new Authorizable() { @Override public Authorizable getParentAuthorizable() { @@ -63,12 +68,16 @@ public class X509IdentityAuthenticationProvider extends IdentityAuthenticationPr AuthenticationRequestToken requestToken, AuthenticationResponse response) { - AuthenticationRequest authenticationRequest = requestToken.getAuthenticationRequest(); + final AuthenticationRequest authenticationRequest = requestToken.getAuthenticationRequest(); + + final Object requestDetails = authenticationRequest.getDetails(); + if (requestDetails == null || !(requestDetails instanceof X509AuthenticationRequestDetails)) { + throw new IllegalStateException("Invalid request details specified"); + } - String proxiedEntitiesChain = authenticationRequest.getDetails() != null - ? (String)authenticationRequest.getDetails() - : null; + final X509AuthenticationRequestDetails x509RequestDetails = (X509AuthenticationRequestDetails) authenticationRequest.getDetails(); + final String proxiedEntitiesChain = x509RequestDetails.getProxiedEntitiesChain(); if (StringUtils.isBlank(proxiedEntitiesChain)) { return super.buildAuthenticatedToken(requestToken, response); } @@ -77,6 +86,10 @@ public class X509IdentityAuthenticationProvider extends IdentityAuthenticationPr final List<String> proxyChain = ProxiedEntitiesUtils.tokenizeProxiedEntitiesChain(proxiedEntitiesChain); proxyChain.add(response.getIdentity()); + final String httpMethodStr = x509RequestDetails.getHttpMethod().toUpperCase(); + final HttpMethod httpMethod = HttpMethod.resolve(httpMethodStr); + LOGGER.debug("HTTP method is {}", new Object[]{httpMethod}); + // add the chain as appropriate to each proxy NiFiUser proxy = null; for (final ListIterator<String> chainIter = proxyChain.listIterator(proxyChain.size()); chainIter.hasPrevious(); ) { @@ -97,16 +110,47 @@ public class X509IdentityAuthenticationProvider extends IdentityAuthenticationPr proxy = createUser(identity, groups, proxy, clientAddress, isAnonymous); if (chainIter.hasPrevious()) { - try { - PROXY_AUTHORIZABLE.authorize(authorizer, RequestAction.WRITE, proxy); - } catch (final AccessDeniedException e) { - throw new UntrustedProxyException(String.format("Untrusted proxy [%s].", identity)); + switch (httpMethod) { + case POST: + case PUT: + case PATCH: + authorizeWrite(proxy); + break; + case DELETE: + authorizeDelete(proxy); + break; + default: + authorizeRead(proxy); + break; } } } return new AuthenticationSuccessToken(new NiFiUserDetails(proxy)); + } + + private void authorizeRead(final NiFiUser proxy) { + try { + PROXY_AUTHORIZABLE.authorize(authorizer, RequestAction.READ, proxy); + } catch (final AccessDeniedException e) { + throw new UntrustedProxyException(String.format("Untrusted proxy for read operation [%s].", proxy.getIdentity())); + } + } + + private void authorizeWrite(final NiFiUser proxy) { + try { + PROXY_AUTHORIZABLE.authorize(authorizer, RequestAction.WRITE, proxy); + } catch (final AccessDeniedException e) { + throw new UntrustedProxyException(String.format("Untrusted proxy for write operation [%s].", proxy.getIdentity())); + } + } + private void authorizeDelete(final NiFiUser proxy) { + try { + PROXY_AUTHORIZABLE.authorize(authorizer, RequestAction.DELETE, proxy); + } catch (final AccessDeniedException e) { + throw new UntrustedProxyException(String.format("Untrusted proxy for delete operation [%s].", proxy.getIdentity())); + } } /** diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityProvider.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityProvider.java index 2a1856e..fc74f66 100644 --- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityProvider.java +++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityProvider.java @@ -113,9 +113,10 @@ public class X509IdentityProvider implements IdentityProvider { final String principal = certificatePrincipal.toString(); // extract the proxiedEntitiesChain header value from the servletRequest - String proxiedEntitiesChainHeader = servletRequest.getHeader(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN); + final String proxiedEntitiesChainHeader = servletRequest.getHeader(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN); + final X509AuthenticationRequestDetails details = new X509AuthenticationRequestDetails(proxiedEntitiesChainHeader, servletRequest.getMethod()); - return new AuthenticationRequest(principal, certificates[0], proxiedEntitiesChainHeader); + return new AuthenticationRequest(principal, certificates[0], details); } diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureFileIT.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureFileIT.java index 67cb2e2..095d258 100644 --- a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureFileIT.java +++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureFileIT.java @@ -65,7 +65,7 @@ public class SecureFileIT extends IntegrationTestBase { "\"buckets\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + "\"tenants\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + "\"policies\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + - "\"proxy\":{\"canRead\":false,\"canWrite\":true,\"canDelete\":false}}" + + "\"proxy\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}}" + "}"; // When: the /access endpoint is queried diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureKerberosIT.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureKerberosIT.java index 8d8ea97..de87fcf 100644 --- a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureKerberosIT.java +++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureKerberosIT.java @@ -195,7 +195,7 @@ public class SecureKerberosIT extends IntegrationTestBase { "\"buckets\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + "\"tenants\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + "\"policies\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + - "\"proxy\":{\"canRead\":false,\"canWrite\":true,\"canDelete\":false}}" + + "\"proxy\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}}" + "}"; // When: the /access endpoint is queried using a JWT for the kerberos user diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java index 543ea87..11d7b33 100644 --- a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java +++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java @@ -256,7 +256,7 @@ public class SecureLdapIT extends IntegrationTestBase { "\"buckets\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + "\"tenants\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + "\"policies\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + - "\"proxy\":{\"canRead\":false,\"canWrite\":true,\"canDelete\":false}}" + + "\"proxy\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}}" + "}"; // When: the /access endpoint is queried using a JWT for the nifiadmin LDAP user @@ -284,7 +284,7 @@ public class SecureLdapIT extends IntegrationTestBase { "\"buckets\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + "\"tenants\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + "\"policies\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + - "\"proxy\":{\"canRead\":false,\"canWrite\":true,\"canDelete\":false}}}," + + "\"proxy\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}}}," + "{\"identity\":\"euler\",\"userGroups\":[{\"identity\":\"mathematicians\"}],\"accessPolicies\":[],\"configurable\":false}," + "{\"identity\":\"euclid\",\"userGroups\":[{\"identity\":\"mathematicians\"}],\"accessPolicies\":[],\"configurable\":false}," + "{\"identity\":\"boyle\",\"userGroups\":[{\"identity\":\"chemists\"}],\"accessPolicies\":[],\"configurable\":false}," + diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureNiFiRegistryClientIT.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureNiFiRegistryClientIT.java index cb14b90..62aa098 100644 --- a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureNiFiRegistryClientIT.java +++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureNiFiRegistryClientIT.java @@ -97,7 +97,7 @@ public class SecureNiFiRegistryClientIT extends IntegrationTestBase { Assert.assertEquals(fullAccess, currentUser.getResourcePermissions().getBuckets()); Assert.assertEquals(fullAccess, currentUser.getResourcePermissions().getTenants()); Assert.assertEquals(fullAccess, currentUser.getResourcePermissions().getPolicies()); - Assert.assertEquals(new Permissions().withCanWrite(true), currentUser.getResourcePermissions().getProxy()); + Assert.assertEquals(new Permissions().withCanWrite(true).withCanRead(true).withCanDelete(true), currentUser.getResourcePermissions().getProxy()); } @Test diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/sidenav/manage-group/nf-registry-manage-group.html b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/sidenav/manage-group/nf-registry-manage-group.html index 8317ee2..31eade4 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/sidenav/manage-group/nf-registry-manage-group.html +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/sidenav/manage-group/nf-registry-manage-group.html @@ -128,12 +128,32 @@ limitations under the License. </mat-checkbox> </div> <mat-checkbox [disabled]="!canEditSpecialPrivileges()" - [checked]="nfRegistryService.group.resourcePermissions.proxy.canWrite" + [checked]="nfRegistryService.group.resourcePermissions.proxy.canRead && nfRegistryService.group.resourcePermissions.proxy.canWrite && nfRegistryService.group.resourcePermissions.proxy.canDelete" (change)="toggleGroupManageProxyPrivileges($event)"> <span class="description">Can proxy user requests<i matTooltip="Allow a connected system (e.g., NiFi) to process requests of authorized users of that system." class="pad-left-sm fa fa-question-circle-o help-icon"></i></span> </mat-checkbox> + <div flex fxLayout="row" fxLayoutAlign="space-around center"> + <mat-checkbox class="pad-left-md" + [disabled]="!canEditSpecialPrivileges()" + [(checked)]="nfRegistryService.group.resourcePermissions.proxy.canRead" + (change)="toggleGroupManageProxyPrivileges($event, 'read')"> + <span class="description">Read</span> + </mat-checkbox> + <mat-checkbox class="pad-left-md" + [disabled]="!canEditSpecialPrivileges()" + [(checked)]="nfRegistryService.group.resourcePermissions.proxy.canWrite" + (change)="toggleGroupManageProxyPrivileges($event, 'write')"> + <span class="description">Write</span> + </mat-checkbox> + <mat-checkbox class="pad-left-md" + [disabled]="!canEditSpecialPrivileges()" + [(checked)]="nfRegistryService.group.resourcePermissions.proxy.canDelete" + (change)="toggleGroupManageProxyPrivileges($event, 'delete')"> + <span class="description">Delete</span> + </mat-checkbox> + </div> </div> <mat-button-toggle-group name="nifi-registry-manage-group-perspective" class="pad-left-md tab-toggle-group"> <mat-button-toggle [checked]="manageGroupPerspective === 'membership'" diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/sidenav/manage-user/nf-registry-manage-user.html b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/sidenav/manage-user/nf-registry-manage-user.html index 409dbd0..2b0a505 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/sidenav/manage-user/nf-registry-manage-user.html +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/sidenav/manage-user/nf-registry-manage-user.html @@ -134,12 +134,32 @@ limitations under the License. </div> <mat-checkbox [disabled]="!canEditSpecialPrivileges()" - [checked]="nfRegistryService.user.resourcePermissions.proxy.canWrite" + [checked]="nfRegistryService.user.resourcePermissions.proxy.canRead && nfRegistryService.user.resourcePermissions.proxy.canWrite && nfRegistryService.user.resourcePermissions.proxy.canDelete" (change)="toggleUserManageProxyPrivileges($event)"> <span class="description">Can proxy user requests<i matTooltip="Allow a connected system (e.g., NiFi) to process requests of authorized users of that system." class="pad-left-sm fa fa-question-circle-o help-icon"></i></span> </mat-checkbox> + <div flex fxLayout="row" fxLayoutAlign="space-around center"> + <mat-checkbox class="pad-left-md" + [disabled]="!canEditSpecialPrivileges()" + [(checked)]="nfRegistryService.user.resourcePermissions.proxy.canRead" + (change)="toggleUserManageProxyPrivileges($event, 'read')"> + <span class="description">Read</span> + </mat-checkbox> + <mat-checkbox class="pad-left-md" + [disabled]="!canEditSpecialPrivileges()" + [(checked)]="nfRegistryService.user.resourcePermissions.proxy.canWrite" + (change)="toggleUserManageProxyPrivileges($event, 'write')"> + <span class="description">Write</span> + </mat-checkbox> + <mat-checkbox class="pad-left-md" + [disabled]="!canEditSpecialPrivileges()" + [(checked)]="nfRegistryService.user.resourcePermissions.proxy.canDelete" + (change)="toggleUserManageProxyPrivileges($event, 'delete')"> + <span class="description">Delete</span> + </mat-checkbox> + </div> </div> <mat-button-toggle-group name="nifi-registry-manage-user-perspective" class="pad-left-md tab-toggle-group"> <mat-button-toggle [checked]="manageUserPerspective === 'membership'" diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.js index 5395acd..060c8dd 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.js +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.js @@ -194,7 +194,7 @@ function NfRegistryService(nfRegistryApi, nfStorage, tdDataTableService, router, // model for proxy privileges this.PROXY_PRIVS = { - '/proxy': ['write'] + '/proxy': ['read', 'write', 'delete'] }; //<editor-fold desc="application state objects">