Skip to content

[oidc] Add stub RPCs #15198

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

Merged
merged 1 commit into from
Dec 7, 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions components/common-go/experiments/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ import "context"

const (
PersonalAccessTokensEnabledFlag = "personalAccessTokensEnabled"
OIDCServiceEnabledFlag = "oidcServiceEnabled"
)

func IsPersonalAccessTokensEnabled(ctx context.Context, client Client, attributes Attributes) bool {
return client.GetBoolValue(ctx, PersonalAccessTokensEnabledFlag, false, attributes)
}

func IsOIDCServiceEnabled(ctx context.Context, client Client, attributes Attributes) bool {
return client.GetBoolValue(ctx, OIDCServiceEnabledFlag, false, attributes)
}
161 changes: 161 additions & 0 deletions components/public-api-server/pkg/apiv1/oidc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License-AGPL.txt in the project root for license information.

package apiv1

import (
"context"
"errors"
"fmt"

connect "github.com/bufbuild/connect-go"
"github.com/gitpod-io/gitpod/common-go/experiments"
"github.com/gitpod-io/gitpod/common-go/log"
v1 "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1"
"github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1/v1connect"
protocol "github.com/gitpod-io/gitpod/gitpod-protocol"
"github.com/gitpod-io/gitpod/public-api-server/pkg/auth"
"github.com/gitpod-io/gitpod/public-api-server/pkg/proxy"
"github.com/google/uuid"
)

func NewOIDCService(connPool proxy.ServerConnectionPool, expClient experiments.Client) *OIDCService {
return &OIDCService{
connectionPool: connPool,
expClient: expClient,
}
}

type OIDCService struct {
expClient experiments.Client
connectionPool proxy.ServerConnectionPool

v1connect.UnimplementedOIDCServiceHandler
}

func (s *OIDCService) CreateClientConfig(ctx context.Context, req *connect.Request[v1.CreateClientConfigRequest]) (*connect.Response[v1.CreateClientConfigResponse], error) {
conn, err := s.getConnection(ctx)
if err != nil {
return nil, err
}

_, _, err = s.getUser(ctx, conn)
if err != nil {
return nil, err
}

return nil, connect.NewError(connect.CodeUnimplemented, errors.New("gitpod.experimental.v1.OIDCService.CreateClientConfig is not implemented"))
}

func (s *OIDCService) GetClientConfig(ctx context.Context, req *connect.Request[v1.GetClientConfigRequest]) (*connect.Response[v1.GetClientConfigResponse], error) {
conn, err := s.getConnection(ctx)
if err != nil {
return nil, err
}

_, _, err = s.getUser(ctx, conn)
if err != nil {
return nil, err
}

return nil, connect.NewError(connect.CodeUnimplemented, errors.New("gitpod.experimental.v1.OIDCService.GetClientConfig is not implemented"))
}

func (s *OIDCService) ListClientConfigs(ctx context.Context, req *connect.Request[v1.ListClientConfigsRequest]) (*connect.Response[v1.ListClientConfigsResponse], error) {
conn, err := s.getConnection(ctx)
if err != nil {
return nil, err
}

_, _, err = s.getUser(ctx, conn)
if err != nil {
return nil, err
}

return nil, connect.NewError(connect.CodeUnimplemented, errors.New("gitpod.experimental.v1.OIDCService.ListClientConfigs is not implemented"))
}

func (s *OIDCService) UpdateClientConfig(ctx context.Context, req *connect.Request[v1.UpdateClientConfigRequest]) (*connect.Response[v1.UpdateClientConfigResponse], error) {
conn, err := s.getConnection(ctx)
if err != nil {
return nil, err
}

_, _, err = s.getUser(ctx, conn)
if err != nil {
return nil, err
}

return nil, connect.NewError(connect.CodeUnimplemented, errors.New("gitpod.experimental.v1.OIDCService.UpdateClientConfig is not implemented"))
}

func (s *OIDCService) DeleteClientConfig(ctx context.Context, req *connect.Request[v1.DeleteClientConfigRequest]) (*connect.Response[v1.DeleteClientConfigResponse], error) {
conn, err := s.getConnection(ctx)
if err != nil {
return nil, err
}

_, _, err = s.getUser(ctx, conn)
if err != nil {
return nil, err
}

return nil, connect.NewError(connect.CodeUnimplemented, errors.New("gitpod.experimental.v1.OIDCService.DeleteClientConfig is not implemented"))
}

func (s *OIDCService) getConnection(ctx context.Context) (protocol.APIInterface, error) {
token, err := auth.TokenFromContext(ctx)
if err != nil {
return nil, connect.NewError(connect.CodeUnauthenticated, fmt.Errorf("No credentials present on request."))
}

conn, err := s.connectionPool.Get(ctx, token)
if err != nil {
log.Log.WithError(err).Error("Failed to get connection to server.")
return nil, connect.NewError(connect.CodeInternal, errors.New("Failed to establish connection to downstream services. If this issue persists, please contact Gitpod Support."))
}

return conn, nil
}

func (s *OIDCService) getUser(ctx context.Context, conn protocol.APIInterface) (*protocol.User, uuid.UUID, error) {
user, err := conn.GetLoggedInUser(ctx)
if err != nil {
return nil, uuid.Nil, proxy.ConvertError(err)
}

if !s.isFeatureEnabled(ctx, conn, user) {
return nil, uuid.Nil, connect.NewError(connect.CodePermissionDenied, errors.New("This feature is currently in beta. If you would like to be part of the beta, please contact us."))
}

userID, err := uuid.Parse(user.ID)
if err != nil {
return nil, uuid.Nil, connect.NewError(connect.CodeInternal, errors.New("Failed to parse user ID as UUID. Please contact support."))
}

return user, userID, nil
}

func (s *OIDCService) isFeatureEnabled(ctx context.Context, conn protocol.APIInterface, user *protocol.User) bool {
if user == nil {
return false
}

if experiments.IsOIDCServiceEnabled(ctx, s.expClient, experiments.Attributes{UserID: user.ID}) {
return true
}

teams, err := conn.GetTeams(ctx)
if err != nil {
log.WithError(err).Warnf("Failed to retreive Teams for user %s, personal access token feature flag will not evaluate team membership.", user.ID)
teams = nil
}
for _, team := range teams {
if experiments.IsOIDCServiceEnabled(ctx, s.expClient, experiments.Attributes{TeamID: team.ID}) {
return true
}
}

return false
}
174 changes: 174 additions & 0 deletions components/public-api-server/pkg/apiv1/oidc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License-AGPL.txt in the project root for license information.

package apiv1

import (
"context"
"net/http"
"net/http/httptest"
"testing"

connect "github.com/bufbuild/connect-go"
"github.com/gitpod-io/gitpod/common-go/experiments"
"github.com/gitpod-io/gitpod/common-go/experiments/experimentstest"
v1 "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1"
"github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1/v1connect"
protocol "github.com/gitpod-io/gitpod/gitpod-protocol"
"github.com/gitpod-io/gitpod/public-api-server/pkg/auth"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
)

var (
withOIDCFeatureDisabled = &experimentstest.Client{
BoolMatcher: func(ctx context.Context, experiment string, defaultValue bool, attributes experiments.Attributes) bool {
return false
},
}
withOIDCFeatureEnabled = &experimentstest.Client{
BoolMatcher: func(ctx context.Context, experiment string, defaultValue bool, attributes experiments.Attributes) bool {
return experiment == experiments.OIDCServiceEnabledFlag
},
}

user = newUser(&protocol.User{})
)

func TestOIDCService_CreateClientConfig(t *testing.T) {
t.Run("feature flag disabled returns unathorized", func(t *testing.T) {
serverMock, client := setupOIDCService(t, withOIDCFeatureDisabled)

serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil)
serverMock.EXPECT().GetTeams(gomock.Any()).Return(teams, nil)

_, err := client.CreateClientConfig(context.Background(), connect.NewRequest(&v1.CreateClientConfigRequest{}))
require.Error(t, err)
require.Equal(t, connect.CodePermissionDenied, connect.CodeOf(err))
})

t.Run("feature flag enabled returns unimplemented", func(t *testing.T) {
serverMock, client := setupOIDCService(t, withOIDCFeatureEnabled)

serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil)

_, err := client.CreateClientConfig(context.Background(), connect.NewRequest(&v1.CreateClientConfigRequest{}))
require.Error(t, err)
require.Equal(t, connect.CodeUnimplemented, connect.CodeOf(err))
})
}

func TestOIDCService_GetClientConfig(t *testing.T) {
t.Run("feature flag disabled returns unathorized", func(t *testing.T) {
serverMock, client := setupOIDCService(t, withOIDCFeatureDisabled)

serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil)
serverMock.EXPECT().GetTeams(gomock.Any()).Return(teams, nil)

_, err := client.GetClientConfig(context.Background(), connect.NewRequest(&v1.GetClientConfigRequest{}))
require.Error(t, err)
require.Equal(t, connect.CodePermissionDenied, connect.CodeOf(err))
})

t.Run("feature flag enabled returns unimplemented", func(t *testing.T) {
serverMock, client := setupOIDCService(t, withOIDCFeatureEnabled)

serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil)

_, err := client.GetClientConfig(context.Background(), connect.NewRequest(&v1.GetClientConfigRequest{}))
require.Error(t, err)
require.Equal(t, connect.CodeUnimplemented, connect.CodeOf(err))
})
}

func TestOIDCService_ListClientConfigs(t *testing.T) {
t.Run("feature flag disabled returns unathorized", func(t *testing.T) {
serverMock, client := setupOIDCService(t, withOIDCFeatureDisabled)

serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil)
serverMock.EXPECT().GetTeams(gomock.Any()).Return(teams, nil)

_, err := client.ListClientConfigs(context.Background(), connect.NewRequest(&v1.ListClientConfigsRequest{}))
require.Error(t, err)
require.Equal(t, connect.CodePermissionDenied, connect.CodeOf(err))
})

t.Run("feature flag enabled returns unimplemented", func(t *testing.T) {
serverMock, client := setupOIDCService(t, withOIDCFeatureEnabled)

serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil)

_, err := client.ListClientConfigs(context.Background(), connect.NewRequest(&v1.ListClientConfigsRequest{}))
require.Error(t, err)
require.Equal(t, connect.CodeUnimplemented, connect.CodeOf(err))
})
}

func TestOIDCService_UpdateClientConfig(t *testing.T) {
t.Run("feature flag disabled returns unathorized", func(t *testing.T) {
serverMock, client := setupOIDCService(t, withOIDCFeatureDisabled)

serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil)
serverMock.EXPECT().GetTeams(gomock.Any()).Return(teams, nil)

_, err := client.UpdateClientConfig(context.Background(), connect.NewRequest(&v1.UpdateClientConfigRequest{}))
require.Error(t, err)
require.Equal(t, connect.CodePermissionDenied, connect.CodeOf(err))
})

t.Run("feature flag enabled returns unimplemented", func(t *testing.T) {
serverMock, client := setupOIDCService(t, withOIDCFeatureEnabled)

serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil)

_, err := client.UpdateClientConfig(context.Background(), connect.NewRequest(&v1.UpdateClientConfigRequest{}))
require.Error(t, err)
require.Equal(t, connect.CodeUnimplemented, connect.CodeOf(err))
})
}

func TestOIDCService_DeleteClientConfig(t *testing.T) {
t.Run("feature flag disabled returns unathorized", func(t *testing.T) {
serverMock, client := setupOIDCService(t, withOIDCFeatureDisabled)

serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil)
serverMock.EXPECT().GetTeams(gomock.Any()).Return(teams, nil)

_, err := client.DeleteClientConfig(context.Background(), connect.NewRequest(&v1.DeleteClientConfigRequest{}))
require.Error(t, err)
require.Equal(t, connect.CodePermissionDenied, connect.CodeOf(err))
})

t.Run("feature flag enabled returns unimplemented", func(t *testing.T) {
serverMock, client := setupOIDCService(t, withOIDCFeatureEnabled)

serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil)

_, err := client.DeleteClientConfig(context.Background(), connect.NewRequest(&v1.DeleteClientConfigRequest{}))
require.Error(t, err)
require.Equal(t, connect.CodeUnimplemented, connect.CodeOf(err))
})
}

func setupOIDCService(t *testing.T, expClient experiments.Client) (*protocol.MockAPIInterface, v1connect.OIDCServiceClient) {
t.Helper()

ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)

serverMock := protocol.NewMockAPIInterface(ctrl)

svc := NewOIDCService(&FakeServerConnPool{api: serverMock}, expClient)

_, handler := v1connect.NewOIDCServiceHandler(svc, connect.WithInterceptors(auth.NewServerInterceptor()))

srv := httptest.NewServer(handler)
t.Cleanup(srv.Close)

client := v1connect.NewOIDCServiceClient(http.DefaultClient, srv.URL, connect.WithInterceptors(
auth.NewClientInterceptor("auth-token"),
))

return serverMock, client
}
3 changes: 3 additions & 0 deletions components/public-api-server/pkg/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ func register(srv *baseserver.Server, connPool proxy.ServerConnectionPool, expCl
projectsRoute, projectsServiceHandler := v1connect.NewProjectsServiceHandler(apiv1.NewProjectsService(connPool), handlerOptions...)
srv.HTTPMux().Handle(projectsRoute, projectsServiceHandler)

oidcRoute, oidcServiceHandler := v1connect.NewOIDCServiceHandler(apiv1.NewOIDCService(connPool, expClient), handlerOptions...)
srv.HTTPMux().Handle(oidcRoute, oidcServiceHandler)

return nil
}

Expand Down