github.com/rigado/snapd@v2.42.5-go-mod+incompatible/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  // HasStoreAuth returns true if the user has store authorization.
    68  func (u *UserState) HasStoreAuth() bool {
    69  	if u == nil {
    70  		return false
    71  	}
    72  	return u.StoreMacaroon != ""
    73  }
    74  
    75  // MacaroonSerialize returns a store-compatible serialized representation of the given macaroon
    76  func MacaroonSerialize(m *macaroon.Macaroon) (string, error) {
    77  	marshalled, err := m.MarshalBinary()
    78  	if err != nil {
    79  		return "", err
    80  	}
    81  	encoded := base64.RawURLEncoding.EncodeToString(marshalled)
    82  	return encoded, nil
    83  }
    84  
    85  // MacaroonDeserialize returns a deserialized macaroon from a given store-compatible serialization
    86  func MacaroonDeserialize(serializedMacaroon string) (*macaroon.Macaroon, error) {
    87  	var m macaroon.Macaroon
    88  	decoded, err := base64.RawURLEncoding.DecodeString(serializedMacaroon)
    89  	if err != nil {
    90  		return nil, err
    91  	}
    92  	err = m.UnmarshalBinary(decoded)
    93  	if err != nil {
    94  		return nil, err
    95  	}
    96  	return &m, nil
    97  }
    98  
    99  // generateMacaroonKey generates a random key to sign snapd macaroons
   100  func generateMacaroonKey() ([]byte, error) {
   101  	key := make([]byte, 32)
   102  	if _, err := rand.Read(key); err != nil {
   103  		return nil, err
   104  	}
   105  	return key, nil
   106  }
   107  
   108  const snapdMacaroonLocation = "snapd"
   109  
   110  // newUserMacaroon returns a snapd macaroon for the given username
   111  func newUserMacaroon(macaroonKey []byte, userID int) (string, error) {
   112  	userMacaroon, err := macaroon.New(macaroonKey, strconv.Itoa(userID), snapdMacaroonLocation)
   113  	if err != nil {
   114  		return "", fmt.Errorf("cannot create macaroon for snapd user: %s", err)
   115  	}
   116  
   117  	serializedMacaroon, err := MacaroonSerialize(userMacaroon)
   118  	if err != nil {
   119  		return "", fmt.Errorf("cannot serialize macaroon for snapd user: %s", err)
   120  	}
   121  
   122  	return serializedMacaroon, nil
   123  }
   124  
   125  // TODO: possibly move users' related functions to a userstate package
   126  
   127  // NewUser tracks a new authenticated user and saves its details in the state
   128  func NewUser(st *state.State, username, email, macaroon string, discharges []string) (*UserState, error) {
   129  	var authStateData AuthState
   130  
   131  	err := st.Get("auth", &authStateData)
   132  	if err == state.ErrNoState {
   133  		authStateData = AuthState{}
   134  	} else if err != nil {
   135  		return nil, err
   136  	}
   137  
   138  	if authStateData.MacaroonKey == nil {
   139  		authStateData.MacaroonKey, err = generateMacaroonKey()
   140  		if err != nil {
   141  			return nil, err
   142  		}
   143  	}
   144  
   145  	authStateData.LastID++
   146  
   147  	localMacaroon, err := newUserMacaroon(authStateData.MacaroonKey, authStateData.LastID)
   148  	if err != nil {
   149  		return nil, err
   150  	}
   151  
   152  	sort.Strings(discharges)
   153  	authenticatedUser := UserState{
   154  		ID:              authStateData.LastID,
   155  		Username:        username,
   156  		Email:           email,
   157  		Macaroon:        localMacaroon,
   158  		Discharges:      nil,
   159  		StoreMacaroon:   macaroon,
   160  		StoreDischarges: discharges,
   161  	}
   162  	authStateData.Users = append(authStateData.Users, authenticatedUser)
   163  
   164  	st.Set("auth", authStateData)
   165  
   166  	return &authenticatedUser, nil
   167  }
   168  
   169  var ErrInvalidUser = errors.New("invalid user")
   170  
   171  // RemoveUser removes a user from the state given its ID
   172  func RemoveUser(st *state.State, userID int) error {
   173  	var authStateData AuthState
   174  
   175  	err := st.Get("auth", &authStateData)
   176  	if err == state.ErrNoState {
   177  		return ErrInvalidUser
   178  	}
   179  	if err != nil {
   180  		return err
   181  	}
   182  
   183  	for i := range authStateData.Users {
   184  		if authStateData.Users[i].ID == userID {
   185  			// delete without preserving order
   186  			n := len(authStateData.Users) - 1
   187  			authStateData.Users[i] = authStateData.Users[n]
   188  			authStateData.Users[n] = UserState{}
   189  			authStateData.Users = authStateData.Users[:n]
   190  			st.Set("auth", authStateData)
   191  			return nil
   192  		}
   193  	}
   194  
   195  	return ErrInvalidUser
   196  }
   197  
   198  func Users(st *state.State) ([]*UserState, error) {
   199  	var authStateData AuthState
   200  
   201  	err := st.Get("auth", &authStateData)
   202  	if err == state.ErrNoState {
   203  		return nil, nil
   204  	}
   205  	if err != nil {
   206  		return nil, err
   207  	}
   208  
   209  	users := make([]*UserState, len(authStateData.Users))
   210  	for i := range authStateData.Users {
   211  		users[i] = &authStateData.Users[i]
   212  	}
   213  	return users, nil
   214  }
   215  
   216  // User returns a user from the state given its ID
   217  func User(st *state.State, id int) (*UserState, error) {
   218  	var authStateData AuthState
   219  
   220  	err := st.Get("auth", &authStateData)
   221  	if err == state.ErrNoState {
   222  		return nil, ErrInvalidUser
   223  	}
   224  	if err != nil {
   225  		return nil, err
   226  	}
   227  
   228  	for _, user := range authStateData.Users {
   229  		if user.ID == id {
   230  			return &user, nil
   231  		}
   232  	}
   233  	return nil, ErrInvalidUser
   234  }
   235  
   236  // UpdateUser updates user in state
   237  func UpdateUser(st *state.State, user *UserState) error {
   238  	var authStateData AuthState
   239  
   240  	err := st.Get("auth", &authStateData)
   241  	if err == state.ErrNoState {
   242  		return ErrInvalidUser
   243  	}
   244  	if err != nil {
   245  		return err
   246  	}
   247  
   248  	for i := range authStateData.Users {
   249  		if authStateData.Users[i].ID == user.ID {
   250  			authStateData.Users[i] = *user
   251  			st.Set("auth", authStateData)
   252  			return nil
   253  		}
   254  	}
   255  
   256  	return ErrInvalidUser
   257  }
   258  
   259  var ErrInvalidAuth = fmt.Errorf("invalid authentication")
   260  
   261  // CheckMacaroon returns the UserState for the given macaroon/discharges credentials
   262  func CheckMacaroon(st *state.State, macaroon string, discharges []string) (*UserState, error) {
   263  	var authStateData AuthState
   264  	err := st.Get("auth", &authStateData)
   265  	if err != nil {
   266  		return nil, ErrInvalidAuth
   267  	}
   268  
   269  	snapdMacaroon, err := MacaroonDeserialize(macaroon)
   270  	if err != nil {
   271  		return nil, ErrInvalidAuth
   272  	}
   273  	// attempt snapd macaroon verification
   274  	if snapdMacaroon.Location() == snapdMacaroonLocation {
   275  		// no caveats to check so far
   276  		check := func(caveat string) error { return nil }
   277  		// ignoring discharges, unused for snapd macaroons atm
   278  		err = snapdMacaroon.Verify(authStateData.MacaroonKey, check, nil)
   279  		if err != nil {
   280  			return nil, ErrInvalidAuth
   281  		}
   282  		macaroonID := snapdMacaroon.Id()
   283  		userID, err := strconv.Atoi(macaroonID)
   284  		if err != nil {
   285  			return nil, ErrInvalidAuth
   286  		}
   287  		user, err := User(st, userID)
   288  		if err != nil {
   289  			return nil, ErrInvalidAuth
   290  		}
   291  		if macaroon != user.Macaroon {
   292  			return nil, ErrInvalidAuth
   293  		}
   294  		return user, nil
   295  	}
   296  
   297  	// if macaroon is not a snapd macaroon, fallback to previous token-style check
   298  NextUser:
   299  	for _, user := range authStateData.Users {
   300  		if user.Macaroon != macaroon {
   301  			continue
   302  		}
   303  		if len(user.Discharges) != len(discharges) {
   304  			continue
   305  		}
   306  		// sort discharges (stored users' discharges are already sorted)
   307  		sort.Strings(discharges)
   308  		for i, d := range user.Discharges {
   309  			if d != discharges[i] {
   310  				continue NextUser
   311  			}
   312  		}
   313  		return &user, nil
   314  	}
   315  	return nil, ErrInvalidAuth
   316  }
   317  
   318  // CloudInfo reflects cloud information for the system (as captured in the core configuration).
   319  type CloudInfo struct {
   320  	Name             string `json:"name"`
   321  	Region           string `json:"region,omitempty"`
   322  	AvailabilityZone string `json:"availability-zone,omitempty"`
   323  }
   324  
   325  type ensureContextKey struct{}
   326  
   327  // EnsureContextTODO returns a provisional context marked as
   328  // pertaining to an Ensure loop.
   329  // TODO: see Overlord.Loop to replace it with a proper context passed to all Ensures.
   330  func EnsureContextTODO() context.Context {
   331  	ctx := context.TODO()
   332  	return context.WithValue(ctx, ensureContextKey{}, struct{}{})
   333  }
   334  
   335  // IsEnsureContext returns whether context was marked as pertaining to an Ensure loop.
   336  func IsEnsureContext(ctx context.Context) bool {
   337  	return ctx.Value(ensureContextKey{}) != nil
   338  }