...

Source file src/golang.conradwood.net/go-easyops/ctx/context_builder.go

Documentation: golang.conradwood.net/go-easyops/ctx

     1  /*
     2  Package ctx contains methods to build authenticated contexts and retrieve information of them.
     3  Package ctx is a "leaf" package - it is imported from many other goeasyops packages but does not import (many) other goeasyops packages.
     4  
     5  This package supports the following usecases:
     6  
     7  * Create a new context with an authenticated user from a service (e.g. a web-proxy)
     8  
     9  * Create a new context with a user and no service (e.g. a commandline)
    10  
    11  * Create a new context from a service without a user (e.g. a service triggering a gRPC periodically)
    12  
    13  * Update a context service (unary inbound gRPC interceptor)
    14  
    15  Furthermore, go-easyops in general will parse "latest" context version and "latest-1" context versions. That is so that functions such as auth.User(context) return the right thing wether or not called from a service that has been updated or from a service that is not yet on latest. The context version it generates is selected via cmdline switches.
    16  
    17  The context returned is ready to be used for outbound calls as-is.
    18  The context also includes a "value" which is only available locally (does not cross gRPC boundaries) but is used to provide information from the context.
    19  
    20  Definition of CallingService: the LocalValue contains the service who called us. The context metadata contains this service definition (which in then is transmitted to downstream services)
    21  
    22  Contexts are transformed on each RPC. typically this goes like this:
    23  
    24    - Context is created ( via ContextBuilder() or authremote.Context() ). This context has a localstate and OUTBOUND metadata.
    25  
    26    - Client calls an RPC
    27  
    28    - Server transforms inbound context into a new context and adds itself as callingservice ( ctx.inbound2outbound() ). The inbound context has no localstate and INBOUND metadata. The new context has a localstate and OUTBOUND metadata.
    29  
    30    - Server becomes client, calls another rpc..
    31  */
    32  package ctx
    33  
    34  import (
    35  	"bytes"
    36  	"context"
    37  	"encoding/base64"
    38  	"fmt"
    39  
    40  	"golang.conradwood.net/apis/auth"
    41  	ge "golang.conradwood.net/apis/goeasyops"
    42  	"golang.conradwood.net/go-easyops/cmdline"
    43  	"golang.conradwood.net/go-easyops/common"
    44  	"golang.conradwood.net/go-easyops/ctx/ctxv2"
    45  	"golang.conradwood.net/go-easyops/ctx/shared"
    46  	"golang.conradwood.net/go-easyops/utils"
    47  
    48  	//	"golang.yacloud.eu/apis/session"
    49  	"strings"
    50  	"time"
    51  
    52  	"google.golang.org/grpc/metadata"
    53  )
    54  
    55  const (
    56  	SER_PREFIX_STR = "CTX_SER_STRING"
    57  )
    58  
    59  var (
    60  	SER_PREFIX_BYT = []byte("CTX_SER_BYTE")
    61  )
    62  
    63  // get a new contextbuilder
    64  func NewContextBuilder() shared.ContextBuilder {
    65  	i := cmdline.GetContextBuilderVersion()
    66  	if i == 1 {
    67  		panic("obsolete codepath")
    68  	} else if i == 2 {
    69  		return ctxv2.NewContextBuilder()
    70  	} else {
    71  		// hm....
    72  		return ctxv2.NewContextBuilder()
    73  	}
    74  }
    75  
    76  // return "localstate" from a context. This is never "nil", but it is not guaranteed that the LocalState interface actually resolves details
    77  func GetLocalState(ctx context.Context) shared.LocalState {
    78  	return shared.GetLocalState(ctx)
    79  }
    80  
    81  // returns all "known" contextbuilders. we use this for received contexts to figure out which version it is
    82  func getAllContextBuilders() map[int]shared.ContextBuilder {
    83  	return map[int]shared.ContextBuilder{
    84  		2: ctxv2.NewContextBuilder(),
    85  	}
    86  }
    87  
    88  /*
    89  we receive a context from gRPC (e.g. in a unary interceptor). To use this context for outbound calls we need to copy the metadata, we also need to add a local callstate for the fancy_picker/balancer/dialer. This is what this function does.
    90  It is intented to convert any (supported) version of context into the current version of this package
    91  */
    92  func Inbound2Outbound(in_ctx context.Context, local_service *auth.SignedUser) context.Context {
    93  	for version, cb := range getAllContextBuilders() {
    94  		octx, found := cb.Inbound2Outbound(in_ctx, local_service)
    95  		if found {
    96  			svc := common.VerifySignedUser(local_service)
    97  			svs := "[none]"
    98  			if svc != nil {
    99  				svs = fmt.Sprintf("%s (%s)", svc.ID, svc.Email)
   100  			}
   101  			cmdline.DebugfContext("converted inbound (version=%d) to outbound context (me.service=%s)", version, svs)
   102  			cmdline.DebugfContext("New Context: %s", Context2String(octx))
   103  			ls := GetLocalState(octx)
   104  			if ls == nil || shared.IsEmptyLocalState(ls) {
   105  				utils.PrintStack("[go-easyops] no localstate for newly created context")
   106  				return nil
   107  			}
   108  			cmdline.DebugfContext("Localstate %s: %#v\n", ls.Info(), ls)
   109  			cmdline.DebugfContext("Localstate Detail:\n%s\n", shared.LocalState2string(ls))
   110  
   111  			return octx
   112  		}
   113  	}
   114  	cmdline.DebugfContext("[go-easyops] could not parse inbound context!")
   115  	return in_ctx
   116  }
   117  
   118  func add_context_to_builder(cb shared.ContextBuilder, ctx context.Context) {
   119  	ls := GetLocalState(ctx)
   120  	cb.WithCreatorService(ls.CreatorService())
   121  	if ls.Debug() {
   122  		cb.WithDebug()
   123  	}
   124  	cb.WithRequestID(ls.RequestID())
   125  	if ls.Trace() {
   126  		cb.WithTrace()
   127  	}
   128  	for _, e := range ls.Experiments() {
   129  		cb.EnableExperiment(e.Name)
   130  	}
   131  	cb.WithUser(ls.User())
   132  	cb.WithSession(ls.Session())
   133  }
   134  
   135  // md,source,version -> metadata source: 0=none,1=inbound,2=outbound
   136  func getMetadataFromContext(ctx context.Context) (string, int, int) {
   137  	source := 1
   138  	md, ex := metadata.FromIncomingContext(ctx)
   139  	if !ex {
   140  		source = 2
   141  		md, ex = metadata.FromOutgoingContext(ctx)
   142  		if !ex {
   143  			// no metadata at all
   144  			return "", 0, 0
   145  		}
   146  	}
   147  
   148  	mdas, fd := md[ctxv2.METANAME]
   149  	if fd {
   150  		if len(mdas) != 1 {
   151  			return "", source, 2
   152  		}
   153  		return mdas[0], source, 2
   154  	}
   155  
   156  	return "", 0, 0
   157  }
   158  func shortSessionText(ls shared.LocalState, maxlen int) string {
   159  	s := ls.Session()
   160  	if s == nil {
   161  		return "nosession"
   162  	}
   163  	sl := s.SessionID
   164  	if len(sl) > maxlen {
   165  		sl = sl[:maxlen]
   166  	}
   167  	return sl
   168  }
   169  
   170  func Context2DetailString(ctx context.Context) string {
   171  	return "TODO context_builder.go"
   172  }
   173  
   174  // for debugging purposes we can convert a context to a human readable string
   175  func Context2String(ctx context.Context) string {
   176  	md, src, version := getMetadataFromContext(ctx)
   177  
   178  	ls := GetLocalState(ctx)
   179  	if ls == nil || shared.IsEmptyLocalState(ls) {
   180  		return fmt.Sprintf("[no localstate] md[src=%d,version=%d]", src, version)
   181  	}
   182  	if ls.User() != nil || ls.CallingService() != nil {
   183  		sesstxt := shortSessionText(ls, 20)
   184  		return fmt.Sprintf("Localstate[userid=%s,callingservice=%s,session=%s] md[src=%d,version=%d]", shared.PrettyUser(ls.User()), shared.PrettyUser(ls.CallingService()), sesstxt, src, version)
   185  	}
   186  	if src == 0 {
   187  		return fmt.Sprintf("no localstate, no metadata (%v)\n", ctx)
   188  	}
   189  	if version == 2 {
   190  		res := &ge.InContext{}
   191  		err := utils.Unmarshal(md, res)
   192  		if err != nil {
   193  			return fmt.Sprintf("v2 %d metadata invalid (%s)", src, err)
   194  		}
   195  		return fmt.Sprintf("v2 (%d) metadata: %#v %#v\n,ls=[%s]", src, res.ImCtx, res.MCtx, shared.LocalState2string(ls))
   196  	} else if version == 1 {
   197  		panic("unsupported context version")
   198  	}
   199  	return fmt.Sprintf("Unsupported metadata version %d\n", version)
   200  
   201  }
   202  
   203  // check if 'buf' contains a context, serialised by the builder. a 'true' result implies that it can be deserialised from this package
   204  func IsSerialisedByBuilder(buf []byte) bool {
   205  	if len(buf) < 2 {
   206  		return false
   207  	}
   208  	if strings.HasPrefix(string(buf), SER_PREFIX_STR) {
   209  		// it was serialised by context_builder - as a string
   210  		return true
   211  	}
   212  	if bytes.HasPrefix(buf, SER_PREFIX_BYT) {
   213  		return true
   214  	}
   215  	/*
   216  		version := buf[0]
   217  		buf = buf[1:]
   218  		var b []byte
   219  		if version == 1 {
   220  			b = ctxv1.GetPrefix()
   221  		} else {
   222  			return false
   223  		}
   224  
   225  		if bytes.HasPrefix(buf, b) {
   226  			return true
   227  		}
   228  	*/
   229  	cmdline.DebugfContext("[go-easyops] Not a ctxbuilder context (%s)", utils.HexStr(buf))
   230  	//	cmdline.DebugfContext(ctx,"a: %s", utils.HexStr(b))
   231  	//	cmdline.DebugfContext(ctx,"b: %s", utils.HexStr(buf[:20]))
   232  	return false
   233  }
   234  
   235  // serialise a context to bunch of bytes
   236  func SerialiseContext(ctx context.Context) ([]byte, error) {
   237  	if !IsContextFromBuilder(ctx) {
   238  		utils.PrintStack("incompatible context")
   239  		return nil, fmt.Errorf("cannot serialise a context which was not built by builder")
   240  	}
   241  	version := byte(2) // to de-serialise later
   242  	b, err := ctxv2.Serialise(ctx)
   243  
   244  	if err != nil {
   245  		return nil, err
   246  	}
   247  	chk := shared.Checksum(b)
   248  	b = append([]byte{version, chk}, b...)
   249  	b = append(SER_PREFIX_BYT, b...)
   250  	return b, nil
   251  }
   252  
   253  // serialise a context to bunch of bytes
   254  func SerialiseContextToString(ctx context.Context) (string, error) {
   255  	b, err := SerialiseContext(ctx)
   256  	if err != nil {
   257  		return "", err
   258  	}
   259  	s := base64.StdEncoding.EncodeToString(b)
   260  	s = SER_PREFIX_STR + s
   261  	return s, nil
   262  }
   263  
   264  // this unmarshals a context from a string into a context. Short for DeserialiseContextFromStringWithTimeout()
   265  func DeserialiseContextFromString(s string) (context.Context, error) {
   266  	return DeserialiseContextFromStringWithTimeout(time.Duration(10)*time.Second, s)
   267  }
   268  
   269  // this unmarshals a context from a binary blob into a context. Short for DeserialiseContextWithTimeout()
   270  func DeserialiseContext(buf []byte) (context.Context, error) {
   271  	return DeserialiseContextWithTimeout(time.Duration(10)*time.Second, buf)
   272  }
   273  
   274  // this unmarshals a context from a string into a context
   275  func DeserialiseContextFromStringWithTimeout(t time.Duration, s string) (context.Context, error) {
   276  	if !strings.HasPrefix(s, SER_PREFIX_STR) {
   277  		return nil, fmt.Errorf("not a valid string to deserialise into a context")
   278  	}
   279  	s = strings.TrimPrefix(s, SER_PREFIX_STR)
   280  	userdata, err := base64.StdEncoding.DecodeString(s)
   281  	if err != nil {
   282  		return nil, err
   283  	}
   284  	return DeserialiseContextWithTimeout(t, userdata)
   285  }
   286  
   287  // this unmarshals a context from a binary blob into a context
   288  func DeserialiseContextWithTimeout(t time.Duration, buf []byte) (context.Context, error) {
   289  	if !IsSerialisedByBuilder(buf) {
   290  		panic("context not serialised by builder")
   291  	}
   292  	if len(buf) < 2 {
   293  		return nil, fmt.Errorf("invalid byte array to deserialise into a context")
   294  	}
   295  	cmdline.DebugfContext("Deserialising %s", utils.HexStr(buf))
   296  	tbuf := buf[len(SER_PREFIX_BYT):]
   297  	s := string(buf)
   298  	if strings.HasPrefix(s, SER_PREFIX_STR) {
   299  		// it's a string...
   300  		return DeserialiseContextFromStringWithTimeout(t, s)
   301  	}
   302  	if !bytes.HasPrefix(buf, SER_PREFIX_BYT) {
   303  		// it's not a byte
   304  		return nil, fmt.Errorf("context does not have ser_prefix_byt (%s)", utils.HexStr(buf))
   305  	}
   306  
   307  	version := tbuf[0]
   308  	chk := tbuf[1]
   309  	tbuf = tbuf[2:]
   310  	c := shared.Checksum(tbuf)
   311  	if c != chk {
   312  		cmdline.DebugfContext("ERROR IN CHECKSUM (%d vs %d)", c, chk)
   313  	}
   314  	cmdline.DebugfContext("deserialising from version %d\n", version)
   315  	var err error
   316  	var res context.Context
   317  	if version == 1 {
   318  		// trying to deser v1 as v2
   319  		res, err = ctxv2.DeserialiseContextWithTimeout(t, tbuf)
   320  	} else if version == 2 {
   321  		res, err = ctxv2.DeserialiseContextWithTimeout(t, tbuf)
   322  	} else {
   323  		cmdline.DebugfContext("a: %s", utils.HexStr(buf))
   324  		utils.PrintStack("incompatible version %d", version)
   325  		return nil, fmt.Errorf("(2) attempt to deserialise incompatible version (%d) to context", version)
   326  	}
   327  	if err != nil {
   328  		cmdline.DebugfContext("unable to create context (%s)\n", err)
   329  		return nil, err
   330  	}
   331  	cerr := res.Err()
   332  	if cerr != nil {
   333  		if cerr != nil {
   334  			fmt.Printf("[go-easyops] created faulty context\n")
   335  		}
   336  	}
   337  	cmdline.DebugfContext("Deserialised context: %s\n", Context2String(res))
   338  	return res, err
   339  }
   340  
   341  // returns true if this context was build by the builder
   342  func IsContextFromBuilder(ctx context.Context) bool {
   343  	if ctx.Value(shared.LOCALSTATENAME) != nil {
   344  		return true
   345  	}
   346  	return false
   347  }
   348  

View as plain text