-
Notifications
You must be signed in to change notification settings - Fork 97
Mdm integration #198
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Mdm integration #198
Changes from 8 commits
456b361
de54bf4
b272d9f
8469ab4
f4d48a6
f41075d
f9919b5
0b62139
7f64edc
8442624
cee5fa9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| package io.netbird.client; | ||
|
|
||
| import android.content.Context; | ||
| import android.content.RestrictionsManager; | ||
| import android.os.Bundle; | ||
| import android.util.Log; | ||
|
|
||
| import org.json.JSONArray; | ||
| import org.json.JSONException; | ||
| import org.json.JSONObject; | ||
|
|
||
| import io.netbird.gomobile.android.PolicyFetcher; | ||
|
|
||
| /** | ||
| * MDMPolicyFetcher reads the current Android managed-config snapshot from | ||
| * RestrictionsManager and returns it as a JSON-encoded string to the Go | ||
| * layer. Registered once at app start via Android.setMobilePolicyFetcher; | ||
| * the Go side invokes fetchJSON() on every LoadPolicy call so the response | ||
| * is always fresh. | ||
| * | ||
| * Returns an empty string when no managed config is set — the daemon side | ||
| * treats that as the "no MDM source present" sentinel. | ||
| */ | ||
| public class MDMPolicyFetcher implements PolicyFetcher { | ||
| private static final String TAG = "MDMPolicyFetcher"; | ||
|
|
||
| private final Context context; | ||
|
|
||
| public MDMPolicyFetcher(Context context) { | ||
| this.context = context.getApplicationContext(); | ||
| } | ||
|
|
||
| @Override | ||
| public String fetchJSON() { | ||
| RestrictionsManager rm = (RestrictionsManager) context.getSystemService(Context.RESTRICTIONS_SERVICE); | ||
| if (rm == null) { | ||
| return ""; | ||
| } | ||
| Bundle restrictions = rm.getApplicationRestrictions(); | ||
| if (restrictions == null || restrictions.isEmpty()) { | ||
| return ""; | ||
| } | ||
| try { | ||
| return bundleToJSON(restrictions).toString(); | ||
| } catch (JSONException e) { | ||
| Log.w(TAG, "Failed to serialize managed restrictions to JSON: " + e); | ||
| return ""; | ||
| } | ||
| } | ||
|
|
||
| private static JSONObject bundleToJSON(Bundle bundle) throws JSONException { | ||
| JSONObject obj = new JSONObject(); | ||
| for (String key : bundle.keySet()) { | ||
| Object value = bundle.get(key); | ||
| if (value instanceof Bundle) { | ||
| obj.put(key, bundleToJSON((Bundle) value)); | ||
| } else if (value instanceof Object[]) { | ||
| JSONArray arr = new JSONArray(); | ||
| for (Object item : (Object[]) value) { | ||
| arr.put(item); | ||
| } | ||
| obj.put(key, arr); | ||
| } else { | ||
| obj.put(key, value); | ||
| } | ||
| } | ||
| return obj; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,13 @@ | ||
| package io.netbird.client.ui.advanced; | ||
|
|
||
| import android.content.Context; | ||
| import android.content.RestrictionsManager; | ||
| import android.content.SharedPreferences; | ||
| import android.os.Bundle; | ||
| import android.util.Log; | ||
| import android.widget.CompoundButton; | ||
| import android.widget.EditText; | ||
| import android.view.View; | ||
| import android.view.LayoutInflater; | ||
| import android.view.View; | ||
| import android.view.ViewGroup; | ||
|
|
@@ -292,11 +296,79 @@ private void initializeEngineConfigSwitches() { | |
| binding.switchDisableIpv6.toggle(); | ||
| }); | ||
|
|
||
| applyMDMLocks(); | ||
|
|
||
| } catch (Exception e) { | ||
| Log.e(LOGTAG, "Failed to initialize engine config switches", e); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Lock and align every UI control whose corresponding key is currently | ||
| * MDM-enforced. The list of managed keys + their enforced values is | ||
| * read directly from RestrictionsManager — the same OS-native source | ||
| * the Go layer uses (via MDMPolicyFetcher). No round-trip to Go is | ||
| * needed, and the two sides cannot diverge. | ||
| * | ||
| * For each managed key: | ||
| * - the switch is forced to the MDM value (overrides the user's | ||
| * on-disk preference); | ||
| * - the switch + its surrounding clickable layout are disabled so | ||
| * the user cannot toggle them. | ||
| */ | ||
| private void applyMDMLocks() { | ||
| Context ctx = getContext(); | ||
| if (ctx == null) { | ||
| return; | ||
| } | ||
| RestrictionsManager rm = (RestrictionsManager) ctx.getSystemService(Context.RESTRICTIONS_SERVICE); | ||
| if (rm == null) { | ||
| return; | ||
| } | ||
| android.os.Bundle restrictions = rm.getApplicationRestrictions(); | ||
| if (restrictions == null || restrictions.isEmpty()) { | ||
| return; | ||
| } | ||
|
|
||
| lockSwitchIfManaged(restrictions, "rosenpassEnabled", binding.switchRosenpass, binding.layoutRosenpas); | ||
| lockSwitchIfManaged(restrictions, "rosenpassPermissive", binding.switchRosenpassPermissive, binding.layoutRosenpassPermissive); | ||
| lockSwitchIfManaged(restrictions, "allowServerSSH", binding.switchAllowSsh, binding.layoutAllowSsh); | ||
| lockSwitchIfManaged(restrictions, "blockInbound", binding.switchBlockInbound, binding.layoutBlockInbound); | ||
| lockSwitchIfManaged(restrictions, "disableClientRoutes", binding.switchDisableClientRoutes, binding.layoutDisableClientRoutes); | ||
| lockSwitchIfManaged(restrictions, "disableServerRoutes", binding.switchDisableServerRoutes, binding.layoutDisableServerRoutes); | ||
|
|
||
| // PreSharedKey is a string, not a bool; lock the field if managed. | ||
| if (restrictions.containsKey("preSharedKey")) { | ||
| EditText psk = binding.presharedKey; | ||
| psk.setEnabled(false); | ||
| // Show the redaction sentinel so the actual MDM value is never | ||
| // leaked into the UI — matches the daemon-side behavior of | ||
| // GetConfig. | ||
| psk.setText(hiddenKey); | ||
| binding.btnSave.setEnabled(false); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Helper: if `key` is present in the OS-pushed restrictions, force the | ||
| * switch to its enforced bool value and disable the switch and its | ||
| * parent layout. The parent layout must be disabled too, otherwise | ||
| * the TV-remote "tap layout to toggle switch" path remains active. | ||
| */ | ||
| private void lockSwitchIfManaged(android.os.Bundle restrictions, String key, | ||
| CompoundButton switchCtrl, View parentLayout) { | ||
| if (switchCtrl == null || !restrictions.containsKey(key)) { | ||
| return; | ||
| } | ||
| boolean value = restrictions.getBoolean(key); | ||
| switchCtrl.setChecked(value); | ||
| switchCtrl.setEnabled(false); | ||
| if (parentLayout != null) { | ||
| parentLayout.setEnabled(false); | ||
| parentLayout.setClickable(false); | ||
| } | ||
| } | ||
|
Comment on lines
+358
to
+370
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Listener side-effects fire during MDM lock application.
Fix by disabling the switch before setting its value, then add guards in listeners: 🔧 Proposed fix for lockSwitchIfManaged private void lockSwitchIfManaged(android.os.Bundle restrictions, String key,
CompoundButton switchCtrl, View parentLayout) {
if (switchCtrl == null || !restrictions.containsKey(key)) {
return;
}
boolean value = restrictions.getBoolean(key);
+ switchCtrl.setEnabled(false);
switchCtrl.setChecked(value);
- switchCtrl.setEnabled(false);
if (parentLayout != null) {
parentLayout.setEnabled(false);
parentLayout.setClickable(false);
}
}Then add an early-exit guard to each listener (example for one switch): binding.switchDisableClientRoutes.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ if (!buttonView.isEnabled()) return; // Skip writes when MDM-locked
try {
goPreferences.setDisableClientRoutes(isChecked);
goPreferences.commit();
} catch (Exception e) {
Log.e(LOGTAG, "Failed to set disable client routes", e);
}
});Apply the same guard to all listeners in 🤖 Prompt for AI Agents |
||
|
|
||
| @Override | ||
| public void onDestroyView() { | ||
| super.onDestroyView(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <resources> | ||
| <!-- Display labels for the splitTunnelMode managed restriction. --> | ||
| <string-array name="restriction_splitTunnelMode_entries"> | ||
| <item>Allow only listed apps (everything else bypasses)</item> | ||
| <item>Disallow listed apps (everything else routes)</item> | ||
| </string-array> | ||
| <!-- Raw values written into RestrictionsManager for splitTunnelMode. --> | ||
| <string-array name="restriction_splitTunnelMode_values"> | ||
| <item>allow</item> | ||
| <item>disallow</item> | ||
| </string-array> | ||
| </resources> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Duplicate import of
android.view.View.Line 10 and line 12 both import
android.view.View.🧹 Proposed fix
import android.widget.CompoundButton; import android.widget.EditText; -import android.view.View; import android.view.LayoutInflater; import android.view.View;📝 Committable suggestion
🤖 Prompt for AI Agents