daemon: add polkit support to /v2/login #3581

Merged
merged 12 commits into from Aug 23, 2017

Conversation

Projects
None yet
5 participants
Contributor

jhenstridge commented Jul 12, 2017

This pull request comes out of discussions on the forum, and draws some code and ideas from @robert-ancell's previous pull request #409.

As a first step, this branch only attempts to add polkit support to a single API endpoint: /v2/login. This effectively lets snapd satisfy the use cases applications are using snapd-login-service for.

This branch can be tested as follows:

  1. ensure that data/polkit/io.snapcraft.snapd.policy is copied to /usr/share/polkit-1/actions/. This should happen automatically if you build a Debian package, but will need to be done manually if running snapd from the build directory.

  2. Move ~/.snap/auth.json out of the way temporarily.

  3. Run snap login (without sudo)

  4. Enter store account details.

A graphical password prompt should be presented by the desktop session's polkit agent. If the user correctly authenticates, then the login API call will succeed. If they cancel the prompt, the login will fail.

codecov-io commented Jul 12, 2017

Codecov Report

Merging #3581 into master will decrease coverage by 0.05%.
The diff coverage is 57.14%.

Impacted file tree graph

@@            Coverage Diff            @@
##           master   #3581      +/-   ##
=========================================
- Coverage   75.86%   75.8%   -0.06%     
=========================================
  Files         400     402       +2     
  Lines       34663   34744      +81     
=========================================
+ Hits        26296   26338      +42     
- Misses       6495    6532      +37     
- Partials     1872    1874       +2
Impacted Files Coverage Δ
polkit/authority.go 0% <0%> (ø)
daemon/ucrednet.go 93.02% <100%> (+2.11%) ⬆️
daemon/api.go 72.62% <50%> (ø) ⬆️
polkit/pid_start_time.go 70.96% <70.96%> (ø)
daemon/daemon.go 62.93% <90.9%> (+1.33%) ⬆️
cmd/snap/cmd_aliases.go 93.33% <0%> (-1.67%) ⬇️
interfaces/sorting.go 98.71% <0%> (-1.29%) ⬇️
wrappers/binaries.go 79.54% <0%> (+6.81%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update bf55de6...f3b9623. Read the comment docs.

Code looks overall sane, but I wonder about the interface of the polkit package.

daemon/daemon.go
if err == nil {
if uid == 0 {
// Superuser does anything.
return true
}
+ if c.ActionID != "" {
+ subject := polkit.ProcessSubject{Pid: int(pid)}
+ if result, err := polkitCheckAuthorization(subject, c.ActionID, nil, polkit.CheckAuthorizationAllowUserInteraction); err == nil {
@chipaca

chipaca Jul 19, 2017

Member

TBH I think I'd rather move make the polkit package handle all of this. That is, have something like a polkit.IsPidAuthorizedFor(int, string) (bool, error). Is there a reason you didn't go for that approach?

@jhenstridge

jhenstridge Jul 19, 2017

Contributor

I think we'll probably want to retain the flags argument, and let the client decide whether interaction should be allowed (e.g. if you run a snap command from a cron job, you probably don't want interactive auth).

The details map is not particularly useful with the version of polkit we currently ship in Ubuntu, but does offer some value with more recent releases. If we protected the package install action with polkit and provided appropriate details, an administrator might be able to write a policy that prevented the user from installing snaps with classic confinement, for instance.

+ "http://www.freedesktop.org/standards/PolicyKit/1.0/policyconfig.dtd">
+<policyconfig>
+
+ <vendor>Snapcraft</vendor>
@chipaca

chipaca Jul 19, 2017

Member

is that the right vendor?

@jhenstridge

jhenstridge Jul 19, 2017

Contributor

I just adapted the policy file from snapd-glib, which had the same vendor value. I'm not even sure if this ever gets displayed to the user.

polkit/authority.go
+)
+
+// CheckAuthorization queries polkit to determine whether a subject is
+// authorized to perform an action.
@chipaca

chipaca Jul 19, 2017

Member

is there a reason for this not to return (bool, error) instead of something you need to talk to in a particular way to get the boolean answer you're looking for and is implied by the docstring?

@jhenstridge

jhenstridge Jul 19, 2017

Contributor

I used the D-Bus API and existing C binding as a guide for the interface. I guess it depends on whether we want something that only covers our immediate use case, or something that can be reused.

The result struct is described here:

https://www.freedesktop.org/software/polkit/docs/latest/eggdbus-interface-org.freedesktop.PolicyKit1.Authority.html#eggdbus-struct-AuthorizationResult

The main pieces of additional information are:

  1. if the user is authorised successfully, and policy allows that authorisation to be retained for a time, it provides an opaque ID that can be used to cancel the retained authorisation.
  2. if you ask for non-interactive auth, it can tell you that authorisation could succeed with interaction.
  3. differentiate between "access denied" and "user cancelled the auth dialog".

(2) and (3) look like they could be handled via custom errors. I'm not sure how important (1) is.

@niemeyer

niemeyer Aug 14, 2017

Contributor

Yes, I think 2 and 3 are better handled as errors, and for 1 we can introduce another entry point which allows handling the more complex case nicely (it will probably involve context.Context).

polkit/authority.go
+func CheckAuthorization(subject Subject, actionId string, details map[string]string, flags CheckAuthorizationFlags) (result AuthorizationResult, err error) {
+ s, err := subject.serialize()
+ if err != nil {
+ return
@chipaca

chipaca Jul 19, 2017

Member

please don't use this style of returns, it makes the code harder to read as it grows larger. Return the explicit thing.

jhenstridge added some commits Jul 11, 2017

daemon, polkit: update based on John Lenton's review
* rename polkit.CheckAuthorization to CheckAuthorizationForPid, and make
  it return (bool, error).  If we need to check other subject types in
  future, we can add extra entry points.

* If the response says the user dismissed the auth dialog, return
  ErrDismissed.  In daemon, don't log failures due to this error.

* If the response says a challenge is required, rewturn
  ErrInteractionRequired.

* Use uint32 to represent process IDs: this is what is used at the D-Bus
  level, and even 64-bit Linux limits IDs to 2^22.  This leads to fewer
  casts.
Contributor

jhenstridge commented Jul 25, 2017

I'm not sure how my changes could have caused the single spread test failure (linode:ubuntu-14.04-64:tests/main/econnreset) -- it doesn't even look like it uses snap login. Could it be a transient failure?

Collaborator

mvo5 commented Jul 27, 2017

@jhenstridge Yeah, the spread test failure is a nown unstable test, I restarted it. Things should be more robust if you merge in master.

A few trivial tweaks suggested, but approach LGTM.

Thanks!

daemon/daemon.go
@@ -77,25 +78,41 @@ type Command struct {
// is this path accessible on the snapd-snap socket?
SnapOK bool
+ // can polkit grant access?
+ ActionID string
@niemeyer

niemeyer Aug 14, 2017

Contributor

Given the pattern established above, I suggest this for more clarity

// can polkit grant admin access? set to polkit action ID if so 
PolkitOK string
data/polkit/io.snapcraft.snapd.policy
+
+ <action id="io.snapcraft.snapd.login">
+ <description>Add a Snap store account</description>
+ <message>Authentication is required to add a Snap store account to the system</message>
@niemeyer

niemeyer Aug 14, 2017

Contributor

Perhaps:

<description>Authenticate on snap daemon</description>
<message>Authorization is required to authenticate on the snap daemon</message>

This seems more inline with the idea of "login" there.

polkit/authority.go
+ "github.com/godbus/dbus"
+)
+
+type CheckAuthorizationFlags uint32
@niemeyer

niemeyer Aug 14, 2017

Contributor

Would be nice to s/Authorization/Auth/ on all strings here, to save some reading and typing.

polkit/authority.go
+
+const (
+ CheckAuthorizationNone CheckAuthorizationFlags = 0x00
+ CheckAuthorizationAllowUserInteraction CheckAuthorizationFlags = 0x01
@niemeyer

niemeyer Aug 14, 2017

Contributor

This one feels over the top. Perhaps we can use CheckFlags, CheckNone, and CheckAllowInteraction here.

polkit/authority.go
+
+var (
+ ErrDismissed = errors.New("Authorization request dismissed")
+ ErrInteractionRequired = errors.New("Authorization requires interaction")
@niemeyer

niemeyer Aug 14, 2017

Contributor

This might also be just ErrInteraction. polkit.ErrInteraction sounds nice.

polkit/authority.go
+ if result.IsChallenge {
+ err = ErrInteractionRequired
+ } else if result.Details["polkit.dismissed"] != "" {
+ err = ErrDismissed
@niemeyer

niemeyer Aug 14, 2017

Contributor

Appreciate the translation into a nice API, thanks.

polkit/authority.go
+// CheckAuthorizationForPid queries polkit to determine whether a process is
+// authorized to perform an action.
+func CheckAuthorizationForPid(pid uint32, actionId string, details map[string]string, flags CheckAuthorizationFlags) (bool, error) {
+ subject := subject{
@niemeyer

niemeyer Aug 14, 2017

Contributor

Can we please not do type and variable with same name? Perhaps authSubject and authResult for the types?

@niemeyer niemeyer changed the title from Add polkit authorization support to /v2/login to daemon: add polkit support to /v2/login Aug 14, 2017

Contributor

jhenstridge commented Aug 16, 2017

Thanks for the review. Most of the type and constant naming in the polkit module came directly from the D-Bus interface. I agree that the shorter versions you suggested make things clearer without adding ambiguity.

LGTM. Let's please just get the error messages fixed to conform to the usual conventions.

+
+// CheckAuthorizationForPid queries polkit to determine whether a process is
+// authorized to perform an action.
+func CheckAuthorizationForPid(pid uint32, actionId string, details map[string]string, flags CheckFlags) (bool, error) {
@niemeyer

niemeyer Aug 16, 2017

Contributor

I'd still prefer to rename "Authorization" to "Auth" here, but that's mostly an irrelevant nitpick considering this is most likely going to be used once over the whole code base.

@jhenstridge

jhenstridge Aug 16, 2017

Contributor

The only nominal concern I can see with shortening it is confusion between authorisation and authentication. Maybe it doesn't matter here, but it seemed easier to just maintain the polkit's terminology here.

polkit/pid_start_time.go
+ // processes trying to fool us
+ idx := strings.IndexByte(contents, ')')
+ if idx < 0 {
+ return 0, fmt.Errorf("Error parsing file %s", filename)
@niemeyer

niemeyer Aug 16, 2017

Contributor

"cannot parse %s"

Lowercase, and we try to standardize on "cannot" as the usual prefix for anything resembling that sort of message (instead of, say, "can not", or "unable to", or "could not", "error while", etc). All errors are also prefixed by "error: " on the way out into the terminal.

Same for the messages below.

Contributor

jhenstridge commented Aug 17, 2017

I've got no idea about this CI error. It isn't clear to me it has anything to do with the contents of my branch:

2017-08-16 15:47:29 Error preparing project on linode:fedora-25-64 : 
-----
[snip thousands of lines of output]
++ cp /root/rpmbuild/RPMS/x86_64/snap-confine-1337.2.27.1-0.fc25.x86_64.rpm /root/rpmbuild/RPMS/x86_64/snapd-1337.2.27.1-0.fc25.x86_64.rpm /root/rpmbuild/RPMS/x86_64/snapd-debuginfo-1337.2.27.1-0.fc25.x86_64.rpm /home/gopath
++ [[ fedora-25-64 = fedora-* ]]
++ cp /root/rpmbuild/RPMS/noarch/snapd-devel-1337.2.27.1-0.fc25.noarch.rpm /root/rpmbuild/RPMS/noarch/snapd-selinux-1337.2.27.1-0.fc25.noarch.rpm /home/gopath
++ go get ./tests/lib/snapbuild

<kill-timeout reached>

Is this possibly a broken builder, or unreliable test?

Collaborator

mvo5 commented Aug 22, 2017

This looks very nice - I'm keen to land it (once tests are green, I merged master which will probably help). One quick question - will it fail gracefully if no polkit is available on the given system?

Contributor

jhenstridge commented Aug 23, 2017

Looks like the main CI tests are passing now.

This branch just adds a new way for the user to assert superuser privileges. If polkit is not available (or if snapd can't connect to the system bus), this is treated the same way as polkit not authorising the access: the user will need to pass one of the other checks in order to use the API (provide a valid macaroon, or have uid 0).

Contributor

niemeyer commented Aug 23, 2017

@jhenstridge Yeah, the mix of auth in the branch looks nice and correct. The question is more whether the implementation behaves properly if polkit is not around, as in will it introduce improper latency if a bus is not available or similar.

This looks great, I have some nitpicks and suggestions for your consideration. Happy to help with any of them, I'm keen to land this for 2.28 :)

polkit/authority.go
+ Details: make(map[string]dbus.Variant),
+ }
+ subject.Details["pid"] = dbus.MakeVariant(pid)
+ if startTime, err := getStartTimeForPid(pid); err == nil {
@mvo5

mvo5 Aug 23, 2017

Collaborator

(nitpick) I think its slightly more idiomatic go to write this with err != nil, i.e.:

startTime, err := getStartTimeForPid(pid)
if err != nil {
    return false, err
}
subject.Details["start-time"] = dbus.MakeVariant(startTime)
polkit/pid_start_time.go
+ file, err := os.Open(filename)
+ if err != nil {
+ return 0, err
+ }
@mvo5

mvo5 Aug 23, 2017

Collaborator

Is there a defer file.Close() missing here? Looking at the code it might be simple to just use:

data, err := ioutil.ReadFile(fmt.Sprintf("/proc/%d/stat", pid)
...

in lines 36-41 (which is slightly shorter and deals with the close for you).

+ contents := string(data)
+
+ // start time is the token at index 19 after the '(process
+ // name)' entry - since only this field can contain the ')'
@mvo5

mvo5 Aug 23, 2017

Collaborator

Maybe a small reference like: "see man proc and see for starttime for details about what the number means".

+
+var _ = check.Suite(&polkitSuite{})
+
+func (s *polkitSuite) TestGetStartTime(c *check.C) {
@mvo5

mvo5 Aug 23, 2017

Collaborator

I wonder if it would make sense to add a test that actually uses mock data for /proc/%d/stat via e.g. a export_test.go that allows to point to a mock-on-disk /proc. The reason is that if we ever refactor getStartTimeForPid() the test for (startTime, ot(Equals), 0) is true for most of the fields in the stat file. So a off-by-one error in the index might escape.

@mvo5 mvo5 added this to the 2.28 milestone Aug 23, 2017

Contributor

jhenstridge commented Aug 23, 2017

So there are a few places where a problem could occur:

  1. connecting to the system bus
    If the system bus isn't running, then there is no socket to connect to. This should fail fast. If the socket exists and we connect, there are a few round trips to dbus-daemon to establish the connction (auth/feature negotiation, and Hello method call to get unique bus name).

  2. making the polkit method call
    If polkit is not installed there will be no service owning polkit's bus name, and there will be no configuration file that can cause it to be activated. So the bus will generate an error reply message.

If polkit is available and we can make the method call, we're not currently imposing any timeout on the response. With that said, the response may be blocked on user interaction, so it isn't clear how long it is sensible to wait.

mvo5 approved these changes Aug 23, 2017

Thanks a lot for addressing the review comments!

@mvo5 mvo5 merged commit d4d622e into snapcore:master Aug 23, 2017

1 of 7 checks passed

artful-amd64 autopkgtest running
Details
xenial-amd64 autopkgtest running
Details
xenial-i386 autopkgtest running
Details
xenial-ppc64el autopkgtest running
Details
yakkety-amd64 autopkgtest running
Details
zesty-amd64 autopkgtest running
Details
continuous-integration/travis-ci/pr The Travis CI build passed
Details
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment