forked from open-telemetry/opentelemetry-collector
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Authentication processor 2/4 - Add auth context and interface (open-t…
…elemetry#1808) * Authentication processor 1/4 - Add configauth Signed-off-by: Juraci Paixão Kröhling <[email protected]> * Authentication processor 2/4 - Add auth context and interface Signed-off-by: Juraci Paixão Kröhling <[email protected]>
- Loading branch information
1 parent
4f06a68
commit 430c002
Showing
4 changed files
with
423 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.