Dear all,

since one concern pronounced in this bug report was that there is no
automatic migration. Therefore, I developed a helper script to convert
most kinds of pkla files to the JS-based format (see attachment). I
created a merge request in the upstream repo as well if they want to
include the converter as well [1].

Maybe it is now possible to upgrade polkit to a newer version including
the new-style rules?

Best,
Jan

[1] https://gitlab.freedesktop.org/polkit/polkit/-/merge_requests/66

On Fri, 6 Dec 2019 00:25:41 +0000 Simon McVittie <s...@debian.org> wrote:
> On Thu, 05 Dec 2019 at 23:07:00 +0100, Geert Stappers wrote:
> > Upstream is at version 0.116 ( 
> > https://gitlab.freedesktop.org/polkit/polkit/-/tags )
> > Debian version is 0.105-something ( 
> > https://salsa.debian.org/utopia-team/polkit/blob/master/debian/changelog )
> > 
> > Debian changelog talks about changes from upstream version 0.114 and 0.116.
> 
> The latest upstream version is available in experimental.
> 
> The version in unstable is exactly what its version number indicates:
> a very old upstream version, with the majority of the upstream changes
> from later versions backported into it via debian/patches.
> 
> The reason for this is that upstream version
> 0.106 removed the "localauthority" backend (which
> configures polkit via .ini-style files like for example
> /var/lib/polkit-1/localauthority/10-vendor.d/org.freedesktop.Flatpak.pkla,
> which can be overridden by sysadmin configuration in
> /etc/polkit-1/localauthority or /var/lib/polkit-1/localauthority)
> and replaced it with a new JavaScript-based backend (which
> configures polkit via short JavaScript fragments like for example
> /usr/share/polkit-1/rules.d/org.freedesktop.Flatpak.rules, which can be
> overridden by sysadmin configuration in /etc/polkit-1/rules.d).
> 
> The migration path between the two is not obvious, and some of the Debian
> maintainers of polkit are strongly opposed to it being configured with
> JavaScript files, so at the moment we are stuck with polkit 0.105.
> 
> One day, I want to stop patching changes from 0.11x into 0.105,
> and instead start patching the "localauthority" backend into 0.11x,
> so that we can stick to the upstream version in all respects except
> for the configuration backend. However, all the maintainers of polkit
> (both in Debian and upstream) mostly work on other things, so it's rare
> that someone has enough uninterrupted time to do something like that.
> 
> There has been some upstream unhappiness with the current configuration
> arrangements, mostly because the mozjs library that is used for the
> JavaScript interpreter does not have a stable API; so it is possible that
> a newer upstream version will either change the configuration language
> (again), or change the JavaScript implementation to something more friendly
> (most likely a smaller interpreter like duktape).
> 
>     smcv
> 
> 
#!/usr/bin/env python3

from __future__ import annotations

import argparse
import configparser
from dataclasses import dataclass
import enum
import sys
import traceback
from textwrap import dedent, indent
from typing import Dict, List, Optional, Union, cast


def ensure_indent(text: str, numspaces: int = 4) -> str:
    return indent(dedent(text), " " * numspaces)


class PolkitConfConverterException(Exception):
    def __init__(self, msg: str):
        super().__init__()
        self.msg = msg


class PolkitIdType(enum.Enum):
    USER = enum.auto()
    GROUP = enum.auto()
    NETGROUP = enum.auto()

    @classmethod
    def parse_identity_type(cls, input: str) -> PolkitIdType:
        result_map = {
            "unix-user": cls.USER,
            "unix-group": cls.GROUP,
            "unix-netgroup": cls.NETGROUP,
        }

        try:
            return result_map[input]
        except KeyError as e:
            raise PolkitConfConverterException("Unknown polkit identity type") from e


class PolkitResult(enum.Enum):
    YES = enum.auto()
    NO = enum.auto()
    AUTH_SELF = enum.auto()
    AUTH_SELF_KEEP = enum.auto()
    AUTH_ADMIN = enum.auto()
    AUTH_ADMIN_KEEP = enum.auto()

    @classmethod
    def parse_polkit_result(
        cls, polkit_result: Optional[str]
    ) -> Optional[PolkitResult]:
        if polkit_result is None:
            return None

        result_map = {
            "yes": cls.YES,
            "no": cls.NO,
            "auth_self": cls.AUTH_SELF,
            "auth_self_keep": cls.AUTH_SELF_KEEP,
            "auth_admin": cls.AUTH_ADMIN,
            "auth_admin_keep": cls.AUTH_ADMIN_KEEP,
        }

        try:
            return result_map[polkit_result]
        except KeyError as e:
            raise PolkitConfConverterException("Unknown polkit result value") from e

    @classmethod
    def polkit_result_str(cls, element: PolkitResult) -> Optional[str]:
        result_map = {
            cls.YES: "polkit.Result.YES",
            cls.NO: "polkit.Result.NO",
            cls.AUTH_SELF: "polkit.Result.AUTH_SELF",
            cls.AUTH_SELF_KEEP: "polkit.Result.AUTH_SELF_KEEP",
            cls.AUTH_ADMIN: "polkit.Result.AUTH_ADMIN",
            cls.AUTH_ADMIN_KEEP: "polkit.Result.AUTH_ADMIN_KEEP",
        }

        try:
            return result_map[element]
        except KeyError:
            return None


@dataclass
class PolkitIdentity:
    identity_type: PolkitIdType
    identity: str

    def __str__(self) -> str:
        if self.identity_type is PolkitIdType.USER:
            return f"unix-user:{self.identity}"
        elif self.identity_type is PolkitIdType.GROUP:
            return f"unix-group:{self.identity}"
        elif self.identity_type is PolkitIdType.NETGROUP:
            return f"unix-netgroup:{self.identity}"
        else:
            raise PolkitConfConverterException("Illegal PolkitIdType")

    @classmethod
    def parse_identity_str(cls, input: str) -> PolkitIdentity:
        id_type, id_name = input.split(":", maxsplit=1)
        return cls(
            identity_type=PolkitIdType.parse_identity_type(id_type), identity=id_name
        )


@dataclass
class PolkitAdminConf:
    admin_identity: List[PolkitIdentity]

    def to_javascript(self) -> str:
        result_list = [str(i) for i in self.admin_identity]
        return dedent(
            f"""\
            polkit.addAdminRule(function(action, subject) {{
                return {result_list};
            }});
            """
        )


@dataclass
class PolkitPKLA:
    identity: List[PolkitIdentity]
    action: List[str]
    result_active: Optional[PolkitResult]
    result_inactive: Optional[PolkitResult]
    result_any: Optional[PolkitResult]
    return_value: Optional[Dict[str, str]]

    def format_action_expression(self, action: str) -> str:
        if "*" in action:
            if action.endswith("*"):
                return f'action.startsWith("{action[:-1]}")'

            raise PolkitConfConverterException(
                "Automatic conversion of globbing only at end of action string supported."
            )

        return f'action.id == "{action}"'

    def format_id_expression(self, id: PolkitIdentity) -> str:
        if id.identity_type is PolkitIdType.USER:
            return f'subject.user == "{id.identity}"'
        elif id.identity_type is PolkitIdType.GROUP:
            return f'subject.isInGroup("{id.identity}")'
        elif id.identity_type is PolkitIdType.NETGROUP:
            return f'subject.isInNetGroup("{id.identity}")'
        else:
            raise PolkitConfConverterException("Illegal PolkitIdType")

    def or_conditionals(self, conds: List[str], numspaces=8) -> str:
        if len(conds) > 1:
            numspaces += 1

        c = ensure_indent(" ||\n".join(conds), numspaces)

        if len(conds) > 1:
            c = " " * (numspaces - 1) + "(" + c.lstrip() + ")"
        return c

    def to_javascript(self) -> str:
        if self.return_value:
            raise PolkitConfConverterException(
                "Automatic conversion of PKLA return values is not supported."
            )

        action_conds = self.or_conditionals([self.format_action_expression(a) for a in self.action])
        identity_conds = self.or_conditionals([self.format_id_expression(i) for i in self.identity])

        if action_conds and identity_conds:
            merged_conds = dedent(
                """\
                {action_conds} &&
                {identity_conds}
                """
            ).format(
                action_conds=action_conds.lstrip(),
                identity_conds=identity_conds.rstrip()
            ).rstrip()
        elif action_conds:
            merged_conds = action_conds
        elif identity_conds:
            merged_conds = identity_conds
        else:
            merged_conds = "true"

        if self.result_any and (
            (not self.result_active and not self.result_inactive)
            or (not self.result_active and self.result_inactive is self.result_any)
            or (not self.result_inactive and self.result_active is self.result_any)
            or (
                self.result_active is self.result_any
                and self.result_inactive is self.result_any
            )
        ):
            results = ensure_indent(
                f"return {PolkitResult.polkit_result_str(self.result_any)};",
                numspaces=8,
            )
        else:
            if self.result_active:
                result_active = ensure_indent(
                    f"""\
                    if (subject.active && subject.local) {{
                        return {PolkitResult.polkit_result_str(self.result_active)};
                    }}
                    """,
                    numspaces=8,
                )
            else:
                result_active = ""

            if self.result_inactive:
                result_inactive = ensure_indent(
                    f"""\
                    if (subject.inactive && subject.local) {{
                        return {PolkitResult.polkit_result_str(self.result_inactive)};
                    }}
                    """,
                    numspaces=8,
                )
            else:
                result_inactive = ""

            if self.result_any:
                result_any = ensure_indent(
                    f"return {PolkitResult.polkit_result_str(self.result_any)};",
                    numspaces=8,
                )
            else:
                result_any = ""

            results = "".join((result_active, result_inactive, result_any))

        body = dedent(
            """\
            polkit.addRule(function(action, subject) {{
                if ({merged_conds})
                {{
            {results}
                }}
            }});
            """
        )

        return body.format(merged_conds=merged_conds, results=results)


def _parse_ini_section(
    section: configparser.SectionProxy,
) -> Union[PolkitAdminConf, PolkitPKLA]:
    if section.get("AdminIdentities"):
        admin_ids = [
            PolkitIdentity.parse_identity_str(i)
            for i in section["AdminIdentities"].split(";") if i
        ]
        return PolkitAdminConf(admin_identity=admin_ids)

    identity = section.get("Identity")
    action = section.get("Action")
    result_active = PolkitResult.parse_polkit_result(section.get("ResultActive"))
    result_inactive = PolkitResult.parse_polkit_result(section.get("ResultInactive"))
    result_any = PolkitResult.parse_polkit_result(section.get("ResultAny"))
    return_value = section.get("ReturnValue")

    if (
        not identity
        or not action
        or not (result_active or result_inactive or result_any)
    ):
        raise PolkitConfConverterException("Invalid input configuration")

    identity = [PolkitIdentity.parse_identity_str(i) for i in identity.split(";") if i]
    action = [i for i in action.split(";") if i]

    if return_value:
        return_value = dict([r.split("=", maxsplit=1) for r in return_value.split(";")])  # type: ignore

    return PolkitPKLA(
        identity=identity,
        action=action,
        result_active=result_active,
        result_inactive=result_inactive,
        result_any=result_any,
        return_value=cast(Optional[Dict[str, str]], return_value),
    )


# Main CLI function
def main() -> bool:
    parser = argparse.ArgumentParser(
        description="Convert old polkit config to new one."
    )
    parser.add_argument(
        "input",
        nargs="?",
        metavar="INPUT",
        type=argparse.FileType("r"),
        default=sys.stdin,
        help='Input file, can be "-" for stdin.',
    )
    parser.add_argument(
        "output",
        nargs="?",
        metavar="OUTPUT",
        type=argparse.FileType("w"),
        default=sys.stdout,
        help='Output file, can be "-" for stdout.',
    )
    parser.add_argument(
        "-d", "--debug", action="store_true", help="Enable debugging output."
    )

    args = parser.parse_args()

    input_config = configparser.ConfigParser()
    input_config.read_file(args.input)

    try:
        for section in input_config.sections():
            conf_obj = _parse_ini_section(input_config[section])
            args.output.write(conf_obj.to_javascript())
    except PolkitConfConverterException as e:
        if args.debug:
            traceback.print_exc()
        else:
            print(e.msg, file=sys.stderr)
        return False

    return True


if __name__ == "__main__":
    sys.exit(main())

Reply via email to