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

feat(daemon): use identities to auth API requests #434

Merged
merged 60 commits into from
Jul 16, 2024
Merged
Show file tree
Hide file tree
Changes from 56 commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
e1a5ee6
feat(state): core of reading/writing identities in state
benhoyt Jun 12, 2024
b90e8dd
feat(daemon): identities API endpoints
benhoyt Jun 12, 2024
de7f220
feat(client): Go client for identities
benhoyt Jun 12, 2024
b9edaa5
feat(cli): identities CLI commands
benhoyt Jun 13, 2024
3a36230
fix: remove replace-identities; it's "update-identities --replace"
benhoyt Jun 13, 2024
0652caa
fix: import order
benhoyt Jun 13, 2024
77f6e9d
feat(daemon): use the new identities system to authorize API requests
benhoyt Jun 17, 2024
f10806e
Print "No identities." to stderr if there are none
benhoyt Jun 17, 2024
387b21d
Merge branch 'identities-cli' into identities-access
benhoyt Jun 17, 2024
79088f5
allow local identity with user-id: 0
benhoyt Jun 18, 2024
a821cc3
Merge branch 'identities-state' into identities-api
benhoyt Jun 18, 2024
3b7cb83
Merge branch 'identities-api' into identities-client
benhoyt Jun 18, 2024
2e96818
client.LocalIdentity: distinguish between user-id not set and 0
benhoyt Jun 18, 2024
afba252
Merge branch 'identities-client' into identities-cli
benhoyt Jun 18, 2024
cc22e97
verify that user IDs are unique
benhoyt Jun 18, 2024
304b781
Merge branch 'identities-state' into identities-api
benhoyt Jun 18, 2024
e615e00
Merge branch 'identities-api' into identities-client
benhoyt Jun 18, 2024
0d261c6
Merge branch 'identities-client' into identities-cli
benhoyt Jun 18, 2024
c0301a2
Merge branch 'identities-cli' into identities-access
benhoyt Jun 18, 2024
da8487b
Add tests, fix a few issues with validation (esp unique user IDs)
benhoyt Jun 19, 2024
d26131d
Merge branch 'identities-state' into identities-api
benhoyt Jun 19, 2024
c352701
Add tests
benhoyt Jun 19, 2024
47b4e9e
Merge branch 'identities-api' into identities-client
benhoyt Jun 19, 2024
9b98215
drive-by: check method/path in notices client tests
benhoyt Jun 19, 2024
43a26b6
Add tests
benhoyt Jun 19, 2024
e0ede42
Merge branch 'identities-client' into identities-cli
benhoyt Jun 19, 2024
069fae8
Added tests of CLI commands
benhoyt Jun 19, 2024
475b96f
Merge branch 'identities-cli' into identities-access
benhoyt Jun 19, 2024
24d8da7
Add support for "pebble run --identities <file>"
benhoyt Jun 19, 2024
e657bd3
Show identities commands in help
benhoyt Jun 19, 2024
6dd0d6d
Merge branch 'identities-cli' into identities-access
benhoyt Jun 19, 2024
effeb6e
Update notices API to look at UserState as well as just uid
benhoyt Jun 21, 2024
dfcacff
Rename "userState" variables to simpler "user"
benhoyt Jun 21, 2024
43adb5e
Additional tests for new AccessChecker logic
benhoyt Jun 21, 2024
17d9135
Add tests for IdentityFromInputs
benhoyt Jun 21, 2024
c0cbd52
Add tests; make named identities override bare UID-based auth
benhoyt Jun 21, 2024
fda598a
Improve error messages for invalid "access" field
benhoyt Jun 24, 2024
abecabc
Merge branch 'identities-state' into identities-api
benhoyt Jun 24, 2024
30075be
Merge branch 'identities-api' into identities-client
benhoyt Jun 24, 2024
4114a0c
Merge branch 'identities-client' into identities-cli
benhoyt Jun 24, 2024
0527445
Improve remove/null error message; add YAML examples in help
benhoyt Jun 24, 2024
f73facf
Merge branch 'identities-cli' into identities-access
benhoyt Jun 24, 2024
227f612
Fixes per code review
benhoyt Jun 27, 2024
bade624
Merge branch 'identities-state' into identities-api
benhoyt Jun 27, 2024
bd63045
Merge branch 'identities-api' into identities-client
benhoyt Jun 27, 2024
c075228
Check that identities in returned map aren't nil
benhoyt Jun 27, 2024
cd6e8e4
Special case for "identities --help" in the help output, per spec
benhoyt Jul 1, 2024
f097f1b
Other tweaks from code review
benhoyt Jul 1, 2024
7028783
More consistent error handling for null/not-null values
benhoyt Jul 1, 2024
085b140
Merge branch 'identities-api' into identities-client
benhoyt Jul 1, 2024
e22e788
Merge branch 'identities-client' into identities-cli
benhoyt Jul 1, 2024
11dc908
Merge branch 'identities-cli' into identities-access
benhoyt Jul 1, 2024
387e95a
Merge branch 'master' into identities-access
benhoyt Jul 8, 2024
ee94ac6
Simplify by always providing UserState even for default users
benhoyt Jul 9, 2024
3f81902
Drive-by to fix "notices --users" help comment
benhoyt Jul 9, 2024
bc5d32d
Add TODO comment per code review
benhoyt Jul 9, 2024
f999367
Improve help message for "pebble notices", especially --users
benhoyt Jul 12, 2024
bc64c76
Always have a non-nil UserState
benhoyt Jul 12, 2024
45b6d8c
Rename variable per code review
benhoyt Jul 12, 2024
284bfdf
Revert "Always have a non-nil UserState"
benhoyt Jul 12, 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
2 changes: 1 addition & 1 deletion internals/cli/cmd_notices.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func init() {
Summary: cmdNoticesSummary,
Description: cmdNoticesDescription,
ArgsHelp: merge(timeArgsHelp, map[string]string{
"--users": "Show all notices with any user ID (admin only; cannot be used with --uid)",
"--users": "'all' to list all notices with any user ID (admin only; cannot be used with --uid)",
"--uid": "Only list notices with this user ID (admin only; cannot be used with --users)",
"--type": "Only list notices of this type (multiple allowed)",
"--key": "Only list notices with this key (multiple allowed)",
Expand Down
45 changes: 30 additions & 15 deletions internals/daemon/access.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,40 +16,55 @@ package daemon

import (
"net/http"
"os"

"github.com/canonical/pebble/internals/overlord/state"
)

const (
accessDenied = "access denied"
)

// 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
// CheckAccess reports whether access should be granted or denied. If
// access is granted, return nil. If access is denied, return a non-nil
// error such as Unauthorized("access denied").
CheckAccess(d *Daemon, r *http.Request, user *UserState) Response
}

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

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

// AdminAccess allows requests over the UNIX domain socket from the root uid and the current user's uid
// 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())) {
func (ac AdminAccess) CheckAccess(d *Daemon, r *http.Request, user *UserState) Response {
if user == nil {
return Unauthorized(accessDenied)
}
if user.Access == state.AdminAccess {
return nil
}
return Unauthorized("access denied")
// An identity explicitly set to "access: read" or "access: untrusted" isn't allowed.
return Unauthorized(accessDenied)
}

// 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")
func (ac UserAccess) CheckAccess(d *Daemon, r *http.Request, user *UserState) Response {
if user == nil {
return Unauthorized(accessDenied)
}
return nil
switch user.Access {
case state.ReadAccess, state.AdminAccess:
return nil
}
// An identity explicitly set to "access: untrusted" isn't allowed.
return Unauthorized(accessDenied)
}
61 changes: 26 additions & 35 deletions internals/daemon/access_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,13 @@
package daemon_test

import (
"os"

. "gopkg.in/check.v1"

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

type accessSuite struct {
}
type accessSuite struct{}

var _ = Suite(&accessSuite{})

Expand All @@ -33,50 +31,43 @@ 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)
c.Check(ac.CheckAccess(nil, nil, nil), IsNil)

// User with "access: admin|read|untrusted" is granted access
user := &daemon.UserState{Access: state.AdminAccess}
c.Check(ac.CheckAccess(nil, nil, user), IsNil)
user = &daemon.UserState{Access: state.ReadAccess}
c.Check(ac.CheckAccess(nil, nil, user), IsNil)
user = &daemon.UserState{Access: state.UntrustedAccess}
c.Check(ac.CheckAccess(nil, nil, user), 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)
c.Check(ac.CheckAccess(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)
// User with "access: admin|read" is granted access
user := &daemon.UserState{Access: state.AdminAccess}
c.Check(ac.CheckAccess(nil, nil, user), IsNil)
user = &daemon.UserState{Access: state.ReadAccess}
c.Check(ac.CheckAccess(nil, nil, user), IsNil)

// UserAccess allows access form normal user
ucred = &daemon.Ucrednet{Uid: 42, Pid: 100}
c.Check(ac.CheckAccess(nil, nil, ucred, nil), IsNil)
// But not UntrustedAccess
user = &daemon.UserState{Access: state.UntrustedAccess}
c.Check(ac.CheckAccess(nil, nil, user), DeepEquals, errUnauthorized)
}

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)
c.Check(ac.CheckAccess(nil, nil, nil), DeepEquals, errUnauthorized)

// Root is granted access
ucred = &daemon.Ucrednet{Uid: 0, Pid: 100}
c.Check(ac.CheckAccess(nil, nil, ucred, nil), IsNil)
// But not ReadAccess or UntrustedAccess
user := &daemon.UserState{Access: state.ReadAccess}
c.Check(ac.CheckAccess(nil, nil, user), DeepEquals, errUnauthorized)
user = &daemon.UserState{Access: state.UntrustedAccess}
c.Check(ac.CheckAccess(nil, nil, user), DeepEquals, errUnauthorized)
}
55 changes: 21 additions & 34 deletions internals/daemon/api_notices.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,30 +42,30 @@ type addedNotice struct {
ID string `json:"id"`
}

func v1GetNotices(c *Command, r *http.Request, _ *UserState) Response {
query := r.URL.Query()

requestUID, err := uidFromRequest(r)
if err != nil {
func v1GetNotices(c *Command, r *http.Request, user *UserState) Response {
// TODO(benhoyt): the design of notices presumes UIDs; if in future when we
// support identities that aren't UID based, we'll need to fix this.
if user == nil || user.UID == nil {
return Forbidden("cannot determine UID of request, so cannot retrieve notices")
}
daemonUID := uint32(sysGetuid())

// By default, return notices with the request UID and public notices.
userID := &requestUID
userID := user.UID

query := r.URL.Query()
if len(query["user-id"]) > 0 {
if !isAdmin(requestUID, daemonUID) {
if user.Access != state.AdminAccess {
return Forbidden(`only admins may use the "user-id" filter`)
}
var err error
userID, err = sanitizeUserIDFilter(query["user-id"])
if err != nil {
return BadRequest(`invalid "user-id" filter: %v`, err)
}
}

if len(query["users"]) > 0 {
if !isAdmin(requestUID, daemonUID) {
if user.Access != state.AdminAccess {
return Forbidden(`only admins may use the "users" filter`)
}
if len(query["user-id"]) > 0 {
Expand Down Expand Up @@ -136,15 +136,6 @@ func v1GetNotices(c *Command, r *http.Request, _ *UserState) Response {
return SyncResponse(notices)
}

// Get the UID of the request. If the UID is not known, return an error.
func uidFromRequest(r *http.Request) (uint32, error) {
ucred, err := ucrednetGet(r.RemoteAddr)
if err != nil {
return 0, fmt.Errorf("could not parse request UID")
}
return ucred.Uid, nil
}

// Construct the user IDs filter which will be passed to state.Notices.
// Must only be called if the query user ID argument is set.
func sanitizeUserIDFilter(queryUserID []string) (*uint32, error) {
Expand Down Expand Up @@ -187,13 +178,8 @@ func sanitizeTypesFilter(queryTypes []string) ([]state.NoticeType, error) {
return types, nil
}

func isAdmin(requestUID, daemonUID uint32) bool {
return requestUID == 0 || requestUID == daemonUID
}

func v1PostNotices(c *Command, r *http.Request, _ *UserState) Response {
requestUID, err := uidFromRequest(r)
if err != nil {
func v1PostNotices(c *Command, r *http.Request, user *UserState) Response {
if user == nil || user.UID == nil {
return Forbidden("cannot determine UID of request, so cannot create notice")
}

Expand Down Expand Up @@ -242,7 +228,7 @@ func v1PostNotices(c *Command, r *http.Request, _ *UserState) Response {
st.Lock()
defer st.Unlock()

noticeId, err := st.AddNotice(&requestUID, state.CustomNotice, payload.Key, &state.AddNoticeOptions{
noticeId, err := st.AddNotice(user.UID, state.CustomNotice, payload.Key, &state.AddNoticeOptions{
Data: data,
RepeatAfter: repeatAfter,
})
Expand All @@ -253,12 +239,10 @@ func v1PostNotices(c *Command, r *http.Request, _ *UserState) Response {
return SyncResponse(addedNotice{ID: noticeId})
}

func v1GetNotice(c *Command, r *http.Request, _ *UserState) Response {
requestUID, err := uidFromRequest(r)
if err != nil {
func v1GetNotice(c *Command, r *http.Request, user *UserState) Response {
if user == nil || user.UID == nil {
return Forbidden("cannot determine UID of request, so cannot retrieve notice")
}
daemonUID := uint32(sysGetuid())
noticeID := muxVars(r)["id"]
st := c.d.overlord.State()
st.Lock()
Expand All @@ -267,19 +251,22 @@ func v1GetNotice(c *Command, r *http.Request, _ *UserState) Response {
if notice == nil {
return NotFound("cannot find notice with ID %q", noticeID)
}
if !noticeViewableByUser(notice, requestUID, daemonUID) {
if !noticeViewableByUser(notice, user) {
return Forbidden("not allowed to access notice with id %q", noticeID)
}
return SyncResponse(notice)
}

func noticeViewableByUser(notice *state.Notice, requestUID, daemonUID uint32) bool {
func noticeViewableByUser(notice *state.Notice, user *UserState) bool {
userID, isSet := notice.UserID()
if !isSet {
// Notice has no UID, so it's viewable by any user (with a UID).
return true
}
if isAdmin(requestUID, daemonUID) {
if user.Access == state.AdminAccess {
// User is admin, they can view anything.
return true
}
return requestUID == userID
// Otherwise user's UID must match notice's UID.
return *user.UID == userID
}
Loading
Loading