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: add a new basic type identity #563

Open
wants to merge 24 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f76470c
poc: a metrics module for pebble
IronCore864 Nov 13, 2024
6d8ee59
chore: undo unnecessary change
IronCore864 Nov 14, 2024
b4abc9a
chore: undo unnecessary change
IronCore864 Nov 14, 2024
a274276
chore: undo unnecessary change
IronCore864 Nov 14, 2024
a2c07e6
chore: metrics identity basic auth poc
IronCore864 Nov 26, 2024
4ebb633
chore: a poc for metrics with labels
IronCore864 Nov 27, 2024
7468b95
poc: remove adding identities using env vars according to comment in …
IronCore864 Nov 28, 2024
790a8f9
chore: update tests for the metrics lib poc
IronCore864 Nov 28, 2024
272005b
chore: refactor identities and access according to spec review
IronCore864 Dec 9, 2024
5be3e96
feat: use sha512 to verify password
IronCore864 Jan 21, 2025
a6c374d
feat: move the metrics api to /v1/metrics
IronCore864 Jan 21, 2025
1bd54cb
chore: remove Username from apiBasicIdentity
IronCore864 Jan 21, 2025
98ea11e
chore: revert changes on user state
IronCore864 Jan 21, 2025
68c18b7
Merge branch 'master' into poc-custom-metrics-lib
IronCore864 Jan 21, 2025
7fc255e
chore: fix failed identity tests
IronCore864 Jan 21, 2025
31a0617
test: unit tests for basic identity
IronCore864 Jan 22, 2025
306f2d3
feat: add basic identity
IronCore864 Jan 23, 2025
6c53491
chore: update comments
IronCore864 Jan 23, 2025
e49419d
chore: refactor according to review and add more unit tests
IronCore864 Feb 10, 2025
35794e6
Merge branch 'master' into basic-identity
IronCore864 Feb 13, 2025
c8f3aba
chore: prioritize basic type identity, add tests
IronCore864 Feb 13, 2025
af5e0b2
test: add more tests
IronCore864 Feb 13, 2025
edccbac
chore: prioritize basic type
IronCore864 Feb 13, 2025
c49f565
chore: refactor after review
IronCore864 Feb 20, 2025
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
9 changes: 8 additions & 1 deletion client/identities.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ type Identity struct {
Access IdentityAccess `json:"access" yaml:"access"`

// One or more of the following type-specific configuration fields must be
// non-nil (currently the only type is "local").
// non-nil (currently the only types are "local" and "basic").
Local *LocalIdentity `json:"local,omitempty" yaml:"local,omitempty"`
Basic *BasicIdentity `json:"basic,omitempty" yaml:"basic,omitempty"`
}

// IdentityAccess defines the access level for an identity.
Expand All @@ -47,6 +48,12 @@ type LocalIdentity struct {
UserID *uint32 `json:"user-id" yaml:"user-id"`
}

// BasicIdentity holds identity configuration specific to the "basic" type
// (for HTTP basic authentication).
type BasicIdentity struct {
Password string `json:"password" yaml:"password"`
}

// For future extension.
type IdentitiesOptions struct{}

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/canonical/pebble
go 1.22

require (
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5
github.com/canonical/go-flags v0.0.0-20230403090104-105d09a091b8
github.com/canonical/x-go v0.0.0-20230522092633-7947a7587f5b
github.com/gorilla/mux v1.8.1
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 h1:IEjq88XO4PuBDcvmjQJcQGg+w+UaafSy8G5Kcb5tBhI=
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5/go.mod h1:exZ0C/1emQJAw5tHOaUDyY1ycttqBAPcxuzf7QbY6ec=
github.com/canonical/go-flags v0.0.0-20230403090104-105d09a091b8 h1:zGaJEJI9qPVyM+QKFJagiyrM91Ke5S9htoL1D470g6E=
github.com/canonical/go-flags v0.0.0-20230403090104-105d09a091b8/go.mod h1:ZZFeR9K9iGgpwOaLYF9PdT44/+lfSJ9sQz3B+SsGsYU=
github.com/canonical/x-go v0.0.0-20230522092633-7947a7587f5b h1:Da2fardddn+JDlVEYtrzBLTtyzoyU3nIS0Cf0GvjmwU=
github.com/canonical/x-go v0.0.0-20230522092633-7947a7587f5b/go.mod h1:upTK9n6rlqITN9rCN69hdreI37dRDFUk2thlGGD5Cg8=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
Expand All @@ -15,6 +19,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pkg/term v1.1.0 h1:xIAAdCMh3QIAy+5FrE8Ad8XoDhEU4ufwbaSozViP9kk=
github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand Down
3 changes: 3 additions & 0 deletions internals/cli/cmd_identities.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ func (cmd *cmdIdentities) writeText(identities map[string]*client.Identity) erro
if identity.Local != nil {
types = append(types, "local")
}
if identity.Basic != nil {
types = append(types, "basic")
}
sort.Strings(types)
if len(types) == 0 {
types = append(types, "unknown")
Expand Down
15 changes: 15 additions & 0 deletions internals/daemon/access.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,18 @@ func (ac UserAccess) CheckAccess(d *Daemon, r *http.Request, user *UserState) Re
// An identity explicitly set to "access: untrusted" isn't allowed.
return Unauthorized(accessDenied)
}

// MetricsAccess allows requests over the HTTP from authenticated users.
type MetricsAccess struct{}

func (ac MetricsAccess) CheckAccess(d *Daemon, r *http.Request, user *UserState) Response {
if user == nil {
return Unauthorized(accessDenied)
}
switch user.Access {
case state.MetricsAccess, state.AdminAccess:
return nil
}
// An identity explicitly set to "access: untrusted" isn't allowed.
return Unauthorized(accessDenied)
}
22 changes: 14 additions & 8 deletions internals/daemon/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,22 +147,27 @@ const (
accessForbidden
)

func userFromRequest(st *state.State, r *http.Request, ucred *Ucrednet) (*UserState, error) {
if ucred == nil {
// No ucred details, no UserState. Currently, "local" (ucred-based) is
// the only type of identity we support.
return nil, nil
func userFromRequest(st *state.State, r *http.Request, ucred *Ucrednet, username, password string) (*UserState, error) {
var userID *uint32
if ucred != nil {
userID = &ucred.Uid
}

st.Lock()
identity := st.IdentityFromInputs(&ucred.Uid)
identity := st.IdentityFromInputs(userID, username, password)
st.Unlock()

if identity == nil {
// No identity that matches these inputs (for now, just UID).
return nil, nil
}
return &UserState{Access: identity.Access, UID: &ucred.Uid}, nil

if identity.Local != nil {
return &UserState{Access: identity.Access, UID: userID}, nil
} else if identity.Basic != nil {
return &UserState{Access: identity.Access}, nil
}
return nil, nil
}

func (d *Daemon) Overlord() *overlord.Overlord {
Expand Down Expand Up @@ -213,7 +218,8 @@ func (c *Command) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// not good: https://github.com/canonical/pebble/pull/369
var user *UserState
if _, isOpen := access.(OpenAccess); !isOpen {
user, err = userFromRequest(c.d.state, r, ucred)
basicAuthUsername, basicAuthPassword, _ := r.BasicAuth()
user, err = userFromRequest(c.d.state, r, ucred, basicAuthUsername, basicAuthPassword)
if err != nil {
Forbidden("forbidden").ServeHTTP(w, r)
return
Expand Down
66 changes: 57 additions & 9 deletions internals/overlord/state/identities.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@ import (
"encoding/json"
"errors"
"fmt"
"regexp"
"sort"
"strings"

"github.com/GehirnInc/crypt/sha512_crypt"
)

// Identity holds the configuration of a single identity.
Expand All @@ -28,8 +31,9 @@ type Identity struct {
Access IdentityAccess

// One or more of the following type-specific configuration fields must be
// non-nil (currently the only type is "local").
// non-nil (currently the only types are "local" and "basic").
Local *LocalIdentity
Basic *BasicIdentity
}

// IdentityAccess defines the access level for an identity.
Expand All @@ -38,6 +42,7 @@ type IdentityAccess string
const (
AdminAccess IdentityAccess = "admin"
ReadAccess IdentityAccess = "read"
MetricsAccess IdentityAccess = "metrics"
UntrustedAccess IdentityAccess = "untrusted"
)

Expand All @@ -47,27 +52,46 @@ type LocalIdentity struct {
UserID uint32
}

// BasicIdentity holds identity configuration specific to the "basic" type
// (for HTTP basic authentication).
type BasicIdentity struct {
Password string // Note: In a real application, store a password hash, not the plaintext password.
}

// validate checks that the identity is valid, returning an error if not.
func (d *Identity) validate() error {
if d == nil {
return errors.New("identity must not be nil")
}

if d.Name != "" {
invalidChars := regexp.MustCompile(`[^a-zA-Z0-9_\-]`)
if invalidChars.MatchString(d.Name) {
return fmt.Errorf("identity name %q contains invalid characters (only alphanumeric, underscore, and hyphen allowed)", d.Name)
}
}

switch d.Access {
case AdminAccess, ReadAccess, UntrustedAccess:
case AdminAccess, ReadAccess, MetricsAccess, UntrustedAccess:
case "":
return fmt.Errorf("access value must be specified (%q, %q, or %q)",
AdminAccess, ReadAccess, UntrustedAccess)
return fmt.Errorf("access value must be specified (%q, %q, %q, or %q)",
AdminAccess, ReadAccess, MetricsAccess, UntrustedAccess)
default:
return fmt.Errorf("invalid access value %q, must be %q, %q, or %q",
d.Access, AdminAccess, ReadAccess, UntrustedAccess)
return fmt.Errorf("invalid access value %q, must be %q, %q, %q, or %q",
d.Access, AdminAccess, ReadAccess, MetricsAccess, UntrustedAccess)
}

switch {
case d.Local != nil:
return nil
case d.Basic != nil:
if d.Basic.Password == "" {
return errors.New("basic identity must specify password")
}

return nil
default:
return errors.New(`identity must have at least one type ("local")`)
return errors.New(`identity must have at least one type ("local" or "basic")`)
}
}

Expand All @@ -77,17 +101,27 @@ func (d *Identity) validate() error {
type apiIdentity struct {
Access string `json:"access"`
Local *apiLocalIdentity `json:"local,omitempty"`
Basic *apiBasicIdentity `json:"basic,omitempty"`
}

type apiLocalIdentity struct {
UserID *uint32 `json:"user-id"`
}

type apiBasicIdentity struct {
Password string `json:"password"`
}

// IMPORTANT NOTE: be sure to exclude secrets when adding to this!
func (d *Identity) MarshalJSON() ([]byte, error) {
ai := apiIdentity{
Access: string(d.Access),
Local: &apiLocalIdentity{UserID: &d.Local.UserID},
}
if d.Local != nil {
ai.Local = &apiLocalIdentity{UserID: &d.Local.UserID}
}
if d.Basic != nil {
ai.Basic = &apiBasicIdentity{Password: d.Basic.Password}
}
return json.Marshal(ai)
}
Expand All @@ -108,6 +142,11 @@ func (d *Identity) UnmarshalJSON(data []byte) error {
return errors.New("local identity must specify user-id")
}
identity.Local = &LocalIdentity{UserID: *ai.Local.UserID}
case ai.Basic != nil:
if ai.Basic.Password == "" {
return errors.New("basic identity must specify password")
}
identity.Basic = &BasicIdentity{Password: ai.Basic.Password}
}
// Perform additional validation using the local Identity type.
err = identity.validate()
Expand All @@ -130,6 +169,9 @@ func (s *State) AddIdentities(identities map[string]*Identity) error {
if _, ok := s.identities[name]; ok {
existing = append(existing, name)
}
if identity != nil {
identity.Name = name
}
err := identity.validate()
if err != nil {
return fmt.Errorf("identity %q invalid: %w", name, err)
Expand Down Expand Up @@ -266,7 +308,7 @@ func (s *State) Identities() map[string]*Identity {

// IdentityFromInputs returns an identity with the given inputs, or nil
// if there is none.
func (s *State) IdentityFromInputs(userID *uint32) *Identity {
func (s *State) IdentityFromInputs(userID *uint32, username, password string) *Identity {
s.reading()

for _, identity := range s.identities {
Expand All @@ -275,6 +317,12 @@ func (s *State) IdentityFromInputs(userID *uint32) *Identity {
if identity.Local.UserID == *userID {
return identity
}
case identity.Basic != nil && username != "" && identity.Name == username && password != "":
crypt := sha512_crypt.New()
err := crypt.Verify(identity.Basic.Password, []byte(password))
if err == nil {
return identity
}
}
}
return nil
Expand Down
Loading
Loading