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") Hide("hide")
} }
enum class NetworkDevices(val value: String) { enum class HiddenNetworkDevices(val value: String) {
currentUser("current-user"), CurrentUser("current-user"),
otherUsers("other-users"), OtherUsers("other-users"),
taggedDevices("tagged-devices"), TaggedDevices("tagged-devices"),
} }

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

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

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

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

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

@ -9,6 +9,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.tailscale.ipn.App import com.tailscale.ipn.App
import com.tailscale.ipn.IPNReceiver import com.tailscale.ipn.IPNReceiver
import com.tailscale.ipn.mdm.BooleanSetting
import com.tailscale.ipn.mdm.MDMSettings import com.tailscale.ipn.mdm.MDMSettings
import com.tailscale.ipn.ui.localapi.Client import com.tailscale.ipn.ui.localapi.Client
import com.tailscale.ipn.ui.model.Ipn import com.tailscale.ipn.ui.model.Ipn
@ -95,6 +96,10 @@ open class IpnViewModel : ViewModel() {
fun stopVPN() { fun stopVPN() {
val context = App.getApplication().applicationContext 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) val intent = Intent(context, IPNReceiver::class.java)
intent.action = IPNReceiver.INTENT_DISCONNECT_VPN intent.action = IPNReceiver.INTENT_DISCONNECT_VPN
context.sendBroadcast(intent) context.sendBroadcast(intent)

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

Loading…
Cancel
Save