Skip to content

Commit

Permalink
Merge pull request #289 from xmidt-org/feature/token-wrapping
Browse files Browse the repository at this point in the history
Feature/token wrapping
  • Loading branch information
johnabass authored Aug 28, 2024
2 parents bd3f926 + 69196cf commit af4b32b
Show file tree
Hide file tree
Showing 4 changed files with 465 additions and 1 deletion.
57 changes: 57 additions & 0 deletions authenticator_examples_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC
// SPDX-License-Identifier: Apache-2.0

package bascule

import (
"context"
"fmt"
)

type Extra struct {
Name string
Age int
}

func (e Extra) Principal() string { return e.Name }

// ExampleJoinTokens_augment shows how to augment a Token as part
// of authentication workflow.
func ExampleJoinTokens_augment() {
original := StubToken("original")
authenticator, _ := NewAuthenticator[string](
WithTokenParsers(
StubTokenParser[string]{
Token: original,
},
),
WithValidators(
AsValidator[string](
func(t Token) (Token, error) {
// augment this token with extra information
return JoinTokens(t, Extra{Name: "extra", Age: 33}), nil
},
),
),
)

authenticated, _ := authenticator.Authenticate(
context.Background(),
"source",
)

fmt.Println("authenticated principal:", authenticated.Principal())

var extra Extra
if !TokenAs(authenticated, &extra) {
panic("token cannot be converted")
}

fmt.Println("extra.Name:", extra.Name)
fmt.Println("extra.Age:", extra.Age)

// Output:
// authenticated principal: original
// extra.Name: extra
// extra.Age: 33
}
28 changes: 28 additions & 0 deletions mocks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,34 @@ func (m *mockTokenWithCapabilities) ExpectCapabilities(caps ...string) *mock.Cal
return m.On("Capabilities").Return(caps)
}

type mockTokenUnwrapOne struct {
mockToken
}

func (m *mockTokenUnwrapOne) Unwrap() Token {
args := m.Called()
t, _ := args.Get(0).(Token)
return t
}

func (m *mockTokenUnwrapOne) ExpectUnwrap(t Token) *mock.Call {
return m.On("Unwrap").Return(t)
}

type mockTokenUnwrapMany struct {
mockToken
}

func (m *mockTokenUnwrapMany) Unwrap() []Token {
args := m.Called()
t, _ := args.Get(0).([]Token)
return t
}

func (m *mockTokenUnwrapMany) ExpectUnwrap(t ...Token) *mock.Call {
return m.On("Unwrap").Return(t)
}

type mockValidator[S any] struct {
mock.Mock
}
Expand Down
143 changes: 142 additions & 1 deletion token.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,154 @@ var (
)

// Token is a runtime representation of credentials. This interface will be further
// customized by infrastructure.
// customized by infrastructure. A Token may have subtokens and may provide access
// to an arbitrary tree of subtokens by supplying either an 'Unwrap() Token' or
// an 'Unwrap() []Token' method. Subtokens are not required to have the same principal.
type Token interface {
// Principal is the security subject of this token, e.g. the user name or other
// user identifier.
Principal() string
}

// MultiToken is an aggregate Token that is the root of a subtree of Tokens.
type MultiToken []Token

// Principal returns the principal for the first token in this set, or
// the empty string if this set is empty.
func (mt MultiToken) Principal() string {
if len(mt) > 0 {
return mt[0].Principal()
}

return ""
}

// Unwrap provides access to this token's children.
func (mt MultiToken) Unwrap() []Token {
return []Token(mt)
}

// JoinTokens joins multiple tokens into one. Any nil tokens are discarded.
// The principal of the returned token will always be the principal of the
// first non-nil token supplied to this function.
//
// If there is only (1) non-nil token, that token is returned as is. Otherwise,
// no attempt is made to flatten the set of tokens. If there are multiple non-nil
// tokens, the returned token will have an 'Unwrap() []Token' method to access
// the joined tokens individually.
//
// If no non-nil tokens are passed to this function, it returns nil.
func JoinTokens(tokens ...Token) Token {
if len(tokens) == 0 {
return nil
}

mt := make(MultiToken, 0, len(tokens))
for _, t := range tokens {
if t != nil {
mt = append(mt, t)
}
}

switch len(mt) {
case 0:
return nil

case 1:
return mt[0]

default:
return mt
}
}

// UnwrapToken does the opposite of JoinTokens.
//
// If the supplied token provides an 'Unwrap() Token' method, and that
// method returns a non-nil Token, the returned slice contains only that Token.
//
// If the supplied token provides an 'Unwrap() []Token' method, the
// result of that method is returned.
//
// Otherwise, this function returns nil.
func UnwrapToken(t Token) []Token {
switch u := t.(type) {
case interface{ Unwrap() Token }:
uu := u.Unwrap()
if uu != nil {
return []Token{uu}
}

case interface{ Unwrap() []Token }:
return u.Unwrap()
}

return nil
}

var tokenType = reflect.TypeOf((*Token)(nil)).Elem()

// tokenTargetValue produces a reflect value to set and the required type that
// a token must be convertible to. This function panics in all the same cases
// as errors.As.
func tokenTarget[T any](target *T) (targetValue reflect.Value, targetType reflect.Type) {
if target == nil {
panic("bascule: token target must be a non-nil pointer")
}

targetValue = reflect.ValueOf(target)
targetType = targetValue.Type().Elem()
if targetType.Kind() != reflect.Interface && !targetType.Implements(tokenType) {
panic("bascule: *target must be an interface or implement Token")
}

return
}

// tokenAs is a recursive function that checks the Token tree to see if
// it can do a coversion to the targetType. targetValue will hold the
// result of the conversion.
func tokenAs(t Token, targetValue reflect.Value, targetType reflect.Type) bool {
if reflect.TypeOf(t).AssignableTo(targetType) {
targetValue.Elem().Set(reflect.ValueOf(t))
return true
}

switch u := t.(type) {
case interface{ Unwrap() Token }:
t = u.Unwrap()
if t != nil {
return tokenAs(t, targetValue, targetType)
}

case interface{ Unwrap() []Token }:
for _, t := range u.Unwrap() {
if t != nil && tokenAs(t, targetValue, targetType) {
return true
}
}
}

return false
}

// TokenAs attempts to coerce the given Token into an arbitrary target. This function
// is similar to errors.As. If target is nil, this function panics. If target is neither
// an interface or a concrete implementation of the Token interface, this function
// also panics.
//
// The Token's tree is examined depth-first beginning with the given token and
// preceding down. If a token is found that is convertible to T, then target is set
// to that token and this function returns true. Otherwise, this function returns false.
func TokenAs[T any](t Token, target *T) bool {
if t == nil {
return false
}

targetValue, targetType := tokenTarget(target)
return tokenAs(t, targetValue, targetType)
}

// TokenParser produces tokens from a source. The original source S of the credentials
// are made available to the parser.
type TokenParser[S any] interface {
Expand Down
Loading

0 comments on commit af4b32b

Please sign in to comment.