Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(daemon): port AccessChecker interface from snapd #358

Merged
merged 26 commits into from
Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c658437
daemon: make ucrednetGet() return *ucrednet
thp-canonical Feb 13, 2024
aa74f21
feat(daemon): Port accessChecker API from snapd
thp-canonical Feb 13, 2024
fe74b54
Merge remote-tracking branch 'origin/master' into access-checker
thp-canonical Feb 14, 2024
778f8eb
pr fix: Header for newly-added files
thp-canonical Feb 14, 2024
2b43c34
pr fix: CheckAccess documentation
thp-canonical Feb 14, 2024
06daf05
pr fix: Drop WriteAccess/ReadAccess without a corresponding verb
thp-canonical Feb 14, 2024
42cbc15
pr fix: Remove WriteAccess from unit test
thp-canonical Feb 14, 2024
89f05b6
pr fix: remove DELETE from Command
thp-canonical Feb 14, 2024
528fae3
pr fix: export AccessChecker
thp-canonical Feb 14, 2024
f315281
pr fix: export Ucrednet
thp-canonical Feb 14, 2024
7694d55
pr fix: export OpenAccess, UserAccess, RootAccess
thp-canonical Feb 14, 2024
bcbc1b0
pr fix: export error responders from daemon.response
thp-canonical Feb 14, 2024
bceb710
pr fix: golangci-lint
thp-canonical Feb 14, 2024
f030019
pr fix: align daemon.ServeHTTP with snapd
thp-canonical Feb 14, 2024
6da7520
pr fix: fix unit test and return value (Unauthorized vs Forbidden)
thp-canonical Feb 14, 2024
2cb2393
pr fix: change RootAccess to AdminAccess + allow current uid
thp-canonical Feb 14, 2024
9c0e195
pr fix: port test cases for guest/user/admin access
thp-canonical Feb 14, 2024
727ab38
Merge remote-tracking branch 'origin/master' into access-checker
thp-canonical Feb 19, 2024
2501679
pr fix: improve comment description
thp-canonical Feb 19, 2024
86e504e
pr fix: remove socketPath, as it isn't used by tests
thp-canonical Feb 19, 2024
4283186
pr fix: remove c parameter from doTestReqFunc
thp-canonical Feb 20, 2024
82a8582
pr fix: don't set RemoteAddr unnecessarily
thp-canonical Feb 20, 2024
5fdeed8
pr fix: access checker test case struct + helper
thp-canonical Feb 20, 2024
f5b35b0
pr fix: golangci-lint
thp-canonical Feb 20, 2024
4b91e33
pr fix: align with previous test cases
thp-canonical Feb 20, 2024
646485a
pr fix: terse brace style
thp-canonical Feb 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions internals/daemon/access.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

thp-canonical marked this conversation as resolved.
Show resolved Hide resolved
/*
* Copyright (C) 2021 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

// Based on: https://github.com/snapcore/snapd/blob/master/daemon/access.go
thp-canonical marked this conversation as resolved.
Show resolved Hide resolved

package daemon

import (
"net/http"
)

// accessChecker checks whether a particular request is allowed.
//
// An access checker will either allow a request (returns nil) or deny it.
thp-canonical marked this conversation as resolved.
Show resolved Hide resolved
type accessChecker interface {
CheckAccess(d *Daemon, r *http.Request, ucred *ucrednet, user *UserState) Response
thp-canonical marked this conversation as resolved.
Show resolved Hide resolved
}

// openAccess allows all requests
type openAccess struct{}

func (ac openAccess) CheckAccess(d *Daemon, r *http.Request, ucred *ucrednet, user *UserState) Response {
return nil
}

// rootAccess allows requests from the root uid
type rootAccess struct{}

func (ac rootAccess) CheckAccess(d *Daemon, r *http.Request, ucred *ucrednet, user *UserState) Response {
if ucred != nil && ucred.Uid == 0 {
thp-canonical marked this conversation as resolved.
Show resolved Hide resolved
return nil
}
return statusForbidden("access denied")
thp-canonical marked this conversation as resolved.
Show resolved Hide resolved
}

// userAccess allows requests from any local user
type userAccess struct{}

func (ac userAccess) CheckAccess(d *Daemon, r *http.Request, ucred *ucrednet, user *UserState) Response {
if ucred == nil {
return statusForbidden("access denied")
}
return nil
}
87 changes: 87 additions & 0 deletions internals/daemon/access_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
thp-canonical marked this conversation as resolved.
Show resolved Hide resolved

/*
* Copyright (C) 2021 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

// Based on: https://github.com/snapcore/snapd/blob/master/daemon/access.go

package daemon_test

import (
. "gopkg.in/check.v1"

"github.com/canonical/pebble/internals/daemon"
)

type accessSuite struct {
}

var _ = Suite(&accessSuite{})

var (
errForbidden = daemon.StatusForbidden("access denied")
errUnauthorized = daemon.StatusUnauthorized("access denied")
)

const (
socketPath = "/tmp/foo.sock"
)

func (s *accessSuite) TestOpenAccess(c *C) {
var ac daemon.AccessChecker = daemon.OpenAccess{}

// openAccess allows access without peer credentials.
c.Check(ac.CheckAccess(nil, nil, nil, nil), IsNil)

// openAccess allows access from normal user
ucred := &daemon.Ucrednet{Uid: 42, Pid: 100, Socket: socketPath}
c.Check(ac.CheckAccess(nil, nil, ucred, nil), IsNil)

// openAccess allows access from root user
ucred = &daemon.Ucrednet{Uid: 0, Pid: 100, Socket: socketPath}
c.Check(ac.CheckAccess(nil, nil, ucred, nil), IsNil)
}

func (s *accessSuite) TestUserAccess(c *C) {
var ac daemon.AccessChecker = daemon.UserAccess{}

// userAccess denies access without peer credentials.
c.Check(ac.CheckAccess(nil, nil, nil, nil), DeepEquals, errForbidden)
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't love how we're using DeepEquals on errors here, but given that this is copying snapd, we can leave it.


// userAccess allows access from root user
ucred := &daemon.Ucrednet{Uid: 0, Pid: 100, Socket: socketPath}
c.Check(ac.CheckAccess(nil, nil, ucred, nil), IsNil)

// userAccess allows access form normal user
ucred = &daemon.Ucrednet{Uid: 42, Pid: 100, Socket: socketPath}
c.Check(ac.CheckAccess(nil, nil, ucred, nil), IsNil)
}

func (s *accessSuite) TestRootAccess(c *C) {
var ac daemon.AccessChecker = daemon.RootAccess{}

// rootAccess denies access without peer credentials.
c.Check(ac.CheckAccess(nil, nil, nil, nil), DeepEquals, errForbidden)

// Non-root users are forbidden
ucred := &daemon.Ucrednet{Uid: 42, Pid: 100, Socket: socketPath}
c.Check(ac.CheckAccess(nil, nil, ucred, nil), DeepEquals, errForbidden)

// Root is granted access
ucred = &daemon.Ucrednet{Uid: 0, Pid: 100, Socket: socketPath}
c.Check(ac.CheckAccess(nil, nil, ucred, nil), IsNil)
}
173 changes: 95 additions & 78 deletions internals/daemon/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,84 +25,101 @@ import (
)

var API = []*Command{{
// See daemon.go:canAccess for details how the access is controlled.
Path: "/v1/system-info",
GuestOK: true,
GET: v1SystemInfo,
}, {
Path: "/v1/health",
GuestOK: true,
GET: v1Health,
}, {
Path: "/v1/warnings",
UserOK: true,
GET: v1GetWarnings,
POST: v1AckWarnings,
}, {
Path: "/v1/changes",
UserOK: true,
GET: v1GetChanges,
}, {
Path: "/v1/changes/{id}",
UserOK: true,
GET: v1GetChange,
POST: v1PostChange,
}, {
Path: "/v1/changes/{id}/wait",
UserOK: true,
GET: v1GetChangeWait,
}, {
Path: "/v1/services",
UserOK: true,
GET: v1GetServices,
POST: v1PostServices,
}, {
Path: "/v1/services/{name}",
UserOK: true,
GET: v1GetService,
POST: v1PostService,
}, {
Path: "/v1/plan",
UserOK: true,
GET: v1GetPlan,
}, {
Path: "/v1/layers",
UserOK: true,
POST: v1PostLayers,
}, {
Path: "/v1/files",
UserOK: true,
GET: v1GetFiles,
POST: v1PostFiles,
}, {
Path: "/v1/logs",
UserOK: true,
GET: v1GetLogs,
}, {
Path: "/v1/exec",
UserOK: true,
POST: v1PostExec,
}, {
Path: "/v1/tasks/{task-id}/websocket/{websocket-id}",
UserOK: true,
GET: v1GetTaskWebsocket,
}, {
Path: "/v1/signals",
UserOK: true,
POST: v1PostSignals,
}, {
Path: "/v1/checks",
UserOK: true,
GET: v1GetChecks,
}, {
Path: "/v1/notices",
UserOK: true,
GET: v1GetNotices,
POST: v1PostNotices,
}, {
Path: "/v1/notices/{id}",
UserOK: true,
GET: v1GetNotice,
Path: "/v1/system-info",
ReadAccess: openAccess{},
WriteAccess: userAccess{},
thp-canonical marked this conversation as resolved.
Show resolved Hide resolved
GET: v1SystemInfo,
}, {
Path: "/v1/health",
ReadAccess: openAccess{},
WriteAccess: userAccess{},
GET: v1Health,
}, {
Path: "/v1/warnings",
ReadAccess: userAccess{},
WriteAccess: userAccess{},
GET: v1GetWarnings,
POST: v1AckWarnings,
}, {
Path: "/v1/changes",
ReadAccess: userAccess{},
WriteAccess: userAccess{},
GET: v1GetChanges,
}, {
Path: "/v1/changes/{id}",
ReadAccess: userAccess{},
WriteAccess: userAccess{},
GET: v1GetChange,
POST: v1PostChange,
}, {
Path: "/v1/changes/{id}/wait",
ReadAccess: userAccess{},
WriteAccess: userAccess{},
GET: v1GetChangeWait,
}, {
Path: "/v1/services",
ReadAccess: userAccess{},
WriteAccess: userAccess{},
GET: v1GetServices,
POST: v1PostServices,
}, {
Path: "/v1/services/{name}",
ReadAccess: userAccess{},
WriteAccess: userAccess{},
GET: v1GetService,
POST: v1PostService,
}, {
Path: "/v1/plan",
ReadAccess: userAccess{},
WriteAccess: userAccess{},
GET: v1GetPlan,
}, {
Path: "/v1/layers",
ReadAccess: userAccess{},
WriteAccess: userAccess{},
POST: v1PostLayers,
}, {
Path: "/v1/files",
ReadAccess: userAccess{},
WriteAccess: userAccess{},
GET: v1GetFiles,
POST: v1PostFiles,
}, {
Path: "/v1/logs",
ReadAccess: userAccess{},
WriteAccess: userAccess{},
GET: v1GetLogs,
}, {
Path: "/v1/exec",
ReadAccess: userAccess{},
WriteAccess: userAccess{},
POST: v1PostExec,
}, {
Path: "/v1/tasks/{task-id}/websocket/{websocket-id}",
ReadAccess: userAccess{},
WriteAccess: userAccess{},
GET: v1GetTaskWebsocket,
}, {
Path: "/v1/signals",
ReadAccess: userAccess{},
WriteAccess: userAccess{},
POST: v1PostSignals,
}, {
Path: "/v1/checks",
ReadAccess: userAccess{},
WriteAccess: userAccess{},
GET: v1GetChecks,
}, {
Path: "/v1/notices",
ReadAccess: userAccess{},
WriteAccess: userAccess{},
GET: v1GetNotices,
POST: v1PostNotices,
}, {
Path: "/v1/notices/{id}",
ReadAccess: userAccess{},
WriteAccess: userAccess{},
GET: v1GetNotice,
}}

var (
Expand Down
Loading
Loading