ContainerImage.Pinniped/internal/idtransform/identity_transformations.go

105 lines
4.0 KiB
Go
Raw Normal View History

// 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
}