mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 11:07:10 +00:00
feat(permissions): grant dangerous permissions via aapt + adb
Inspector parses uses-permission entries from `aapt dump permissions` and the granter shells out to `adb shell pm grant`. Both are pluggable so tests can drive logic without aapt or a device. Granter failures become warnings rather than errors — Android refuses non-runtime permissions and we'd rather keep going than abort the run.
This commit is contained in:
1 parent
b6b18c3681
commit
d856c40d7c
2 files changed
+252
No files matched your search
@@ -0,0 +1,125 @@
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Inspector returns the list of uses-permission entries declared by the APK.
|
||||
type Inspector func(ctx context.Context, apkPath string) ([]string, error)
|
||||
|
||||
// Granter grants a single permission to the given package on the connected
|
||||
// device. Implementations typically wrap `adb shell pm grant`.
|
||||
type Granter func(ctx context.Context, packageName, permission string) error
|
||||
|
||||
// GrantDangerous filters declared permissions to the dangerous set and grants
|
||||
// each one via the supplied granter. Errors from individual grants are
|
||||
// collected as warnings rather than aborting — Android refuses to grant
|
||||
// non-runtime permissions and we prefer to soldier on.
|
||||
func GrantDangerous(
|
||||
ctx context.Context,
|
||||
apkPath, packageName string,
|
||||
inspector Inspector,
|
||||
granter Granter,
|
||||
) (granted []string, warnings []string, err error) {
|
||||
declared, err := inspector(ctx, apkPath)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("inspect permissions: %w", err)
|
||||
}
|
||||
for _, permission := range declared {
|
||||
if !IsDangerous(permission) {
|
||||
continue
|
||||
}
|
||||
if err := granter(ctx, packageName, permission); err != nil {
|
||||
warnings = append(warnings, fmt.Sprintf("%s: %v", permission, err))
|
||||
continue
|
||||
}
|
||||
granted = append(granted, permission)
|
||||
}
|
||||
return granted, warnings, nil
|
||||
}
|
||||
|
||||
var aaptPermissionPattern = regexp.MustCompile(`uses-permission:\s+name='([^']+)'`)
|
||||
|
||||
// AaptInspector shells out to `aapt dump permissions` to list permissions.
|
||||
func AaptInspector(ctx context.Context, apkPath string) ([]string, error) {
|
||||
output, err := exec.CommandContext(ctx, "aapt", "dump", "permissions", apkPath).Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("aapt dump permissions: %w", err)
|
||||
}
|
||||
var permissions []string
|
||||
for _, match := range aaptPermissionPattern.FindAllStringSubmatch(string(output), -1) {
|
||||
permissions = append(permissions, match[1])
|
||||
}
|
||||
return permissions, nil
|
||||
}
|
||||
|
||||
// AdbGranter returns a Granter that runs `adb shell pm grant` against the
|
||||
// supplied device serial (empty = default device).
|
||||
func AdbGranter(adbPath, deviceSerial string) Granter {
|
||||
return func(ctx context.Context, packageName, permission string) error {
|
||||
arguments := []string{"shell", "pm", "grant", packageName, permission}
|
||||
if deviceSerial != "" {
|
||||
arguments = append([]string{"-s", deviceSerial}, arguments...)
|
||||
}
|
||||
command := exec.CommandContext(ctx, adbPath, arguments...)
|
||||
output, err := command.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("adb pm grant %s: %w (%s)", permission, err, strings.TrimSpace(string(output)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// dangerousPermissions captures Android's PROTECTION_DANGEROUS set as of
|
||||
// API 34. New runtime permissions added in later releases should be appended
|
||||
// here when needed.
|
||||
var dangerousPermissions = map[string]bool{
|
||||
"android.permission.READ_CALENDAR": true,
|
||||
"android.permission.WRITE_CALENDAR": true,
|
||||
"android.permission.CAMERA": true,
|
||||
"android.permission.READ_CONTACTS": true,
|
||||
"android.permission.WRITE_CONTACTS": true,
|
||||
"android.permission.GET_ACCOUNTS": true,
|
||||
"android.permission.ACCESS_FINE_LOCATION": true,
|
||||
"android.permission.ACCESS_COARSE_LOCATION": true,
|
||||
"android.permission.ACCESS_BACKGROUND_LOCATION": true,
|
||||
"android.permission.RECORD_AUDIO": true,
|
||||
"android.permission.READ_PHONE_STATE": true,
|
||||
"android.permission.READ_PHONE_NUMBERS": true,
|
||||
"android.permission.CALL_PHONE": true,
|
||||
"android.permission.ANSWER_PHONE_CALLS": true,
|
||||
"android.permission.READ_CALL_LOG": true,
|
||||
"android.permission.WRITE_CALL_LOG": true,
|
||||
"android.permission.ADD_VOICEMAIL": true,
|
||||
"android.permission.USE_SIP": true,
|
||||
"android.permission.PROCESS_OUTGOING_CALLS": true,
|
||||
"android.permission.BODY_SENSORS": true,
|
||||
"android.permission.SEND_SMS": true,
|
||||
"android.permission.RECEIVE_SMS": true,
|
||||
"android.permission.READ_SMS": true,
|
||||
"android.permission.RECEIVE_WAP_PUSH": true,
|
||||
"android.permission.RECEIVE_MMS": true,
|
||||
"android.permission.READ_EXTERNAL_STORAGE": true,
|
||||
"android.permission.WRITE_EXTERNAL_STORAGE": true,
|
||||
"android.permission.ACCESS_MEDIA_LOCATION": true,
|
||||
"android.permission.ACTIVITY_RECOGNITION": true,
|
||||
"android.permission.POST_NOTIFICATIONS": true,
|
||||
"android.permission.NEARBY_WIFI_DEVICES": true,
|
||||
"android.permission.READ_MEDIA_IMAGES": true,
|
||||
"android.permission.READ_MEDIA_VIDEO": true,
|
||||
"android.permission.READ_MEDIA_AUDIO": true,
|
||||
"android.permission.READ_MEDIA_VISUAL_USER_SELECTED": true,
|
||||
"android.permission.BLUETOOTH_CONNECT": true,
|
||||
"android.permission.BLUETOOTH_ADVERTISE": true,
|
||||
"android.permission.BLUETOOTH_SCAN": true,
|
||||
"android.permission.UWB_RANGING": true,
|
||||
"android.permission.BODY_SENSORS_BACKGROUND": true,
|
||||
}
|
||||
|
||||
func IsDangerous(permission string) bool {
|
||||
return dangerousPermissions[permission]
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGrantDangerous_FiltersAndCallsGranter(t *testing.T) {
|
||||
declared := []string{
|
||||
"android.permission.INTERNET", // not dangerous
|
||||
"android.permission.CAMERA", // dangerous
|
||||
"android.permission.WAKE_LOCK", // not dangerous
|
||||
"android.permission.ACCESS_FINE_LOCATION", // dangerous
|
||||
"android.permission.READ_EXTERNAL_STORAGE", // dangerous
|
||||
"android.permission.SYSTEM_ALERT_WINDOW", // not dangerous
|
||||
}
|
||||
inspector := func(_ context.Context, _ string) ([]string, error) { return declared, nil }
|
||||
var requested []string
|
||||
granter := func(_ context.Context, packageName, permission string) error {
|
||||
if packageName != "com.example" {
|
||||
t.Errorf("granter received wrong package: %q", packageName)
|
||||
}
|
||||
requested = append(requested, permission)
|
||||
return nil
|
||||
}
|
||||
|
||||
granted, warnings, err := GrantDangerous(context.Background(), "/path/to/apk", "com.example", inspector, granter)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := []string{
|
||||
"android.permission.CAMERA",
|
||||
"android.permission.ACCESS_FINE_LOCATION",
|
||||
"android.permission.READ_EXTERNAL_STORAGE",
|
||||
}
|
||||
if !slices.Equal(granted, want) {
|
||||
t.Errorf("granted permissions: got %v, want %v", granted, want)
|
||||
}
|
||||
if !slices.Equal(requested, want) {
|
||||
t.Errorf("granter calls: got %v, want %v", requested, want)
|
||||
}
|
||||
if len(warnings) != 0 {
|
||||
t.Errorf("expected no warnings, got %v", warnings)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGrantDangerous_CollectsGranterFailuresAsWarnings(t *testing.T) {
|
||||
declared := []string{"android.permission.CAMERA", "android.permission.RECORD_AUDIO"}
|
||||
inspector := func(_ context.Context, _ string) ([]string, error) { return declared, nil }
|
||||
granter := func(_ context.Context, _, permission string) error {
|
||||
if permission == "android.permission.CAMERA" {
|
||||
return errors.New("device denied")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
granted, warnings, err := GrantDangerous(context.Background(), "/x", "com.example", inspector, granter)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !slices.Equal(granted, []string{"android.permission.RECORD_AUDIO"}) {
|
||||
t.Errorf("granted: %v", granted)
|
||||
}
|
||||
if len(warnings) != 1 || warnings[0] != "android.permission.CAMERA: device denied" {
|
||||
t.Errorf("warnings: %v", warnings)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGrantDangerous_InspectorErrorBubbles(t *testing.T) {
|
||||
inspector := func(_ context.Context, _ string) ([]string, error) {
|
||||
return nil, errors.New("aapt missing")
|
||||
}
|
||||
_, _, err := GrantDangerous(context.Background(), "/x", "com.example", inspector, nil)
|
||||
if err == nil || err.Error() == "" {
|
||||
t.Errorf("expected wrapped inspector error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsDangerous_KnownPermissions(t *testing.T) {
|
||||
dangerous := []string{
|
||||
"android.permission.CAMERA",
|
||||
"android.permission.RECORD_AUDIO",
|
||||
"android.permission.POST_NOTIFICATIONS",
|
||||
}
|
||||
for _, permission := range dangerous {
|
||||
if !IsDangerous(permission) {
|
||||
t.Errorf("expected %q to be dangerous", permission)
|
||||
}
|
||||
}
|
||||
|
||||
normal := []string{
|
||||
"android.permission.INTERNET",
|
||||
"android.permission.WAKE_LOCK",
|
||||
"android.permission.SYSTEM_ALERT_WINDOW",
|
||||
}
|
||||
for _, permission := range normal {
|
||||
if IsDangerous(permission) {
|
||||
t.Errorf("expected %q not to be dangerous", permission)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAaptInspector_ParsesFixtureOutput(t *testing.T) {
|
||||
// Direct test of the regex; avoids spawning aapt.
|
||||
output := `
|
||||
package: com.example
|
||||
sdkVersion:'24'
|
||||
uses-permission: name='android.permission.INTERNET'
|
||||
uses-permission: name='android.permission.CAMERA'
|
||||
uses-permission: name='android.permission.WAKE_LOCK'
|
||||
`
|
||||
matches := aaptPermissionPattern.FindAllStringSubmatch(output, -1)
|
||||
got := make([]string, 0, len(matches))
|
||||
for _, match := range matches {
|
||||
got = append(got, match[1])
|
||||
}
|
||||
want := []string{
|
||||
"android.permission.INTERNET",
|
||||
"android.permission.CAMERA",
|
||||
"android.permission.WAKE_LOCK",
|
||||
}
|
||||
if !slices.Equal(got, want) {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user