Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/token wrapping #289

Merged
merged 2 commits into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading