Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
186 changes: 145 additions & 41 deletions daemon/access.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import (
"github.com/snapcore/snapd/overlord/ifacestate"
"github.com/snapcore/snapd/polkit"
"github.com/snapcore/snapd/sandbox/cgroup"
"github.com/snapcore/snapd/seclog"
"github.com/snapcore/snapd/strutil"
)

Expand Down Expand Up @@ -77,21 +78,12 @@ func checkPolkitActionImpl(r *http.Request, ucred *ucrednet, action string) *api
// An access checker will either allow a request, deny it, or return
// accessUnknown, which indicates the decision should be delegated to
// the next access checker.
//
// The CheckAccess method returns an *apiError if the request is denied,
// a seclog.AuthzChecks struct describing the result of each authorization
// check performed, and the intended access level for the check.
type accessChecker interface {
CheckAccess(d *Daemon, r *http.Request, ucred *ucrednet, user *auth.UserState) *apiError
}

// requireSockets ensures the request was received via one of the specified sockets.
func requireSockets(ucred *ucrednet, sockets []string) *apiError {
if ucred == nil {
return Forbidden("access denied")
}

if !strutil.ListContains(sockets, ucred.Socket) {
return Forbidden("access denied")
}

return nil
CheckAccess(d *Daemon, r *http.Request, ucred *ucrednet, user *auth.UserState) (*apiError, seclog.AuthzChecks, accessLevel)
}

type accessLevel string
Expand All @@ -100,6 +92,9 @@ const (
accessLevelRoot accessLevel = "root"
accessLevelAuthenticated accessLevel = "authenticated"
accessLevelOpen accessLevel = "open"
// accessLevelNotEvaluated is returned when byActionAccess fails before
// delegation; no authorization checks ran so the level is unknown.
accessLevelNotEvaluated accessLevel = "not-evaluated"
)

type accessOptions struct {
Expand Down Expand Up @@ -130,56 +125,162 @@ func (o accessOptions) validate() error {
return nil
}

func checkAccess(d *Daemon, r *http.Request, ucred *ucrednet, user *auth.UserState, opts accessOptions) *apiError {
if err := opts.validate(); err != nil {
return InternalError(err.Error())
// checkPrerequisites runs prerequisite authorization checks that must all pass
// before access level checks are evaluated. These include peer credentials,
// socket restrictions, and interface requirements.
func checkPrerequisites(d *Daemon, r *http.Request, ucred *ucrednet, checks *seclog.AuthzChecks, opts accessOptions) *apiError {
// Mark applicable checks as AuthzNotReached (will become Pass/Fail during evaluation).
// AccessOptions is intentionally not reset here: it is set by the caller after
// opts.validate() has run.
checks.PeerCreds = seclog.AuthzNotReached
if len(opts.Sockets) != 0 {
checks.Socket = seclog.AuthzNotReached
}
if opts.InterfaceAccess != nil {
checks.Interface = seclog.AuthzNotReached
}

if rspe := requireSockets(ucred, opts.Sockets); rspe != nil {
return rspe
// Peer credentials check
if ucred == nil {
checks.PeerCreds = seclog.AuthzFail
return Forbidden("access denied")
}
checks.PeerCreds = seclog.AuthzPass

// Socket check
if len(opts.Sockets) != 0 {
if !strutil.ListContains(opts.Sockets, ucred.Socket) {
checks.Socket = seclog.AuthzFail
return Forbidden("access denied")
}
checks.Socket = seclog.AuthzPass
}

// Interface check
if opts.InterfaceAccess != nil {
// No interface checks are made if request is coming from snapd.socket
// to account for the snapd-control interface.
rspe := requireInterfaceApiAccess(d, r, ucred, *opts.InterfaceAccess)
if rspe != nil {
checks.Interface = seclog.AuthzFail
return rspe
}
checks.Interface = seclog.AuthzPass
}

return nil
}

// checkAccessLevelAuthorization determines if the user is authorized at the
// required access level by evaluating multiple authorization methods. Any single
// method can grant access: open access, user authentication, root UID, or polkit.
func checkAccessLevelAuthorization(r *http.Request, ucred *ucrednet, user *auth.UserState, checks *seclog.AuthzChecks, opts accessOptions) *apiError {
// Mark applicable checks as AuthzNotReached (will become Pass/Fail during evaluation).
// Checks that do not apply to the current access level remain AuthzNotApplicable.
switch opts.AccessLevel {
case accessLevelOpen:
checks.OpenAccess = seclog.AuthzNotReached
case accessLevelAuthenticated:
checks.UserAuth = seclog.AuthzNotReached
checks.Root = seclog.AuthzNotReached
if opts.PolkitAction != "" {
checks.Polkit = seclog.AuthzNotReached
}
case accessLevelRoot:
checks.Root = seclog.AuthzNotReached
if opts.PolkitAction != "" {
checks.Polkit = seclog.AuthzNotReached
}
}

// Access level checks. All except the final polkit check returns on success.

if opts.AccessLevel == accessLevelOpen {
checks.OpenAccess = seclog.AuthzPass
return nil
}
// accessLevelOpen cannot fail

// Snapd local macaroon check - snapd user authentication
if opts.AccessLevel == accessLevelAuthenticated && user != nil {
// user != nil means we have an authenticated user
checks.UserAuth = seclog.AuthzPass
return nil
}
if opts.AccessLevel == accessLevelAuthenticated {
checks.UserAuth = seclog.AuthzFail
// Even though snapd user authentication failed,
// we still accept root or polkit authorization as
// permitted alternatives.
}

// System UID based root check for privileged actions.
if ucred.Uid == 0 {
checks.Root = seclog.AuthzPass
return nil
}
checks.Root = seclog.AuthzFail

// We check polkit last because it may result in the user
// being prompted for authorisation. This should be avoided if
// access is otherwise granted.
// Polkit check - system user authentication/authorization (policy dependant) for privileged actions.
// This happens last because it may prompt the user.
if opts.PolkitAction != "" {
return checkPolkitAction(r, ucred, opts.PolkitAction)
rspe := checkPolkitAction(r, ucred, opts.PolkitAction)
if rspe == nil {
checks.Polkit = seclog.AuthzPass
return nil
}
checks.Polkit = seclog.AuthzFail
return rspe
}

// XXX: when to 403 vs 401?
// If we reach here, all access level checks failed
if opts.AccessLevel == accessLevelAuthenticated || opts.InterfaceAccess != nil {
// At this point, if authenticated access and/or interface access is required,
// it means that accessLevelRoot (root or polkit) was either implicitly or
// explicitly required and failed
return Unauthorized("access denied")
}
// Explicitly required accessLevelRoot, via system UID or polkit checks was not authorized.
return Forbidden("access denied")
}

func checkAccess(d *Daemon, r *http.Request, ucred *ucrednet, user *auth.UserState, opts accessOptions) (*apiError, seclog.AuthzChecks, accessLevel) {
// All checks default to AuthzNotApplicable
checks := seclog.NewAuthzChecks()
level := opts.AccessLevel

if err := opts.validate(); err != nil {
checks.AccessOptions = seclog.AuthzFail
return InternalError(err.Error()), checks, level
}
checks.AccessOptions = seclog.AuthzPass

if err := checkPrerequisites(d, r, ucred, &checks, opts); err != nil {
return err, checks, level
}

if err := checkAccessLevelAuthorization(r, ucred, user, &checks, opts); err != nil {
return err, checks, level
}

return nil, checks, level
}

// isAdministrativeAccess reports whether authz audit events should be emitted:
// the intended access level is authenticated or root and any authorization
// checks were evaluated. Dispatch-only failures return accessLevelNotEvaluated
// with empty checks and are excluded.
func isAdministrativeAccess(level accessLevel, checks seclog.AuthzChecks) bool {
switch level {
case accessLevelAuthenticated, accessLevelRoot:
return checks.AnyPerformed()
default:
return false
}
}

// openAccess allows requests without authentication, provided they
// have peer credentials and were not received on snapd-snap.socket
type openAccess struct{}

func (ac openAccess) CheckAccess(d *Daemon, r *http.Request, ucred *ucrednet, user *auth.UserState) *apiError {
func (ac openAccess) CheckAccess(d *Daemon, r *http.Request, ucred *ucrednet, user *auth.UserState) (*apiError, seclog.AuthzChecks, accessLevel) {
opts := accessOptions{
AccessLevel: accessLevelOpen,
Sockets: []string{dirs.SnapdSocket},
Expand All @@ -202,7 +303,7 @@ type authenticatedAccess struct {
Polkit string
}

func (ac authenticatedAccess) CheckAccess(d *Daemon, r *http.Request, ucred *ucrednet, user *auth.UserState) *apiError {
func (ac authenticatedAccess) CheckAccess(d *Daemon, r *http.Request, ucred *ucrednet, user *auth.UserState) (*apiError, seclog.AuthzChecks, accessLevel) {
opts := accessOptions{
AccessLevel: accessLevelAuthenticated,
Sockets: []string{dirs.SnapdSocket},
Expand All @@ -215,7 +316,7 @@ func (ac authenticatedAccess) CheckAccess(d *Daemon, r *http.Request, ucred *ucr
// were not received on snapd-snap.socket
type rootAccess struct{}

func (ac rootAccess) CheckAccess(d *Daemon, r *http.Request, ucred *ucrednet, user *auth.UserState) *apiError {
func (ac rootAccess) CheckAccess(d *Daemon, r *http.Request, ucred *ucrednet, user *auth.UserState) (*apiError, seclog.AuthzChecks, accessLevel) {
opts := accessOptions{
AccessLevel: accessLevelRoot,
Sockets: []string{dirs.SnapdSocket},
Expand All @@ -226,7 +327,7 @@ func (ac rootAccess) CheckAccess(d *Daemon, r *http.Request, ucred *ucrednet, us
// snapAccess allows requests from the snapd-snap.socket only.
type snapAccess struct{}

func (ac snapAccess) CheckAccess(d *Daemon, r *http.Request, ucred *ucrednet, user *auth.UserState) *apiError {
func (ac snapAccess) CheckAccess(d *Daemon, r *http.Request, ucred *ucrednet, user *auth.UserState) (*apiError, seclog.AuthzChecks, accessLevel) {
opts := accessOptions{
AccessLevel: accessLevelOpen,
Sockets: []string{dirs.SnapSocket},
Expand Down Expand Up @@ -323,7 +424,7 @@ type interfaceOpenAccess struct {
Interfaces []string
}

func (ac interfaceOpenAccess) CheckAccess(d *Daemon, r *http.Request, ucred *ucrednet, user *auth.UserState) *apiError {
func (ac interfaceOpenAccess) CheckAccess(d *Daemon, r *http.Request, ucred *ucrednet, user *auth.UserState) (*apiError, seclog.AuthzChecks, accessLevel) {
opts := accessOptions{
AccessLevel: accessLevelOpen,
Sockets: []string{dirs.SnapdSocket, dirs.SnapSocket},
Expand All @@ -348,7 +449,7 @@ type interfaceAuthenticatedAccess struct {
Polkit string
}

func (ac interfaceAuthenticatedAccess) CheckAccess(d *Daemon, r *http.Request, ucred *ucrednet, user *auth.UserState) *apiError {
func (ac interfaceAuthenticatedAccess) CheckAccess(d *Daemon, r *http.Request, ucred *ucrednet, user *auth.UserState) (*apiError, seclog.AuthzChecks, accessLevel) {
opts := accessOptions{
AccessLevel: accessLevelAuthenticated,
Sockets: []string{dirs.SnapdSocket, dirs.SnapSocket},
Expand All @@ -368,7 +469,7 @@ type interfaceProviderRootAccess struct {
Interfaces []string
}

func (ac interfaceProviderRootAccess) CheckAccess(d *Daemon, r *http.Request, ucred *ucrednet, user *auth.UserState) *apiError {
func (ac interfaceProviderRootAccess) CheckAccess(d *Daemon, r *http.Request, ucred *ucrednet, user *auth.UserState) (*apiError, seclog.AuthzChecks, accessLevel) {
opts := accessOptions{
AccessLevel: accessLevelRoot,
Sockets: []string{dirs.SnapdSocket, dirs.SnapSocket},
Expand Down Expand Up @@ -398,7 +499,7 @@ type interfaceRootAccess struct {
Polkit string
}

func (ac interfaceRootAccess) CheckAccess(d *Daemon, r *http.Request, ucred *ucrednet, user *auth.UserState) *apiError {
func (ac interfaceRootAccess) CheckAccess(d *Daemon, r *http.Request, ucred *ucrednet, user *auth.UserState) (*apiError, seclog.AuthzChecks, accessLevel) {
opts := accessOptions{
AccessLevel: accessLevelRoot,
Sockets: []string{dirs.SnapdSocket, dirs.SnapSocket},
Expand Down Expand Up @@ -432,19 +533,22 @@ type byActionAccess struct {

const maxBodySize = 4 * 1024 * 1024 // 4MB

func (ac byActionAccess) CheckAccess(d *Daemon, r *http.Request, ucred *ucrednet, user *auth.UserState) *apiError {
// CheckAccess routes by JSON "action" to a delegated checker. Failures before
// delegation are dispatch errors (BadRequest/InternalError), not authorization;
// they return empty AuthzChecks and accessLevelNotEvaluated.
func (ac byActionAccess) CheckAccess(d *Daemon, r *http.Request, ucred *ucrednet, user *auth.UserState) (*apiError, seclog.AuthzChecks, accessLevel) {
switch ac.Default.(type) {
// TODO: If less strict interfaces are needed as defaults then
// we might need to introduce access checker sorting so that the
// default access checker is at least as strict as the strictest
// action access checker.
case rootAccess, interfaceRootAccess, interfaceProviderRootAccess:
default:
return InternalError("internal error: default access checker must have root-level access: got %T", ac.Default)
return InternalError("internal error: default access checker must have root-level access: got %T", ac.Default), seclog.NewAuthzChecks(), accessLevelNotEvaluated
}

if contentType := r.Header.Get("Content-Type"); contentType != "application/json" {
return BadRequest("unexpected content type: %q", contentType)
return BadRequest("unexpected content type: %q", contentType), seclog.NewAuthzChecks(), accessLevelNotEvaluated
}

req := actionRequest{}
Expand All @@ -461,13 +565,13 @@ func (ac byActionAccess) CheckAccess(d *Daemon, r *http.Request, ucred *ucrednet
err := decoder.Decode(&req)
if err != nil {
if (errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF)) && lr.N <= 0 {
return BadRequest("body size limit exceeded")
return BadRequest("body size limit exceeded"), seclog.NewAuthzChecks(), accessLevelNotEvaluated
}
// Content type is JSON, but it's invalid
return BadRequest(err.Error())
return BadRequest(err.Error()), seclog.NewAuthzChecks(), accessLevelNotEvaluated
}
if decoder.More() {
return BadRequest("unexpected data after request body")
return BadRequest("unexpected data after request body"), seclog.NewAuthzChecks(), accessLevelNotEvaluated
}

r.Body.Close()
Expand Down
Loading
Loading