Skip to content

Commit

Permalink
Authentication processor 2/4 - Add auth context and interface (#1808)
Browse files Browse the repository at this point in the history
* Authentication processor 1/4 - Add configauth

Signed-off-by: Juraci Paixão Kröhling <juraci@kroehling.de>

* Authentication processor 2/4 - Add auth context and interface

Signed-off-by: Juraci Paixão Kröhling <juraci@kroehling.de>
  • Loading branch information
jpkrohling committed Sep 24, 2020
1 parent 4f06a68 commit 430c002
Show file tree
Hide file tree
Showing 4 changed files with 423 additions and 0 deletions.
95 changes: 95 additions & 0 deletions internal/auth/authenticator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package auth

import (
"context"
"errors"
"io"

"google.golang.org/grpc"
"google.golang.org/grpc/metadata"

"go.opentelemetry.io/collector/config/configauth"
)

var (
errNoOIDCProvided = errors.New("no OIDC information provided")
errMetadataNotFound = errors.New("no request metadata found")
errNotImplemented = errors.New("not implemented")
defaultAttribute = "authorization"
)

// Authenticator will authenticate the incoming request/RPC
type Authenticator interface {
io.Closer

// Authenticate checks whether the given context contains valid auth data. Successfully authenticated calls will always return a nil error and a context with the auth data.
Authenticate(context.Context, map[string][]string) (context.Context, error)

// Start will
Start(context.Context) error

// UnaryInterceptor is a helper method to provide a gRPC-compatible UnaryInterceptor, typically calling the authenticator's Authenticate method.
UnaryInterceptor(context.Context, interface{}, *grpc.UnaryServerInfo, grpc.UnaryHandler) (interface{}, error)

// StreamInterceptor is a helper method to provide a gRPC-compatible StreamInterceptor, typically calling the authenticator's Authenticate method.
StreamInterceptor(interface{}, grpc.ServerStream, *grpc.StreamServerInfo, grpc.StreamHandler) error
}

type authenticateFunc func(context.Context, map[string][]string) (context.Context, error)

// New creates an authenticator based on the given configuration
func New(cfg configauth.Authentication) (Authenticator, error) {
if cfg.OIDC == nil {
return nil, errNoOIDCProvided
}

if len(cfg.Attribute) == 0 {
cfg.Attribute = defaultAttribute
}

return nil, errNotImplemented
}

func defaultUnaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, authenticate authenticateFunc) (interface{}, error) {
headers, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, errMetadataNotFound
}

ctx, err := authenticate(ctx, headers)
if err != nil {
return nil, err
}

return handler(ctx, req)
}

func defaultStreamInterceptor(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler, authenticate authenticateFunc) error {
ctx := stream.Context()
headers, ok := metadata.FromIncomingContext(ctx)
if !ok {
return errMetadataNotFound
}

// TODO: how to replace the context from the stream?
_, err := authenticate(ctx, headers)
if err != nil {
return err
}

return handler(srv, stream)
}
197 changes: 197 additions & 0 deletions internal/auth/authenticator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package auth

import (
"context"
"fmt"
"testing"

"github.com/stretchr/testify/assert"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"

"go.opentelemetry.io/collector/config/configauth"
)

func TestNew(t *testing.T) {
// test
p, err := New(configauth.Authentication{
OIDC: &configauth.OIDC{
Audience: "some-audience",
IssuerURL: "http://example.com",
},
})

// verify
assert.Nil(t, p)
assert.Equal(t, errNotImplemented, err)
}

func TestMissingOIDC(t *testing.T) {
// test
p, err := New(configauth.Authentication{})

// verify
assert.Nil(t, p)
assert.Equal(t, errNoOIDCProvided, err)
}

func TestDefaultUnaryInterceptorAuthSucceeded(t *testing.T) {
// prepare
handlerCalled := false
authCalled := false
authFunc := func(context.Context, map[string][]string) (context.Context, error) {
authCalled = true
return context.Background(), nil
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
handlerCalled = true
return nil, nil
}
ctx := metadata.NewIncomingContext(context.Background(), metadata.Pairs("authorization", "some-auth-data"))

// test
res, err := defaultUnaryInterceptor(ctx, nil, &grpc.UnaryServerInfo{}, handler, authFunc)

// verify
assert.Nil(t, res)
assert.NoError(t, err)
assert.True(t, authCalled)
assert.True(t, handlerCalled)
}

func TestDefaultUnaryInterceptorAuthFailure(t *testing.T) {
// prepare
authCalled := false
expectedErr := fmt.Errorf("not authenticated")
authFunc := func(context.Context, map[string][]string) (context.Context, error) {
authCalled = true
return context.Background(), expectedErr
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
assert.FailNow(t, "the handler should not have been called on auth failure!")
return nil, nil
}
ctx := metadata.NewIncomingContext(context.Background(), metadata.Pairs("authorization", "some-auth-data"))

// test
res, err := defaultUnaryInterceptor(ctx, nil, &grpc.UnaryServerInfo{}, handler, authFunc)

// verify
assert.Nil(t, res)
assert.Equal(t, expectedErr, err)
assert.True(t, authCalled)
}

func TestDefaultUnaryInterceptorMissingMetadata(t *testing.T) {
// prepare
authFunc := func(context.Context, map[string][]string) (context.Context, error) {
assert.FailNow(t, "the auth func should not have been called!")
return context.Background(), nil
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
assert.FailNow(t, "the handler should not have been called!")
return nil, nil
}

// test
res, err := defaultUnaryInterceptor(context.Background(), nil, &grpc.UnaryServerInfo{}, handler, authFunc)

// verify
assert.Nil(t, res)
assert.Equal(t, errMetadataNotFound, err)
}

func TestDefaultStreamInterceptorAuthSucceeded(t *testing.T) {
// prepare
handlerCalled := false
authCalled := false
authFunc := func(context.Context, map[string][]string) (context.Context, error) {
authCalled = true
return context.Background(), nil
}
handler := func(srv interface{}, stream grpc.ServerStream) error {
handlerCalled = true
return nil
}
ctx := metadata.NewIncomingContext(context.Background(), metadata.Pairs("authorization", "some-auth-data"))
streamServer := &mockServerStream{
ctx: ctx,
}

// test
err := defaultStreamInterceptor(nil, streamServer, &grpc.StreamServerInfo{}, handler, authFunc)

// verify
assert.NoError(t, err)
assert.True(t, authCalled)
assert.True(t, handlerCalled)
}

func TestDefaultStreamInterceptorAuthFailure(t *testing.T) {
// prepare
authCalled := false
expectedErr := fmt.Errorf("not authenticated")
authFunc := func(context.Context, map[string][]string) (context.Context, error) {
authCalled = true
return context.Background(), expectedErr
}
handler := func(srv interface{}, stream grpc.ServerStream) error {
assert.FailNow(t, "the handler should not have been called on auth failure!")
return nil
}
ctx := metadata.NewIncomingContext(context.Background(), metadata.Pairs("authorization", "some-auth-data"))
streamServer := &mockServerStream{
ctx: ctx,
}

// test
err := defaultStreamInterceptor(nil, streamServer, &grpc.StreamServerInfo{}, handler, authFunc)

// verify
assert.Equal(t, expectedErr, err)
assert.True(t, authCalled)
}

func TestDefaultStreamInterceptorMissingMetadata(t *testing.T) {
// prepare
authFunc := func(context.Context, map[string][]string) (context.Context, error) {
assert.FailNow(t, "the auth func should not have been called!")
return context.Background(), nil
}
handler := func(srv interface{}, stream grpc.ServerStream) error {
assert.FailNow(t, "the handler should not have been called!")
return nil
}
streamServer := &mockServerStream{
ctx: context.Background(),
}

// test
err := defaultStreamInterceptor(nil, streamServer, &grpc.StreamServerInfo{}, handler, authFunc)

// verify
assert.Equal(t, errMetadataNotFound, err)
}

type mockServerStream struct {
grpc.ServerStream
ctx context.Context
}

func (m *mockServerStream) Context() context.Context {
return m.ctx
}
37 changes: 37 additions & 0 deletions internal/auth/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package auth

import "context"

var (
subjectKey = subjectType{}
groupsKey = groupsType{}
)

type subjectType struct{}
type groupsType struct{}

// SubjectFromContext returns a list of groups the subject in the context belongs to
func SubjectFromContext(ctx context.Context) (string, bool) {
value, ok := ctx.Value(subjectKey).(string)
return value, ok
}

// GroupsFromContext returns a list of groups the subject in the context belongs to
func GroupsFromContext(ctx context.Context) ([]string, bool) {
value, ok := ctx.Value(groupsKey).([]string)
return value, ok
}

0 comments on commit 430c002

Please sign in to comment.