diff options
| author | Tobias Järvelöv <tobias.jarvelov@mullvad.net> | 2025-09-30 10:25:01 +0200 |
|---|---|---|
| committer | Tobias Järvelöv <tobias.jarvelov@mullvad.net> | 2025-09-30 10:25:01 +0200 |
| commit | a937623372b83783d5867e45c14fc32dd1db5bb5 (patch) | |
| tree | 4742f4dd17ae1c0f92403dc9ac892494b10e060c | |
| parent | cadcae1712b4243577b1a12a13eea8b785e62e3f (diff) | |
| parent | bbb67b47481df706c2fc602a3668182676c48534 (diff) | |
| download | mullvadvpn-a937623372b83783d5867e45c14fc32dd1db5bb5.tar.xz mullvadvpn-a937623372b83783d5867e45c14fc32dd1db5bb5.zip | |
Merge branch 'dont-allow-duplicate-names-for-api-access-methods-des-1136'
| -rw-r--r-- | CHANGELOG.md | 3 | ||||
| -rw-r--r-- | desktop/packages/mullvad-vpn/locales/messages.pot | 4 | ||||
| -rw-r--r-- | desktop/packages/mullvad-vpn/src/main/daemon-rpc.ts | 39 | ||||
| -rw-r--r-- | desktop/packages/mullvad-vpn/src/renderer/components/EditApiAccessMethod.tsx | 29 | ||||
| -rw-r--r-- | desktop/packages/mullvad-vpn/src/renderer/components/ProxyForm.tsx | 65 | ||||
| -rw-r--r-- | desktop/packages/mullvad-vpn/src/renderer/components/cell/SettingsRow.tsx | 3 | ||||
| -rw-r--r-- | desktop/packages/mullvad-vpn/src/renderer/components/cell/SettingsTextInput.tsx | 2 | ||||
| -rw-r--r-- | desktop/packages/mullvad-vpn/src/shared/daemon-rpc-types.ts | 2 | ||||
| -rw-r--r-- | desktop/packages/mullvad-vpn/src/shared/ipc-schema.ts | 5 | ||||
| -rw-r--r-- | mullvad-api/src/access_mode.rs | 2 | ||||
| -rw-r--r-- | mullvad-daemon/src/access_method.rs | 34 | ||||
| -rw-r--r-- | mullvad-daemon/src/lib.rs | 10 | ||||
| -rw-r--r-- | mullvad-daemon/src/settings/mod.rs | 19 | ||||
| -rw-r--r-- | mullvad-management-interface/src/client.rs | 34 | ||||
| -rw-r--r-- | mullvad-management-interface/src/lib.rs | 4 | ||||
| -rw-r--r-- | mullvad-types/src/access_method.rs | 64 |
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 }, |
