Skip to content

Conversation

@Cu3PO42
Copy link

@Cu3PO42 Cu3PO42 commented Oct 11, 2025

This implements a new service module that enables users to write PolKit agents by integrating with libpolkit and libpolkitagent. A test configuration is added to the source.

Status: I've done everything I have planned.

Original description

This implements a new service module that enables users to write PolKit agents. To this end, I'm integrating with the existing polkit-qt-1 library that is also used by the KDE agent.

A relatively minimal example configuration using this new service can be found here.

There's a minor TODO left around cleanup of the Identity objects that I haven't been able to get quite right due to my inexperience with Qt. I'm hoping you might have a clean solution for this.

Closes #271.

@Cu3PO42 Cu3PO42 changed the title scope/polkit: add service module to write PolKit agents Add service module to write PolKit agents Oct 11, 2025
@outfoxxed
Copy link
Member

While I am not opposed to the addition of a polkit integration, I would rather avoid all KDE dependencies as including them leads to massive dependency trees on most distros, and they're usually pretty replaceable.

polkit-qt-1 appears to be a very thin wrapper around the polkit api that doesn't provide much. I would prefer to avoid it unless you have a good reason to pull it in.

@Cu3PO42
Copy link
Author

Cu3PO42 commented Oct 12, 2025

polkit-qt-1 is indeed a very thin wrapper. The 'good' reason for using it is that it deals with some of the more error-prone aspects of the upstream libraries and doesn't really bring anything that we don't need. While it is maintained by the KDE team, the package doesn't actually pull in anything but Polkit itself and Qt on the distros I checked (Debian, Ubuntu, Arch, Fedora, RHEL, NixOS, or Gentoo). I find it unlikely that others would have wildly different dependencies.

That said, it should also be fairly easy to not use it at the cost of a few hundred LOC, which would have the benefit of me getting to correct one of the limitations of polkit-qt-1.

@outfoxxed
Copy link
Member

It sounds like one of the more reasonable ones if we had to use it then, though I would personally still prefer not to.

@Cu3PO42
Copy link
Author

Cu3PO42 commented Oct 12, 2025

Alright. I'll see what I can do.

@Cu3PO42
Copy link
Author

Cu3PO42 commented Oct 12, 2025

I removed the dependency on polkit-qt, interacting with the PolKit libraries directly, instead.

Copy link
Member

@outfoxxed outfoxxed left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partial review on the interface.

Before continuing further, I've noticed the polkit agent interface appears to be available directly over dbus. Is there a reason to use any library at all for it? (Especially a gobject library)

@Cu3PO42
Copy link
Author

Cu3PO42 commented Oct 13, 2025

Thanks for the review!

I don't like using a gobject library here either, but yes, there is a good reason for using it instead of interacting directly over DBus. PolKit requires the client that sends the confirmation message to be privileged. To this end, libpolkit has a setuid helper binary that it invokes, which handles the actual PAM conversation, and shuffles all of the data between the parent and the helper.

To avoid libpolkit, Quickshell would either need to run as root or we would need our own setuid helper. Besides the implementation overhead, this would break simply running it from your Flake via Nix or installing it via Home-Manager. I imagine (although I don't positively know) many distros might also look more sceptically at any setuid binaries.

Finally, the DBus interface isn't actually stable because it is currently considered an implementation detail. I don't think it has changed significantly in recent years, but I would still prefer the more stable solution.

@outfoxxed
Copy link
Member

That's horrible on so many levels.

@Cu3PO42
Copy link
Author

Cu3PO42 commented Oct 13, 2025

I... yeah. But that's Polkit for you. There's many things I wish I didn't know about it.

If, given these circumstances, you'd rather not have the code upstream, I'm just going to package it up as a separate library.

@outfoxxed
Copy link
Member

I'm fine with the gobject code since its an actual requirement, and this is a feature many people want. I'll finish the review tomorrow probably.

@Cu3PO42
Copy link
Author

Cu3PO42 commented Oct 13, 2025

Thank you! I'm not in any rush. Also, fair warning: this is the first 'not insignificant' thing I've done with Qt, so while I tried to do things right, some things might be the way they are because I didn't know any better. I'll happily take any pointers.

@outfoxxed
Copy link
Member

Thats fine. One further thing that would be helpful would be to add a manual test qml file similar to what exists in other parts of the source. Just cover a reasonable amount of the api for a simple manual regression test

@Cu3PO42
Copy link
Author

Cu3PO42 commented Oct 13, 2025

I got started on some of your comments. Here's an additional thing we might consider:

PolKit can send either 'show-info' or 'show-error'. I am currently storing both into the same property with an isError flag. I.e. an incoming error overwrites a previous info. PolKit itself does not specify the way the UI should look, but many agents handle it this way. However, for some scenarios showing both might make sense, e.g. fprintd will show 'Place your right index finger on the reader' as info and 'Fingerprint did not match' as info. I am considering having both individual properties and a combined one for the latest message so users can choose how to design their dialog.

One thing I'm a bit unsure on is object destruction/cleanup and its behavior in Qt. When an authentication request finishes, we need to clean up all objects, but does this cause the UI to flicker, if we animate a window out, for example?

@outfoxxed
Copy link
Member

outfoxxed commented Oct 14, 2025

PolKit can send either 'show-info' or 'show-error'. I am currently storing both into the same property with an isError flag. I.e. an incoming error overwrites a previous info. PolKit itself does not specify the way the UI should look, but many agents handle it this way. However, for some scenarios showing both might make sense, e.g. fprintd will show 'Place your right index finger on the reader' as info and 'Fingerprint did not match' as info. I am considering having both individual properties and a combined one for the latest message so users can choose how to design their dialog.

Does the api provide any sort of hint as to if both should be present or not? If it does something like sending a new version of the error message thats an empty string to reset thats a pretty clear signal we could have multiple, but if not I'm less sure. If the messages are coming from pam, our pam module currently replaces the last message with the new one so it probably makes sense to keep that design.

One thing I'm a bit unsure on is object destruction/cleanup and its behavior in Qt. When an authentication request finishes, we need to clean up all objects, but does this cause the UI to flicker, if we animate a window out, for example?

This is a bit of a mess, Quickshell in particular has a hack for it called Retainable which you can see here https://quickshell.org/docs/v0.2.1/types/Quickshell/Retainable/. Retainable objects generally hold their last state and become immutable while retained to make it easier to write reactive UI against them.

Note: I tested the dialog and see you have a lot of DEBUG: PolkitAgent: lines. Use logging categories (qCDebug etc) for this. We hide them by default but they can be as detailed as you want (though you should make an effort to not put passwords in them)

@Cu3PO42
Copy link
Author

Cu3PO42 commented Oct 14, 2025

Does the api provide any sort of hint as to if both should be present or not? If it does something like sending a new version of the error message thats an empty string to reset thats a pretty clear signal we could have multiple, but if not I'm less sure. If the messages are coming from pam, our pam module currently replaces the last message with the new one so it probably makes sense to keep that design.

Unfortunately not. The messages are indeed forwarded from PAM. I am therefore inclined to leave it as is.

This is a bit of a mess, Quickshell in particular has a hack for it called Retainable which you can see here https://quickshell.org/docs/v0.2.1/types/Quickshell/Retainable/. Retainable objects generally hold their last state and become immutable while retained to make it easier to write reactive UI against them.

Thanks! I'll look into that to figure out how best to use it in this scenario.

Note: I tested the dialog and see you have a lot of DEBUG: PolkitAgent: lines. Use logging categories (qCDebug etc) for this. We hide them by default but they can be as detailed as you want (though you should make an effort to not put passwords in them)

That's exactly what I was looking for. I'll migrate to that.

Copy link
Member

@outfoxxed outfoxxed left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not super in depth as I've been busy, but I gave it a look-over.

Comment on lines 74 to 75
// First, destroy all but the first request in the queue, so that the cancel
// method doesn't start a new one.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would think its not unreasonable to handle a request right after cancelling a different one, especially if they're for different programs.

Also, when reloading the graph, preserving the old polkit session may be the most reasonable case, especially if the user wants to do something like edit their auth dialog with a real app running to test with.

Copy link
Author

@Cu3PO42 Cu3PO42 Oct 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handling one request after another is the default behavior in finishAuthenticationRequest. The cancelAuthenticationRequest method calls finishAuthenticationRequest and therefore would start another request immediately.

However, we are in the destructor of the agent here, so there will be no-one to handle the queued requests after the destructor has run. Therefore I want to reject all currently queued requests and not start processing them. To this end, I start by rejecting all requests after the current one, so that when the cancel method runs, it won't have any more to start.

In the case of reloading, I agree, preservation would be ideal. I'll look into how I can solve that cleanly only for the case of the reload, but still dismiss everything when actually destroying the agent.

Copy link
Member

@outfoxxed outfoxxed Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As for reloading see my comment below about using an internal singleton. you can wait for the destruction of the last engine generation with a ref to the backing agent to cancel all requests and deregister the agent, or some similar pattern.

Comment on lines 94 to 96
// In case of config reloads new objects are constructed before the old ones
// are destroyed. Therefore the new agent cannot take over the path still in
// use by the old agent.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can as long as its negotiated. I think we established it doesn't make sense to have more than one agent, so a single agent per process solves this nicely.

More than one polkit agent object at a time in a single generation however does not make sense and should print an error message and render any agent objects >1 inoperable.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can as long as its negotiated.

Are you referring to some particular Quickshell interface or is this a general remark? If the former, do you have a pointer? If the latter, this code is the early version of this negotiation. I meant to express 'it cannot take over the path without doing something to support it explicitly'. In the old version using polkit-qt, it wasn't possible to transfer the listener over, so closing the path and then opening it again was necessary. Now it might be possible and cleaner to transfer the listener. I'll look into it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I fully understand the particular problem here, but singleton resources that live longer than a single engine generation are used in a few places in Quickshell.

Since the qml engine controls the lifecycle of all objects created in it, Quickshell often has an internal singleton object with a thin wrapper object owned by the qml engine instead, so it doesn't neeto die with the generation.

With the restriction that we can only reasonably have one polkit agent at once, it seems logical to me to represent it this way.

What I meant by negotiation was only allowing one qml owner of the polkit agent per generation, but keeping tha actual agent object the same.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we agree conceptually, I'm just not sure of the exact API surface that permits me to do this cleanly. I can obviously put the listener itself into a singleton and introduce a thin wrapper. Since we want to permit only one such wrapper per generation, I need to add a check to this effect: If a new PolkitAgent is constructed, I can check this singleton and see one already exists and set an error state.

In the case of a config reload, I will also see the listener is already in use, but in this case I want to take ownership of it. I could attempt to use currentGeneration to detect this, but this seems mildly heuristic. Alternatively, I could extend Reloadable and do it in Reloadable#onReload. Do you have a preference for any of these approaches or is there some other idiomatic way I'm not seeing currently?

Thanks!

Copy link
Member

@outfoxxed outfoxxed Oct 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Theres a couple differences in the approaches you could take:

  • Reloadable uses heuristics to try to match an object in the old generation to one in the new generation. This is the system that allows allocated windows to be reused. You could use this if you wanted to only persist the polkit agent if the user placed it in the same spot in the same file across generations.
  • PostReloadHook fires unconditionally after Reloadable and has no concept of the scene graph. Combining this with findObjectGeneration on the user facing wrapper objects from the current and previous generations will tell you if we're switching generations or the user created a second invalid agent. Avoid currentGeneration.

Either of these interfaces you would implement on the wrapper object and not the internal singleton.

I recommend the PostReloadHook approach but would accept either.

@Cu3PO42
Copy link
Author

Cu3PO42 commented Oct 19, 2025

I performed a fairly significant refactoring to accommodate the changes you recommended.

  • PolkitAgent is now a very thin wrapper with the business logic living in PolkitAgentImpl.
  • PostReloadHook is used to handle transitions between generations.
  • State of the currently active request as well as any queued requests is preserved on reload.
  • AuthFlow is a new class that houses the actual state of an in-progress authentication. It is Retainable to accommodate animating out dialogs.
  • The code is just better organized overall.

I'll look at the lint errors soon.

Copy link
Member

@outfoxxed outfoxxed left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed the pr as a whole

/// The icon to present to the user in association with the message.
///
/// The icon name follows the [FreeDesktop icon naming specification](https://specifications.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html).
Q_PROPERTY(QString iconName READ iconName CONSTANT);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Best to link to @@Quickshell.Quickshell.iconPath() to display it in an image

Comment on lines 48 to 49
/// Setting this will abort any ongoing authentication conversations and start a new one.
Q_PROPERTY(Identity* selectedIdentity READ selectedIdentity WRITE setSelectedIdentity NOTIFY selectedIdentityChanged);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Setting" -> "Changing"

Reactive changes can cause a lot of "Setting" so this wording could be confusing

Comment on lines 133 to 134
emit completedChanged();
emit authenticationSucceeded();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this-> on method calls including emit, as well as members. ci should be catching this one if its not

Comment on lines 147 to 155
Identity* mSelectedIdentity = nullptr;
bool mIsResponseRequired = false;
QString mResponseMessage;
bool mResponseVisible = false;
QString mExtraMessage;
bool mExtraIsError = false;
bool mIsCompleted = false;
bool mIsSuccessful = false;
bool mIsCancelled = false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While acceptable as-is, properties like this are better handled with QObject bindable properties, which are basically C++-side reactive properties.

Comment on lines 18 to 20
void PolkitAgent::classBegin() {
// Nothing to do here.
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just leave classbegin () {} in the header

namespace qs::service::polkit {
PolkitAgent::PolkitAgent(QObject* parent): PostReloadHook(parent) {}

PolkitAgent::~PolkitAgent() = default;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can also go in the header

Comment on lines 64 to 66
emit isRegisteredChanged();
emit isActiveChanged();
emit flowChanged();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another good site for bindable properties

Copy link
Author

@Cu3PO42 Cu3PO42 Oct 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on my current understanding of bindable properties (at least as far as the Q_OBJECT_BINDABLE_PROPERTY) is concerned, are backed by a field in the object. This is not the case for the properties that are associated with these signals. I am sure they can be hooked up to work anyway, but I'm not sure if that would still be simpler?

I think I've figured it out. Will hopefully push something later.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are field backed but when used with a binding callback the field acts as more of a cache than anything else. It also compares old/new values to see if anything changed and emit signals or not.

There are cases where it improves readability, generally the more values that depend on eachother the greater the improvement.

Here's an example of it being used with a binding:

this->bcExclusiveZone.setBinding([this]() -> qint32 {

And heres an example of it being used as a field that just so happens to emit signals automatically on prop change:

Qt::beginPropertyUpdateGroup();

Up to you where you use it but it does generally make things clearer.

@Cu3PO42
Copy link
Author

Cu3PO42 commented Oct 20, 2025

Thank you for the extensive review! I addressed most of your comments. I also introduced a RAII helper GObjectRef to make handling of GObject objects less error prone. I still need to look into bindable properties.

I selectively turned off some lints in specific instances where I felt they were not helpful or actively wrong (e.g. it won't let me forward declare any structs from the Polkit libraries). There should always be an accompanying comment explaining why the specific rule doesn't make sense in that case. Let me know if you disagree with any of my reasons.

@Cu3PO42
Copy link
Author

Cu3PO42 commented Oct 20, 2025

I do love a good reactivity system. I switched over to bindable properties where I figured it makes sense. With that I believe I have addressed everything you pointed out. I appreciate your patience, this has been quite the learning experience.

@Cu3PO42
Copy link
Author

Cu3PO42 commented Oct 23, 2025

I rebased onto current master, squashed and fixed a small bug in the reloading logic.

I have also integrated this into Noctalia in a branch to validate in a more complex config.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE]: Polkit Authentication Agent

2 participants