summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorTobias Järvelöv <tobias.jarvelov@mullvad.net>2025-09-30 10:25:01 +0200
committerTobias Järvelöv <tobias.jarvelov@mullvad.net>2025-09-30 10:25:01 +0200
commita937623372b83783d5867e45c14fc32dd1db5bb5 (patch)
tree4742f4dd17ae1c0f92403dc9ac892494b10e060c
parentcadcae1712b4243577b1a12a13eea8b785e62e3f (diff)
parentbbb67b47481df706c2fc602a3668182676c48534 (diff)
downloadmullvadvpn-a937623372b83783d5867e45c14fc32dd1db5bb5.tar.xz
mullvadvpn-a937623372b83783d5867e45c14fc32dd1db5bb5.zip
Merge branch 'dont-allow-duplicate-names-for-api-access-methods-des-1136'
-rw-r--r--CHANGELOG.md3
-rw-r--r--desktop/packages/mullvad-vpn/locales/messages.pot4
-rw-r--r--desktop/packages/mullvad-vpn/src/main/daemon-rpc.ts39
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/EditApiAccessMethod.tsx29
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/ProxyForm.tsx65
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/cell/SettingsRow.tsx3
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/cell/SettingsTextInput.tsx2
-rw-r--r--desktop/packages/mullvad-vpn/src/shared/daemon-rpc-types.ts2
-rw-r--r--desktop/packages/mullvad-vpn/src/shared/ipc-schema.ts5
-rw-r--r--mullvad-api/src/access_mode.rs2
-rw-r--r--mullvad-daemon/src/access_method.rs34
-rw-r--r--mullvad-daemon/src/lib.rs10
-rw-r--r--mullvad-daemon/src/settings/mod.rs19
-rw-r--r--mullvad-management-interface/src/client.rs34
-rw-r--r--mullvad-management-interface/src/lib.rs4
-rw-r--r--mullvad-types/src/access_method.rs64
16 files changed, 232 insertions, 87 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6b69258519..538858aa97 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,6 +25,9 @@ Line wrap the file at 100 chars. Th
### Added
- Add settings reset command to the CLI ('mullvad reset-settings').
+### Changed
+- Add validation for API access methods to only allow unique names. Access methods with
+ duplicated names will be renamed.
## [2025.10-beta1] - 2025-09-16
### Added
diff --git a/desktop/packages/mullvad-vpn/locales/messages.pot b/desktop/packages/mullvad-vpn/locales/messages.pot
index 29c71034c2..cdb25307b6 100644
--- a/desktop/packages/mullvad-vpn/locales/messages.pot
+++ b/desktop/packages/mullvad-vpn/locales/messages.pot
@@ -550,6 +550,10 @@ msgid "Please enter a valid remote server port."
msgstr ""
msgctxt "api-access-methods-view"
+msgid "Please select a name for the access method not already in use."
+msgstr ""
+
+msgctxt "api-access-methods-view"
msgid "Remote Server"
msgstr ""
diff --git a/desktop/packages/mullvad-vpn/src/main/daemon-rpc.ts b/desktop/packages/mullvad-vpn/src/main/daemon-rpc.ts
index 5c8e6de6e1..c3594b9590 100644
--- a/desktop/packages/mullvad-vpn/src/main/daemon-rpc.ts
+++ b/desktop/packages/mullvad-vpn/src/main/daemon-rpc.ts
@@ -4,6 +4,7 @@ import { BoolValue, StringValue } from 'google-protobuf/google/protobuf/wrappers
import { types as grpcTypes } from 'management-interface';
import {
+ AccessMethodExistsError,
AccessMethodSetting,
AccountDataError,
AccountDataResponse,
@@ -601,16 +602,38 @@ export class DaemonRpc extends GrpcClient {
}
}
- public async addApiAccessMethod(method: NewAccessMethodSetting): Promise<string> {
- const result = await this.call<grpcTypes.NewAccessMethodSetting, grpcTypes.UUID>(
- this.client.addApiAccessMethod,
- convertToNewApiAccessMethodSetting(method),
- );
- return result.getValue();
+ public async addApiAccessMethod(
+ method: NewAccessMethodSetting,
+ ): Promise<string | AccessMethodExistsError> {
+ try {
+ const result = await this.call<grpcTypes.NewAccessMethodSetting, grpcTypes.UUID>(
+ this.client.addApiAccessMethod,
+ convertToNewApiAccessMethodSetting(method),
+ );
+ return result.getValue();
+ } catch (e) {
+ const error = e as grpc.ServiceError;
+ if (error.code === 6) {
+ return { type: 'name already exists' };
+ } else {
+ throw error;
+ }
+ }
}
- public async updateApiAccessMethod(method: AccessMethodSetting) {
- await this.call(this.client.updateApiAccessMethod, convertToApiAccessMethodSetting(method));
+ public async updateApiAccessMethod(
+ method: AccessMethodSetting,
+ ): Promise<void | AccessMethodExistsError> {
+ try {
+ await this.call(this.client.updateApiAccessMethod, convertToApiAccessMethodSetting(method));
+ } catch (e) {
+ const error = e as grpc.ServiceError;
+ if (error.code === 6) {
+ return { type: 'name already exists' };
+ } else {
+ throw error;
+ }
+ }
}
public async getCurrentApiAccessMethod() {
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/EditApiAccessMethod.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/EditApiAccessMethod.tsx
index f3c55965d8..d404b72408 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/EditApiAccessMethod.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/EditApiAccessMethod.tsx
@@ -21,7 +21,7 @@ import { BackAction } from './KeyboardNavigation';
import { Layout, SettingsContainer, SettingsContent, SettingsNavigationScrollbars } from './Layout';
import { ModalAlert, ModalAlertType } from './Modal';
import { NavigationContainer } from './NavigationContainer';
-import { NamedProxyForm } from './ProxyForm';
+import { NamedProxyForm, ProxyFormButtons, ProxyFormInner, ProxyFormNameField } from './ProxyForm';
import SettingsHeader, { HeaderSubTitle, HeaderTitle } from './SettingsHeader';
export function EditApiAccessMethod() {
@@ -91,6 +91,19 @@ function AccessMethodForm() {
const title = getTitle(id === undefined);
const subtitle = getSubtitle(id === undefined);
+ const customAccessMethods = useSelector((state) => state.settings.apiAccessMethods.custom);
+ const onValidate = useCallback(
+ (value: string) => {
+ const nameUsedInOtherAccessMethod = customAccessMethods.some(
+ (customAccessMethod) =>
+ method?.id !== customAccessMethod.id && customAccessMethod.name === value,
+ );
+
+ return !nameUsedInOtherAccessMethod;
+ },
+ [customAccessMethods, method],
+ );
+
return (
<BackAction action={pop}>
<Layout>
@@ -108,7 +121,19 @@ function AccessMethodForm() {
{id !== undefined && method === undefined ? (
<span>Failed to open method</span>
) : (
- <NamedProxyForm proxy={method} onSave={onSave} onCancel={pop} />
+ <NamedProxyForm proxy={method} onSave={onSave} onCancel={pop}>
+ <ProxyFormNameField
+ rowProps={{
+ errorMessage: messages.pgettext(
+ 'api-access-methods-view',
+ 'Please select a name for the access method not already in use.',
+ ),
+ }}
+ inputProps={{ validate: onValidate }}
+ />
+ <ProxyFormInner />
+ <ProxyFormButtons />
+ </NamedProxyForm>
)}
<TestingDialog
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/ProxyForm.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/ProxyForm.tsx
index 0eda7027ca..9cb0686173 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/ProxyForm.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/ProxyForm.tsx
@@ -17,9 +17,13 @@ import * as Cell from './cell';
import { SettingsForm, useSettingsFormSubmittable } from './cell/SettingsForm';
import { SettingsGroup } from './cell/SettingsGroup';
import { SettingsRadioGroup } from './cell/SettingsRadioGroup';
-import { SettingsRow } from './cell/SettingsRow';
+import { IndentedRowProps, SettingsRow } from './cell/SettingsRow';
import { SettingsSelect, SettingsSelectItem } from './cell/SettingsSelect';
-import { SettingsNumberInput, SettingsTextInput } from './cell/SettingsTextInput';
+import {
+ SettingsNumberInput,
+ SettingsTextInput,
+ SettingsTextInputProps,
+} from './cell/SettingsTextInput';
interface ProxyFormContext {
proxy?: CustomProxy;
@@ -78,7 +82,7 @@ export function ProxyForm(props: ProxyFormContextProviderProps) {
<ProxyFormContextProvider {...props}>
<SettingsForm>
<ProxyFormInner />
- <ProxyFormButtons new={props.proxy === undefined} />
+ <ProxyFormButtons />
</SettingsForm>
</ProxyFormContextProvider>
);
@@ -100,12 +104,13 @@ const namedProxyFormContext = React.createContext<NamedProxyFormContext>({
interface NamedProxyFormContainerProps
extends Omit<ProxyFormContextProviderProps, 'proxy' | 'onSave'> {
+ children?: React.ReactNode;
proxy?: NamedCustomProxy;
onSave: (proxy: NamedCustomProxy) => void;
}
export function NamedProxyForm(props: NamedProxyFormContainerProps) {
- const { onSave, ...otherProps } = props;
+ const { children, onSave, ...otherProps } = props;
const [name, setName] = useState<string>(props.proxy?.name ?? '');
@@ -123,36 +128,36 @@ export function NamedProxyForm(props: NamedProxyFormContainerProps) {
return (
<namedProxyFormContext.Provider value={nameContextValue}>
<ProxyFormContextProvider {...otherProps} onSave={save}>
- <SettingsForm>
- <ProxyFormNameField />
- <ProxyFormInner />
- <ProxyFormButtons new={props.proxy === undefined} />
- </SettingsForm>
+ <SettingsForm>{children}</SettingsForm>
</ProxyFormContextProvider>
</namedProxyFormContext.Provider>
);
}
-function ProxyFormNameField() {
+type ProxyFormNameFieldProps = {
+ inputProps?: Partial<SettingsTextInputProps>;
+ rowProps?: Partial<IndentedRowProps>;
+};
+
+export function ProxyFormNameField(props: ProxyFormNameFieldProps) {
const { name, setName } = useContext(namedProxyFormContext);
return (
- <SettingsRow label={messages.gettext('Name')}>
- <SettingsTextInput
- defaultValue={name}
- placeholder={messages.pgettext('api-access-methods-view', 'Enter name')}
- onUpdate={setName}
- />
- </SettingsRow>
+ <SettingsGroup>
+ <SettingsRow label={messages.gettext('Name')} {...props?.rowProps}>
+ <SettingsTextInput
+ defaultValue={name}
+ placeholder={messages.pgettext('api-access-methods-view', 'Enter name')}
+ onUpdate={setName}
+ {...props?.inputProps}
+ />
+ </SettingsRow>
+ </SettingsGroup>
);
}
-interface ProxyFormButtonsProps {
- new: boolean;
-}
-
-export function ProxyFormButtons(props: ProxyFormButtonsProps) {
- const { onSave, onCancel, onDelete } = useContext(proxyFormContext);
+export function ProxyFormButtons() {
+ const { onSave, onCancel, onDelete, proxy } = useContext(proxyFormContext);
// Contains form submittability to know whether or not to enable the Add/Save button.
const formSubmittable = useSettingsFormSubmittable();
@@ -170,16 +175,14 @@ export function ProxyFormButtons(props: ProxyFormButtonsProps) {
<Button.Text>{messages.gettext('Cancel')}</Button.Text>
</Button>
<Button onClick={onSave} disabled={!formSubmittable}>
- <Button.Text>
- {props.new ? messages.gettext('Add') : messages.gettext('Save')}
- </Button.Text>
+ <Button.Text>{proxy ? messages.gettext('Save') : messages.gettext('Add')}</Button.Text>
</Button>
</FlexRow>
</Flex>
);
}
-function ProxyFormInner() {
+export function ProxyFormInner() {
const { proxy, setProxy } = useContext(proxyFormContext);
// Available custom proxies
@@ -214,9 +217,11 @@ function ProxyFormInner() {
return (
<>
- <SettingsRow label={messages.gettext('Type')}>
- <SettingsSelect defaultValue={type} onUpdate={setType} items={types} />
- </SettingsRow>
+ <SettingsGroup>
+ <SettingsRow label={messages.gettext('Type')}>
+ <SettingsSelect defaultValue={type} onUpdate={setType} items={types} />
+ </SettingsRow>
+ </SettingsGroup>
{type === 'shadowsocks' && (
<EditShadowsocks
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/cell/SettingsRow.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/cell/SettingsRow.tsx
index d8b4b29d82..9b4804c4a3 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/cell/SettingsRow.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/cell/SettingsRow.tsx
@@ -60,6 +60,7 @@ const StyledSettingsRowErrorMessage = styled.div(tinyText, {
display: 'flex',
alignItems: 'center',
marginLeft: measurements.horizontalViewMargin,
+ marginRight: measurements.horizontalViewMargin,
marginTop: '5px',
color: colors.whiteAlpha60,
});
@@ -85,7 +86,7 @@ export function useSettingsRowContext() {
return useContext(settingsRowContext);
}
-interface IndentedRowProps {
+export interface IndentedRowProps {
label: string;
infoMessage?: string | Array<string>;
errorMessage?: string;
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/cell/SettingsTextInput.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/cell/SettingsTextInput.tsx
index 45ced44d66..22b55c3a49 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/cell/SettingsTextInput.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/cell/SettingsTextInput.tsx
@@ -21,7 +21,7 @@ const StyledInput = styled.input(smallNormalText, {
},
});
-interface SettingsTextInputProps extends InputProps<'text'> {
+export interface SettingsTextInputProps extends InputProps<'text'> {
defaultValue?: string;
}
diff --git a/desktop/packages/mullvad-vpn/src/shared/daemon-rpc-types.ts b/desktop/packages/mullvad-vpn/src/shared/daemon-rpc-types.ts
index 117f002289..026df6d189 100644
--- a/desktop/packages/mullvad-vpn/src/shared/daemon-rpc-types.ts
+++ b/desktop/packages/mullvad-vpn/src/shared/daemon-rpc-types.ts
@@ -487,6 +487,8 @@ export type NewCustomList = Pick<ICustomList, 'name' | 'locations'>;
export type CustomListError = { type: 'name already exists' };
+export type AccessMethodExistsError = { type: 'name already exists' };
+
export interface ISettings {
allowLan: boolean;
autoConnect: boolean;
diff --git a/desktop/packages/mullvad-vpn/src/shared/ipc-schema.ts b/desktop/packages/mullvad-vpn/src/shared/ipc-schema.ts
index cd33b994fe..a685a2974e 100644
--- a/desktop/packages/mullvad-vpn/src/shared/ipc-schema.ts
+++ b/desktop/packages/mullvad-vpn/src/shared/ipc-schema.ts
@@ -2,6 +2,7 @@ import { GetTextTranslations } from 'gettext-parser';
import { ILinuxSplitTunnelingApplication, ISplitTunnelingApplication } from './application-types';
import {
+ AccessMethodExistsError,
AccessMethodSetting,
AccountDataError,
AccountNumber,
@@ -203,8 +204,8 @@ export const ipcSchema = {
updateBridgeSettings: invoke<BridgeSettings, void>(),
setDnsOptions: invoke<IDnsOptions, void>(),
setObfuscationSettings: invoke<ObfuscationSettings, void>(),
- addApiAccessMethod: invoke<NewAccessMethodSetting, string>(),
- updateApiAccessMethod: invoke<AccessMethodSetting, void>(),
+ addApiAccessMethod: invoke<NewAccessMethodSetting, string | AccessMethodExistsError>(),
+ updateApiAccessMethod: invoke<AccessMethodSetting, void | AccessMethodExistsError>(),
removeApiAccessMethod: invoke<string, void>(),
setApiAccessMethod: invoke<string, void>(),
testApiAccessMethodById: invoke<string, boolean>(),
diff --git a/mullvad-api/src/access_mode.rs b/mullvad-api/src/access_mode.rs
index 51c37fff8a..4872b18c9d 100644
--- a/mullvad-api/src/access_mode.rs
+++ b/mullvad-api/src/access_mode.rs
@@ -255,7 +255,7 @@ impl<B: AccessMethodResolver + 'static> AccessModeSelector<B> {
{
if api_endpoint.force_direct {
access_method_settings
- .update(|setting| setting.is_direct(), |setting| setting.enable());
+ .update_builtin(|setting| setting.is_direct(), |setting| setting.enable());
}
}
diff --git a/mullvad-daemon/src/access_method.rs b/mullvad-daemon/src/access_method.rs
index b20c040f2f..3e3811fb83 100644
--- a/mullvad-daemon/src/access_method.rs
+++ b/mullvad-daemon/src/access_method.rs
@@ -39,12 +39,13 @@ impl Daemon {
name: String,
enabled: bool,
access_method: AccessMethod,
- ) -> Result<access_method::Id, Error> {
+ ) -> Result<access_method::Id, crate::Error> {
let access_method_setting = AccessMethodSetting::new(name, enabled, access_method);
let id = access_method_setting.get_id();
self.settings
- .update(|settings| settings.api_access_methods.append(access_method_setting))
- .await?;
+ .try_update(|settings| settings.api_access_methods.append(access_method_setting))
+ .await
+ .map_err(crate::Error::SettingsError)?;
Ok(id)
}
@@ -80,11 +81,12 @@ impl Daemon {
access_method: access_method::Id,
) -> Result<(), Error> {
self.settings
- .update(|settings| {
+ .try_update(|settings| -> Result<(), Error> {
settings.api_access_methods.update(
|setting| setting.get_id() == access_method,
|setting| setting.enable(),
- );
+ )?;
+ Ok(())
})
.await?;
self.access_mode_handler
@@ -111,16 +113,20 @@ impl Daemon {
pub async fn update_access_method(
&mut self,
access_method_update: AccessMethodSetting,
- ) -> Result<(), Error> {
+ ) -> Result<(), crate::Error> {
self.settings
- .update(|settings: &mut Settings| {
- let target = access_method_update.get_id();
- settings.api_access_methods.update(
- |access_method| access_method.get_id() == target,
- |method| *method = access_method_update,
- );
- })
- .await?;
+ .try_update(
+ |settings: &mut Settings| -> Result<(), access_method::Error> {
+ let target = access_method_update.get_id();
+ settings.api_access_methods.update(
+ |access_method| access_method.get_id() == target,
+ |method| *method = access_method_update,
+ )?;
+ Ok(())
+ },
+ )
+ .await
+ .map_err(crate::Error::SettingsError)?;
Ok(())
}
diff --git a/mullvad-daemon/src/lib.rs b/mullvad-daemon/src/lib.rs
index f371b9a91a..33f904947c 100644
--- a/mullvad-daemon/src/lib.rs
+++ b/mullvad-daemon/src/lib.rs
@@ -3019,10 +3019,7 @@ impl Daemon {
enabled: bool,
access_method: AccessMethod,
) {
- let result = self
- .add_access_method(name, enabled, access_method)
- .await
- .map_err(Error::AccessMethodError);
+ let result = self.add_access_method(name, enabled, access_method).await;
Self::oneshot_send(tx, result, "add_api_access_method response");
}
@@ -3055,10 +3052,7 @@ impl Daemon {
tx: ResponseTx<(), Error>,
method: AccessMethodSetting,
) {
- let result = self
- .update_access_method(method)
- .await
- .map_err(Error::AccessMethodError);
+ let result = self.update_access_method(method).await;
Self::oneshot_send(tx, result, "update_api_access_method response");
}
diff --git a/mullvad-daemon/src/settings/mod.rs b/mullvad-daemon/src/settings/mod.rs
index ff605dd79b..a1d9434299 100644
--- a/mullvad-daemon/src/settings/mod.rs
+++ b/mullvad-daemon/src/settings/mod.rs
@@ -1,5 +1,6 @@
use futures::TryFutureExt;
use mullvad_types::{
+ access_method::Error as ApiAccessMethodError,
custom_list::Error as CustomListError,
relay_constraints::{RelayConstraints, RelaySettings, WireguardConstraints},
settings::{DnsState, Settings},
@@ -60,6 +61,10 @@ impl From<Error> for mullvad_management_interface::Status {
let custom_list_err = *err.downcast::<CustomListError>().unwrap();
handle_custom_list_error(custom_list_err)
}
+ Error::UpdateFailed(err) if err.downcast_ref::<ApiAccessMethodError>().is_some() => {
+ let api_access_method_err = *err.downcast::<ApiAccessMethodError>().unwrap();
+ handle_api_access_method_error(api_access_method_err)
+ }
Error::SerializeError(..)
| Error::ParseError(..)
| Error::UpdateFailed(..)
@@ -68,6 +73,20 @@ impl From<Error> for mullvad_management_interface::Status {
}
}
+fn handle_api_access_method_error(
+ api_access_method_err: ApiAccessMethodError,
+) -> mullvad_management_interface::Status {
+ use mullvad_management_interface::{Code, Status};
+ match api_access_method_err {
+ error @ ApiAccessMethodError::DuplicateName => Status::with_details(
+ Code::AlreadyExists,
+ error.to_string(),
+ mullvad_management_interface::API_ACCESS_METHOD_EXISTS_DETAILS.into(),
+ ),
+ error => Status::unknown(error.to_string()),
+ }
+}
+
fn handle_custom_list_error(
custom_list_err: CustomListError,
) -> mullvad_management_interface::Status {
diff --git a/mullvad-management-interface/src/client.rs b/mullvad-management-interface/src/client.rs
index 1ea9578993..afadfd0875 100644
--- a/mullvad-management-interface/src/client.rs
+++ b/mullvad-management-interface/src/client.rs
@@ -510,7 +510,10 @@ impl MullvadProxyClient {
enabled,
access_method: Some(types::AccessMethod::from(access_method)),
};
- self.0.add_api_access_method(request).await?;
+ self.0
+ .add_api_access_method(request)
+ .await
+ .map_err(map_api_access_method_error)?;
Ok(())
}
@@ -530,7 +533,8 @@ impl MullvadProxyClient {
) -> Result<()> {
self.0
.update_api_access_method(types::AccessMethodSetting::from(access_method_update))
- .await?;
+ .await
+ .map_err(map_api_access_method_error)?;
Ok(())
}
@@ -654,20 +658,20 @@ fn map_device_error(status: Status) -> Error {
#[cfg(not(target_os = "android"))]
fn map_custom_list_error(status: Status) -> Error {
- match status.code() {
- Code::NotFound => {
- if status.details() == crate::CUSTOM_LIST_LIST_NOT_FOUND_DETAILS {
- Error::CustomListListNotFound
- } else {
- Error::Rpc(Box::new(status))
- }
+ match (status.code(), status.details()) {
+ (Code::NotFound, crate::CUSTOM_LIST_LIST_NOT_FOUND_DETAILS) => {
+ Error::CustomListListNotFound
}
- Code::AlreadyExists => {
- if status.details() == crate::CUSTOM_LIST_LIST_EXISTS_DETAILS {
- Error::CustomListExists
- } else {
- Error::Rpc(Box::new(status))
- }
+ (Code::AlreadyExists, crate::CUSTOM_LIST_LIST_EXISTS_DETAILS) => Error::CustomListExists,
+ _other => Error::Rpc(Box::new(status)),
+ }
+}
+
+#[cfg(not(target_os = "android"))]
+fn map_api_access_method_error(status: Status) -> Error {
+ match (status.code(), status.details()) {
+ (Code::AlreadyExists, crate::API_ACCESS_METHOD_EXISTS_DETAILS) => {
+ Error::ApiAccessMethodExists
}
_other => Error::Rpc(Box::new(status)),
}
diff --git a/mullvad-management-interface/src/lib.rs b/mullvad-management-interface/src/lib.rs
index 09f0e6243a..bb57914caa 100644
--- a/mullvad-management-interface/src/lib.rs
+++ b/mullvad-management-interface/src/lib.rs
@@ -29,6 +29,7 @@ use std::sync::LazyLock;
static MULLVAD_MANAGEMENT_SOCKET_GROUP: LazyLock<Option<String>> =
LazyLock::new(|| env::var("MULLVAD_MANAGEMENT_SOCKET_GROUP").ok());
+pub const API_ACCESS_METHOD_EXISTS_DETAILS: &[u8] = b"api_access_method_exists";
pub const CUSTOM_LIST_LIST_NOT_FOUND_DETAILS: &[u8] = b"custom_list_list_not_found";
pub const CUSTOM_LIST_LIST_EXISTS_DETAILS: &[u8] = b"custom_list_list_exists";
pub const CUSTOM_LIST_LIST_NAME_TOO_LONG_DETAILS: &[u8] = b"custom_list_list_name_too_long";
@@ -114,6 +115,9 @@ pub enum Error {
#[error("An access method with that id does not exist")]
ApiAccessMethodNotFound,
+
+ #[error("An access method with that name already exists")]
+ ApiAccessMethodExists,
}
impl From<tonic::Status> for Error {
diff --git a/mullvad-types/src/access_method.rs b/mullvad-types/src/access_method.rs
index f823e5d83d..8ba50c91c3 100644
--- a/mullvad-types/src/access_method.rs
+++ b/mullvad-types/src/access_method.rs
@@ -30,8 +30,10 @@ impl Settings {
}
/// Append an [`AccessMethod`] to the end of `api_access_methods`.
- pub fn append(&mut self, api_access_method: AccessMethodSetting) {
- self.custom.push(api_access_method)
+ pub fn append(&mut self, api_access_method: AccessMethodSetting) -> Result<(), Error> {
+ self.check_custom_access_method_name_is_unique(&api_access_method)?;
+ self.custom.push(api_access_method);
+ Ok(())
}
/// Remove an [`AccessMethod`] from `api_access_methods`.
@@ -69,14 +71,49 @@ impl Settings {
&mut self,
predicate: impl Fn(&AccessMethodSetting) -> bool,
f: impl FnOnce(&mut AccessMethodSetting),
- ) -> bool {
+ ) -> Result<bool, Error> {
let mut updated = false;
+
+ let handle = self.clone();
+ let update_check = |new_access_method| {
+ handle.check_custom_access_method_name_is_unique(new_access_method)?;
+ Ok(())
+ };
+
if let Some(access_method) = self.iter_mut().find(|setting| predicate(setting)) {
- f(access_method);
+ let mut new_access_method = access_method.clone();
+ f(&mut new_access_method);
+ update_check(&new_access_method)?;
+ *access_method = new_access_method;
+
updated = true;
}
self.ensure_consistent_state();
+ Ok(updated)
+ }
+
+ /// Update an existing builtin [`AccessMethodSetting`] chosen by
+ /// `predicate`, in a closure `f`, saving the result to `self`.
+ ///
+ /// Returns a bool to indicate whether some [`AccessMethodSetting`] was
+ /// updated.
+ pub fn update_builtin(
+ &mut self,
+ predicate: impl Fn(&AccessMethodSetting) -> bool,
+ f: impl FnOnce(&mut AccessMethodSetting),
+ ) -> bool {
+ let mut updated = false;
+
+ if let Some(access_method) = self
+ .iter_mut()
+ .find(|setting| setting.is_builtin() && predicate(setting))
+ {
+ f(access_method);
+
+ updated = true;
+ }
+
updated
}
@@ -94,6 +131,21 @@ impl Settings {
}
}
+ /// This function will return an error if a custom access method with
+ /// the same name already exists.
+ fn check_custom_access_method_name_is_unique(
+ &self,
+ new_api_access_method: &AccessMethodSetting,
+ ) -> Result<(), Error> {
+ if self.custom.iter().any(|api_access_method| {
+ api_access_method.id != new_api_access_method.id
+ && api_access_method.name == new_api_access_method.name
+ }) {
+ return Err(Error::DuplicateName);
+ }
+ Ok(())
+ }
+
/// Iterate over references of built-in & custom access methods.
pub fn iter(&self) -> impl Iterator<Item = &AccessMethodSetting> + Clone {
use std::iter::once;
@@ -162,8 +214,10 @@ impl Default for Settings {
}
}
-#[derive(thiserror::Error, Debug)]
+#[derive(thiserror::Error, Debug, PartialEq)]
pub enum Error {
+ #[error("Access method with name already exists")]
+ DuplicateName,
/// Built-in access methods can not be removed
#[error("Cannot remove built-in access method {}", attempted)]
RemoveBuiltin { attempted: BuiltInAccessMethod },