github.com/david-imola/snapd@v0.0.0-20210611180407-2de8ddeece6d/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 }