Skip to content

Permissions-Policy Builder — Java source

Build a Permissions-Policy header interactively. Control which browser features (camera, microphone, geolocation, etc.) your site can use.

This is the Java implementation — the same logic the interactive tool runs, in a shareable, citable form.

// Permissions-Policy Header Builder — build / parse / validate / score. Pure logic.
//
// Language: Java (17+, standard library only)
// Ported from src/lib/permissions-policy.ts
// display source — part of CosmoDev's polyglot tool pages.
//
// The Permissions-Policy header is a comma-separated list of directives:
//   Permissions-Policy: geolocation=(self), camera=(), microphone=*, usb=(https://a.example)
// Each directive maps a browser feature to an allowlist. An empty allowlist
// `()` disables the feature outright; `*` allows it everywhere; `self` limits
// it to the page's own origin; anything else is a space-separated origin list.
//
// A policy is modeled here as a sorted map of feature -> allowlist tokens:
//   { geolocation=[self], camera=[], usb=[https://a.example] }
//   - []            => camera=()        (disabled)
//   - [*]           => microphone=*     (every origin)
//   - [self]        => geolocation=(self)
//   - [origins...]  => usb=(https://a.example https://b.example)
// Features absent from the map are absent from the header (browser default).

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Pattern;

public final class PermissionsPolicy {

    /** How much a feature's abuse could leak about the user. */
    public enum PrivacyImpact { HIGH, MEDIUM, LOW }

    /** One catalogued browser feature. */
    public record FeatureInfo(
            String name,
            String description,
            PrivacyImpact privacyImpact,
            String defaultBrowserBehavior) {
    }

    /** feature name -> allowlist tokens. Empty list = disabled, ["*"] = all origins. */
    public static final class FeatureMap extends TreeMap<String, List<String>> {
        @java.io.Serial
        private static final long serialVersionUID = 1L;
    }

    /** The feature catalogue, in display order (privacy impact high -> low). */
    public static final List<FeatureInfo> FEATURES = List.of(
            // --- high privacy impact ---
            new FeatureInfo("camera", "Access the device camera for photos / video calls.", PrivacyImpact.HIGH, "Same-origin only; prompts the user."),
            new FeatureInfo("microphone", "Capture audio from the device microphone.", PrivacyImpact.HIGH, "Same-origin only; prompts the user."),
            new FeatureInfo("geolocation", "Read the precise GPS location of the visitor.", PrivacyImpact.HIGH, "Same-origin only; prompts the user."),
            new FeatureInfo("display-capture", "Screen / window sharing via getDisplayMedia.", PrivacyImpact.HIGH, "Same-origin only; prompts the user."),
            new FeatureInfo("idle-detection", "Detects when the user is away from the device — reveals usage patterns.", PrivacyImpact.HIGH, "Disabled; prompts the user."),
            new FeatureInfo("serial", "Talk to serial devices (Arduinos, POS terminals) over a physical port.", PrivacyImpact.HIGH, "Disabled; prompts the user."),
            new FeatureInfo("usb", "WebUSB — direct access to connected USB devices.", PrivacyImpact.HIGH, "Disabled; prompts the user."),
            new FeatureInfo("hid", "Human Interface Devices — raw access to unusual keyboards, gamepads, sensors.", PrivacyImpact.HIGH, "Disabled; prompts the user."),
            new FeatureInfo("xr-spatial-tracking", "Tracks head / hand position in WebXR sessions.", PrivacyImpact.HIGH, "Same-origin only; prompts the user."),
            // --- medium privacy impact ---
            new FeatureInfo("accelerometer", "Device motion sensor — can fingerprint and infer behaviour.", PrivacyImpact.MEDIUM, "Same-origin only."),
            new FeatureInfo("ambient-light-sensor", "Reads ambient light level around the device.", PrivacyImpact.MEDIUM, "Same-origin only."),
            new FeatureInfo("battery", "Battery Status API — a classic fingerprinting vector.", PrivacyImpact.MEDIUM, "Same-origin only."),
            new FeatureInfo("gyroscope", "Device orientation sensor — fingerprinting and behaviour inference.", PrivacyImpact.MEDIUM, "Same-origin only."),
            new FeatureInfo("magnetometer", "Compass readings — can leak details of the user's surroundings.", PrivacyImpact.MEDIUM, "Same-origin only."),
            new FeatureInfo("keyboard-map", "Reads the physical keyboard layout — a small but real fingerprint.", PrivacyImpact.MEDIUM, "Same-origin only."),
            new FeatureInfo("gamepad", "Enumerates connected controllers and their button state.", PrivacyImpact.MEDIUM, "Same-origin only."),
            new FeatureInfo("midi", "Web MIDI — access to attached music hardware.", PrivacyImpact.MEDIUM, "Same-origin only; prompts the user."),
            new FeatureInfo("payment", "Payment Request API — can expose stored payment handles.", PrivacyImpact.MEDIUM, "Same-origin only."),
            new FeatureInfo("publickey-credentials-get", "WebAuthn credential requests.", PrivacyImpact.MEDIUM, "Same-origin only."),
            new FeatureInfo("screen-wake-lock", "Keeps the screen awake — drains battery and signals intent.", PrivacyImpact.MEDIUM, "Same-origin only."),
            new FeatureInfo("speaker-selection", "Enumerates and switches audio output devices.", PrivacyImpact.MEDIUM, "Same-origin only."),
            new FeatureInfo("web-share", "Invokes the OS share sheet with chosen content.", PrivacyImpact.MEDIUM, "Same-origin only."),
            new FeatureInfo("encrypted-media", "DRM (EME) — playback identity can be correlated.", PrivacyImpact.MEDIUM, "Same-origin only."),
            new FeatureInfo("document-domain", "Let frames relax the same-origin policy via document.domain.", PrivacyImpact.MEDIUM, "Allowed in same-origin pages; deprecated."),
            // --- low privacy impact ---
            new FeatureInfo("autoplay", "Autoplay media with/without sound — not a data leak, an annoyance knob.", PrivacyImpact.LOW, "Muted autoplay allowed; audible blocked."),
            new FeatureInfo("cross-origin-isolated", "COOP/COEP isolation for SharedArrayBuffer — hardens the page.", PrivacyImpact.LOW, "Not isolated."),
            new FeatureInfo("fullscreen", "Element.requestFullscreen().", PrivacyImpact.LOW, "Same-origin only."),
            new FeatureInfo("navigation-override", "Lets a frame intercept its own top-level navigations.", PrivacyImpact.LOW, "Disabled."),
            new FeatureInfo("picture-in-picture", "Floating always-on-top video window.", PrivacyImpact.LOW, "Same-origin only."));

    private static final Pattern FEATURE_NAME_RE = Pattern.compile("[a-z][a-z0-9-]*");
    private static final Pattern ORIGIN_RE =
            Pattern.compile("(https?://|https?:)[\\w.-]+(:\\d+)?", Pattern.CASE_INSENSITIVE);
    private static final Pattern HEADER_PREFIX_RE =
            Pattern.compile("(?i)permissions-policy\\s*:\\s*");

    private PermissionsPolicy() {
    }

    private static String stripQuotes(String token) {
        return token.replaceFirst("^\"(.*)\"$", "$1");
    }

    private static String formatAllowlist(List<String> tokens) {
        if (tokens.size() == 1 && tokens.get(0).equals("*")) return "*";
        return "(" + String.join(" ", tokens) + ")";
    }

    /** Assemble a Permissions-Policy header value from a feature map. */
    public static String buildPermissionsPolicy(FeatureMap features) {
        List<String> directives = new ArrayList<>(); // FeatureMap is a TreeMap: keys iterate sorted
        for (Map.Entry<String, List<String>> e : features.entrySet()) {
            directives.add(e.getKey() + "=" + formatAllowlist(e.getValue()));
        }
        return String.join(", ", directives);
    }

    /**
     * Parse a Permissions-Policy header value back into a feature map.
     * Accepts an optional `Permissions-Policy:` prefix. Returns null when the
     * syntax is malformed (bad directive, missing allowlist, bad token).
     */
    public static FeatureMap parsePermissionsPolicy(String header) {
        String cleaned = HEADER_PREFIX_RE.matcher(header.trim()).replaceFirst("");
        if (cleaned.isEmpty()) return null;
        FeatureMap map = new FeatureMap();
        for (String rawDirective : cleaned.split(",")) {
            String directive = rawDirective.trim();
            if (directive.isEmpty()) return null;
            int eq = directive.indexOf('=');
            if (eq <= 0) return null;
            String name = directive.substring(0, eq).trim();
            String value = directive.substring(eq + 1).trim();
            if (!FEATURE_NAME_RE.matcher(name).matches()) return null;
            if (value.equals("*") || value.equals("self")) {
                map.put(name, List.of(value));
                continue;
            }
            if (!value.startsWith("(") || !value.endsWith(")")) return null;
            String inner = value.substring(1, value.length() - 1).trim();
            if (inner.isEmpty()) {
                map.put(name, List.of());
                continue;
            }
            List<String> tokens = new ArrayList<>();
            for (String t : inner.split("\\s+")) tokens.add(stripQuotes(t));
            for (String token : tokens) {
                if (!token.equals("self") && !token.equals("*") && !ORIGIN_RE.matcher(token).matches()) {
                    return null;
                }
            }
            map.put(name, tokens);
        }
        return map;
    }

    /** One directive-level problem found by validatePermissionsPolicy. */
    public record Validation(boolean valid, List<String> errors) {
    }

    /** Syntax-check a header the same way parsePermissionsPolicy does, with messages. */
    public static Validation validatePermissionsPolicy(String header) {
        List<String> errors = new ArrayList<>();
        String cleaned = HEADER_PREFIX_RE.matcher(header.trim()).replaceFirst("");
        if (cleaned.isEmpty()) return new Validation(false, List.of("Header is empty."));
        String[] directives = cleaned.split(",");
        for (int i = 0; i < directives.length; i++) {
            String directive = directives[i].trim();
            String where = "Directive " + (i + 1);
            if (directive.isEmpty()) {
                errors.add(where + ": empty (stray comma?).");
                continue;
            }
            int eq = directive.indexOf('=');
            if (eq <= 0) {
                errors.add(where + ": expected `feature=allowlist`, got `" + directive + "`.");
                continue;
            }
            String name = directive.substring(0, eq).trim();
            String value = directive.substring(eq + 1).trim();
            if (!FEATURE_NAME_RE.matcher(name).matches()) {
                errors.add(where + ": `" + name + "` is not a valid feature name.");
                continue;
            }
            if (value.equals("*") || value.equals("self")) continue;
            if (!value.startsWith("(") || !value.endsWith(")")) {
                errors.add(where + ": `" + name + "` allowlist must be `*`, `self`, or `(...)` — got `" + value + "`.");
                continue;
            }
            String inner = value.substring(1, value.length() - 1).trim();
            if (inner.isEmpty()) continue; // () = disabled, valid
            for (String raw : inner.split("\\s+")) {
                String token = stripQuotes(raw);
                if (!token.equals("self") && !token.equals("*") && !ORIGIN_RE.matcher(token).matches()) {
                    errors.add(where + ": `" + name + "` has an invalid allowlist token `" + raw + "`.");
                }
            }
        }
        return new Validation(errors.isEmpty(), errors);
    }

    /**
     * Privacy score 0-100 for a policy: how much it locks down the catalog.
     * Each feature is weighted by privacy impact (high 3, medium 2, low 1).
     * Disabled `()` earns full credit, `self` or an origin list half, `*` or
     * "absent from the policy" none (the browser default stays in force).
     */
    public static int privacyScore(FeatureMap features) {
        double earned = 0;
        double possible = 0;
        for (FeatureInfo feature : FEATURES) {
            int w = privacyWeight(feature.privacyImpact());
            possible += w;
            List<String> allowlist = features.get(feature.name());
            if (allowlist != null && allowlist.isEmpty()) earned += w; // () disabled
            else if (allowlist != null && !(allowlist.size() == 1 && allowlist.get(0).equals("*"))) {
                earned += w / 2.0; // self / origins
            }
        }
        return (int) Math.round(earned / possible * 100);
    }

    /** Privacy-impact weight: high 3, medium 2, low 1. */
    private static int privacyWeight(PrivacyImpact impact) {
        return switch (impact) {
            case HIGH -> 3;
            case MEDIUM -> 2;
            case LOW -> 1;
        };
    }
}

Also available in 8 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →