From 608540a2b36fb549813e72129d130740ac07e3e3 Mon Sep 17 00:00:00 2001 From: b5 Date: Wed, 3 Mar 2021 23:27:26 -0500 Subject: [PATCH] feat(lib): attach active user to scope --- auth/token/token.go | 5 ++--- lib/dispatch.go | 26 +++++++++++++++++++++++--- lib/http.go | 6 +++++- lib/logbook.qfb | Bin 0 -> 420 bytes lib/scope.go | 38 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 lib/logbook.qfb diff --git a/auth/token/token.go b/auth/token/token.go index d95d68cf4..d3718db1e 100644 --- a/auth/token/token.go +++ b/auth/token/token.go @@ -38,8 +38,7 @@ type Token = jwt.Token // Claims is a JWT Claims object type Claims struct { *jwt.StandardClaims - // TODO(b5): this needs to be replaced with a profileID - Username string `json:"username"` + ProfileID string `json:"profileID"` } // Parse will parse, validate and return a token @@ -203,7 +202,7 @@ func (a *pkSource) CreateToken(pro *profile.Profile, ttl time.Duration) (string, // see http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-20#section-4.1.4 ExpiresAt: exp, }, - Username: pro.Peername, + ProfileID: pro.ID.String(), } // Creat token string diff --git a/lib/dispatch.go b/lib/dispatch.go index 6e3a6e351..63104affe 100644 --- a/lib/dispatch.go +++ b/lib/dispatch.go @@ -5,6 +5,10 @@ import ( "fmt" "reflect" "strings" + "time" + + "github.com/qri-io/qri/auth/token" + "github.com/qri-io/qri/profile" ) // dispatcher isolates the dispatch method @@ -35,6 +39,22 @@ func (inst *Instance) Dispatch(ctx context.Context, method string, param interfa // If the http rpc layer is engaged, use it to dispatch methods // This happens when another process is running `qri connect` if inst.http != nil { + if tok := token.FromCtx(ctx); tok == "" { + // If no token exists, create one from configured profile private key & + // add it to the request context + // TODO(b5): we're falling back to the configured user to make requests, + // is this the right default? + p, err := profile.NewProfile(inst.cfg.Profile) + if err != nil { + return nil, err + } + tokstr, err := token.NewPrivKeyAuthToken(p.PrivKey, time.Minute) + if err != nil { + return nil, err + } + ctx = token.AddToContext(ctx, tokstr) + } + if c, ok := inst.regMethods.lookup(method); ok { // TODO(dustmop): This is always using the "POST" verb currently. We need some // mechanism of tagging methods as being read-only and "GET"-able. Once that @@ -62,9 +82,9 @@ func (inst *Instance) Dispatch(ctx context.Context, method string, param interfa // or use copy-on-write semantics, so that one method running at the same time as // another cannot modify the out-of-scope data of the other. This will mostly // involve making copies of the right things - scope := scope{ - ctx: ctx, - inst: inst, + scope, err := newScope(ctx, inst) + if err != nil { + return nil, err } // Construct the parameter list for the function call, then call it diff --git a/lib/http.go b/lib/http.go index 40290581e..2b7e04227 100644 --- a/lib/http.go +++ b/lib/http.go @@ -11,6 +11,7 @@ import ( ma "github.com/multiformats/go-multiaddr" manet "github.com/multiformats/go-multiaddr/net" apiutil "github.com/qri-io/qri/api/util" + "github.com/qri-io/qri/auth/token" ) const jsonMimeType = "application/json" @@ -72,7 +73,6 @@ func (c HTTPClient) CallMethod(ctx context.Context, apiEndpoint APIEndpoint, htt // TODO(arqu): work out mimeType configuration/override per API endpoint mimeType := jsonMimeType addr := fmt.Sprintf("%s://%s%s", c.Protocol, c.Address, apiEndpoint) - // TODO(arqu): inject context values into headers return c.do(ctx, addr, httpMethod, mimeType, params, result, false) } @@ -112,6 +112,10 @@ func (c HTTPClient) do(ctx context.Context, addr string, httpMethod string, mime req.Header.Set("Content-Type", mimeType) req.Header.Set("Accept", mimeType) + if s := token.FromCtx(ctx); s != "" { + req.Header.Set("authorization", fmt.Sprintf("Bearer %s", s)) + } + res, err := http.DefaultClient.Do(req) if err != nil { return err diff --git a/lib/logbook.qfb b/lib/logbook.qfb new file mode 100644 index 0000000000000000000000000000000000000000..ef0a481153cb517591b191a72d809f51e5e652de GIT binary patch literal 420 zcmV;V0bBlLv9rN92-;vY+Hy*ylax<0Df8;8iIkp5N>@DY2b}$@u4m`y-8a`GjI*2Y zR&soDM_t5lzXk_f9MMfaa93}tD1Y}wFP1n)vH&FNr0ZT{vD;V||F z=D{YcbF4tsiNaD(JJcMkKxx)6f`XP?536RugtOcp0e`w25w~&C?_r^@;BK(iU}NSq zBiD*>xoyb46cwUGQHw7~Ci8kelS6N^c79_HX0j5H3)!XLuno=Ji(Hk^G`NI9co8wW zDa*WO3MYsyW=c(0LO)i4-Q^j$Ifn-BMz%*nUIK89ZQ}AQtf~3K^aRDgd89ldFo9(P z6`o>k=fN|?k>MNxmY`uF&IysifErWgFcO;wy1CGJiGXRnG4B4}zm?K+>%O%-4vjgG zXyGF9wu*-P;M#Z2Rp31AS4@(f-(pE>iYn2o>a=&q|x}|fEjKWb@cMXT*)&tW3Sp|dF OaGBf1Ux}@9WCI0#6wZ$T literal 0 HcmV?d00001 diff --git a/lib/scope.go b/lib/scope.go index 2b368306a..3c712a796 100644 --- a/lib/scope.go +++ b/lib/scope.go @@ -5,6 +5,7 @@ import ( "github.com/qri-io/dataset" "github.com/qri-io/qfs/muxfs" + "github.com/qri-io/qri/auth/token" "github.com/qri-io/qri/dscache" "github.com/qri-io/qri/dsref" "github.com/qri-io/qri/event" @@ -21,9 +22,46 @@ import ( type scope struct { ctx context.Context inst *Instance + pro *profile.Profile // TODO(dustmop): Additional information, such as user identity, their profile, keys } +func newScope(ctx context.Context, inst *Instance) (scope, error) { + var pro *profile.Profile + // TODO(b5): shouldn't need this nil checking + if inst != nil && inst.profiles != nil { + pro = inst.profiles.Owner() + if tokenString := token.FromCtx(ctx); tokenString != "" { + tok, err := token.ParseAuthToken(tokenString, inst.keystore) + if err != nil { + return scope{}, err + } + + if claims, ok := tok.Claims.(*token.Claims); ok { + // TODO(b5): at this point we have a valid signature of a profileID string + // but no proof that this profile is owned by the key that signed the + // token. We either need ProfileID == KeyID, or we need a UCAN. we need to + // check for those, ideally in a method within the profile package that + // abstracts over profile & key agreement + pro, err = inst.profiles.GetProfile(profile.IDB58DecodeOrEmpty(claims.ProfileID)) + if err != nil { + return scope{}, err + } + } + } + } + + return scope{ + ctx: ctx, + inst: inst, + pro: pro, + }, nil +} + +func (s *scope) ActiveProfile() *profile.Profile { + return s.pro +} + // Context returns the context for this scope. Though this pattern is usually discouraged, // we're following http.Request's lead, as scope plays the same role. The lifetime of a // single scope matches the lifetime of the Context; this ownership is not long-lived