github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/overlord/auth/auth.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2016-2019 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package auth
    21  
    22  import (
    23  	"context"
    24  	"crypto/rand"
    25  	"encoding/base64"
    26  	"errors"
    27  	"fmt"
    28  	"sort"
    29  	"strconv"
    30  
    31  	"gopkg.in/macaroon.v1"
    32  
    33  	"github.com/snapcore/snapd/overlord/state"
    34  )
    35  
    36  // AuthState represents current authenticated users as tracked in state
    37  type AuthState struct {
    38  	LastID      int          `json:"last-id"`
    39  	Users       []UserState  `json:"users"`
    40  	Device      *DeviceState `json:"device,omitempty"`
    41  	MacaroonKey []byte       `json:"macaroon-key,omitempty"`
    42  }
    43  
    44  // DeviceState represents the device's identity and store credentials
    45  type DeviceState struct {
    46  	// Brand refers to the brand-id
    47  	Brand  string `json:"brand,omitempty"`
    48  	Model  string `json:"model,omitempty"`
    49  	Serial string `json:"serial,omitempty"`
    50  
    51  	KeyID string `json:"key-id,omitempty"`
    52  
    53  	SessionMacaroon string `json:"session-macaroon,omitempty"`
    54  }
    55  
    56  // UserState represents an authenticated user
    57  type UserState struct {
    58  	ID              int      `json:"id"`
    59  	Username        string   `json:"username,omitempty"`
    60  	Email           string   `json:"email,omitempty"`
    61  	Macaroon        string   `json:"macaroon,omitempty"`
    62  	Discharges      []string `json:"discharges,omitempty"`
    63  	StoreMacaroon   string   `json:"store-macaroon,omitempty"`
    64  	StoreDischarges []string `json:"store-discharges,omitempty"`
    65  }
    66  
    67  // identificationOnly returns a *UserState with only the
    68  // identification information from u.
    69  func (u *UserState) identificationOnly() *UserState {
    70  	return &UserState{
    71  		ID:       u.ID,
    72  		Username: u.Username,
    73  		Email:    u.Email,
    74  	}
    75  }
    76  
    77  // HasStoreAuth returns true if the user has store authorization.
    78  func (u *UserState) HasStoreAuth() bool {
    79  	if u == nil {
    80  		return false
    81  	}
    82  	return u.StoreMacaroon != ""
    83  }
    84  
    85  // MacaroonSerialize returns a store-compatible serialized representation of the given macaroon
    86  func MacaroonSerialize(m *macaroon.Macaroon) (string, error) {
    87  	marshalled, err := m.MarshalBinary()
    88  	if err != nil {
    89  		return "", err
    90  	}
    91  	encoded := base64.RawURLEncoding.EncodeToString(marshalled)
    92  	return encoded, nil
    93  }
    94  
    95  // MacaroonDeserialize returns a deserialized macaroon from a given store-compatible serialization
    96  func MacaroonDeserialize(serializedMacaroon string) (*macaroon.Macaroon, error) {
    97  	var m macaroon.Macaroon
    98  	decoded, err := base64.RawURLEncoding.DecodeString(serializedMacaroon)
    99  	if err != nil {
   100  		return nil, err
   101  	}
   102  	err = m.UnmarshalBinary(decoded)
   103  	if err != nil {
   104  		return nil, err
   105  	}
   106  	return &m, nil
   107  }
   108  
   109  // generateMacaroonKey generates a random key to sign snapd macaroons
   110  func generateMacaroonKey() ([]byte, error) {
   111  	key := make([]byte, 32)
   112  	if _, err := rand.Read(key); err != nil {
   113  		return nil, err
   114  	}
   115  	return key, nil
   116  }
   117  
   118  const snapdMacaroonLocation = "snapd"
   119  
   120  // newUserMacaroon returns a snapd macaroon for the given username
   121  func newUserMacaroon(macaroonKey []byte, userID int) (string, error) {
   122  	userMacaroon, err := macaroon.New(macaroonKey, strconv.Itoa(userID), snapdMacaroonLocation)
   123  	if err != nil {
   124  		return "", fmt.Errorf("cannot create macaroon for snapd user: %s", err)
   125  	}
   126  
   127  	serializedMacaroon, err := MacaroonSerialize(userMacaroon)
   128  	if err != nil {
   129  		return "", fmt.Errorf("cannot serialize macaroon for snapd user: %s", err)
   130  	}
   131  
   132  	return serializedMacaroon, nil
   133  }
   134  
   135  // TODO: possibly move users' related functions to a userstate package
   136  
   137  // NewUser tracks a new authenticated user and saves its details in the state
   138  func NewUser(st *state.State, username, email, macaroon string, discharges []string) (*UserState, error) {
   139  	var authStateData AuthState
   140  
   141  	err := st.Get("auth", &authStateData)
   142  	if err == state.ErrNoState {
   143  		authStateData = AuthState{}
   144  	} else if err != nil {
   145  		return nil, err
   146  	}
   147  
   148  	if authStateData.MacaroonKey == nil {
   149  		authStateData.MacaroonKey, err = generateMacaroonKey()
   150  		if err != nil {
   151  			return nil, err
   152  		}
   153  	}
   154  
   155  	authStateData.LastID++
   156  
   157  	localMacaroon, err := newUserMacaroon(authStateData.MacaroonKey, authStateData.LastID)
   158  	if err != nil {
   159  		return nil, err
   160  	}
   161  
   162  	sort.Strings(discharges)
   163  	authenticatedUser := UserState{
   164  		ID:              authStateData.LastID,
   165  		Username:        username,
   166  		Email:           email,
   167  		Macaroon:        localMacaroon,
   168  		Discharges:      nil,
   169  		StoreMacaroon:   macaroon,
   170  		StoreDischarges: discharges,
   171  	}
   172  	authStateData.Users = append(authStateData.Users, authenticatedUser)
   173  
   174  	st.Set("auth", authStateData)
   175  
   176  	return &authenticatedUser, nil
   177  }
   178  
   179  var ErrInvalidUser = errors.New("invalid user")
   180  
   181  // RemoveUser removes a user from the state given its ID.
   182  func RemoveUser(st *state.State, userID int) (removed *UserState, err error) {
   183  	return removeUser(st, func(u *UserState) bool { return u.ID == userID })
   184  }
   185  
   186  // RemoveUserByUsername removes a user from the state given its username. Returns a *UserState with the identification information for them.
   187  func RemoveUserByUsername(st *state.State, username string) (removed *UserState, err error) {
   188  	return removeUser(st, func(u *UserState) bool { return u.Username == username })
   189  }
   190  
   191  // removeUser removes the first user matching given predicate.
   192  func removeUser(st *state.State, p func(*UserState) bool) (*UserState, error) {
   193  	var authStateData AuthState
   194  
   195  	err := st.Get("auth", &authStateData)
   196  	if err == state.ErrNoState {
   197  		return nil, ErrInvalidUser
   198  	}
   199  	if err != nil {
   200  		return nil, err
   201  	}
   202  
   203  	for i := range authStateData.Users {
   204  		u := &authStateData.Users[i]
   205  		if p(u) {
   206  			removed := u.identificationOnly()
   207  			// delete without preserving order
   208  			n := len(authStateData.Users) - 1
   209  			authStateData.Users[i] = authStateData.Users[n]
   210  			authStateData.Users[n] = UserState{}
   211  			authStateData.Users = authStateData.Users[:n]
   212  			st.Set("auth", authStateData)
   213  			return removed, nil
   214  		}
   215  	}
   216  
   217  	return nil, ErrInvalidUser
   218  }
   219  
   220  func Users(st *state.State) ([]*UserState, error) {
   221  	var authStateData AuthState
   222  
   223  	err := st.Get("auth", &authStateData)
   224  	if err == state.ErrNoState {
   225  		return nil, nil
   226  	}
   227  	if err != nil {
   228  		return nil, err
   229  	}
   230  
   231  	users := make([]*UserState, len(authStateData.Users))
   232  	for i := range authStateData.Users {
   233  		users[i] = &authStateData.Users[i]
   234  	}
   235  	return users, nil
   236  }
   237  
   238  // User returns a user from the state given its ID.
   239  func User(st *state.State, id int) (*UserState, error) {
   240  	return findUser(st, func(u *UserState) bool { return u.ID == id })
   241  }
   242  
   243  // UserByUsername returns a user from the state given its username.
   244  func UserByUsername(st *state.State, username string) (*UserState, error) {
   245  	return findUser(st, func(u *UserState) bool { return u.Username == username })
   246  }
   247  
   248  // findUser finds the first user matching given predicate.
   249  func findUser(st *state.State, p func(*UserState) bool) (*UserState, error) {
   250  	var authStateData AuthState
   251  
   252  	err := st.Get("auth", &authStateData)
   253  	if err == state.ErrNoState {
   254  		return nil, ErrInvalidUser
   255  	}
   256  	if err != nil {
   257  		return nil, err
   258  	}
   259  
   260  	for i := range authStateData.Users {
   261  		u := &authStateData.Users[i]
   262  		if p(u) {
   263  			return u, nil
   264  		}
   265  	}
   266  	return nil, ErrInvalidUser
   267  }
   268  
   269  // UpdateUser updates user in state
   270  func UpdateUser(st *state.State, user *UserState) error {
   271  	var authStateData AuthState
   272  
   273  	err := st.Get("auth", &authStateData)
   274  	if err == state.ErrNoState {
   275  		return ErrInvalidUser
   276  	}
   277  	if err != nil {
   278  		return err
   279  	}
   280  
   281  	for i := range authStateData.Users {
   282  		if authStateData.Users[i].ID == user.ID {
   283  			authStateData.Users[i] = *user
   284  			st.Set("auth", authStateData)
   285  			return nil
   286  		}
   287  	}
   288  
   289  	return ErrInvalidUser
   290  }
   291  
   292  var ErrInvalidAuth = fmt.Errorf("invalid authentication")
   293  
   294  // CheckMacaroon returns the UserState for the given macaroon/discharges credentials
   295  func CheckMacaroon(st *state.State, macaroon string, discharges []string) (*UserState, error) {
   296  	var authStateData AuthState
   297  	err := st.Get("auth", &authStateData)
   298  	if err != nil {
   299  		return nil, ErrInvalidAuth
   300  	}
   301  
   302  	snapdMacaroon, err := MacaroonDeserialize(macaroon)
   303  	if err != nil {
   304  		return nil, ErrInvalidAuth
   305  	}
   306  	// attempt snapd macaroon verification
   307  	if snapdMacaroon.Location() == snapdMacaroonLocation {
   308  		// no caveats to check so far
   309  		check := func(caveat string) error { return nil }
   310  		// ignoring discharges, unused for snapd macaroons atm
   311  		err = snapdMacaroon.Verify(authStateData.MacaroonKey, check, nil)
   312  		if err != nil {
   313  			return nil, ErrInvalidAuth
   314  		}
   315  		macaroonID := snapdMacaroon.Id()
   316  		userID, err := strconv.Atoi(macaroonID)
   317  		if err != nil {
   318  			return nil, ErrInvalidAuth
   319  		}
   320  		user, err := User(st, userID)
   321  		if err != nil {
   322  			return nil, ErrInvalidAuth
   323  		}
   324  		if macaroon != user.Macaroon {
   325  			return nil, ErrInvalidAuth
   326  		}
   327  		return user, nil
   328  	}
   329  
   330  	// if macaroon is not a snapd macaroon, fallback to previous token-style check
   331  NextUser:
   332  	for _, user := range authStateData.Users {
   333  		if user.Macaroon != macaroon {
   334  			continue
   335  		}
   336  		if len(user.Discharges) != len(discharges) {
   337  			continue
   338  		}
   339  		// sort discharges (stored users' discharges are already sorted)
   340  		sort.Strings(discharges)
   341  		for i, d := range user.Discharges {
   342  			if d != discharges[i] {
   343  				continue NextUser
   344  			}
   345  		}
   346  		return &user, nil
   347  	}
   348  	return nil, ErrInvalidAuth
   349  }
   350  
   351  // CloudInfo reflects cloud information for the system (as captured in the core configuration).
   352  type CloudInfo struct {
   353  	Name             string `json:"name"`
   354  	Region           string `json:"region,omitempty"`
   355  	AvailabilityZone string `json:"availability-zone,omitempty"`
   356  }
   357  
   358  type ensureContextKey struct{}
   359  
   360  // EnsureContextTODO returns a provisional context marked as
   361  // pertaining to an Ensure loop.
   362  // TODO: see Overlord.Loop to replace it with a proper context passed to all Ensures.
   363  func EnsureContextTODO() context.Context {
   364  	ctx := context.TODO()
   365  	return context.WithValue(ctx, ensureContextKey{}, struct{}{})
   366  }
   367  
   368  // IsEnsureContext returns whether context was marked as pertaining to an Ensure loop.
   369  func IsEnsureContext(ctx context.Context) bool {
   370  	return ctx.Value(ensureContextKey{}) != nil
   371  }