// Copyright 2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // Package idtransform defines upstream-to-downstream identity transformations which could be // implemented using various approaches or languages. package idtransform import ( "context" "fmt" "sort" "strings" "k8s.io/apimachinery/pkg/util/sets" ) // TransformationResult is the result of evaluating a transformation against some inputs. type TransformationResult struct { Username string // the new username for an allowed auth Groups []string // the new group names for an allowed auth AuthenticationAllowed bool // when false, disallow this authentication attempt RejectedAuthenticationMessage string // should be set when AuthenticationAllowed is false } // IdentityTransformation is an individual identity transformation which can be evaluated. type IdentityTransformation interface { Evaluate(ctx context.Context, username string, groups []string) (*TransformationResult, error) // Source returns some representation of the original source code of the transformation, which is // useful for tests to be able to check that a compiled transformation came from the right source. Source() interface{} } // TransformationPipeline is a list of identity transforms, which can be evaluated in order against some given input // values. type TransformationPipeline struct { transforms []IdentityTransformation } // NewTransformationPipeline creates an empty TransformationPipeline. func NewTransformationPipeline() *TransformationPipeline { return &TransformationPipeline{transforms: []IdentityTransformation{}} } // AppendTransformation adds a transformation to the end of the list of transformations for this pipeline. // This is not thread-safe, so be sure to add all transformations from a single goroutine before using Evaluate // from multiple goroutines. func (p *TransformationPipeline) AppendTransformation(t IdentityTransformation) { p.transforms = append(p.transforms, t) } // Evaluate runs the transformation pipeline for a given input identity. It returns a potentially transformed or // rejected identity, or an error. If any transformation in the list rejects the authentication, then the list is // short-circuited but no error is returned. Only unexpected errors are returned as errors. This is safe to call // from multiple goroutines. func (p *TransformationPipeline) Evaluate(ctx context.Context, username string, groups []string) (*TransformationResult, error) { if groups == nil { groups = []string{} } accumulatedResult := &TransformationResult{ Username: username, Groups: groups, AuthenticationAllowed: true, } for i, transform := range p.transforms { var err error accumulatedResult, err = transform.Evaluate(ctx, accumulatedResult.Username, accumulatedResult.Groups) if err != nil { // There was an unexpected error evaluating a transformation. return nil, fmt.Errorf("identity transformation at index %d: %w", i, err) } if !accumulatedResult.AuthenticationAllowed { // Auth has been rejected by a policy. Stop evaluating the rest of the transformations. return accumulatedResult, nil } if strings.TrimSpace(accumulatedResult.Username) == "" { return nil, fmt.Errorf("identity transformation returned an empty username, which is not allowed") } if accumulatedResult.Groups == nil { return nil, fmt.Errorf("identity transformation returned a null list of groups, which is not allowed") } } accumulatedResult.Groups = sortAndUniq(accumulatedResult.Groups) // There were no unexpected errors and no policy which rejected auth. return accumulatedResult, nil } func (p *TransformationPipeline) Source() []interface{} { result := []interface{}{} for _, transform := range p.transforms { result = append(result, transform.Source()) } return result } func sortAndUniq(s []string) []string { unique := sets.New(s...).UnsortedList() sort.Strings(unique) return unique }