Skip to content

Commit

Permalink
chore: start adding role attribute path/jmes
Browse files Browse the repository at this point in the history
  • Loading branch information
markphelps committed Apr 30, 2024
1 parent 86f3b9b commit 73cdaf0
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 43 deletions.
29 changes: 16 additions & 13 deletions internal/config/authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,10 +302,11 @@ func (s StaticAuthenticationMethodInfo) SetCleanup(t *testing.T, c Authenticatio
// of a particular authentication method.
// i.e. the name and whether or not the method is session compatible.
type AuthenticationMethodInfo struct {
Method auth.Method
SessionCompatible bool
RequiresDatabase bool
Metadata *structpb.Struct
Method auth.Method
SessionCompatible bool
RequiresDatabase bool
SupportsAuthorization bool
Metadata *structpb.Struct
}

// Name returns the friendly lower-case name for the authentication method.
Expand Down Expand Up @@ -403,9 +404,10 @@ func (a AuthenticationMethodOIDCConfig) setDefaults(map[string]any) {}
// info describes properties of the authentication method "oidc".
func (a AuthenticationMethodOIDCConfig) info() AuthenticationMethodInfo {
info := AuthenticationMethodInfo{
Method: auth.Method_METHOD_OIDC,
SessionCompatible: true,
RequiresDatabase: true,
Method: auth.Method_METHOD_OIDC,
SessionCompatible: true,
SupportsAuthorization: true,
RequiresDatabase: true,
}

var (
Expand Down Expand Up @@ -441,12 +443,13 @@ func (a AuthenticationMethodOIDCConfig) validate() error {

// AuthenticationOIDCProvider configures provider credentials
type AuthenticationMethodOIDCProvider struct {
IssuerURL string `json:"issuerURL,omitempty" mapstructure:"issuer_url" yaml:"issuer_url,omitempty"`
ClientID string `json:"-,omitempty" mapstructure:"client_id" yaml:"-"`
ClientSecret string `json:"-" mapstructure:"client_secret" yaml:"-"`
RedirectAddress string `json:"redirectAddress,omitempty" mapstructure:"redirect_address" yaml:"redirect_address,omitempty"`
Scopes []string `json:"scopes,omitempty" mapstructure:"scopes" yaml:"scopes,omitempty"`
UsePKCE bool `json:"usePKCE,omitempty" mapstructure:"use_pkce" yaml:"use_pkce,omitempty"`
IssuerURL string `json:"issuerURL,omitempty" mapstructure:"issuer_url" yaml:"issuer_url,omitempty"`
ClientID string `json:"-,omitempty" mapstructure:"client_id" yaml:"-"`
ClientSecret string `json:"-" mapstructure:"client_secret" yaml:"-"`
RedirectAddress string `json:"redirectAddress,omitempty" mapstructure:"redirect_address" yaml:"redirect_address,omitempty"`
Scopes []string `json:"scopes,omitempty" mapstructure:"scopes" yaml:"scopes,omitempty"`
UsePKCE bool `json:"usePKCE,omitempty" mapstructure:"use_pkce" yaml:"use_pkce,omitempty"`
RoleAttributePath string `json:"roleAttributePath,omitempty" mapstructure:"role_attribute_path" yaml:"role_attribute_path,omitempty"`
}

func (a AuthenticationMethodOIDCProvider) validate() error {
Expand Down
3 changes: 3 additions & 0 deletions internal/server/authn/method/metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package method

const StorageMetadataRole = "io.flipt.auth.role"
47 changes: 37 additions & 10 deletions internal/server/authn/method/oidc/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,25 @@ func (s *Server) Callback(ctx context.Context, req *auth.CallbackRequest) (_ *au
storageMetadataOIDCProvider: req.Provider,
}

cfg, err := s.configFor(req.Provider)
if err != nil {
return nil, err
}

if cfg.RoleAttributePath != "" {
rawClaims := make(map[string]interface{})
if err := responseToken.IDToken().Claims(&rawClaims); err != nil {
return nil, err
}

role, err := method.SearchJSONForAttr[string](cfg.RoleAttributePath, rawClaims)
if err != nil {
return nil, err
}

metadata[method.StorageMetadataRole] = role
}

// Extract custom claims
var claims claims
if err := responseToken.IDToken().Claims(&claims); err != nil {
Expand Down Expand Up @@ -173,24 +192,32 @@ func callbackURL(host, provider string) string {
return host + "/auth/v1/method/oidc/" + provider + "/callback"
}

func (s *Server) configFor(provider string) (config.AuthenticationMethodOIDCProvider, error) {
providerCfg, ok := s.config.Methods.OIDC.Method.Providers[provider]
if !ok {
return config.AuthenticationMethodOIDCProvider{}, fmt.Errorf("requested provider %q: %w", provider, errProviderNotFound)
}

return providerCfg, nil
}

func (s *Server) providerFor(provider string, state string) (*capoidc.Provider, *capoidc.Req, error) {
var (
config *capoidc.Config
callback string
)

pConfig, ok := s.config.Methods.OIDC.Method.Providers[provider]
if !ok {
return nil, nil, fmt.Errorf("requested provider %q: %w", provider, errProviderNotFound)
providerCfg, err := s.configFor(provider)
if err != nil {
return nil, nil, err
}

callback = callbackURL(pConfig.RedirectAddress, provider)
callback = callbackURL(providerCfg.RedirectAddress, provider)

var err error
config, err = capoidc.NewConfig(
pConfig.IssuerURL,
pConfig.ClientID,
capoidc.ClientSecret(pConfig.ClientSecret),
providerCfg.IssuerURL,
providerCfg.ClientID,
capoidc.ClientSecret(providerCfg.ClientSecret),
[]capoidc.Alg{oidc.RS256},
[]string{callback},
)
Expand All @@ -205,11 +232,11 @@ func (s *Server) providerFor(provider string, state string) (*capoidc.Provider,

var oidcOpts = []capoidc.Option{
capoidc.WithState(state),
capoidc.WithScopes(pConfig.Scopes...),
capoidc.WithScopes(providerCfg.Scopes...),
capoidc.WithNonce("static"), // TODO(georgemac): dropping nonce for now
}

if pConfig.UsePKCE {
if providerCfg.UsePKCE {
oidcOpts = append(oidcOpts, capoidc.WithPKCE(PKCEVerifier))
}

Expand Down
47 changes: 47 additions & 0 deletions internal/server/authn/method/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ package method
import (
"context"

"encoding/json"
"fmt"

"github.com/jmespath/go-jmespath"
"go.flipt.io/flipt/errors"
"google.golang.org/grpc/metadata"
)
Expand All @@ -26,3 +30,46 @@ func CallbackValidateState(ctx context.Context, state string) error {

return nil
}

func SearchJSONForAttr[T ~string](attributePath string, data any) (T, error) {
v, err := searchJSONForAttr(attributePath, data)
if err != nil {
return "", err
}
return v.(T), nil
}

func searchJSONForAttr(attributePath string, data any) (any, error) {
if attributePath == "" {
return "", fmt.Errorf("attribute path: %q", attributePath)
}

if data == nil {
return "", fmt.Errorf("empty json, attribute path: %q", attributePath)
}

// Copy the data to a new variable
var jsonData = data

// If the data is a byte slice, try to unmarshal it into a JSON object
if dataBytes, ok := data.([]byte); ok {
// If the byte slice is empty, return an error
if len(dataBytes) == 0 {
return "", fmt.Errorf("empty json, attribute path: %q", attributePath)
}

// Try to unmarshal the byte slice
if err := json.Unmarshal(dataBytes, &jsonData); err != nil {
return "", fmt.Errorf("%v: %w", "failed to unmarshal user info JSON response", err)
}
}

// Search for the attribute in the JSON object
value, err := jmespath.Search(attributePath, jsonData)
if err != nil {
return "", fmt.Errorf("failed to search user info JSON response with provided path: %q: %w", attributePath, err)
}

// Return the value and nil error
return value, nil
}
26 changes: 6 additions & 20 deletions internal/server/middleware/grpc/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -465,27 +465,13 @@ func AuditEventUnaryInterceptor(logger *zap.Logger, eventPairChecker EventPairCh

// Delete request(s) have to be handled separately because they do not
// return the concrete type but rather an *empty.Empty response.
switch r := req.(type) {
case *flipt.DeleteFlagRequest:
event = audit.NewEvent(request, actor, r)
case *flipt.DeleteVariantRequest:
event = audit.NewEvent(request, actor, r)
case *flipt.DeleteSegmentRequest:
event = audit.NewEvent(request, actor, r)
case *flipt.DeleteDistributionRequest:
event = audit.NewEvent(request, actor, r)
case *flipt.DeleteConstraintRequest:
event = audit.NewEvent(request, actor, r)
case *flipt.DeleteNamespaceRequest:
event = audit.NewEvent(request, actor, r)
case *flipt.OrderRulesRequest:
event = audit.NewEvent(request, actor, r)
case *flipt.DeleteRuleRequest:
event = audit.NewEvent(request, actor, r)
case *flipt.OrderRolloutsRequest:
event = audit.NewEvent(request, actor, r)
case *flipt.DeleteRolloutRequest:
if request.Action == flipt.ActionDelete {
event = audit.NewEvent(request, actor, r)
} else {
switch r := resp.(type) {
case *flipt.OrderRulesRequest, *flipt.OrderRolloutsRequest:
event = audit.NewEvent(request, actor, r)
}
}

// Short circuiting the middleware here since we have a non-nil event from
Expand Down

0 comments on commit 73cdaf0

Please sign in to comment.