Skip to content

Commit 114a3bd

Browse files
easyCZroboquat
authored andcommitted
[oidc] Add stub RPCs
1 parent 227beab commit 114a3bd

File tree

4 files changed

+343
-0
lines changed

4 files changed

+343
-0
lines changed

components/common-go/experiments/flags.go

+5
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,13 @@ import "context"
88

99
const (
1010
PersonalAccessTokensEnabledFlag = "personalAccessTokensEnabled"
11+
OIDCServiceEnabledFlag = "oidcServiceEnabled"
1112
)
1213

1314
func IsPersonalAccessTokensEnabled(ctx context.Context, client Client, attributes Attributes) bool {
1415
return client.GetBoolValue(ctx, PersonalAccessTokensEnabledFlag, false, attributes)
1516
}
17+
18+
func IsOIDCServiceEnabled(ctx context.Context, client Client, attributes Attributes) bool {
19+
return client.GetBoolValue(ctx, OIDCServiceEnabledFlag, false, attributes)
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
2+
// Licensed under the GNU Affero General Public License (AGPL).
3+
// See License-AGPL.txt in the project root for license information.
4+
5+
package apiv1
6+
7+
import (
8+
"context"
9+
"errors"
10+
"fmt"
11+
12+
connect "github.com/bufbuild/connect-go"
13+
"github.com/gitpod-io/gitpod/common-go/experiments"
14+
"github.com/gitpod-io/gitpod/common-go/log"
15+
v1 "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1"
16+
"github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1/v1connect"
17+
protocol "github.com/gitpod-io/gitpod/gitpod-protocol"
18+
"github.com/gitpod-io/gitpod/public-api-server/pkg/auth"
19+
"github.com/gitpod-io/gitpod/public-api-server/pkg/proxy"
20+
"github.com/google/uuid"
21+
)
22+
23+
func NewOIDCService(connPool proxy.ServerConnectionPool, expClient experiments.Client) *OIDCService {
24+
return &OIDCService{
25+
connectionPool: connPool,
26+
expClient: expClient,
27+
}
28+
}
29+
30+
type OIDCService struct {
31+
expClient experiments.Client
32+
connectionPool proxy.ServerConnectionPool
33+
34+
v1connect.UnimplementedOIDCServiceHandler
35+
}
36+
37+
func (s *OIDCService) CreateClientConfig(ctx context.Context, req *connect.Request[v1.CreateClientConfigRequest]) (*connect.Response[v1.CreateClientConfigResponse], error) {
38+
conn, err := s.getConnection(ctx)
39+
if err != nil {
40+
return nil, err
41+
}
42+
43+
_, _, err = s.getUser(ctx, conn)
44+
if err != nil {
45+
return nil, err
46+
}
47+
48+
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("gitpod.experimental.v1.OIDCService.CreateClientConfig is not implemented"))
49+
}
50+
51+
func (s *OIDCService) GetClientConfig(ctx context.Context, req *connect.Request[v1.GetClientConfigRequest]) (*connect.Response[v1.GetClientConfigResponse], error) {
52+
conn, err := s.getConnection(ctx)
53+
if err != nil {
54+
return nil, err
55+
}
56+
57+
_, _, err = s.getUser(ctx, conn)
58+
if err != nil {
59+
return nil, err
60+
}
61+
62+
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("gitpod.experimental.v1.OIDCService.GetClientConfig is not implemented"))
63+
}
64+
65+
func (s *OIDCService) ListClientConfigs(ctx context.Context, req *connect.Request[v1.ListClientConfigsRequest]) (*connect.Response[v1.ListClientConfigsResponse], error) {
66+
conn, err := s.getConnection(ctx)
67+
if err != nil {
68+
return nil, err
69+
}
70+
71+
_, _, err = s.getUser(ctx, conn)
72+
if err != nil {
73+
return nil, err
74+
}
75+
76+
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("gitpod.experimental.v1.OIDCService.ListClientConfigs is not implemented"))
77+
}
78+
79+
func (s *OIDCService) UpdateClientConfig(ctx context.Context, req *connect.Request[v1.UpdateClientConfigRequest]) (*connect.Response[v1.UpdateClientConfigResponse], error) {
80+
conn, err := s.getConnection(ctx)
81+
if err != nil {
82+
return nil, err
83+
}
84+
85+
_, _, err = s.getUser(ctx, conn)
86+
if err != nil {
87+
return nil, err
88+
}
89+
90+
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("gitpod.experimental.v1.OIDCService.UpdateClientConfig is not implemented"))
91+
}
92+
93+
func (s *OIDCService) DeleteClientConfig(ctx context.Context, req *connect.Request[v1.DeleteClientConfigRequest]) (*connect.Response[v1.DeleteClientConfigResponse], error) {
94+
conn, err := s.getConnection(ctx)
95+
if err != nil {
96+
return nil, err
97+
}
98+
99+
_, _, err = s.getUser(ctx, conn)
100+
if err != nil {
101+
return nil, err
102+
}
103+
104+
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("gitpod.experimental.v1.OIDCService.DeleteClientConfig is not implemented"))
105+
}
106+
107+
func (s *OIDCService) getConnection(ctx context.Context) (protocol.APIInterface, error) {
108+
token, err := auth.TokenFromContext(ctx)
109+
if err != nil {
110+
return nil, connect.NewError(connect.CodeUnauthenticated, fmt.Errorf("No credentials present on request."))
111+
}
112+
113+
conn, err := s.connectionPool.Get(ctx, token)
114+
if err != nil {
115+
log.Log.WithError(err).Error("Failed to get connection to server.")
116+
return nil, connect.NewError(connect.CodeInternal, errors.New("Failed to establish connection to downstream services. If this issue persists, please contact Gitpod Support."))
117+
}
118+
119+
return conn, nil
120+
}
121+
122+
func (s *OIDCService) getUser(ctx context.Context, conn protocol.APIInterface) (*protocol.User, uuid.UUID, error) {
123+
user, err := conn.GetLoggedInUser(ctx)
124+
if err != nil {
125+
return nil, uuid.Nil, proxy.ConvertError(err)
126+
}
127+
128+
if !s.isFeatureEnabled(ctx, conn, user) {
129+
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."))
130+
}
131+
132+
userID, err := uuid.Parse(user.ID)
133+
if err != nil {
134+
return nil, uuid.Nil, connect.NewError(connect.CodeInternal, errors.New("Failed to parse user ID as UUID. Please contact support."))
135+
}
136+
137+
return user, userID, nil
138+
}
139+
140+
func (s *OIDCService) isFeatureEnabled(ctx context.Context, conn protocol.APIInterface, user *protocol.User) bool {
141+
if user == nil {
142+
return false
143+
}
144+
145+
if experiments.IsOIDCServiceEnabled(ctx, s.expClient, experiments.Attributes{UserID: user.ID}) {
146+
return true
147+
}
148+
149+
teams, err := conn.GetTeams(ctx)
150+
if err != nil {
151+
log.WithError(err).Warnf("Failed to retreive Teams for user %s, personal access token feature flag will not evaluate team membership.", user.ID)
152+
teams = nil
153+
}
154+
for _, team := range teams {
155+
if experiments.IsOIDCServiceEnabled(ctx, s.expClient, experiments.Attributes{TeamID: team.ID}) {
156+
return true
157+
}
158+
}
159+
160+
return false
161+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
2+
// Licensed under the GNU Affero General Public License (AGPL).
3+
// See License-AGPL.txt in the project root for license information.
4+
5+
package apiv1
6+
7+
import (
8+
"context"
9+
"net/http"
10+
"net/http/httptest"
11+
"testing"
12+
13+
connect "github.com/bufbuild/connect-go"
14+
"github.com/gitpod-io/gitpod/common-go/experiments"
15+
"github.com/gitpod-io/gitpod/common-go/experiments/experimentstest"
16+
v1 "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1"
17+
"github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1/v1connect"
18+
protocol "github.com/gitpod-io/gitpod/gitpod-protocol"
19+
"github.com/gitpod-io/gitpod/public-api-server/pkg/auth"
20+
"github.com/golang/mock/gomock"
21+
"github.com/stretchr/testify/require"
22+
)
23+
24+
var (
25+
withOIDCFeatureDisabled = &experimentstest.Client{
26+
BoolMatcher: func(ctx context.Context, experiment string, defaultValue bool, attributes experiments.Attributes) bool {
27+
return false
28+
},
29+
}
30+
withOIDCFeatureEnabled = &experimentstest.Client{
31+
BoolMatcher: func(ctx context.Context, experiment string, defaultValue bool, attributes experiments.Attributes) bool {
32+
return experiment == experiments.OIDCServiceEnabledFlag
33+
},
34+
}
35+
36+
user = newUser(&protocol.User{})
37+
)
38+
39+
func TestOIDCService_CreateClientConfig(t *testing.T) {
40+
t.Run("feature flag disabled returns unathorized", func(t *testing.T) {
41+
serverMock, client := setupOIDCService(t, withOIDCFeatureDisabled)
42+
43+
serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil)
44+
serverMock.EXPECT().GetTeams(gomock.Any()).Return(teams, nil)
45+
46+
_, err := client.CreateClientConfig(context.Background(), connect.NewRequest(&v1.CreateClientConfigRequest{}))
47+
require.Error(t, err)
48+
require.Equal(t, connect.CodePermissionDenied, connect.CodeOf(err))
49+
})
50+
51+
t.Run("feature flag enabled returns unimplemented", func(t *testing.T) {
52+
serverMock, client := setupOIDCService(t, withOIDCFeatureEnabled)
53+
54+
serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil)
55+
56+
_, err := client.CreateClientConfig(context.Background(), connect.NewRequest(&v1.CreateClientConfigRequest{}))
57+
require.Error(t, err)
58+
require.Equal(t, connect.CodeUnimplemented, connect.CodeOf(err))
59+
})
60+
}
61+
62+
func TestOIDCService_GetClientConfig(t *testing.T) {
63+
t.Run("feature flag disabled returns unathorized", func(t *testing.T) {
64+
serverMock, client := setupOIDCService(t, withOIDCFeatureDisabled)
65+
66+
serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil)
67+
serverMock.EXPECT().GetTeams(gomock.Any()).Return(teams, nil)
68+
69+
_, err := client.GetClientConfig(context.Background(), connect.NewRequest(&v1.GetClientConfigRequest{}))
70+
require.Error(t, err)
71+
require.Equal(t, connect.CodePermissionDenied, connect.CodeOf(err))
72+
})
73+
74+
t.Run("feature flag enabled returns unimplemented", func(t *testing.T) {
75+
serverMock, client := setupOIDCService(t, withOIDCFeatureEnabled)
76+
77+
serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil)
78+
79+
_, err := client.GetClientConfig(context.Background(), connect.NewRequest(&v1.GetClientConfigRequest{}))
80+
require.Error(t, err)
81+
require.Equal(t, connect.CodeUnimplemented, connect.CodeOf(err))
82+
})
83+
}
84+
85+
func TestOIDCService_ListClientConfigs(t *testing.T) {
86+
t.Run("feature flag disabled returns unathorized", func(t *testing.T) {
87+
serverMock, client := setupOIDCService(t, withOIDCFeatureDisabled)
88+
89+
serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil)
90+
serverMock.EXPECT().GetTeams(gomock.Any()).Return(teams, nil)
91+
92+
_, err := client.ListClientConfigs(context.Background(), connect.NewRequest(&v1.ListClientConfigsRequest{}))
93+
require.Error(t, err)
94+
require.Equal(t, connect.CodePermissionDenied, connect.CodeOf(err))
95+
})
96+
97+
t.Run("feature flag enabled returns unimplemented", func(t *testing.T) {
98+
serverMock, client := setupOIDCService(t, withOIDCFeatureEnabled)
99+
100+
serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil)
101+
102+
_, err := client.ListClientConfigs(context.Background(), connect.NewRequest(&v1.ListClientConfigsRequest{}))
103+
require.Error(t, err)
104+
require.Equal(t, connect.CodeUnimplemented, connect.CodeOf(err))
105+
})
106+
}
107+
108+
func TestOIDCService_UpdateClientConfig(t *testing.T) {
109+
t.Run("feature flag disabled returns unathorized", func(t *testing.T) {
110+
serverMock, client := setupOIDCService(t, withOIDCFeatureDisabled)
111+
112+
serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil)
113+
serverMock.EXPECT().GetTeams(gomock.Any()).Return(teams, nil)
114+
115+
_, err := client.UpdateClientConfig(context.Background(), connect.NewRequest(&v1.UpdateClientConfigRequest{}))
116+
require.Error(t, err)
117+
require.Equal(t, connect.CodePermissionDenied, connect.CodeOf(err))
118+
})
119+
120+
t.Run("feature flag enabled returns unimplemented", func(t *testing.T) {
121+
serverMock, client := setupOIDCService(t, withOIDCFeatureEnabled)
122+
123+
serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil)
124+
125+
_, err := client.UpdateClientConfig(context.Background(), connect.NewRequest(&v1.UpdateClientConfigRequest{}))
126+
require.Error(t, err)
127+
require.Equal(t, connect.CodeUnimplemented, connect.CodeOf(err))
128+
})
129+
}
130+
131+
func TestOIDCService_DeleteClientConfig(t *testing.T) {
132+
t.Run("feature flag disabled returns unathorized", func(t *testing.T) {
133+
serverMock, client := setupOIDCService(t, withOIDCFeatureDisabled)
134+
135+
serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil)
136+
serverMock.EXPECT().GetTeams(gomock.Any()).Return(teams, nil)
137+
138+
_, err := client.DeleteClientConfig(context.Background(), connect.NewRequest(&v1.DeleteClientConfigRequest{}))
139+
require.Error(t, err)
140+
require.Equal(t, connect.CodePermissionDenied, connect.CodeOf(err))
141+
})
142+
143+
t.Run("feature flag enabled returns unimplemented", func(t *testing.T) {
144+
serverMock, client := setupOIDCService(t, withOIDCFeatureEnabled)
145+
146+
serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil)
147+
148+
_, err := client.DeleteClientConfig(context.Background(), connect.NewRequest(&v1.DeleteClientConfigRequest{}))
149+
require.Error(t, err)
150+
require.Equal(t, connect.CodeUnimplemented, connect.CodeOf(err))
151+
})
152+
}
153+
154+
func setupOIDCService(t *testing.T, expClient experiments.Client) (*protocol.MockAPIInterface, v1connect.OIDCServiceClient) {
155+
t.Helper()
156+
157+
ctrl := gomock.NewController(t)
158+
t.Cleanup(ctrl.Finish)
159+
160+
serverMock := protocol.NewMockAPIInterface(ctrl)
161+
162+
svc := NewOIDCService(&FakeServerConnPool{api: serverMock}, expClient)
163+
164+
_, handler := v1connect.NewOIDCServiceHandler(svc, connect.WithInterceptors(auth.NewServerInterceptor()))
165+
166+
srv := httptest.NewServer(handler)
167+
t.Cleanup(srv.Close)
168+
169+
client := v1connect.NewOIDCServiceClient(http.DefaultClient, srv.URL, connect.WithInterceptors(
170+
auth.NewClientInterceptor("auth-token"),
171+
))
172+
173+
return serverMock, client
174+
}

components/public-api-server/pkg/server/server.go

+3
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,9 @@ func register(srv *baseserver.Server, connPool proxy.ServerConnectionPool, expCl
146146
projectsRoute, projectsServiceHandler := v1connect.NewProjectsServiceHandler(apiv1.NewProjectsService(connPool), handlerOptions...)
147147
srv.HTTPMux().Handle(projectsRoute, projectsServiceHandler)
148148

149+
oidcRoute, oidcServiceHandler := v1connect.NewOIDCServiceHandler(apiv1.NewOIDCService(connPool, expClient), handlerOptions...)
150+
srv.HTTPMux().Handle(oidcRoute, oidcServiceHandler)
151+
149152
return nil
150153
}
151154

0 commit comments

Comments
 (0)