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

feat(auth): public list authentication methods endpoint #1240

Merged
merged 6 commits into from Dec 22, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion internal/cleanup/cleanup_test.go
Expand Up @@ -24,7 +24,7 @@ func TestCleanup(t *testing.T) {
lock = inmemoplock.New()
authConfig = config.AuthenticationConfig{
Methods: config.AuthenticationMethods{
Token: config.AuthenticationMethodTokenConfig{
Token: config.AuthenticationMethod[config.AuthenticationMethodTokenConfig]{
Enabled: true,
Cleanup: &config.AuthenticationCleanupSchedule{
Interval: time.Second,
Expand Down
54 changes: 27 additions & 27 deletions internal/cmd/auth.go
Expand Up @@ -14,6 +14,7 @@ import (
"go.flipt.io/flipt/internal/server/auth"
authoidc "go.flipt.io/flipt/internal/server/auth/method/oidc"
authtoken "go.flipt.io/flipt/internal/server/auth/method/token"
"go.flipt.io/flipt/internal/server/auth/public"
storageauth "go.flipt.io/flipt/internal/storage/auth"
storageoplock "go.flipt.io/flipt/internal/storage/oplock"
rpcauth "go.flipt.io/flipt/rpc/flipt/auth"
Expand All @@ -29,9 +30,14 @@ func authenticationGRPC(
oplock storageoplock.Service,
) (grpcRegisterers, []grpc.UnaryServerInterceptor, func(context.Context) error, error) {
var (
public = public.NewServer(logger, cfg)
register = grpcRegisterers{
public,
auth.NewServer(logger, store),
}
authOpts = []containers.Option[auth.InterceptorOptions]{
auth.WithServerSkipsAuthentication(public),
}
interceptors []grpc.UnaryServerInterceptor
shutdown = func(context.Context) error {
return nil
Expand All @@ -55,7 +61,6 @@ func authenticationGRPC(
logger.Debug("authentication method \"token\" server registered")
}

var authOpts []containers.Option[auth.InterceptorOptions]
// register auth method oidc service
if cfg.Methods.OIDC.Enabled {
oidcServer := authoidc.NewServer(logger, store, cfg)
Expand Down Expand Up @@ -96,52 +101,47 @@ func authenticationGRPC(
return register, interceptors, shutdown, nil
}

func registerFunc(ctx context.Context, conn *grpc.ClientConn, fn func(context.Context, *runtime.ServeMux, *grpc.ClientConn) error) runtime.ServeMuxOption {
return func(mux *runtime.ServeMux) {
if err := fn(ctx, mux, conn); err != nil {
panic(err)
}
}
}

func authenticationHTTPMount(
ctx context.Context,
cfg config.AuthenticationConfig,
r chi.Router,
conn *grpc.ClientConn,
) error {
) {
var (
muxOpts []runtime.ServeMuxOption
muxOpts = []runtime.ServeMuxOption{
registerFunc(ctx, conn, rpcauth.RegisterPublicAuthenticationServiceHandler),
registerFunc(ctx, conn, rpcauth.RegisterAuthenticationServiceHandler),
}
middleware = func(next http.Handler) http.Handler {
return next
}
)

// register OIDC middleware if method is enabled
if cfg.Methods.Token.Enabled {
muxOpts = append(muxOpts, registerFunc(ctx, conn, rpcauth.RegisterAuthenticationMethodTokenServiceHandler))
}

if cfg.Methods.OIDC.Enabled {
oidcmiddleware := authoidc.NewHTTPMiddleware(cfg.Session)

muxOpts = append(muxOpts,
runtime.WithMetadata(authoidc.ForwardCookies),
runtime.WithForwardResponseOption(oidcmiddleware.ForwardResponseOption))
middleware = oidcmiddleware.Handler
}
runtime.WithForwardResponseOption(oidcmiddleware.ForwardResponseOption),
registerFunc(ctx, conn, rpcauth.RegisterAuthenticationMethodOIDCServiceHandler))

mux := gateway.NewGatewayServeMux(muxOpts...)

if err := rpcauth.RegisterAuthenticationServiceHandler(ctx, mux, conn); err != nil {
return fmt.Errorf("registering auth grpc gateway: %w", err)
}

if cfg.Methods.Token.Enabled {
if err := rpcauth.RegisterAuthenticationMethodTokenServiceHandler(ctx, mux, conn); err != nil {
return fmt.Errorf("registering auth grpc gateway: %w", err)
}
}

if cfg.Methods.OIDC.Enabled {
if err := rpcauth.RegisterAuthenticationMethodOIDCServiceHandler(ctx, mux, conn); err != nil {
return fmt.Errorf("registering auth grpc gateway: %w", err)
}
middleware = oidcmiddleware.Handler
}

r.Group(func(r chi.Router) {
r.Use(middleware)

r.Mount("/auth/v1", mux)
r.Mount("/auth/v1", gateway.NewGatewayServeMux(muxOpts...))
})

return nil
}
6 changes: 3 additions & 3 deletions internal/cmd/http.go
Expand Up @@ -98,9 +98,9 @@ func NewHTTPServer(
r.Mount("/metrics", promhttp.Handler())
r.Mount("/api/v1", api)

if err := authenticationHTTPMount(ctx, cfg.Authentication, r, conn); err != nil {
return nil, err
}
// mount all authentication related HTTP components
// to the chi router.
authenticationHTTPMount(ctx, cfg.Authentication, r, conn)

r.Route("/meta", func(r chi.Router) {
r.Use(middleware.SetHeader("Content-Type", "application/json"))
Expand Down
135 changes: 99 additions & 36 deletions internal/config/authentication.go
Expand Up @@ -15,16 +15,20 @@ var (
)

func init() {
for method, v := range auth.Method_value {
if auth.Method(v) == auth.Method_METHOD_NONE {
for _, v := range auth.Method_value {
method := auth.Method(v)
if method == auth.Method_METHOD_NONE {
continue
}

name := strings.ToLower(strings.TrimPrefix(method, "METHOD_"))
stringToAuthMethod[name] = auth.Method(v)
stringToAuthMethod[methodName(method)] = method
}
}

func methodName(method auth.Method) string {
return strings.ToLower(strings.TrimPrefix(auth.Method_name[int32(method)], "METHOD_"))
}

// AuthenticationConfig configures Flipts authentication mechanisms
type AuthenticationConfig struct {
// Required designates whether authentication credentials are validated.
Expand All @@ -39,31 +43,31 @@ type AuthenticationConfig struct {
// ShouldRunCleanup returns true if the cleanup background process should be started.
// It returns true given at-least 1 method is enabled and it's associated schedule
// has been configured (non-nil).
func (c AuthenticationConfig) ShouldRunCleanup() bool {
return (c.Methods.Token.Enabled && c.Methods.Token.Cleanup != nil) ||
(c.Methods.OIDC.Enabled && c.Methods.OIDC.Cleanup != nil)
func (c AuthenticationConfig) ShouldRunCleanup() (shouldCleanup bool) {
for _, info := range c.Methods.AllMethods() {
shouldCleanup = shouldCleanup || (info.Enabled && info.Cleanup != nil)
}

return
}

func (c *AuthenticationConfig) setDefaults(v *viper.Viper) {
methods := map[string]any{
"token": nil,
"oidc": nil,
}
methods := map[string]any{}

// set default for each methods
for k := range methods {
for _, info := range c.Methods.AllMethods() {
method := map[string]any{"enabled": false}
// if the method has been enabled then set the defaults
// for its cleanup strategy
prefix := fmt.Sprintf("authentication.methods.%s", k)
prefix := fmt.Sprintf("authentication.methods.%s", info.Name())
if v.GetBool(prefix + ".enabled") {
method["cleanup"] = map[string]any{
"interval": time.Hour,
"grace_period": 30 * time.Minute,
}
}

methods[k] = method
methods[info.Name()] = method
}

v.SetDefault("authentication", map[string]any{
Expand All @@ -77,32 +81,27 @@ func (c *AuthenticationConfig) setDefaults(v *viper.Viper) {
}

func (c *AuthenticationConfig) validate() error {
for _, cleanup := range []struct {
name string
schedule *AuthenticationCleanupSchedule
}{
// add additional schedules as token methods are created
{"token", c.Methods.Token.Cleanup},
{"oidc", c.Methods.OIDC.Cleanup},
} {
if cleanup.schedule == nil {
var sessionEnabled bool
for _, info := range c.Methods.AllMethods() {
sessionEnabled = sessionEnabled || (info.Enabled && info.SessionCompatible)
if info.Cleanup == nil {
continue
}

field := "authentication.method" + cleanup.name
if cleanup.schedule.Interval <= 0 {
field := "authentication.method" + info.Name()
if info.Cleanup.Interval <= 0 {
return errFieldWrap(field+".cleanup.interval", errPositiveNonZeroDuration)
}

if cleanup.schedule.GracePeriod <= 0 {
if info.Cleanup.GracePeriod <= 0 {
return errFieldWrap(field+".cleanup.grace_period", errPositiveNonZeroDuration)
}
}

// ensure that when a session compatible authentication method has been
// enabled that the session cookie domain has been configured with a non
// empty value.
if c.Methods.OIDC.Enabled {
if sessionEnabled {
if c.Session.Domain == "" {
err := errFieldWrap("authentication.session.domain", errValidationRequired)
return fmt.Errorf("when session compatible auth method enabled: %w", err)
Expand All @@ -129,27 +128,91 @@ type AuthenticationSession struct {
// AuthenticationMethods is a set of configuration for each authentication
// method available for use within Flipt.
type AuthenticationMethods struct {
Token AuthenticationMethodTokenConfig `json:"token,omitempty" mapstructure:"token"`
OIDC AuthenticationMethodOIDCConfig `json:"oidc,omitempty" mapstructure:"oidc"`
Token AuthenticationMethod[AuthenticationMethodTokenConfig] `json:"token,omitempty" mapstructure:"token"`
OIDC AuthenticationMethod[AuthenticationMethodOIDCConfig] `json:"oidc,omitempty" mapstructure:"oidc"`
}

// AllMethods returns all the AuthenticationMethod instances available.
func (a AuthenticationMethods) AllMethods() []StaticAuthenticationMethodInfo {
return []StaticAuthenticationMethodInfo{
a.Token.Info(),
a.OIDC.Info(),
}
}

// StaticAuthenticationMethodInfo embeds an AuthenticationMethodInfo alongside
// the other properties of an AuthenticationMethod.
type StaticAuthenticationMethodInfo struct {
AuthenticationMethodInfo
Enabled bool
Cleanup *AuthenticationCleanupSchedule
}

// AuthenticationMethodInfo is a structure which describes properties
// 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
}

// Name returns the friendly lower-case name for the authentication method.
func (a AuthenticationMethodInfo) Name() string {
return methodName(a.Method)
}

// AuthenticationMethodInfoProvider is a type with a single method Info
// which returns an AuthenticationMethodInfo describing the underlying
// methods properties.
type AuthenticationMethodInfoProvider interface {
Info() AuthenticationMethodInfo
}

// AuthenticationMethod is a container for authentication methods.
// It describes the common properties of all authentication methods.
// Along with leaving a generic slot for the particular method to declare
// its own structural fields. This generic field (Method) must implement
// the AuthenticationMethodInfoProvider to be valid at compile time.
type AuthenticationMethod[C AuthenticationMethodInfoProvider] struct {
Method C `mapstructure:",squash"`
Enabled bool `json:"enabled,omitempty" mapstructure:"enabled"`
Cleanup *AuthenticationCleanupSchedule `json:"cleanup,omitempty" mapstructure:"cleanup"`
}

func (a AuthenticationMethod[C]) Info() StaticAuthenticationMethodInfo {
return StaticAuthenticationMethodInfo{
AuthenticationMethodInfo: a.Method.Info(),
Enabled: a.Enabled,
Cleanup: a.Cleanup,
}
}

// AuthenticationMethodTokenConfig contains fields used to configure the authentication
// method "token".
// This authentication method supports the ability to create static tokens via the
// /auth/v1/method/token prefix of endpoints.
type AuthenticationMethodTokenConfig struct {
// Enabled designates whether or not static token authentication is enabled
// and whether Flipt will mount the "token" method APIs.
Enabled bool `json:"enabled,omitempty" mapstructure:"enabled"`
Cleanup *AuthenticationCleanupSchedule `json:"cleanup,omitempty" mapstructure:"cleanup"`
type AuthenticationMethodTokenConfig struct{}

// Info describes properties of the authentication method "token".
func (a AuthenticationMethodTokenConfig) Info() AuthenticationMethodInfo {
return AuthenticationMethodInfo{
Method: auth.Method_METHOD_TOKEN,
SessionCompatible: false,
}
}

// AuthenticationMethodOIDCConfig configures the OIDC authentication method.
// This method can be used to establish browser based sessions.
type AuthenticationMethodOIDCConfig struct {
Enabled bool `json:"enabled,omitempty" mapstructure:"enabled"`
Providers map[string]AuthenticationMethodOIDCProvider `json:"providers,omitempty" mapstructure:"providers"`
Cleanup *AuthenticationCleanupSchedule `json:"cleanup,omitempty" mapstructure:"cleanup"`
}

// Info describes properties of the authentication method "oidc".
func (a AuthenticationMethodOIDCConfig) Info() AuthenticationMethodInfo {
return AuthenticationMethodInfo{
Method: auth.Method_METHOD_OIDC,
SessionCompatible: true,
}
}

// AuthenticationOIDCProvider configures provider credentials
Expand Down
24 changes: 18 additions & 6 deletions internal/config/config.go
Expand Up @@ -104,8 +104,12 @@ func Load(path string) (*Result, error) {
// search for all expected env vars since Viper cannot
// infer when doing Unmarshal + AutomaticEnv.
// see: https://github.com/spf13/viper/issues/761
structField := val.Type().Field(i)
bindEnvVars(v, getFliptEnvs(), []string{fieldKey(structField)}, structField.Type)
var (
structField = val.Type().Field(i)
key = fieldKey(structField)
)

bindEnvVars(v, getFliptEnvs(), []string{key}, structField.Type)

field := val.Field(i).Addr().Interface()
f(field)
Expand Down Expand Up @@ -150,9 +154,15 @@ type deprecator interface {
deprecations(v *viper.Viper) []deprecation
}

// fieldKey returns the name to be used when deriving a fields env var key.
// If marked as squash the key will be the empty string.
// Otherwise, it is derived from the lowercase name of the field.
func fieldKey(field reflect.StructField) string {
if tag := field.Tag.Get("mapstructure"); tag != "" {
return tag
tag, attr, ok := strings.Cut(tag, ",")
if !ok || attr == "squash" {
return tag
}
}

return strings.ToLower(field.Name)
Expand All @@ -174,14 +184,16 @@ func bindEnvVars(v envBinder, env, prefixes []string, typ reflect.Type) {
case reflect.Map:
// recurse into bindEnvVars while signifying that the last
// key was unbound using the wildcard "*".
bindEnvVars(v, env, append(prefixes, "*"), typ.Elem())
bindEnvVars(v, env, append(prefixes, wildcard), typ.Elem())

return
case reflect.Struct:
for i := 0; i < typ.NumField(); i++ {
structField := typ.Field(i)
var (
structField = typ.Field(i)
key = fieldKey(structField)
)

key := fieldKey(structField)
bind(env, prefixes, key, func(prefixes []string) {
bindEnvVars(v, env, prefixes, structField.Type)
})
Expand Down