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 }