android/mdm: enforce MDM settings in UI items

Fixes ENG-2851

Uses our MDM settings in any place where we are drawing UI that should be hidden/disabled depending on the selected system policy value.

Signed-off-by: Andrea Gottardo <andrea@gottardo.me>
pull/247/head
Andrea Gottardo 2 months ago
parent fb5635b8a5
commit 0d09fab307

@ -53,8 +53,8 @@ enum class ShowHideValue(val value: String) {
Hide("hide")
}
enum class NetworkDevices(val value: String) {
currentUser("current-user"),
otherUsers("other-users"),
taggedDevices("tagged-devices"),
enum class HiddenNetworkDevices(val value: String) {
CurrentUser("current-user"),
OtherUsers("other-users"),
TaggedDevices("tagged-devices"),
}

@ -3,10 +3,13 @@
package com.tailscale.ipn.ui.util
import com.tailscale.ipn.mdm.HiddenNetworkDevices
import com.tailscale.ipn.mdm.StringArraySetting
import com.tailscale.ipn.ui.model.Netmap
import com.tailscale.ipn.ui.model.Tailcfg
import com.tailscale.ipn.ui.model.UserID
import com.tailscale.ipn.ui.notifier.Notifier
import com.tailscale.ipn.ui.viewModel.IpnViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@ -35,14 +38,25 @@ class PeerCategorizer(scope: CoroutineScope) {
}
}
val mdmHiddenCategories =
IpnViewModel.mdmSettings.value.get(StringArraySetting.HiddenNetworkDevices)
val shouldHideCurrentUser =
mdmHiddenCategories?.contains(HiddenNetworkDevices.CurrentUser.value) ?: false
val shouldHideOtherUsers =
mdmHiddenCategories?.contains(HiddenNetworkDevices.OtherUsers.value) ?: false
val shouldHideTaggedDevices =
mdmHiddenCategories?.contains(HiddenNetworkDevices.TaggedDevices.value) ?: false
private fun regenerateGroupedPeers(netmap: Netmap.NetworkMap): List<PeerSet> {
val peers: List<Tailcfg.Node> = netmap.Peers ?: return emptyList()
val selfNode = netmap.SelfNode
var grouped = mutableMapOf<UserID, MutableList<Tailcfg.Node>>()
for (peer in (peers + selfNode)) {
// (jonathan) TODO: MDM -> There are a number of MDM settings to hide devices from the user
// (jonathan) TODO: MDM -> currentUser, otherUsers, taggedDevices
var peersToConsider: List<Tailcfg.Node> = peers
if (!shouldHideCurrentUser) {
peersToConsider = peers + selfNode
}
for (peer in peersToConsider) {
val userId = peer.User
if (!grouped.containsKey(userId)) {
@ -55,9 +69,15 @@ class PeerCategorizer(scope: CoroutineScope) {
val peerSets =
grouped
.map { (userId, peers) ->
.mapNotNull { (userId, peers) ->
val profile = netmap.userProfile(userId)
PeerSet(profile, peers.sortedBy { it.ComputedName })
if (shouldHideTaggedDevices && profile?.isTaggedDevice() == true) {
return@mapNotNull null
}
if (shouldHideCurrentUser && userId == selfNode.ID) {
return@mapNotNull null
}
return@mapNotNull PeerSet(profile, peers.sortedBy { it.ComputedName })
}
.sortedBy {
if (it.user?.ID == me?.ID) {
@ -85,25 +105,23 @@ class PeerCategorizer(scope: CoroutineScope) {
this.searchTerm = searchTerm
val matchingSets =
setsToSearch
.map { peerSet ->
val user = peerSet.user
val peers = peerSet.peers
val userMatches = user?.DisplayName?.contains(searchTerm, ignoreCase = true) ?: false
if (userMatches) {
return@map peerSet
}
val matchingPeers =
peers.filter { it.ComputedName.contains(searchTerm, ignoreCase = true) }
if (matchingPeers.isNotEmpty()) {
PeerSet(user, matchingPeers)
} else {
null
}
}
.filterNotNull()
setsToSearch.mapNotNull { peerSet ->
val user = peerSet.user
val peers = peerSet.peers
val userMatches = user?.DisplayName?.contains(searchTerm, ignoreCase = true) ?: false
if (userMatches) {
return@mapNotNull peerSet
}
val matchingPeers =
peers.filter { it.ComputedName.contains(searchTerm, ignoreCase = true) }
if (matchingPeers.isNotEmpty()) {
PeerSet(user, matchingPeers)
} else {
null
}
}
return matchingSets
}

@ -21,6 +21,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.viewmodel.compose.viewModel
import com.tailscale.ipn.R
import com.tailscale.ipn.mdm.ShowHideSetting
import com.tailscale.ipn.mdm.ShowHideValue
import com.tailscale.ipn.ui.util.Lists
import com.tailscale.ipn.ui.util.LoadingIndicator
import com.tailscale.ipn.ui.util.clickableOrGrayedOut
@ -28,6 +30,7 @@ import com.tailscale.ipn.ui.util.itemsWithDividers
import com.tailscale.ipn.ui.viewModel.ExitNodePickerNav
import com.tailscale.ipn.ui.viewModel.ExitNodePickerViewModel
import com.tailscale.ipn.ui.viewModel.ExitNodePickerViewModelFactory
import com.tailscale.ipn.ui.viewModel.IpnViewModel
import com.tailscale.ipn.ui.viewModel.selected
@OptIn(ExperimentalMaterial3Api::class)
@ -45,9 +48,11 @@ fun ExitNodePicker(
val anyActive = model.anyActive.collectAsState()
LazyColumn(modifier = Modifier.padding(innerPadding)) {
item(key = "runExitNode") {
RunAsExitNodeItem(nav = nav, viewModel = model)
Lists.SectionDivider()
if (IpnViewModel.mdmSettings.value.get(ShowHideSetting.RunExitNode) == ShowHideValue.Show) {
item(key = "runExitNode") {
RunAsExitNodeItem(nav = nav, viewModel = model)
Lists.SectionDivider()
}
}
item(key = "none") {

@ -50,6 +50,8 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.tailscale.ipn.R
import com.tailscale.ipn.mdm.ShowHideSetting
import com.tailscale.ipn.mdm.ShowHideValue
import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.model.IpnLocal
import com.tailscale.ipn.ui.model.StableNodeID
@ -59,6 +61,7 @@ import com.tailscale.ipn.ui.util.LoadingIndicator
import com.tailscale.ipn.ui.util.PeerSet
import com.tailscale.ipn.ui.util.flag
import com.tailscale.ipn.ui.util.itemsWithDividers
import com.tailscale.ipn.ui.viewModel.IpnViewModel
import com.tailscale.ipn.ui.viewModel.MainViewModel
import kotlinx.coroutines.flow.StateFlow
@ -107,15 +110,17 @@ fun MainView(navigation: MainViewNavigation, viewModel: MainViewModel = viewMode
when (state.value) {
Ipn.State.Running -> {
val mdmSettings = IpnViewModel.mdmSettings.collectAsState().value
val selfPeerId = viewModel.selfPeerId.collectAsState(initial = "")
Row(
modifier =
Modifier.background(MaterialTheme.colorScheme.secondaryContainer)
.padding(top = 10.dp, bottom = 20.dp)) {
ExitNodeStatus(
navAction = navigation.onNavigateToExitNodes, viewModel = viewModel)
}
if (mdmSettings.get(ShowHideSetting.ExitNodesPicker) != ShowHideValue.Hide) {
Row(
modifier =
Modifier.background(MaterialTheme.colorScheme.secondaryContainer)
.padding(top = 10.dp, bottom = 20.dp)) {
ExitNodeStatus(
navAction = navigation.onNavigateToExitNodes, viewModel = viewModel)
}
}
PeerList(
searchTerm = viewModel.searchTerm,
state = viewModel.ipnState,

@ -10,6 +10,8 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.tailscale.ipn.R
import com.tailscale.ipn.mdm.AlwaysNeverUserDecidesSetting
import com.tailscale.ipn.mdm.AlwaysNeverUserDecidesValue
import com.tailscale.ipn.ui.localapi.Client
import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.model.Tailcfg
@ -41,6 +43,11 @@ class DNSSettingsViewModel() : IpnViewModel() {
R.string.use_ts_dns,
SettingType.SWITCH,
isOn = MutableStateFlow(Notifier.prefs.value?.CorpDNS),
enabled =
MutableStateFlow(
IpnViewModel.mdmSettings.value.get(
AlwaysNeverUserDecidesSetting.UseTailscaleDNSSettings) ==
AlwaysNeverUserDecidesValue.UserDecides),
onToggle = {
LoadingIndicator.start()
toggleCorpDNS { LoadingIndicator.stop() }

@ -7,6 +7,8 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.tailscale.ipn.R
import com.tailscale.ipn.mdm.AlwaysNeverUserDecidesSetting
import com.tailscale.ipn.mdm.AlwaysNeverUserDecidesValue
import com.tailscale.ipn.ui.localapi.Client
import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.model.StableNodeID
@ -62,7 +64,11 @@ class ExitNodePickerViewModel(private val nav: ExitNodePickerNav) : IpnViewModel
R.string.allow_lan_access,
SettingType.SWITCH,
isOn = MutableStateFlow(Notifier.prefs.value?.ExitNodeAllowLANAccess),
enabled = MutableStateFlow(true),
enabled =
MutableStateFlow(
IpnViewModel.mdmSettings.value.get(
AlwaysNeverUserDecidesSetting.ExitNodeAllowLANAccess) ==
AlwaysNeverUserDecidesValue.UserDecides),
onToggle = {
LoadingIndicator.start()
toggleAllowLANAccess { LoadingIndicator.stop() }

@ -9,6 +9,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tailscale.ipn.App
import com.tailscale.ipn.IPNReceiver
import com.tailscale.ipn.mdm.BooleanSetting
import com.tailscale.ipn.mdm.MDMSettings
import com.tailscale.ipn.ui.localapi.Client
import com.tailscale.ipn.ui.model.Ipn
@ -95,6 +96,10 @@ open class IpnViewModel : ViewModel() {
fun stopVPN() {
val context = App.getApplication().applicationContext
if (mdmSettings.value.get(BooleanSetting.ForceEnabled)) {
Log.d(TAG, "Not stopping VPN due to ForceEnabled MDM setting.")
return
}
val intent = Intent(context, IPNReceiver::class.java)
intent.action = IPNReceiver.INTENT_DISCONNECT_VPN
context.sendBroadcast(intent)

@ -11,7 +11,11 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.tailscale.ipn.BuildConfig
import com.tailscale.ipn.R
import com.tailscale.ipn.mdm.AlwaysNeverUserDecidesSetting
import com.tailscale.ipn.mdm.AlwaysNeverUserDecidesValue
import com.tailscale.ipn.mdm.MDMSettings
import com.tailscale.ipn.mdm.ShowHideSetting
import com.tailscale.ipn.mdm.ShowHideValue
import com.tailscale.ipn.mdm.StringSetting
import com.tailscale.ipn.ui.notifier.Notifier
import com.tailscale.ipn.ui.util.set
@ -119,12 +123,20 @@ class SettingsViewModel(val navigation: SettingsNav) : IpnViewModel() {
titleRes = R.string.dns_settings,
SettingType.NAV,
onClick = { navigation.onNavigateToDNSSettings() },
enabled = MutableStateFlow(true)),
enabled =
MutableStateFlow(
IpnViewModel.mdmSettings.value.get(
AlwaysNeverUserDecidesSetting.UseTailscaleDNSSettings) ==
AlwaysNeverUserDecidesValue.UserDecides)),
Setting(
titleRes = R.string.tailnet_lock,
SettingType.NAV,
onClick = { navigation.onNavigateToTailnetLock() },
enabled = MutableStateFlow(true)),
titleRes = R.string.tailnet_lock,
SettingType.NAV,
onClick = { navigation.onNavigateToTailnetLock() },
enabled = MutableStateFlow(true))
.takeIf {
IpnViewModel.mdmSettings.value.get(ShowHideSetting.ManageTailnetLock) ==
ShowHideValue.Show
},
Setting(
titleRes = R.string.about,
SettingType.NAV,

Loading…
Cancel
Save