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 20 commits
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
55 changes: 55 additions & 0 deletions internals/daemon/access.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright (C) 2024 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/>.

package daemon

import (
"net/http"
"os"
)

// AccessChecker checks whether a particular request is allowed.
type AccessChecker interface {
// Check if access should be granted or denied. In case of granting access,
// return nil. In case access is denied, return a non-nil error response,
// such as Unauthorized("access denied").
CheckAccess(d *Daemon, r *http.Request, ucred *Ucrednet, user *UserState) Response
}

// OpenAccess allows all requests, including non-local sockets (e.g. TCP)
type OpenAccess struct{}

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

// AdminAccess allows requests over the UNIX domain socket from the root uid and the current user's uid
type AdminAccess struct{}

func (ac AdminAccess) CheckAccess(d *Daemon, r *http.Request, ucred *Ucrednet, user *UserState) Response {
if ucred != nil && (ucred.Uid == 0 || ucred.Uid == uint32(os.Getuid())) {
return nil
}
return Unauthorized("access denied")
}

// UserAccess allows requests over the UNIX domain socket 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 Unauthorized("access denied")
}
return nil
}
82 changes: 82 additions & 0 deletions internals/daemon/access_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright (C) 2024 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/>.

package daemon_test

import (
"os"

. "gopkg.in/check.v1"

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

type accessSuite struct {
}

var _ = Suite(&accessSuite{})

var errUnauthorized = daemon.Unauthorized("access denied")

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}
c.Check(ac.CheckAccess(nil, nil, ucred, nil), IsNil)

// OpenAccess allows access from root user
ucred = &daemon.Ucrednet{Uid: 0, Pid: 100}
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, errUnauthorized)

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

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

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

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

// Current user's UID
uid := uint32(os.Getuid())

// Non-root users that are different from the current user are forbidden
ucred := &daemon.Ucrednet{Uid: uid + 1, Pid: 100}
c.Check(ac.CheckAccess(nil, nil, ucred, nil), DeepEquals, errUnauthorized)

// The current user is granted access
ucred = &daemon.Ucrednet{Uid: uid, Pid: 100}
c.Check(ac.CheckAccess(nil, nil, ucred, nil), IsNil)

// Root is granted access
ucred = &daemon.Ucrednet{Uid: 0, Pid: 100}
c.Check(ac.CheckAccess(nil, nil, ucred, nil), IsNil)
}
161 changes: 83 additions & 78 deletions internals/daemon/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,84 +25,89 @@ 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{},
GET: v1SystemInfo,
}, {
Path: "/v1/health",
ReadAccess: OpenAccess{},
GET: v1Health,
}, {
Path: "/v1/warnings",
ReadAccess: UserAccess{},
WriteAccess: UserAccess{},
GET: v1GetWarnings,
POST: v1AckWarnings,
}, {
Path: "/v1/changes",
ReadAccess: UserAccess{},
GET: v1GetChanges,
}, {
Path: "/v1/changes/{id}",
ReadAccess: UserAccess{},
WriteAccess: UserAccess{},
GET: v1GetChange,
POST: v1PostChange,
}, {
Path: "/v1/changes/{id}/wait",
ReadAccess: 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{},
GET: v1GetPlan,
}, {
Path: "/v1/layers",
WriteAccess: UserAccess{},
POST: v1PostLayers,
}, {
Path: "/v1/files",
ReadAccess: UserAccess{},
WriteAccess: UserAccess{},
GET: v1GetFiles,
POST: v1PostFiles,
}, {
Path: "/v1/logs",
ReadAccess: UserAccess{},
GET: v1GetLogs,
}, {
Path: "/v1/exec",
WriteAccess: UserAccess{},
POST: v1PostExec,
}, {
Path: "/v1/tasks/{task-id}/websocket/{websocket-id}",
ReadAccess: UserAccess{},
GET: v1GetTaskWebsocket,
}, {
Path: "/v1/signals",
WriteAccess: UserAccess{},
POST: v1PostSignals,
}, {
Path: "/v1/checks",
ReadAccess: UserAccess{},
GET: v1GetChecks,
}, {
Path: "/v1/notices",
ReadAccess: UserAccess{},
WriteAccess: UserAccess{},
GET: v1GetNotices,
POST: v1PostNotices,
}, {
Path: "/v1/notices/{id}",
ReadAccess: UserAccess{},
GET: v1GetNotice,
}}

var (
Expand Down
1 change: 0 additions & 1 deletion internals/daemon/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ func (s *apiSuite) TestSysInfo(c *check.C) {
c.Assert(sysInfoCmd.GET, check.NotNil)
c.Check(sysInfoCmd.PUT, check.IsNil)
c.Check(sysInfoCmd.POST, check.IsNil)
c.Check(sysInfoCmd.DELETE, check.IsNil)

rec := httptest.NewRecorder()

Expand Down
Loading
Loading