android/ui: add mdm hooks (#364)

Updates tailscale/corp#19743

Adds the hooks for the various MDM settings applicable to Android with
the exception of the keyExpirationNotice which we'll handle separately.

Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
pull/365/head
Jonathan Nobels 7 months ago committed by GitHub
parent e6f6d35a99
commit a2471d38cb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -18,41 +18,71 @@ object MDMSettings {
val forceEnabled = BooleanMDMSetting("ForceEnabled", "Force Enabled Connection Toggle")
// Handled on the backed
val exitNodeID = StringMDMSetting("ExitNodeID", "Forced Exit Node: Stable ID")
// (jonathan) TODO: Unused but required. There is some funky go string duration parsing required
// here.
val keyExpirationNotice = StringMDMSetting("KeyExpirationNotice", "Key Expiration Notice Period")
val loginURL = StringMDMSetting("LoginURL", "Custom control server URL")
val managedByCaption = StringMDMSetting("ManagedByCaption", "Managed By - Caption")
val managedByOrganizationName =
StringMDMSetting("ManagedByOrganizationName", "Managed By - Organization Name")
val managedByURL = StringMDMSetting("ManagedByURL", "Managed By - Support URL")
// Handled on the backend
val tailnet = StringMDMSetting("Tailnet", "Recommended/Required Tailnet Name")
val hiddenNetworkDevices =
StringArrayListMDMSetting("HiddenNetworkDevices", "Hidden Network Device Categories")
// Unused on Android
val allowIncomingConnections =
AlwaysNeverUserDecidesMDMSetting("AllowIncomingConnections", "Allow Incoming Connections")
// Unused on Android
val detectThirdPartyAppConflicts =
AlwaysNeverUserDecidesMDMSetting(
"DetectThirdPartyAppConflicts", "Detect potentially problematic third-party apps")
val exitNodeAllowLANAccess =
AlwaysNeverUserDecidesMDMSetting(
"ExitNodeAllowLANAccess", "Allow LAN Access when using an exit node")
// Handled on the backend
val postureChecking =
AlwaysNeverUserDecidesMDMSetting("PostureChecking", "Enable Posture Checking")
val useTailscaleDNSSettings =
AlwaysNeverUserDecidesMDMSetting("UseTailscaleDNSSettings", "Use Tailscale DNS Settings")
// Unused on Android
val useTailscaleSubnets =
AlwaysNeverUserDecidesMDMSetting("UseTailscaleSubnets", "Use Tailscale Subnets")
val exitNodesPicker = ShowHideMDMSetting("ExitNodesPicker", "Exit Nodes Picker")
val manageTailnetLock = ShowHideMDMSetting("ManageTailnetLock", "“Manage Tailnet lock” menu item")
// Unused on Android
val resetToDefaults = ShowHideMDMSetting("ResetToDefaults", "“Reset to Defaults” menu item")
val runExitNode = ShowHideMDMSetting("RunExitNode", "Run as Exit Node")
// Unused on Android
val testMenu = ShowHideMDMSetting("TestMenu", "Show Debug Menu")
// Unused on Android
val updateMenu = ShowHideMDMSetting("UpdateMenu", "“Update Available” menu item")
// (jonathan) TODO: Use this when suggested exit nodes are implemented
val allowedSuggestedExitNodes =
StringArrayListMDMSetting("AllowedSuggestedExitNodes", "Allowed Suggested Exit Nodes")
val allSettings by lazy {
MDMSettings::class
.declaredMemberProperties

@ -81,7 +81,12 @@ class ShowHideMDMSetting(key: String, localizedTitle: String) :
enum class AlwaysNeverUserDecides(val value: String) {
Always("always"),
Never("never"),
UserDecides("user-decides")
UserDecides("user-decides");
val hiddenFromUser: Boolean
get() {
return this != UserDecides
}
}
enum class ShowHide(val value: String) {

@ -3,6 +3,7 @@
package com.tailscale.ipn.ui.util
import com.tailscale.ipn.mdm.MDMSettings
import com.tailscale.ipn.ui.model.Netmap
import com.tailscale.ipn.ui.model.Tailcfg
import com.tailscale.ipn.ui.model.UserID
@ -19,21 +20,41 @@ class PeerCategorizer {
val selfNode = netmap.SelfNode
var grouped = mutableMapOf<UserID, MutableList<Tailcfg.Node>>()
val mdm = MDMSettings.hiddenNetworkDevices.flow.value
val hideMyDevices = mdm?.contains("current-user") ?: false
val hideOtherDevices = mdm?.contains("other-users") ?: false
val hideTaggedDevices = mdm?.contains("tagged-devices") ?: false
val me = netmap.currentUserProfile()
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
val userId = peer.User
val profile = netmap.userProfile(userId)
// Mullvad based nodes should not be shown in the peer list
if (!peer.isMullvadNode) {
if (!grouped.containsKey(userId)) {
grouped[userId] = mutableListOf()
}
grouped[userId]?.add(peer)
// Mullvad nodes should not be shown in the peer list
if (peer.isMullvadNode) {
continue
}
}
val me = netmap.currentUserProfile()
// Hide devices based on MDM settings
if (hideMyDevices && userId == me?.ID) {
continue
}
if (hideOtherDevices && userId != me?.ID) {
continue
}
if (hideTaggedDevices && (profile?.isTaggedDevice() == true)) {
continue
}
if (!grouped.containsKey(userId)) {
grouped[userId] = mutableListOf()
}
grouped[userId]?.add(peer)
}
peerSets =
grouped

@ -21,6 +21,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.tailscale.ipn.R
import com.tailscale.ipn.mdm.MDMSettings
import com.tailscale.ipn.ui.model.DnsType
import com.tailscale.ipn.ui.notifier.Notifier
import com.tailscale.ipn.ui.util.ClipboardValueView
@ -48,6 +49,7 @@ fun DNSSettingsView(
entry.value?.let { resolvers -> ViewableRoute(name = entry.key, resolvers) } ?: run { null }
} ?: emptyList()
val useCorpDNS = Notifier.prefs.collectAsState().value?.CorpDNS == true
val dnsSettingsMDMDisposition = MDMSettings.useTailscaleDNSSettings.flow.collectAsState().value
Scaffold(topBar = { Header(R.string.dns_settings, onBack = backToSettings) }) { innerPadding ->
LoadingIndicator.Wrap {
@ -66,14 +68,16 @@ fun DNSSettingsView(
},
supportingContent = { Text(stringResource(state.caption)) })
Lists.ItemDivider()
Setting.Switch(
R.string.use_ts_dns,
isOn = useCorpDNS,
onToggle = {
LoadingIndicator.start()
model.toggleCorpDNS { LoadingIndicator.stop() }
})
if (!dnsSettingsMDMDisposition.hiddenFromUser) {
Lists.ItemDivider()
Setting.Switch(
R.string.use_ts_dns,
isOn = useCorpDNS,
onToggle = {
LoadingIndicator.start()
model.toggleCorpDNS { LoadingIndicator.stop() }
})
}
}
if (resolvers.isNotEmpty()) {
@ -107,5 +111,5 @@ fun DNSSettingsView(
fun DNSSettingsViewPreview() {
val vm = DNSSettingsViewModel()
vm.enablementState.set(DNSEnablementState.ENABLED)
DNSSettingsView(backToSettings = { }, vm)
DNSSettingsView(backToSettings = {}, vm)
}

@ -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.MDMSettings
import com.tailscale.ipn.mdm.ShowHide
import com.tailscale.ipn.ui.notifier.Notifier
import com.tailscale.ipn.ui.theme.disabledListItem
import com.tailscale.ipn.ui.theme.listItem
@ -45,6 +47,9 @@ fun ExitNodePicker(
val mullvadExitNodeCount = model.mullvadExitNodeCount.collectAsState().value
val anyActive = model.anyActive.collectAsState()
val allowLANAccess = Notifier.prefs.collectAsState().value?.ExitNodeAllowLANAccess == true
val showRunAsExitNode = MDMSettings.runExitNode.flow.collectAsState().value
val allowLanAccessMDMDisposition =
MDMSettings.exitNodeAllowLANAccess.flow.collectAsState().value
LazyColumn(modifier = Modifier.padding(innerPadding)) {
item(key = "header") {
@ -55,8 +60,11 @@ fun ExitNodePicker(
online = true,
selected = !anyActive.value,
))
Lists.ItemDivider()
RunAsExitNodeItem(nav = nav, viewModel = model)
if (showRunAsExitNode == ShowHide.Show) {
Lists.ItemDivider()
RunAsExitNodeItem(nav = nav, viewModel = model)
}
}
item(key = "divider1") { Lists.SectionDivider() }
@ -71,13 +79,14 @@ fun ExitNodePicker(
}
}
// TODO: make sure this actually works, and if not, leave it out for now
item(key = "allowLANAccess") {
Lists.SectionDivider()
if (!allowLanAccessMDMDisposition.hiddenFromUser) {
item(key = "allowLANAccess") {
Lists.SectionDivider()
Setting.Switch(R.string.allow_lan_access, isOn = allowLANAccess) {
LoadingIndicator.start()
model.toggleAllowLANAccess { LoadingIndicator.stop() }
Setting.Switch(R.string.allow_lan_access, isOn = allowLANAccess) {
LoadingIndicator.start()
model.toggleAllowLANAccess { LoadingIndicator.stop() }
}
}
}
}

@ -64,6 +64,8 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.tailscale.ipn.R
import com.tailscale.ipn.mdm.MDMSettings
import com.tailscale.ipn.mdm.ShowHide
import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.model.IpnLocal
import com.tailscale.ipn.ui.model.Netmap
@ -113,11 +115,20 @@ fun MainView(
val stateVal = viewModel.stateRes.collectAsState(initial = R.string.placeholder).value
val stateStr = stringResource(id = stateVal)
val netmap = viewModel.netmap.collectAsState(initial = null).value
val showExitNodePicker = MDMSettings.exitNodesPicker.flow.collectAsState().value
val allowToggle = MDMSettings.forceEnabled.flow.collectAsState().value
ListItem(
colors = MaterialTheme.colorScheme.surfaceContainerListItem,
leadingContent = {
TintedSwitch(onCheckedChange = { viewModel.toggleVpn() }, checked = isOn.value)
TintedSwitch(
onCheckedChange = {
if (allowToggle) {
viewModel.toggleVpn()
}
},
enabled = !allowToggle,
checked = isOn.value)
},
headlineContent = {
user?.NetworkProfile?.DomainName?.let { domain ->
@ -157,7 +168,10 @@ fun MainView(
ExpiryNotificationIfNecessary(netmap = netmap, action = { viewModel.login() })
ExitNodeStatus(navAction = navigation.onNavigateToExitNodes, viewModel = viewModel)
if (showExitNodePicker == ShowHide.Show) {
ExitNodeStatus(
navAction = navigation.onNavigateToExitNodes, viewModel = viewModel)
}
PeerList(
viewModel = viewModel,
@ -263,7 +277,10 @@ fun ExitNodeStatus(navAction: () -> Unit, viewModel: MainViewModel) {
@Composable
fun SettingsButton(action: () -> Unit) {
IconButton(modifier = Modifier.size(24.dp), onClick = { action() }) {
Icon(Icons.Outlined.Settings, contentDescription = "Open settings", tint = MaterialTheme.colorScheme.onSurfaceVariant)
Icon(
Icons.Outlined.Settings,
contentDescription = "Open settings",
tint = MaterialTheme.colorScheme.onSurfaceVariant)
}
}

@ -26,6 +26,8 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.viewmodel.compose.viewModel
import com.tailscale.ipn.BuildConfig
import com.tailscale.ipn.R
import com.tailscale.ipn.mdm.MDMSettings
import com.tailscale.ipn.mdm.ShowHide
import com.tailscale.ipn.ui.Links
import com.tailscale.ipn.ui.theme.link
import com.tailscale.ipn.ui.theme.listItem
@ -43,6 +45,8 @@ fun SettingsView(settingsNav: SettingsNav, viewModel: SettingsViewModel = viewMo
val tailnetLockEnabled = viewModel.tailNetLockEnabled.collectAsState().value
val corpDNSEnabled = viewModel.corpDNSEnabled.collectAsState().value
val showTailnetLock = MDMSettings.manageTailnetLock.flow.collectAsState().value
Scaffold(
topBar = {
Header(titleRes = R.string.settings_title, onBack = settingsNav.onNavigateBackHome)
@ -68,14 +72,16 @@ fun SettingsView(settingsNav: SettingsNav, viewModel: SettingsViewModel = viewMo
},
onClick = settingsNav.onNavigateToDNSSettings)
Lists.ItemDivider()
Setting.Text(
R.string.tailnet_lock,
subtitle =
tailnetLockEnabled?.let {
stringResource(if (it) R.string.enabled else R.string.disabled)
},
onClick = settingsNav.onNavigateToTailnetLock)
if (showTailnetLock == ShowHide.Show) {
Lists.ItemDivider()
Setting.Text(
R.string.tailnet_lock,
subtitle =
tailnetLockEnabled?.let {
stringResource(if (it) R.string.enabled else R.string.disabled)
},
onClick = settingsNav.onNavigateToTailnetLock)
}
Lists.ItemDivider()
Setting.Text(R.string.permissions, onClick = settingsNav.onNavigateToPermissions)

@ -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.MDMSettings
import com.tailscale.ipn.ui.localapi.Client
import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.model.IpnLocal
@ -86,6 +87,12 @@ open class IpnViewModel : ViewModel() {
fun login(options: Ipn.Options = Ipn.Options(), completionHandler: (Result<Unit>) -> Unit = {}) {
MDMSettings.loginURL.flow.value?.let {
Log.d(TAG, "Using MDM derived control URL: $it")
loginWithCustomControlURL(it, completionHandler)
return
}
val loginAction = {
Client(viewModelScope).startLoginInteractive { result ->
result

Loading…
Cancel
Save