From b926238e031f75c5c3c1f44383635fb8412395e4 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Mon, 3 Aug 2020 14:40:39 +0200 Subject: [PATCH 01/19] enhance anonymous access from two state mode to three state mode --- lerna.json | 2 +- .../sonia/scm/config/ScmConfiguration.java | 13 +++++---- .../ScmConfigurationChangedListener.java | 3 +- .../sonia/scm/security/AnonymousMode.java | 29 +++++++++++++++++++ .../scm/web/filter/AuthenticationFilter.java | 3 +- .../ScmConfigurationChangedListenerTest.java | 5 ++-- .../sonia/scm/it/AnonymousAccessITCase.java | 7 +++-- scm-plugins/scm-git-plugin/package.json | 4 +-- scm-plugins/scm-hg-plugin/package.json | 4 +-- scm-plugins/scm-legacy-plugin/package.json | 4 +-- scm-plugins/scm-svn-plugin/package.json | 4 +-- scm-ui/ui-components/package.json | 2 +- scm-ui/ui-plugins/package.json | 4 +-- scm-ui/ui-styles/package.json | 2 +- scm-ui/ui-types/src/Config.ts | 4 ++- scm-ui/ui-types/src/index.ts | 2 +- scm-ui/ui-webapp/package.json | 4 +-- .../ui-webapp/public/locales/de/config.json | 7 ++++- .../ui-webapp/public/locales/en/config.json | 7 ++++- .../src/admin/components/form/ConfigForm.tsx | 4 +-- .../admin/components/form/GeneralSettings.tsx | 26 ++++++++++------- .../sonia/scm/api/v2/resources/ConfigDto.java | 3 +- .../scm/filter/PropagatePrincipleFilter.java | 3 +- .../scm/lifecycle/SetupContextListener.java | 3 +- .../AnonymousUserDeletionEventHandler.java | 5 ++-- ...ConfigDtoToScmConfigurationMapperTest.java | 5 ++-- ...ScmConfigurationToConfigDtoMapperTest.java | 6 ++-- .../filter/PropagatePrincipleFilterTest.java | 3 +- .../lifecycle/SetupContextListenerTest.java | 7 +++-- ...AnonymousUserDeletionEventHandlerTest.java | 5 ++-- 30 files changed, 120 insertions(+), 60 deletions(-) create mode 100644 scm-core/src/main/java/sonia/scm/security/AnonymousMode.java diff --git a/lerna.json b/lerna.json index dd2664e01c..3111260147 100644 --- a/lerna.json +++ b/lerna.json @@ -5,5 +5,5 @@ ], "npmClient": "yarn", "useWorkspaces": true, - "version": "2.3.0" + "version": "2.4.0-SNAPSHOT" } diff --git a/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java b/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java index 50202a3410..7d1d2cb288 100644 --- a/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java +++ b/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java @@ -30,6 +30,7 @@ import com.google.inject.Singleton; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.event.ScmEventBus; +import sonia.scm.security.AnonymousMode; import sonia.scm.util.HttpUtil; import sonia.scm.xml.XmlSetStringAdapter; @@ -161,7 +162,7 @@ public class ScmConfiguration implements Configuration { * @see http://momentjs.com/docs/#/parsing/ */ private String dateFormat = DEFAULT_DATEFORMAT; - private boolean anonymousAccessEnabled = false; + private AnonymousMode anonymousMode = AnonymousMode.OFF; /** * Enables xsrf cookie protection. @@ -200,7 +201,7 @@ public class ScmConfiguration implements Configuration { this.realmDescription = other.realmDescription; this.dateFormat = other.dateFormat; this.pluginUrl = other.pluginUrl; - this.anonymousAccessEnabled = other.anonymousAccessEnabled; + this.anonymousMode = other.anonymousMode; this.enableProxy = other.enableProxy; this.proxyPort = other.proxyPort; this.proxyServer = other.proxyServer; @@ -311,8 +312,8 @@ public class ScmConfiguration implements Configuration { return realmDescription; } - public boolean isAnonymousAccessEnabled() { - return anonymousAccessEnabled; + public AnonymousMode getAnonymousMode() { + return anonymousMode; } public boolean isDisableGroupingGrid() { @@ -360,8 +361,8 @@ public class ScmConfiguration implements Configuration { return skipFailedAuthenticators; } - public void setAnonymousAccessEnabled(boolean anonymousAccessEnabled) { - this.anonymousAccessEnabled = anonymousAccessEnabled; + public void setAnonymousMode(AnonymousMode mode) { + this.anonymousMode = mode; } public void setBaseUrl(String baseUrl) { diff --git a/scm-core/src/main/java/sonia/scm/config/ScmConfigurationChangedListener.java b/scm-core/src/main/java/sonia/scm/config/ScmConfigurationChangedListener.java index 7079c8fc15..48912b5179 100644 --- a/scm-core/src/main/java/sonia/scm/config/ScmConfigurationChangedListener.java +++ b/scm-core/src/main/java/sonia/scm/config/ScmConfigurationChangedListener.java @@ -29,6 +29,7 @@ import com.google.inject.Inject; import sonia.scm.EagerSingleton; import sonia.scm.SCMContext; import sonia.scm.plugin.Extension; +import sonia.scm.security.AnonymousMode; import sonia.scm.user.UserManager; @Extension @@ -48,7 +49,7 @@ public class ScmConfigurationChangedListener { } private void createAnonymousUserIfRequired(ScmConfigurationChangedEvent event) { - if (event.getConfiguration().isAnonymousAccessEnabled() && !userManager.contains(SCMContext.USER_ANONYMOUS)) { + if (event.getConfiguration().getAnonymousMode() != AnonymousMode.OFF && !userManager.contains(SCMContext.USER_ANONYMOUS)) { userManager.create(SCMContext.ANONYMOUS); } } diff --git a/scm-core/src/main/java/sonia/scm/security/AnonymousMode.java b/scm-core/src/main/java/sonia/scm/security/AnonymousMode.java new file mode 100644 index 0000000000..d08b65eb77 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/security/AnonymousMode.java @@ -0,0 +1,29 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.security; + +public enum AnonymousMode { + FULL, PROTOCOL_ONLY, OFF +} diff --git a/scm-core/src/main/java/sonia/scm/web/filter/AuthenticationFilter.java b/scm-core/src/main/java/sonia/scm/web/filter/AuthenticationFilter.java index 5a9b30ca4e..9c0837fd53 100644 --- a/scm-core/src/main/java/sonia/scm/web/filter/AuthenticationFilter.java +++ b/scm-core/src/main/java/sonia/scm/web/filter/AuthenticationFilter.java @@ -36,6 +36,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.SCMContext; import sonia.scm.config.ScmConfiguration; +import sonia.scm.security.AnonymousMode; import sonia.scm.security.AnonymousToken; import sonia.scm.util.HttpUtil; import sonia.scm.util.Util; @@ -303,7 +304,7 @@ public class AuthenticationFilter extends HttpFilter */ private boolean isAnonymousAccessEnabled() { - return (configuration != null) && configuration.isAnonymousAccessEnabled(); + return (configuration != null) && configuration.getAnonymousMode() != AnonymousMode.OFF; } //~--- fields --------------------------------------------------------------- diff --git a/scm-core/src/test/java/sonia/scm/config/ScmConfigurationChangedListenerTest.java b/scm-core/src/test/java/sonia/scm/config/ScmConfigurationChangedListenerTest.java index e95ebc2187..cc6bc4272f 100644 --- a/scm-core/src/test/java/sonia/scm/config/ScmConfigurationChangedListenerTest.java +++ b/scm-core/src/test/java/sonia/scm/config/ScmConfigurationChangedListenerTest.java @@ -29,6 +29,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.security.AnonymousMode; import sonia.scm.user.UserManager; import static org.mockito.ArgumentMatchers.any; @@ -52,7 +53,7 @@ class ScmConfigurationChangedListenerTest { when(userManager.contains(any())).thenReturn(false); ScmConfiguration changes = new ScmConfiguration(); - changes.setAnonymousAccessEnabled(true); + changes.setAnonymousMode(AnonymousMode.FULL); scmConfiguration.load(changes); listener.handleEvent(new ScmConfigurationChangedEvent(scmConfiguration)); @@ -64,7 +65,7 @@ class ScmConfigurationChangedListenerTest { when(userManager.contains(any())).thenReturn(true); ScmConfiguration changes = new ScmConfiguration(); - changes.setAnonymousAccessEnabled(true); + changes.setAnonymousMode(AnonymousMode.FULL); scmConfiguration.load(changes); listener.handleEvent(new ScmConfigurationChangedEvent(scmConfiguration)); diff --git a/scm-it/src/test/java/sonia/scm/it/AnonymousAccessITCase.java b/scm-it/src/test/java/sonia/scm/it/AnonymousAccessITCase.java index 0994ca0dcb..ad74c0ffae 100644 --- a/scm-it/src/test/java/sonia/scm/it/AnonymousAccessITCase.java +++ b/scm-it/src/test/java/sonia/scm/it/AnonymousAccessITCase.java @@ -41,6 +41,7 @@ import sonia.scm.it.utils.ScmTypes; import sonia.scm.it.utils.TestData; import sonia.scm.repository.client.api.RepositoryClient; import sonia.scm.repository.client.api.RepositoryClientException; +import sonia.scm.security.AnonymousMode; import javax.json.Json; import javax.json.JsonArray; @@ -148,7 +149,7 @@ class AnonymousAccessITCase { private static void setAnonymousAccess(boolean anonymousAccessEnabled) { RestUtil.given("application/vnd.scmm-config+json;v=2") - .body(createConfig(anonymousAccessEnabled)) + .body(createConfig(AnonymousMode.FULL)) .when() .put(RestUtil.REST_BASE_URL.toASCIIString() + "config") @@ -157,12 +158,12 @@ class AnonymousAccessITCase { .statusCode(HttpServletResponse.SC_NO_CONTENT); } - private static String createConfig(boolean anonymousAccessEnabled) { + private static String createConfig(AnonymousMode anonymousMode) { JsonArray emptyArray = Json.createBuilderFactory(emptyMap()).createArrayBuilder().build(); return JSON_BUILDER .add("adminGroups", emptyArray) .add("adminUsers", emptyArray) - .add("anonymousAccessEnabled", anonymousAccessEnabled) + .add("anonymousMode", anonymousMode.toString()) .add("baseUrl", "https://next-scm.cloudogu.com/scm") .add("dateFormat", "YYYY-MM-DD HH:mm:ss") .add("disableGroupingGrid", false) diff --git a/scm-plugins/scm-git-plugin/package.json b/scm-plugins/scm-git-plugin/package.json index 4c8fbd8cc0..0f9d2f7d9d 100644 --- a/scm-plugins/scm-git-plugin/package.json +++ b/scm-plugins/scm-git-plugin/package.json @@ -1,7 +1,7 @@ { "name": "@scm-manager/scm-git-plugin", "private": true, - "version": "2.3.0", + "version": "2.4.0-SNAPSHOT", "license": "MIT", "main": "./src/main/js/index.ts", "scripts": { @@ -20,6 +20,6 @@ }, "prettier": "@scm-manager/prettier-config", "dependencies": { - "@scm-manager/ui-plugins": "^2.3.0" + "@scm-manager/ui-plugins": "^2.4.0-SNAPSHOT" } } diff --git a/scm-plugins/scm-hg-plugin/package.json b/scm-plugins/scm-hg-plugin/package.json index 237ce626e3..af7f6e41ad 100644 --- a/scm-plugins/scm-hg-plugin/package.json +++ b/scm-plugins/scm-hg-plugin/package.json @@ -1,7 +1,7 @@ { "name": "@scm-manager/scm-hg-plugin", "private": true, - "version": "2.3.0", + "version": "2.4.0-SNAPSHOT", "license": "MIT", "main": "./src/main/js/index.ts", "scripts": { @@ -19,6 +19,6 @@ }, "prettier": "@scm-manager/prettier-config", "dependencies": { - "@scm-manager/ui-plugins": "^2.3.0" + "@scm-manager/ui-plugins": "^2.4.0-SNAPSHOT" } } diff --git a/scm-plugins/scm-legacy-plugin/package.json b/scm-plugins/scm-legacy-plugin/package.json index 4733ac70a7..badb215e52 100644 --- a/scm-plugins/scm-legacy-plugin/package.json +++ b/scm-plugins/scm-legacy-plugin/package.json @@ -1,7 +1,7 @@ { "name": "@scm-manager/scm-legacy-plugin", "private": true, - "version": "2.3.0", + "version": "2.4.0-SNAPSHOT", "license": "MIT", "main": "./src/main/js/index.tsx", "scripts": { @@ -19,6 +19,6 @@ }, "prettier": "@scm-manager/prettier-config", "dependencies": { - "@scm-manager/ui-plugins": "^2.3.0" + "@scm-manager/ui-plugins": "^2.4.0-SNAPSHOT" } } diff --git a/scm-plugins/scm-svn-plugin/package.json b/scm-plugins/scm-svn-plugin/package.json index ca3b81cfed..907a9d670c 100644 --- a/scm-plugins/scm-svn-plugin/package.json +++ b/scm-plugins/scm-svn-plugin/package.json @@ -1,7 +1,7 @@ { "name": "@scm-manager/scm-svn-plugin", "private": true, - "version": "2.3.0", + "version": "2.4.0-SNAPSHOT", "license": "MIT", "main": "./src/main/js/index.ts", "scripts": { @@ -19,6 +19,6 @@ }, "prettier": "@scm-manager/prettier-config", "dependencies": { - "@scm-manager/ui-plugins": "^2.3.0" + "@scm-manager/ui-plugins": "^2.4.0-SNAPSHOT" } } diff --git a/scm-ui/ui-components/package.json b/scm-ui/ui-components/package.json index 74dce43cd0..244bc66c61 100644 --- a/scm-ui/ui-components/package.json +++ b/scm-ui/ui-components/package.json @@ -1,6 +1,6 @@ { "name": "@scm-manager/ui-components", - "version": "2.3.0", + "version": "2.4.0-SNAPSHOT", "description": "UI Components for SCM-Manager and its plugins", "main": "src/index.ts", "files": [ diff --git a/scm-ui/ui-plugins/package.json b/scm-ui/ui-plugins/package.json index 93b7836c88..ece6c6434e 100644 --- a/scm-ui/ui-plugins/package.json +++ b/scm-ui/ui-plugins/package.json @@ -1,12 +1,12 @@ { "name": "@scm-manager/ui-plugins", - "version": "2.3.0", + "version": "2.4.0-SNAPSHOT", "license": "MIT", "bin": { "ui-plugins": "./bin/ui-plugins.js" }, "dependencies": { - "@scm-manager/ui-components": "^2.3.0", + "@scm-manager/ui-components": "^2.4.0-SNAPSHOT", "@scm-manager/ui-extensions": "^2.1.0", "classnames": "^2.2.6", "query-string": "^5.0.1", diff --git a/scm-ui/ui-styles/package.json b/scm-ui/ui-styles/package.json index 8efa51cd75..75659a4cd5 100644 --- a/scm-ui/ui-styles/package.json +++ b/scm-ui/ui-styles/package.json @@ -1,6 +1,6 @@ { "name": "@scm-manager/ui-styles", - "version": "2.1.0", + "version": "2.4.0-SNAPSHOT", "description": "Styles for SCM-Manager", "main": "src/scm.scss", "license": "MIT", diff --git a/scm-ui/ui-types/src/Config.ts b/scm-ui/ui-types/src/Config.ts index bf20c48b77..0579854bbf 100644 --- a/scm-ui/ui-types/src/Config.ts +++ b/scm-ui/ui-types/src/Config.ts @@ -24,6 +24,8 @@ import { Links } from "./hal"; +export type AnonymousMode = "FULL" | "PROTOCOL_ONLY" | "OFF"; + export type Config = { proxyPassword: string | null; proxyPort: number; @@ -33,7 +35,7 @@ export type Config = { realmDescription: string; disableGroupingGrid: boolean; dateFormat: string; - anonymousAccessEnabled: boolean; + anonymousMode: AnonymousMode; baseUrl: string; forceBaseUrl: boolean; loginAttemptLimit: number; diff --git a/scm-ui/ui-types/src/index.ts b/scm-ui/ui-types/src/index.ts index a0ce96dac9..6fc5205662 100644 --- a/scm-ui/ui-types/src/index.ts +++ b/scm-ui/ui-types/src/index.ts @@ -42,7 +42,7 @@ export { AnnotatedSource, AnnotatedLine } from "./Annotate"; export { Tag } from "./Tags"; -export { Config } from "./Config"; +export { Config, AnonymousMode } from "./Config"; export { IndexResources } from "./IndexResources"; diff --git a/scm-ui/ui-webapp/package.json b/scm-ui/ui-webapp/package.json index 1cc39be1bc..1a26d15ecc 100644 --- a/scm-ui/ui-webapp/package.json +++ b/scm-ui/ui-webapp/package.json @@ -1,9 +1,9 @@ { "name": "@scm-manager/ui-webapp", - "version": "2.3.0", + "version": "2.4.0-SNAPSHOT", "private": true, "dependencies": { - "@scm-manager/ui-components": "^2.3.0", + "@scm-manager/ui-components": "^2.4.0-SNAPSHOT", "@scm-manager/ui-extensions": "^2.1.0", "classnames": "^2.2.5", "history": "^4.10.1", diff --git a/scm-ui/ui-webapp/public/locales/de/config.json b/scm-ui/ui-webapp/public/locales/de/config.json index 99e810215b..2016568fdd 100644 --- a/scm-ui/ui-webapp/public/locales/de/config.json +++ b/scm-ui/ui-webapp/public/locales/de/config.json @@ -38,7 +38,12 @@ "realm-description": "Realm Beschreibung", "disable-grouping-grid": "Gruppen deaktivieren", "date-format": "Datumsformat", - "anonymous-access-enabled": "Anonyme Zugriffe erlauben", + "anonymousMode": { + "title": "Anonyme Zugriffe", + "full": "Aktivieren für Web-Oberfläche und Protokolle", + "protocolOnly": "Aktivieren für Protokolle", + "off": "Deaktivieren" + }, "skip-failed-authenticators": "Fehlgeschlagene Authentifizierer überspringen", "plugin-url": "Plugin Center URL", "enabled-xsrf-protection": "XSRF Protection aktivieren", diff --git a/scm-ui/ui-webapp/public/locales/en/config.json b/scm-ui/ui-webapp/public/locales/en/config.json index 068238f132..6f789e747c 100644 --- a/scm-ui/ui-webapp/public/locales/en/config.json +++ b/scm-ui/ui-webapp/public/locales/en/config.json @@ -38,7 +38,12 @@ "realm-description": "Realm Description", "disable-grouping-grid": "Disable Grouping Grid", "date-format": "Date Format", - "anonymous-access-enabled": "Anonymous Access Enabled", + "anonymousMode": { + "title": "Anonymous Access", + "full": "Enabled for Web UI and protocols", + "protocolOnly": "Enabled for protocols", + "off": "Disabled" + }, "skip-failed-authenticators": "Skip Failed Authenticators", "plugin-url": "Plugin Center URL", "enabled-xsrf-protection": "Enabled XSRF Protection", diff --git a/scm-ui/ui-webapp/src/admin/components/form/ConfigForm.tsx b/scm-ui/ui-webapp/src/admin/components/form/ConfigForm.tsx index 4f7cb9ae15..973945b2f8 100644 --- a/scm-ui/ui-webapp/src/admin/components/form/ConfigForm.tsx +++ b/scm-ui/ui-webapp/src/admin/components/form/ConfigForm.tsx @@ -63,7 +63,7 @@ class ConfigForm extends React.Component { realmDescription: "", disableGroupingGrid: false, dateFormat: "", - anonymousAccessEnabled: false, + anonymousMode: "OFF", baseUrl: "", forceBaseUrl: false, loginAttemptLimit: 0, @@ -140,7 +140,7 @@ class ConfigForm extends React.Component { realmDescription={config.realmDescription} disableGroupingGrid={config.disableGroupingGrid} dateFormat={config.dateFormat} - anonymousAccessEnabled={config.anonymousAccessEnabled} + anonymousMode={config.anonymousMode} skipFailedAuthenticators={config.skipFailedAuthenticators} pluginUrl={config.pluginUrl} enabledXsrfProtection={config.enabledXsrfProtection} diff --git a/scm-ui/ui-webapp/src/admin/components/form/GeneralSettings.tsx b/scm-ui/ui-webapp/src/admin/components/form/GeneralSettings.tsx index c8a9ade949..9e2f3f8850 100644 --- a/scm-ui/ui-webapp/src/admin/components/form/GeneralSettings.tsx +++ b/scm-ui/ui-webapp/src/admin/components/form/GeneralSettings.tsx @@ -23,8 +23,8 @@ */ import React from "react"; import { WithTranslation, withTranslation } from "react-i18next"; -import { Checkbox, InputField } from "@scm-manager/ui-components"; -import { NamespaceStrategies } from "@scm-manager/ui-types"; +import { Checkbox, InputField, Select } from "@scm-manager/ui-components"; +import { NamespaceStrategies, AnonymousMode } from "@scm-manager/ui-types"; import NamespaceStrategySelect from "./NamespaceStrategySelect"; type Props = WithTranslation & { @@ -32,7 +32,7 @@ type Props = WithTranslation & { loginInfoUrl: string; disableGroupingGrid: boolean; dateFormat: string; - anonymousAccessEnabled: boolean; + anonymousMode: AnonymousMode; skipFailedAuthenticators: boolean; pluginUrl: string; enabledXsrfProtection: boolean; @@ -50,7 +50,7 @@ class GeneralSettings extends React.Component { loginInfoUrl, pluginUrl, enabledXsrfProtection, - anonymousAccessEnabled, + anonymousMode, namespaceStrategy, hasUpdatePermission, namespaceStrategies @@ -111,12 +111,16 @@ class GeneralSettings extends React.Component { />
-
@@ -134,8 +138,8 @@ class GeneralSettings extends React.Component { handleEnabledXsrfProtectionChange = (value: boolean) => { this.props.onChange(true, value, "enabledXsrfProtection"); }; - handleEnableAnonymousAccess = (value: boolean) => { - this.props.onChange(true, value, "anonymousAccessEnabled"); + handleAnonymousMode = (value: string) => { + this.props.onChange(true, value, "anonymousMode"); }; handleNamespaceStrategyChange = (value: string) => { this.props.onChange(true, value, "namespaceStrategy"); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigDto.java index c81b62c8f4..5a30599b1c 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigDto.java @@ -29,6 +29,7 @@ import de.otto.edison.hal.Links; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import sonia.scm.security.AnonymousMode; import java.util.Set; @@ -45,7 +46,7 @@ public class ConfigDto extends HalRepresentation { private String realmDescription; private boolean disableGroupingGrid; private String dateFormat; - private boolean anonymousAccessEnabled; + private AnonymousMode anonymousMode; private String baseUrl; private boolean forceBaseUrl; private int loginAttemptLimit; diff --git a/scm-webapp/src/main/java/sonia/scm/filter/PropagatePrincipleFilter.java b/scm-webapp/src/main/java/sonia/scm/filter/PropagatePrincipleFilter.java index 86ec288f80..6711d753a4 100644 --- a/scm-webapp/src/main/java/sonia/scm/filter/PropagatePrincipleFilter.java +++ b/scm-webapp/src/main/java/sonia/scm/filter/PropagatePrincipleFilter.java @@ -33,6 +33,7 @@ import org.apache.shiro.subject.Subject; import sonia.scm.Priority; import sonia.scm.SCMContext; import sonia.scm.config.ScmConfiguration; +import sonia.scm.security.AnonymousMode; import sonia.scm.web.filter.HttpFilter; import sonia.scm.web.filter.PropagatePrincipleServletRequestWrapper; @@ -89,7 +90,7 @@ public class PropagatePrincipleFilter extends HttpFilter private boolean hasPermission(Subject subject) { return ((configuration != null) - && configuration.isAnonymousAccessEnabled()) || subject.isAuthenticated() + && configuration.getAnonymousMode() != AnonymousMode.OFF) || subject.isAuthenticated() || subject.isRemembered(); } diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/SetupContextListener.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/SetupContextListener.java index 0f84a9f629..af95c09aa8 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/SetupContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/SetupContextListener.java @@ -31,6 +31,7 @@ import org.slf4j.LoggerFactory; import sonia.scm.SCMContext; import sonia.scm.config.ScmConfiguration; import sonia.scm.plugin.Extension; +import sonia.scm.security.AnonymousMode; import sonia.scm.security.PermissionAssigner; import sonia.scm.security.PermissionDescriptor; import sonia.scm.user.User; @@ -94,7 +95,7 @@ public class SetupContextListener implements ServletContextListener { } private boolean anonymousUserRequiredButNotExists() { - return scmConfiguration.isAnonymousAccessEnabled() && !userManager.contains(SCMContext.USER_ANONYMOUS); + return scmConfiguration.getAnonymousMode() != AnonymousMode.OFF && !userManager.contains(SCMContext.USER_ANONYMOUS); } private boolean shouldCreateAdminAccount() { diff --git a/scm-webapp/src/main/java/sonia/scm/user/AnonymousUserDeletionEventHandler.java b/scm-webapp/src/main/java/sonia/scm/user/AnonymousUserDeletionEventHandler.java index 5ff6dc02b5..343239729f 100644 --- a/scm-webapp/src/main/java/sonia/scm/user/AnonymousUserDeletionEventHandler.java +++ b/scm-webapp/src/main/java/sonia/scm/user/AnonymousUserDeletionEventHandler.java @@ -31,6 +31,7 @@ import sonia.scm.HandlerEventType; import sonia.scm.SCMContext; import sonia.scm.config.ScmConfiguration; import sonia.scm.plugin.Extension; +import sonia.scm.security.AnonymousMode; import javax.inject.Inject; @@ -38,7 +39,7 @@ import javax.inject.Inject; @Extension public class AnonymousUserDeletionEventHandler { - private ScmConfiguration scmConfiguration; + private final ScmConfiguration scmConfiguration; @Inject public AnonymousUserDeletionEventHandler(ScmConfiguration scmConfiguration) { @@ -55,6 +56,6 @@ public class AnonymousUserDeletionEventHandler { private boolean isAnonymousUserDeletionNotAllowed(UserEvent event) { return event.getEventType() == HandlerEventType.BEFORE_DELETE && event.getItem().getName().equals(SCMContext.USER_ANONYMOUS) - && scmConfiguration.isAnonymousAccessEnabled(); + && scmConfiguration.getAnonymousMode() != AnonymousMode.OFF; } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigDtoToScmConfigurationMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigDtoToScmConfigurationMapperTest.java index 5f24384452..10bf3d66f2 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigDtoToScmConfigurationMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigDtoToScmConfigurationMapperTest.java @@ -29,6 +29,7 @@ import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.internal.util.collections.Sets; import sonia.scm.config.ScmConfiguration; +import sonia.scm.security.AnonymousMode; import java.util.Arrays; @@ -63,7 +64,7 @@ public class ConfigDtoToScmConfigurationMapperTest { assertEquals("realm" , config.getRealmDescription()); assertTrue(config.isDisableGroupingGrid()); assertEquals("yyyy" , config.getDateFormat()); - assertTrue(config.isAnonymousAccessEnabled()); + assertTrue(config.getAnonymousMode() == AnonymousMode.FULL); assertEquals("baseurl" , config.getBaseUrl()); assertTrue(config.isForceBaseUrl()); assertEquals(41 , config.getLoginAttemptLimit()); @@ -86,7 +87,7 @@ public class ConfigDtoToScmConfigurationMapperTest { configDto.setRealmDescription("realm"); configDto.setDisableGroupingGrid(true); configDto.setDateFormat("yyyy"); - configDto.setAnonymousAccessEnabled(true); + configDto.setAnonymousMode(AnonymousMode.FULL); configDto.setBaseUrl("baseurl"); configDto.setForceBaseUrl(true); configDto.setLoginAttemptLimit(41); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmConfigurationToConfigDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmConfigurationToConfigDtoMapperTest.java index 28394348a1..7c342eaec3 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmConfigurationToConfigDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmConfigurationToConfigDtoMapperTest.java @@ -34,12 +34,14 @@ import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.internal.util.collections.Sets; import sonia.scm.config.ScmConfiguration; +import sonia.scm.security.AnonymousMode; import java.net.URI; import java.util.Arrays; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -93,7 +95,7 @@ public class ScmConfigurationToConfigDtoMapperTest { assertEquals("description" , dto.getRealmDescription()); assertTrue(dto.isDisableGroupingGrid()); assertEquals("dd" , dto.getDateFormat()); - assertTrue(dto.isAnonymousAccessEnabled()); + assertSame(dto.getAnonymousMode(), AnonymousMode.FULL); assertEquals("baseurl" , dto.getBaseUrl()); assertTrue(dto.isForceBaseUrl()); assertEquals(1 , dto.getLoginAttemptLimit()); @@ -131,7 +133,7 @@ public class ScmConfigurationToConfigDtoMapperTest { config.setRealmDescription("description"); config.setDisableGroupingGrid(true); config.setDateFormat("dd"); - config.setAnonymousAccessEnabled(true); + config.setAnonymousMode(AnonymousMode.FULL); config.setBaseUrl("baseurl"); config.setForceBaseUrl(true); config.setLoginAttemptLimit(1); diff --git a/scm-webapp/src/test/java/sonia/scm/filter/PropagatePrincipleFilterTest.java b/scm-webapp/src/test/java/sonia/scm/filter/PropagatePrincipleFilterTest.java index 6403489e40..6071ec32dd 100644 --- a/scm-webapp/src/test/java/sonia/scm/filter/PropagatePrincipleFilterTest.java +++ b/scm-webapp/src/test/java/sonia/scm/filter/PropagatePrincipleFilterTest.java @@ -38,6 +38,7 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.SCMContext; import sonia.scm.config.ScmConfiguration; +import sonia.scm.security.AnonymousMode; import sonia.scm.user.User; import sonia.scm.user.UserTestData; @@ -110,7 +111,7 @@ public class PropagatePrincipleFilterTest { */ @Test public void testAnonymousWithAccessEnabled() throws IOException, ServletException { - configuration.setAnonymousAccessEnabled(true); + configuration.setAnonymousMode(AnonymousMode.FULL); // execute propagatePrincipleFilter.doFilter(request, response, chain); diff --git a/scm-webapp/src/test/java/sonia/scm/lifecycle/SetupContextListenerTest.java b/scm-webapp/src/test/java/sonia/scm/lifecycle/SetupContextListenerTest.java index ff276cd1fa..fab7b789ad 100644 --- a/scm-webapp/src/test/java/sonia/scm/lifecycle/SetupContextListenerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/lifecycle/SetupContextListenerTest.java @@ -37,6 +37,7 @@ import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; import sonia.scm.SCMContext; import sonia.scm.config.ScmConfiguration; +import sonia.scm.security.AnonymousMode; import sonia.scm.security.PermissionAssigner; import sonia.scm.security.PermissionDescriptor; import sonia.scm.user.User; @@ -82,7 +83,7 @@ class SetupContextListenerTest { @BeforeEach void mockScmConfiguration() { - when(scmConfiguration.isAnonymousAccessEnabled()).thenReturn(false); + when(scmConfiguration.getAnonymousMode()).thenReturn(AnonymousMode.OFF); } @BeforeEach @@ -145,7 +146,7 @@ class SetupContextListenerTest { void shouldCreateAnonymousUserIfRequired() { List users = Lists.newArrayList(UserTestData.createTrillian()); when(userManager.getAll()).thenReturn(users); - when(scmConfiguration.isAnonymousAccessEnabled()).thenReturn(true); + when(scmConfiguration.getAnonymousMode()).thenReturn(AnonymousMode.FULL); setupContextListener.contextInitialized(null); @@ -166,7 +167,7 @@ class SetupContextListenerTest { void shouldNotCreateAnonymousUserIfAlreadyExists() { List users = Lists.newArrayList(SCMContext.ANONYMOUS); when(userManager.getAll()).thenReturn(users); - when(scmConfiguration.isAnonymousAccessEnabled()).thenReturn(true); + when(scmConfiguration.getAnonymousMode()).thenReturn(AnonymousMode.FULL); setupContextListener.contextInitialized(null); diff --git a/scm-webapp/src/test/java/sonia/scm/user/AnonymousUserDeletionEventHandlerTest.java b/scm-webapp/src/test/java/sonia/scm/user/AnonymousUserDeletionEventHandlerTest.java index fdbaa55401..fba3de44e1 100644 --- a/scm-webapp/src/test/java/sonia/scm/user/AnonymousUserDeletionEventHandlerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/user/AnonymousUserDeletionEventHandlerTest.java @@ -29,6 +29,7 @@ import org.junit.jupiter.api.Test; import sonia.scm.HandlerEventType; import sonia.scm.SCMContext; import sonia.scm.config.ScmConfiguration; +import sonia.scm.security.AnonymousMode; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -45,7 +46,7 @@ class AnonymousUserDeletionEventHandlerTest { @Test void shouldThrowAnonymousUserDeletionExceptionIfAnonymousAccessIsEnabled() { - scmConfiguration.setAnonymousAccessEnabled(true); + scmConfiguration.setAnonymousMode(AnonymousMode.FULL); hook = new AnonymousUserDeletionEventHandler(scmConfiguration); UserEvent deletionEvent = new UserEvent(HandlerEventType.BEFORE_DELETE, SCMContext.ANONYMOUS); @@ -55,7 +56,7 @@ class AnonymousUserDeletionEventHandlerTest { @Test void shouldNotThrowAnonymousUserDeletionException() { - scmConfiguration.setAnonymousAccessEnabled(false); + scmConfiguration.setAnonymousMode(AnonymousMode.OFF); hook = new AnonymousUserDeletionEventHandler(scmConfiguration); UserEvent deletionEvent = new UserEvent(HandlerEventType.BEFORE_DELETE, SCMContext.ANONYMOUS); From 4c9e96f7e2eb306d1a9e4e30dbc1bdde8a0ebf95 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Mon, 3 Aug 2020 17:41:40 +0200 Subject: [PATCH 02/19] add anonymous mode for webclient / change footer and redirects if user is anonymous / add login button if user is anonymous --- .../sonia/scm/security/Authentications.java | 2 + .../scm/web/filter/AuthenticationFilter.java | 150 +++++++----------- scm-ui/ui-components/package.json | 2 +- scm-ui/ui-components/src/layout/Footer.tsx | 14 +- .../src/navigation/PrimaryNavigation.tsx | 18 +++ scm-ui/ui-plugins/package.json | 2 +- scm-ui/ui-types/package.json | 2 +- .../ui-webapp/public/locales/de/commons.json | 1 + .../ui-webapp/public/locales/en/commons.json | 1 + scm-ui/ui-webapp/src/containers/Login.tsx | 4 +- scm-ui/ui-webapp/src/containers/Logout.tsx | 4 +- scm-ui/ui-webapp/src/containers/Profile.tsx | 24 +-- scm-ui/ui-webapp/src/modules/auth.test.ts | 25 +-- scm-ui/ui-webapp/src/modules/auth.ts | 31 ++-- scm-ui/ui-webapp/src/modules/failure.ts | 4 + scm-ui/ui-webapp/src/modules/indexResource.ts | 6 + scm-ui/ui-webapp/src/modules/pending.ts | 3 + .../api/v2/resources/IndexDtoGenerator.java | 9 +- .../scm/api/v2/resources/MeDtoFactory.java | 21 ++- .../v2/resources/IndexDtoGeneratorTest.java | 137 ++++++++++++++++ .../api/v2/resources/MeDtoFactoryTest.java | 15 +- 21 files changed, 324 insertions(+), 151 deletions(-) create mode 100644 scm-webapp/src/test/java/sonia/scm/api/v2/resources/IndexDtoGeneratorTest.java diff --git a/scm-core/src/main/java/sonia/scm/security/Authentications.java b/scm-core/src/main/java/sonia/scm/security/Authentications.java index e2ddd1aae2..d99643ee60 100644 --- a/scm-core/src/main/java/sonia/scm/security/Authentications.java +++ b/scm-core/src/main/java/sonia/scm/security/Authentications.java @@ -29,6 +29,8 @@ import sonia.scm.SCMContext; public class Authentications { + private Authentications() {} + public static boolean isAuthenticatedSubjectAnonymous() { return isSubjectAnonymous((String) SecurityUtils.getSubject().getPrincipal()); } diff --git a/scm-core/src/main/java/sonia/scm/web/filter/AuthenticationFilter.java b/scm-core/src/main/java/sonia/scm/web/filter/AuthenticationFilter.java index 9c0837fd53..797799bfab 100644 --- a/scm-core/src/main/java/sonia/scm/web/filter/AuthenticationFilter.java +++ b/scm-core/src/main/java/sonia/scm/web/filter/AuthenticationFilter.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.web.filter; //~--- non-JDK imports -------------------------------------------------------- @@ -59,16 +59,21 @@ import java.util.Set; * @since 2.0.0 */ @Singleton -public class AuthenticationFilter extends HttpFilter -{ +public class AuthenticationFilter extends HttpFilter { - /** marker for failed authentication */ + /** + * marker for failed authentication + */ private static final String ATTRIBUTE_FAILED_AUTH = "sonia.scm.auth.failed"; - /** Field description */ + /** + * Field description + */ private static final String HEADER_AUTHORIZATION = "Authorization"; - /** the logger for AuthenticationFilter */ + /** + * the logger for AuthenticationFilter + */ private static final Logger logger = LoggerFactory.getLogger(AuthenticationFilter.class); @@ -77,7 +82,7 @@ public class AuthenticationFilter extends HttpFilter /** * Constructs a new basic authenticaton filter. * - * @param configuration scm-manager global configuration + * @param configuration scm-manager global configuration * @param tokenGenerators web token generators */ @Inject @@ -92,41 +97,32 @@ public class AuthenticationFilter extends HttpFilter * Handles authentication, if a one of the {@link WebTokenGenerator} returns * an {@link AuthenticationToken}. * - * @param request servlet request + * @param request servlet request * @param response servlet response - * @param chain filter chain - * + * @param chain filter chain * @throws IOException * @throws ServletException */ @Override protected void doFilter(HttpServletRequest request, - HttpServletResponse response, FilterChain chain) - throws IOException, ServletException - { + HttpServletResponse response, FilterChain chain) + throws IOException, ServletException { Subject subject = SecurityUtils.getSubject(); AuthenticationToken token = createToken(request); - if (token != null) - { + if (token != null) { logger.trace( "found authentication token on request, start authentication"); handleAuthentication(request, response, chain, subject, token); - } - else if (subject.isAuthenticated()) - { + } else if (subject.isAuthenticated()) { logger.trace("user is already authenticated"); processChain(request, response, chain, subject); - } - else if (isAnonymousAccessEnabled() && !HttpUtil.isWUIRequest(request)) - { + } else if (isAnonymousAccessEnabled()) { logger.trace("anonymous access granted"); subject.login(new AnonymousToken()); processChain(request, response, chain, subject); - } - else - { + } else { logger.trace("could not find user send unauthorized"); handleUnauthorized(request, response, chain); } @@ -136,28 +132,22 @@ public class AuthenticationFilter extends HttpFilter * Sends status code 403 back to client, if the authentication has failed. * In all other cases the method will send status code 403 back to client. * - * @param request servlet request + * @param request servlet request * @param response servlet response - * @param chain filter chain - * + * @param chain filter chain * @throws IOException * @throws ServletException - * * @since 1.8 */ protected void handleUnauthorized(HttpServletRequest request, - HttpServletResponse response, FilterChain chain) - throws IOException, ServletException - { + HttpServletResponse response, FilterChain chain) + throws IOException, ServletException { // send only forbidden, if the authentication has failed. // see https://bitbucket.org/sdorra/scm-manager/issue/545/git-clone-with-username-in-url-does-not - if (Boolean.TRUE.equals(request.getAttribute(ATTRIBUTE_FAILED_AUTH))) - { + if (Boolean.TRUE.equals(request.getAttribute(ATTRIBUTE_FAILED_AUTH))) { sendFailedAuthenticationError(request, response); - } - else - { + } else { sendUnauthorizedError(request, response); } } @@ -165,16 +155,13 @@ public class AuthenticationFilter extends HttpFilter /** * Sends an error for a failed authentication back to client. * - * - * @param request http request + * @param request http request * @param response http response - * * @throws IOException */ protected void sendFailedAuthenticationError(HttpServletRequest request, - HttpServletResponse response) - throws IOException - { + HttpServletResponse response) + throws IOException { HttpUtil.sendUnauthorized(request, response, configuration.getRealmDescription()); } @@ -182,16 +169,13 @@ public class AuthenticationFilter extends HttpFilter /** * Sends an unauthorized error back to client. * - * - * @param request http request + * @param request http request * @param response http response - * * @throws IOException */ protected void sendUnauthorizedError(HttpServletRequest request, - HttpServletResponse response) - throws IOException - { + HttpServletResponse response) + throws IOException { HttpUtil.sendUnauthorized(request, response, configuration.getRealmDescription()); } @@ -200,20 +184,15 @@ public class AuthenticationFilter extends HttpFilter * Iterates all {@link WebTokenGenerator} and creates an * {@link AuthenticationToken} from the given request. * - * * @param request http servlet request - * * @return authentication token of {@code null} */ - private AuthenticationToken createToken(HttpServletRequest request) - { + private AuthenticationToken createToken(HttpServletRequest request) { AuthenticationToken token = null; - for (WebTokenGenerator generator : tokenGenerators) - { + for (WebTokenGenerator generator : tokenGenerators) { token = generator.createToken(request); - if (token != null) - { + if (token != null) { logger.trace("generated web token {} from generator {}", token.getClass(), generator.getClass()); @@ -227,30 +206,24 @@ public class AuthenticationFilter extends HttpFilter /** * Handle authentication with the given {@link AuthenticationToken}. * - * - * @param request http servlet request + * @param request http servlet request * @param response http servlet response - * @param chain filter chain - * @param subject subject - * @param token authentication token - * + * @param chain filter chain + * @param subject subject + * @param token authentication token * @throws IOException * @throws ServletException */ private void handleAuthentication(HttpServletRequest request, - HttpServletResponse response, FilterChain chain, Subject subject, - AuthenticationToken token) - throws IOException, ServletException - { + HttpServletResponse response, FilterChain chain, Subject subject, + AuthenticationToken token) + throws IOException, ServletException { logger.trace("found basic authorization header, start authentication"); - try - { + try { subject.login(token); processChain(request, response, chain, subject); - } - catch (AuthenticationException ex) - { + } catch (AuthenticationException ex) { logger.warn("authentication failed", ex); handleUnauthorized(request, response, chain); } @@ -259,33 +232,26 @@ public class AuthenticationFilter extends HttpFilter /** * Process the filter chain. * - * - * @param request http servlet request + * @param request http servlet request * @param response http servlet response - * @param chain filter chain - * @param subject subject - * + * @param chain filter chain + * @param subject subject * @throws IOException * @throws ServletException */ private void processChain(HttpServletRequest request, - HttpServletResponse response, FilterChain chain, Subject subject) - throws IOException, ServletException - { + HttpServletResponse response, FilterChain chain, Subject subject) + throws IOException, ServletException { String username = Util.EMPTY_STRING; - if (!subject.isAuthenticated()) - { + if (!subject.isAuthenticated()) { // anonymous access username = SCMContext.USER_ANONYMOUS; - } - else - { + } else { Object obj = subject.getPrincipal(); - if (obj != null) - { + if (obj != null) { username = obj.toString(); } } @@ -299,19 +265,21 @@ public class AuthenticationFilter extends HttpFilter /** * Returns {@code true} if anonymous access is enabled. * - * * @return {@code true} if anonymous access is enabled */ - private boolean isAnonymousAccessEnabled() - { + private boolean isAnonymousAccessEnabled() { return (configuration != null) && configuration.getAnonymousMode() != AnonymousMode.OFF; } //~--- fields --------------------------------------------------------------- - /** set of web token generators */ + /** + * set of web token generators + */ private final Set tokenGenerators; - /** scm main configuration */ + /** + * scm main configuration + */ protected ScmConfiguration configuration; } diff --git a/scm-ui/ui-components/package.json b/scm-ui/ui-components/package.json index 244bc66c61..153dae9c17 100644 --- a/scm-ui/ui-components/package.json +++ b/scm-ui/ui-components/package.json @@ -47,7 +47,7 @@ }, "dependencies": { "@scm-manager/ui-extensions": "^2.1.0", - "@scm-manager/ui-types": "^2.3.0", + "@scm-manager/ui-types": "^2.4.0-SNAPSHOT", "classnames": "^2.2.6", "date-fns": "^2.4.1", "gitdiff-parser": "^0.1.2", diff --git a/scm-ui/ui-components/src/layout/Footer.tsx b/scm-ui/ui-components/src/layout/Footer.tsx index 355a74c67e..a609b0bf6c 100644 --- a/scm-ui/ui-components/src/layout/Footer.tsx +++ b/scm-ui/ui-components/src/layout/Footer.tsx @@ -89,14 +89,24 @@ const Footer: FC = ({ me, version, links }) => { meSectionTile = ; } + let meSectionBody =
; + { + if (me.name !== "_anonymous") + meSectionBody = ( + <> + + + + ); + } + return (