gitee.com/mysnapcore/mysnapd@v0.1.0/store/tooling/auth.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2014-2022 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 tooling
    21  
    22  import (
    23  	"bytes"
    24  	"encoding/base64"
    25  	"encoding/json"
    26  	"fmt"
    27  	"io/ioutil"
    28  	"net/http"
    29  	"os"
    30  
    31  	"gitee.com/mysnapcore/mysnapd/overlord/auth"
    32  	"gitee.com/mysnapcore/mysnapd/snapdenv"
    33  	"gitee.com/mysnapcore/mysnapd/store"
    34  	"github.com/mvo5/goconfigparser"
    35  )
    36  
    37  type authData struct {
    38  	// Simple
    39  	Scheme string
    40  	Value  string
    41  
    42  	// U1/SSO
    43  	Macaroon   string
    44  	Discharges []string
    45  }
    46  
    47  type parseAuthFunc func(data []byte, what string) (parsed *authData, likely bool, err error)
    48  
    49  func getAuthorizer() (store.Authorizer, error) {
    50  	var data []byte
    51  	var what string
    52  	parsers := []parseAuthFunc{parseAuthBase64JSON, parseAuthJSON, parseSnapcraftLoginFile}
    53  	if envStr := os.Getenv("UBUNTU_STORE_AUTH"); envStr != "" {
    54  		data = []byte(envStr)
    55  		what = "credentials from UBUNTU_STORE_AUTH"
    56  		parsers = []parseAuthFunc{parseAuthBase64JSON}
    57  	} else {
    58  		authFn := os.Getenv("UBUNTU_STORE_AUTH_DATA_FILENAME")
    59  		if authFn == "" {
    60  			return nil, nil
    61  		}
    62  
    63  		var err error
    64  		data, err = ioutil.ReadFile(authFn)
    65  		if err != nil {
    66  			return nil, fmt.Errorf("cannot read auth file %q: %v", authFn, err)
    67  		}
    68  		data = bytes.TrimSpace(data)
    69  		what = fmt.Sprintf("file %q", authFn)
    70  	}
    71  	if len(data) == 0 {
    72  		return nil, fmt.Errorf("invalid auth %s: empty", what)
    73  	}
    74  
    75  	creds, err := parseAuthData(data, what, parsers...)
    76  	if err != nil {
    77  		return nil, err
    78  	}
    79  
    80  	if creds.Scheme != "" {
    81  		return &SimpleCreds{
    82  			Scheme: creds.Scheme,
    83  			Value:  creds.Value,
    84  		}, nil
    85  	}
    86  
    87  	return &UbuntuOneCreds{User: auth.UserState{
    88  		StoreMacaroon:   creds.Macaroon,
    89  		StoreDischarges: creds.Discharges,
    90  	}}, nil
    91  }
    92  
    93  func parseAuthData(data []byte, what string, parsers ...parseAuthFunc) (*authData, error) {
    94  	var firstErr error
    95  	for _, p := range parsers {
    96  		parsed, likely, err := p(data, what)
    97  		if err == nil {
    98  			return parsed, nil
    99  		}
   100  		if likely && firstErr == nil {
   101  			firstErr = err
   102  		}
   103  	}
   104  	if firstErr == nil {
   105  		return nil, fmt.Errorf("invalid auth %s: not a recognizable format", what)
   106  	}
   107  	return nil, firstErr
   108  }
   109  
   110  func parseAuthJSON(data []byte, what string) (*authData, bool, error) {
   111  	var creds struct {
   112  		Macaroon   string   `json:"macaroon"`
   113  		Discharges []string `json:"discharges"`
   114  	}
   115  	err := json.Unmarshal(data, &creds)
   116  	if err != nil {
   117  		likely := data[0] == '{'
   118  		return nil, likely, fmt.Errorf("cannot decode auth %s: %v", what, err)
   119  	}
   120  	if creds.Macaroon == "" || len(creds.Discharges) == 0 {
   121  		return nil, true, fmt.Errorf("invalid auth %s: missing fields", what)
   122  	}
   123  	return &authData{
   124  		Macaroon:   creds.Macaroon,
   125  		Discharges: creds.Discharges,
   126  	}, false, nil
   127  }
   128  
   129  func snapcraftLoginSection() string {
   130  	if snapdenv.UseStagingStore() {
   131  		return "login.staging.ubuntu.com"
   132  	}
   133  	return "login.ubuntu.com"
   134  }
   135  
   136  // parseSnapcraftLoginFile parses the content of snapcraft <v7 exported
   137  // login credentials files.
   138  func parseSnapcraftLoginFile(data []byte, what string) (*authData, bool, error) {
   139  	errPrefix := fmt.Sprintf("invalid snapcraft login %s", what)
   140  
   141  	cfg := goconfigparser.New()
   142  	// XXX this seems to almost always succeed
   143  	if err := cfg.ReadString(string(data)); err != nil {
   144  		likely := data[0] == '['
   145  		return nil, likely, fmt.Errorf("%s: %v", errPrefix, err)
   146  	}
   147  	sec := snapcraftLoginSection()
   148  	macaroon, err := cfg.Get(sec, "macaroon")
   149  	if err != nil {
   150  		return nil, true, fmt.Errorf("%s: %s", errPrefix, err)
   151  	}
   152  	unboundDischarge, err := cfg.Get(sec, "unbound_discharge")
   153  	if err != nil {
   154  		return nil, true, fmt.Errorf("%s: %v", errPrefix, err)
   155  	}
   156  	if macaroon == "" || unboundDischarge == "" {
   157  		return nil, true, fmt.Errorf("%s: empty fields", errPrefix)
   158  	}
   159  	return &authData{
   160  		Macaroon:   macaroon,
   161  		Discharges: []string{unboundDischarge},
   162  	}, false, nil
   163  }
   164  
   165  // parseAuthBase64JSON parses snapcraft v7+ base64-encoded auth credential data.
   166  func parseAuthBase64JSON(data []byte, what string) (*authData, bool, error) {
   167  	jsonData := make([]byte, base64.StdEncoding.DecodedLen(len(data)))
   168  	dataLen, err := base64.StdEncoding.Decode(jsonData, data)
   169  	if err != nil {
   170  		return nil, false, fmt.Errorf("cannot decode base64-encoded auth %s: %v", what, err)
   171  	}
   172  	var m map[string]interface{}
   173  	if err := json.Unmarshal(jsonData[:dataLen], &m); err != nil {
   174  		return nil, true, fmt.Errorf("cannot unmarshal base64-decoded auth %s: %v", what, err)
   175  	}
   176  	r, _ := m["r"].(string)
   177  	d, _ := m["d"].(string)
   178  	t, _ := m["t"].(string)
   179  	switch {
   180  	case t == "u1-macaroon":
   181  		v, _ := m["v"].(map[string]interface{})
   182  		r, _ = v["r"].(string)
   183  		d, _ = v["d"].(string)
   184  		if r == "" || d == "" {
   185  			break
   186  		}
   187  		fallthrough
   188  	case r != "" && d != "":
   189  		return &authData{
   190  			Macaroon:   r,
   191  			Discharges: []string{d},
   192  		}, false, nil
   193  	case t == "macaroon":
   194  		v, _ := m["v"].(string)
   195  		if v != "" {
   196  			return &authData{
   197  				Scheme: "Macaroon",
   198  				Value:  v,
   199  			}, false, nil
   200  		}
   201  	case t == "bearer":
   202  		v, _ := m["v"].(string)
   203  		if v != "" {
   204  			return &authData{
   205  				Scheme: "Bearer",
   206  				Value:  v,
   207  			}, false, nil
   208  		}
   209  	}
   210  	return nil, true, fmt.Errorf("cannot recognize unmarshalled base64-decoded auth %s: no known field combination set", what)
   211  }
   212  
   213  // UbuntuOneCreds can authorize requests using the implicitly carried
   214  // SSO/U1 user credentials.
   215  type UbuntuOneCreds struct {
   216  	User auth.UserState
   217  }
   218  
   219  // expected interfaces
   220  var _ store.Authorizer = (*UbuntuOneCreds)(nil)
   221  var _ store.RefreshingAuthorizer = (*UbuntuOneCreds)(nil)
   222  
   223  func (c *UbuntuOneCreds) Authorize(r *http.Request, _ store.DeviceAndAuthContext, _ *auth.UserState, _ *store.AuthorizeOptions) error {
   224  	return store.UserAuthorizer{}.Authorize(r, nil, &c.User, nil)
   225  }
   226  
   227  func (c *UbuntuOneCreds) CanAuthorizeForUser(_ *auth.UserState) bool {
   228  	// UbuntuOneCreds carries a UserState with auth data by construction
   229  	// so we can authorize using that
   230  	return true
   231  }
   232  
   233  func (c *UbuntuOneCreds) RefreshAuth(_ store.AuthRefreshNeed, _ store.DeviceAndAuthContext, user *auth.UserState, client *http.Client) error {
   234  	return store.UserAuthorizer{}.RefreshUser(&c.User, c, client)
   235  }
   236  
   237  func (c *UbuntuOneCreds) UpdateUserAuth(user *auth.UserState, discharges []string) (*auth.UserState, error) {
   238  	user.StoreDischarges = discharges
   239  	return user, nil
   240  }
   241  
   242  // SimpleCreds can authorize requests using simply scheme/auth value.
   243  type SimpleCreds struct {
   244  	Scheme string
   245  	Value  string
   246  }
   247  
   248  func (c *SimpleCreds) Authorize(r *http.Request, _ store.DeviceAndAuthContext, user *auth.UserState, _ *store.AuthorizeOptions) error {
   249  	r.Header.Set("Authorization", fmt.Sprintf("%s %s", c.Scheme, c.Value))
   250  	return nil
   251  }
   252  
   253  func (c *SimpleCreds) CanAuthorizeForUser(_ *auth.UserState) bool {
   254  	// SimpleCreds can authorize with the implicit auth data it carries
   255  	// on behalf of the user they were generated for
   256  	return true
   257  }