mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Merge pull request #21356 from AustralianBioCommons/oidc-external-profile
Link to an external profile when user accounts are managed by a single OIDC provider
This commit is contained in:
@@ -3,26 +3,18 @@ import axios from "axios";
|
||||
import { withPrefix } from "@/utils/redirect";
|
||||
import { rethrowSimple } from "@/utils/simple-error";
|
||||
|
||||
/** Shape of the OIDC config object coming from Galaxy’s `/api/config`. */
|
||||
export type OIDCConfig = Record<
|
||||
string,
|
||||
{
|
||||
icon?: string;
|
||||
label?: string;
|
||||
custom_button_text?: string;
|
||||
end_user_registration_endpoint?: string;
|
||||
}
|
||||
>;
|
||||
export type OIDCConfigEntry = {
|
||||
icon?: string;
|
||||
label?: string;
|
||||
custom_button_text?: string;
|
||||
end_user_registration_endpoint?: string;
|
||||
profile_url?: string;
|
||||
};
|
||||
|
||||
export type OIDCConfigWithRegistration = Record<
|
||||
string,
|
||||
{
|
||||
icon?: string;
|
||||
label?: string;
|
||||
custom_button_text?: string;
|
||||
end_user_registration_endpoint: string;
|
||||
}
|
||||
>;
|
||||
/** Shape of the OIDC config object coming from Galaxy’s `/api/config`. */
|
||||
export type OIDCConfig = Record<string, OIDCConfigEntry>;
|
||||
|
||||
export type OIDCConfigWithRegistration = Record<string, OIDCConfigEntry & { end_user_registration_endpoint: string }>;
|
||||
|
||||
/** Return the per-IDP config, minus anything the caller wants to hide. */
|
||||
export function getFilteredOIDCIdps(oidcConfig: OIDCConfig, exclude: string[] = []): OIDCConfig {
|
||||
@@ -95,6 +87,26 @@ export function isOnlyOneOIDCProviderConfigured(config: OIDCConfig): boolean {
|
||||
return Object.keys(config).length === 1;
|
||||
}
|
||||
|
||||
export function getSingleOidcConfig(config: OIDCConfig): OIDCConfigEntry | null {
|
||||
const providers = Object.keys(config);
|
||||
if (providers.length !== 1) {
|
||||
return null;
|
||||
}
|
||||
const idp = providers[0];
|
||||
if (idp === undefined) {
|
||||
throw new Error("OIDC provider key is undefined.");
|
||||
}
|
||||
return config[idp] || null;
|
||||
}
|
||||
|
||||
export function hasSingleOidcProfile(config: OIDCConfig): boolean {
|
||||
if (!isOnlyOneOIDCProviderConfigured(config)) {
|
||||
return false;
|
||||
}
|
||||
const idp_config = getSingleOidcConfig(config);
|
||||
return !!idp_config?.profile_url;
|
||||
}
|
||||
|
||||
export async function redirectToSingleProvider(config: OIDCConfig): Promise<string | null> {
|
||||
const providers = Object.keys(config);
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { createTestingPinia } from "@pinia/testing";
|
||||
import { getFakeRegisteredUser } from "@tests/test-data";
|
||||
import { getLocalVue } from "@tests/vitest/helpers";
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { ref } from "vue";
|
||||
import VueRouter from "vue-router";
|
||||
|
||||
import { useUserStore } from "@/stores/userStore";
|
||||
|
||||
import UserOidcProfile from "./UserOidcProfile.vue";
|
||||
import GButton from "@/components/BaseComponents/GButton.vue";
|
||||
|
||||
const PROFILE_URL = "https://profile.example.com";
|
||||
const MOCK_CONFIG = {
|
||||
oidc: {
|
||||
provider: {
|
||||
label: "Example Provider",
|
||||
profile_url: PROFILE_URL,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
vi.mock("@/composables/config", () => ({
|
||||
useConfig: vi.fn(() => ({
|
||||
config: ref(MOCK_CONFIG),
|
||||
isConfigLoaded: ref(true),
|
||||
})),
|
||||
}));
|
||||
|
||||
const localVue = getLocalVue();
|
||||
localVue.use(VueRouter);
|
||||
|
||||
function mountProfile(userOverrides = {}) {
|
||||
const pinia = createTestingPinia({ createSpy: vi.fn });
|
||||
const userStore = useUserStore(pinia);
|
||||
userStore.currentUser = getFakeRegisteredUser(userOverrides);
|
||||
const router = new VueRouter();
|
||||
|
||||
return mount(UserOidcProfile, {
|
||||
localVue,
|
||||
pinia,
|
||||
router,
|
||||
stubs: {
|
||||
FontAwesomeIcon: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe("UserOidcProfile", () => {
|
||||
it("shows the profile link from config", async () => {
|
||||
const wrapper = mountProfile();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const profileButton = wrapper.findComponent(GButton);
|
||||
expect(profileButton.exists()).toBe(true);
|
||||
expect(profileButton.props("href")).toBe(PROFILE_URL);
|
||||
});
|
||||
|
||||
it("displays username and email from the user store", async () => {
|
||||
const username = "oidc_user";
|
||||
const email = "oidc_user@example.com";
|
||||
const wrapper = mountProfile({ username, email });
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const details = wrapper.findAll("dd");
|
||||
expect(details.at(0)!.text()).toBe(email);
|
||||
expect(details.at(1)!.text()).toBe(username);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,115 @@
|
||||
<script setup lang="ts">
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { faUser } from "font-awesome-6";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { computed } from "vue";
|
||||
|
||||
import { isRegisteredUser } from "@/api";
|
||||
import { getSingleOidcConfig, type OIDCConfigEntry } from "@/components/User/ExternalIdentities/ExternalIDHelper";
|
||||
import { useConfig } from "@/composables/config";
|
||||
import { useUserStore } from "@/stores/userStore";
|
||||
import localize from "@/utils/localization";
|
||||
|
||||
import GButton from "@/components/BaseComponents/GButton.vue";
|
||||
import BreadcrumbHeading from "@/components/Common/BreadcrumbHeading.vue";
|
||||
|
||||
const { config, isConfigLoaded } = useConfig(true);
|
||||
|
||||
const userStore = useUserStore();
|
||||
const { currentUser } = storeToRefs(userStore);
|
||||
|
||||
const breadcrumbItems = computed(() => [{ title: "User Preferences", to: "/user" }, { title: "Manage Profile" }]);
|
||||
|
||||
const username = computed(() => {
|
||||
if (isRegisteredUser(currentUser.value)) {
|
||||
return currentUser.value.username;
|
||||
}
|
||||
return localize("Not available");
|
||||
});
|
||||
const email = computed(() => {
|
||||
if (isRegisteredUser(currentUser.value)) {
|
||||
return currentUser.value.email;
|
||||
}
|
||||
return localize("Not available");
|
||||
});
|
||||
/** Get the config for the OIDC provider. Note we currently
|
||||
* assume there is only a single OIDC provider when it is being
|
||||
* used to manage profile details (username/email/password)
|
||||
*/
|
||||
const oidcProviderConfig = computed<OIDCConfigEntry | null>(() => {
|
||||
if (!isConfigLoaded.value) {
|
||||
return null;
|
||||
}
|
||||
return getSingleOidcConfig(config.value.oidc);
|
||||
});
|
||||
const profileUrl = computed(() => {
|
||||
if (oidcProviderConfig.value === null) {
|
||||
// Need to return a value to ensure the GButton is
|
||||
// rendered as a link
|
||||
return "#";
|
||||
}
|
||||
return oidcProviderConfig.value.profile_url;
|
||||
});
|
||||
const profileButtonLabel = computed(() => {
|
||||
if (oidcProviderConfig.value === null) {
|
||||
return "Update profile details";
|
||||
}
|
||||
const providerLabel = oidcProviderConfig.value.custom_button_text || oidcProviderConfig.value.label;
|
||||
if (providerLabel) {
|
||||
return `Update profile details at ${providerLabel}`;
|
||||
}
|
||||
return "Update profile details";
|
||||
});
|
||||
|
||||
const buttonDisabled = computed(() => !isConfigLoaded.value || !profileUrl.value);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<BreadcrumbHeading :items="breadcrumbItems" />
|
||||
<div id="manage-profile-card" class="ui-portlet-section">
|
||||
<div class="portlet-header">
|
||||
<span class="portlet-title">
|
||||
<FontAwesomeIcon :icon="faUser" fixed-width class="mr-1" />
|
||||
<span class="portlet-title-text">{{ localize("Manage Profile") }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="portlet-content">
|
||||
<dl class="d-flex flex-column flex-gapy-1">
|
||||
<div class="my-2">
|
||||
<dt class="text-md-left">{{ localize("Email") }}</dt>
|
||||
<dd>{{ email }}</dd>
|
||||
</div>
|
||||
<div class="my-2">
|
||||
<dt class="text-md-left">{{ localize("Username") }}</dt>
|
||||
<dd>{{ username }}</dd>
|
||||
<span class="text-sm-left">
|
||||
<em>
|
||||
{{
|
||||
localize(
|
||||
"Your username is an identifier that will be used to generate addresses for information you share publicly.",
|
||||
)
|
||||
}}
|
||||
</em>
|
||||
</span>
|
||||
</div>
|
||||
<div class="my-2">
|
||||
<dt>{{ localize("Password") }}</dt>
|
||||
<dd>●●●●●●●●●●</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<GButton
|
||||
color="blue"
|
||||
size="medium"
|
||||
class="mt-3"
|
||||
:disabled="buttonDisabled"
|
||||
:href="profileUrl"
|
||||
target="_blank">
|
||||
<span>{{ localize(profileButtonLabel) }}</span>
|
||||
<span class="mr-1 fa fa-external-link-alt" />
|
||||
</GButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,201 @@
|
||||
import { createTestingPinia } from "@pinia/testing";
|
||||
import { getLocalVue } from "@tests/vitest/helpers";
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import { faUnlockAlt } from "font-awesome-6";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { computed } from "vue";
|
||||
import VueRouter from "vue-router";
|
||||
|
||||
import { hasSingleOidcProfile } from "@/components/User/ExternalIdentities/ExternalIDHelper";
|
||||
import { getUserPreferencesModel } from "@/components/User/UserPreferencesModel";
|
||||
import { useConfig } from "@/composables/config";
|
||||
|
||||
import UserPreferences from "./UserPreferences.vue";
|
||||
|
||||
vi.mock("@/composables/config", () => ({
|
||||
useConfig: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/User/ExternalIdentities/ExternalIDHelper", () => ({
|
||||
hasSingleOidcProfile: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/User/UserPreferencesModel", () => ({
|
||||
getUserPreferencesModel: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/app", () => ({
|
||||
getGalaxyInstance: () => ({
|
||||
session_csrf_token: "mock-token",
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/composables/confirmDialog", () => ({
|
||||
useConfirmDialog: () => ({
|
||||
confirm: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
const localVue = getLocalVue();
|
||||
localVue.use(VueRouter);
|
||||
|
||||
describe("UserPreferences.vue", () => {
|
||||
const mockPreferences = (passwordDisabled: boolean) => {
|
||||
// @ts-expect-error - getUserPreferencesModel is mocked
|
||||
vi.mocked(getUserPreferencesModel).mockReturnValue({
|
||||
password: {
|
||||
id: "edit-preferences-password",
|
||||
title: "Change Password",
|
||||
description: "Edit your password.",
|
||||
icon: faUnlockAlt,
|
||||
disabled: passwordDisabled,
|
||||
url: "/password",
|
||||
redirect: "/user",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset mocks
|
||||
mockPreferences(false);
|
||||
});
|
||||
|
||||
it("shows oidc-profile element when OIDC is enabled and configured correctly", async () => {
|
||||
vi.mocked(useConfig).mockReturnValue({
|
||||
config: computed(() => ({
|
||||
enable_oidc: true,
|
||||
disable_local_accounts: true,
|
||||
oidc: {},
|
||||
themes: {},
|
||||
})),
|
||||
isConfigLoaded: computed(() => true),
|
||||
});
|
||||
|
||||
vi.mocked(hasSingleOidcProfile).mockReturnValue(true);
|
||||
|
||||
const wrapper = shallowMount(UserPreferences, {
|
||||
localVue,
|
||||
router: new VueRouter(),
|
||||
pinia: createTestingPinia({ createSpy: vi.fn }),
|
||||
stubs: {
|
||||
BreadcrumbHeading: true,
|
||||
UserDetailsElement: true,
|
||||
UserPreferencesElement: true,
|
||||
Heading: true,
|
||||
BAlert: true,
|
||||
BModal: true,
|
||||
UserPickTheme: true,
|
||||
UserBeaconSettings: true,
|
||||
UserPreferredObjectStore: true,
|
||||
UserDeletion: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.find("#oidc-profile").exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("does not show oidc-profile element when profile_url is not set", async () => {
|
||||
vi.mocked(useConfig).mockReturnValue({
|
||||
config: computed(() => ({
|
||||
enable_oidc: true,
|
||||
disable_local_accounts: true,
|
||||
oidc: {},
|
||||
themes: {},
|
||||
})),
|
||||
isConfigLoaded: computed(() => true),
|
||||
});
|
||||
|
||||
vi.mocked(hasSingleOidcProfile).mockReturnValue(false);
|
||||
|
||||
const wrapper = shallowMount(UserPreferences, {
|
||||
localVue,
|
||||
router: new VueRouter(),
|
||||
pinia: createTestingPinia({ createSpy: vi.fn }),
|
||||
stubs: {
|
||||
BreadcrumbHeading: true,
|
||||
UserDetailsElement: true,
|
||||
UserPreferencesElement: true,
|
||||
Heading: true,
|
||||
BAlert: true,
|
||||
BModal: true,
|
||||
UserPickTheme: true,
|
||||
UserBeaconSettings: true,
|
||||
UserPreferredObjectStore: true,
|
||||
UserDeletion: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.find("#oidc-profile").exists()).toBe(false);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[{ enable_account_interface: false }],
|
||||
[{ enable_account_interface: true, use_remote_user: true }],
|
||||
[{ enable_account_interface: true, disable_local_accounts: true }],
|
||||
])("hides password preference when config is %o", async (configOverrides) => {
|
||||
// Mock the config returned by useConfig (used by the component for other things)
|
||||
vi.mocked(useConfig).mockReturnValue({
|
||||
config: computed(() => ({
|
||||
enable_oidc: false,
|
||||
disable_local_accounts: false,
|
||||
oidc: {},
|
||||
themes: {},
|
||||
...configOverrides,
|
||||
})),
|
||||
isConfigLoaded: computed(() => true),
|
||||
});
|
||||
mockPreferences(true);
|
||||
|
||||
const wrapper = shallowMount(UserPreferences, {
|
||||
localVue,
|
||||
router: new VueRouter(),
|
||||
pinia: createTestingPinia({ createSpy: vi.fn }),
|
||||
stubs: {
|
||||
BreadcrumbHeading: true,
|
||||
UserDetailsElement: true,
|
||||
UserPreferencesElement: true,
|
||||
Heading: true,
|
||||
BAlert: true,
|
||||
BModal: true,
|
||||
UserPickTheme: true,
|
||||
UserBeaconSettings: true,
|
||||
UserPreferredObjectStore: true,
|
||||
UserDeletion: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.find("#edit-preferences-password").exists()).toBe(false);
|
||||
});
|
||||
|
||||
it("shows password preference when allowed", async () => {
|
||||
vi.mocked(useConfig).mockReturnValue({
|
||||
config: computed(() => ({
|
||||
disable_local_accounts: false,
|
||||
enable_account_interface: true,
|
||||
use_remote_user: false,
|
||||
themes: {},
|
||||
})),
|
||||
isConfigLoaded: computed(() => true),
|
||||
});
|
||||
mockPreferences(false);
|
||||
const wrapper = shallowMount(UserPreferences, {
|
||||
localVue,
|
||||
router: new VueRouter(),
|
||||
pinia: createTestingPinia({ createSpy: vi.fn }),
|
||||
stubs: {
|
||||
BreadcrumbHeading: true,
|
||||
UserDetailsElement: true,
|
||||
UserPreferencesElement: true,
|
||||
Heading: true,
|
||||
BAlert: true,
|
||||
BModal: true,
|
||||
UserPickTheme: true,
|
||||
UserBeaconSettings: true,
|
||||
UserPreferredObjectStore: true,
|
||||
UserDeletion: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.find("#edit-preferences-password").exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
faKey,
|
||||
faLock,
|
||||
faPalette,
|
||||
faPerson,
|
||||
faRadiation,
|
||||
faSignOut,
|
||||
faUsers,
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
|
||||
import { getGalaxyInstance } from "@/app";
|
||||
import { hasSingleOidcProfile, type OIDCConfig } from "@/components/User/ExternalIdentities/ExternalIDHelper";
|
||||
import { getUserPreferencesModel } from "@/components/User/UserPreferencesModel";
|
||||
import { useConfig } from "@/composables/config";
|
||||
import { useConfirmDialog } from "@/composables/confirmDialog";
|
||||
@@ -59,6 +61,16 @@ const activePreferences = computed(() => {
|
||||
const enabledPreferences = Object.entries(userPreferencesEntries).filter(([, value]) => !value.disabled);
|
||||
return Object.fromEntries(enabledPreferences);
|
||||
});
|
||||
// Show the OIDC profile management widget if local account editing is disabled and OIDC profile is configured
|
||||
// through a single provider
|
||||
const showOidcProfile = computed<boolean>(() => {
|
||||
if (isConfigLoaded.value) {
|
||||
const oidcConfig: OIDCConfig = config.value.oidc;
|
||||
return config.value.enable_oidc && !config.value.enable_account_interface && hasSingleOidcProfile(oidcConfig);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
const hasLogout = computed(() => {
|
||||
if (isConfigLoaded.value) {
|
||||
const Galaxy = getGalaxyInstance();
|
||||
@@ -169,6 +181,14 @@ onMounted(async () => {
|
||||
|
||||
<div class="d-flex flex-gapy-1 flex-column">
|
||||
<div class="d-flex flex-wrap mb-4 user-preferences-cards">
|
||||
<UserPreferencesElement
|
||||
v-if="showOidcProfile"
|
||||
id="oidc-profile"
|
||||
title="Manage my profile"
|
||||
:icon="faPerson"
|
||||
description="Manage my profile information (username, email, password)."
|
||||
to="/user/oidc-profile" />
|
||||
|
||||
<UserPreferencesElement
|
||||
v-for="(link, index) in activePreferences"
|
||||
:id="link.id"
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { createTestingPinia } from "@pinia/testing";
|
||||
import { setActivePinia } from "pinia";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { computed } from "vue";
|
||||
|
||||
import { getUserPreferencesModel } from "@/components/User/UserPreferencesModel";
|
||||
import { useConfig } from "@/composables/config";
|
||||
|
||||
vi.mock("@/composables/config", () => ({
|
||||
useConfig: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockConfig = (cfg: any) =>
|
||||
vi.mocked(useConfig).mockReturnValue({
|
||||
config: computed(() => cfg),
|
||||
isConfigLoaded: computed(() => true),
|
||||
});
|
||||
|
||||
describe("getUserPreferencesModel", () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ createSpy: vi.fn }));
|
||||
});
|
||||
|
||||
it("disables password when account interface is off", () => {
|
||||
mockConfig({
|
||||
enable_account_interface: false,
|
||||
use_remote_user: false,
|
||||
disable_local_accounts: false,
|
||||
has_user_tool_filters: false,
|
||||
themes: {},
|
||||
});
|
||||
|
||||
const prefs = getUserPreferencesModel("user-id");
|
||||
expect(prefs.password.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("disables password when using remote user", () => {
|
||||
mockConfig({
|
||||
enable_account_interface: true,
|
||||
use_remote_user: true,
|
||||
disable_local_accounts: false,
|
||||
has_user_tool_filters: false,
|
||||
themes: {},
|
||||
});
|
||||
|
||||
const prefs = getUserPreferencesModel("user-id");
|
||||
expect(prefs.password.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("enables password when local accounts allowed and interface enabled", () => {
|
||||
mockConfig({
|
||||
enable_account_interface: true,
|
||||
use_remote_user: false,
|
||||
disable_local_accounts: false,
|
||||
has_user_tool_filters: false,
|
||||
themes: {},
|
||||
});
|
||||
|
||||
const prefs = getUserPreferencesModel("user-id");
|
||||
expect(prefs.password.disabled).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -34,9 +34,12 @@ export const getUserPreferencesModel: (user_id?: string) => UserPreferencesModel
|
||||
title: localize("Manage Information"),
|
||||
id: "edit-preferences-information",
|
||||
description:
|
||||
isConfigLoaded.value && config.value.enable_account_interface && !config.value.use_remote_user
|
||||
isConfigLoaded.value &&
|
||||
config.value.enable_account_interface &&
|
||||
!config.value.use_remote_user &&
|
||||
!config.value.disable_local_accounts
|
||||
? localize("Edit your email, addresses and custom parameters or change your public name.")
|
||||
: localize("Edit your custom parameters."),
|
||||
: localize("Edit your addresses and custom parameters."),
|
||||
url: `/api/users/${user_id}/information/inputs`,
|
||||
icon: faUser,
|
||||
redirect: "/user",
|
||||
@@ -49,7 +52,11 @@ export const getUserPreferencesModel: (user_id?: string) => UserPreferencesModel
|
||||
url: `/api/users/${user_id}/password/inputs`,
|
||||
submitTitle: "Save Password",
|
||||
redirect: "/user",
|
||||
disabled: isConfigLoaded.value && (config.value.use_remote_user || !config.value.enable_account_interface),
|
||||
disabled:
|
||||
isConfigLoaded.value &&
|
||||
(config.value.use_remote_user ||
|
||||
config.value.disable_local_accounts ||
|
||||
!config.value.enable_account_interface),
|
||||
},
|
||||
toolbox_filters: {
|
||||
title: localize("Manage Toolbox Filters"),
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getGalaxyInstance } from "@/app";
|
||||
import { HistoryExport } from "@/components/HistoryExport/index";
|
||||
import { APIKey } from "@/components/User/APIKey";
|
||||
import { ExternalIdentities } from "@/components/User/ExternalIdentities";
|
||||
import { hasSingleOidcProfile } from "@/components/User/ExternalIdentities/ExternalIDHelper";
|
||||
import AdminRoutes from "@/entry/analysis/routes/admin-routes";
|
||||
import LibraryRoutes from "@/entry/analysis/routes/library-routes";
|
||||
import StorageRoutes from "@/entry/analysis/routes/storage-routes";
|
||||
@@ -81,6 +82,7 @@ import CustomBuilds from "@/components/User/CustomBuilds.vue";
|
||||
import HistoryStorageOverview from "@/components/User/DiskUsage/Visualizations/HistoryStorageOverview.vue";
|
||||
import NotificationsPreferences from "@/components/User/Notifications/NotificationsPreferences.vue";
|
||||
import UserDatasetPermissions from "@/components/User/UserDatasetPermissions.vue";
|
||||
import UserOidcProfile from "@/components/User/UserOidcProfile.vue";
|
||||
import UserPreferences from "@/components/User/UserPreferences.vue";
|
||||
import UserPreferencesForm from "@/components/User/UserPreferencesForm.vue";
|
||||
import DisplayApplication from "@/components/Visualizations/DisplayApplication.vue";
|
||||
@@ -614,6 +616,17 @@ export function getRouter(Galaxy) {
|
||||
component: CredentialsManagement,
|
||||
redirect: redirectAnon(),
|
||||
},
|
||||
{
|
||||
path: "user/oidc-profile",
|
||||
component: UserOidcProfile,
|
||||
redirect:
|
||||
redirectIf(
|
||||
!Galaxy.config.enable_oidc ||
|
||||
Galaxy.config.enable_account_interface ||
|
||||
!hasSingleOidcProfile(Galaxy.config.oidc),
|
||||
"/user",
|
||||
) || redirectAnon(),
|
||||
},
|
||||
{
|
||||
path: "user/external_ids",
|
||||
component: ExternalIdentities,
|
||||
|
||||
@@ -131,6 +131,8 @@ class AuthnzManager:
|
||||
self.app.config.oidc[idp]["end_user_registration_endpoint"] = self.oidc_backends_config[idp][
|
||||
"end_user_registration_endpoint"
|
||||
]
|
||||
if "profile_url" in self.oidc_backends_config[idp]:
|
||||
self.app.config.oidc[idp]["profile_url"] = self.oidc_backends_config[idp]["profile_url"]
|
||||
|
||||
if len(self.oidc_backends_config) == 0:
|
||||
raise etree.ParseError("No valid provider configuration parsed.")
|
||||
@@ -174,6 +176,8 @@ class AuthnzManager:
|
||||
rtv["username_key"] = config_xml.find("username_key").text
|
||||
if config_xml.find("end_user_registration_endpoint") is not None:
|
||||
rtv["end_user_registration_endpoint"] = config_xml.find("end_user_registration_endpoint").text
|
||||
if config_xml.find("profile_url") is not None:
|
||||
rtv["profile_url"] = config_xml.find("profile_url").text
|
||||
|
||||
# this is a EGI Check-in specific config
|
||||
if config_xml.find("checkin_env") is not None:
|
||||
|
||||
@@ -170,6 +170,13 @@
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="profile_url" minOccurs="0" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
URL for profile management in the OIDC provider.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
</xs:all>
|
||||
<xs:attribute name="name" type="xs:string" use="required">
|
||||
<xs:annotation>
|
||||
|
||||
@@ -2829,7 +2829,7 @@ mapping:
|
||||
deprecated_alias: allow_user_creation
|
||||
desc: |
|
||||
Allow unregistered users to create new local (non-OIDC) accounts (otherwise, they will have to
|
||||
be created by an admin). This option will be overridden to false in case disable_local_accounts
|
||||
be created by an admin). This option will be overridden to false in case disable_local_accounts
|
||||
is set to true.
|
||||
|
||||
disable_local_accounts:
|
||||
@@ -2837,8 +2837,8 @@ mapping:
|
||||
default: false
|
||||
required: true
|
||||
desc: |
|
||||
Disable local accounts. If this option is set to true, at least one OIDC provider needs
|
||||
to be configured and will serve as the account provider. If this option is set to true,
|
||||
Disable local accounts. If this option is set to true, at least one OIDC provider needs
|
||||
to be configured and will serve as the account provider. If this option is set to true,
|
||||
allow_local_account creation will be overridden with false.
|
||||
|
||||
allow_user_deletion:
|
||||
|
||||
@@ -820,7 +820,12 @@ class UserAPIController(BaseGalaxyAPIController, UsesTagsMixin, BaseUIController
|
||||
"username": username,
|
||||
}
|
||||
is_galaxy_app = trans.webapp.name == "galaxy"
|
||||
if (trans.app.config.enable_account_interface and not trans.app.config.use_remote_user) or not is_galaxy_app:
|
||||
allow_profile_edit = (
|
||||
trans.app.config.enable_account_interface
|
||||
and not trans.app.config.use_remote_user
|
||||
and not trans.app.config.disable_local_accounts
|
||||
)
|
||||
if allow_profile_edit or not is_galaxy_app:
|
||||
inputs.append(
|
||||
{
|
||||
"id": "email_input",
|
||||
@@ -836,7 +841,7 @@ class UserAPIController(BaseGalaxyAPIController, UsesTagsMixin, BaseUIController
|
||||
}
|
||||
)
|
||||
if is_galaxy_app:
|
||||
if trans.app.config.enable_account_interface and not trans.app.config.use_remote_user:
|
||||
if allow_profile_edit:
|
||||
inputs.append(
|
||||
{
|
||||
"id": "name_input",
|
||||
|
||||
Reference in New Issue
Block a user