Skip to content

Commit 760c4a8

Browse files
committed
feat: add owner_oidc_access_token to coder_workspace data source
See the discussion in Discord here: https://discord.com/channels/747933592273027093/1071182088490987542/1071182088490987542 Related provider PR: coder/terraform-provider-coder#91
1 parent f096915 commit 760c4a8

File tree

6 files changed

+270
-106
lines changed

6 files changed

+270
-106
lines changed

coderd/provisionerdserver/provisionerdserver.go

+64-7
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@ import (
1818
"github.com/tabbed/pqtype"
1919
"golang.org/x/exp/maps"
2020
"golang.org/x/exp/slices"
21+
"golang.org/x/oauth2"
2122
"golang.org/x/xerrors"
2223
protobuf "google.golang.org/protobuf/proto"
2324

2425
"cdr.dev/slog"
2526

2627
"github.com/coder/coder/coderd/audit"
2728
"github.com/coder/coder/coderd/database"
29+
"github.com/coder/coder/coderd/httpmw"
2830
"github.com/coder/coder/coderd/parameter"
2931
"github.com/coder/coder/coderd/telemetry"
3032
"github.com/coder/coder/codersdk"
@@ -52,6 +54,7 @@ type Server struct {
5254
Auditor *atomic.Pointer[audit.Auditor]
5355

5456
AcquireJobDebounce time.Duration
57+
OIDCConfig httpmw.OAuth2Config
5558
}
5659

5760
// AcquireJob queries the database to lock a job.
@@ -155,6 +158,14 @@ func (server *Server) AcquireJob(ctx context.Context, _ *proto.Empty) (*proto.Ac
155158
return nil, failJob(fmt.Sprintf("publish workspace update: %s", err))
156159
}
157160

161+
var workspaceOwnerOIDCAccessToken string
162+
if server.OIDCConfig != nil {
163+
workspaceOwnerOIDCAccessToken, err = obtainOIDCAccessToken(ctx, server.Database, server.OIDCConfig, owner.ID)
164+
if err != nil {
165+
return nil, failJob(fmt.Sprintf("obtain OIDC access token: %s", err))
166+
}
167+
}
168+
158169
// Compute parameters for the workspace to consume.
159170
parameters, err := parameter.Compute(ctx, server.Database, parameter.ComputeScope{
160171
TemplateImportJobID: templateVersion.JobID,
@@ -194,13 +205,14 @@ func (server *Server) AcquireJob(ctx context.Context, _ *proto.Empty) (*proto.Ac
194205
ParameterValues: protoParameters,
195206
RichParameterValues: convertRichParameterValues(workspaceBuildParameters),
196207
Metadata: &sdkproto.Provision_Metadata{
197-
CoderUrl: server.AccessURL.String(),
198-
WorkspaceTransition: transition,
199-
WorkspaceName: workspace.Name,
200-
WorkspaceOwner: owner.Username,
201-
WorkspaceOwnerEmail: owner.Email,
202-
WorkspaceId: workspace.ID.String(),
203-
WorkspaceOwnerId: owner.ID.String(),
208+
CoderUrl: server.AccessURL.String(),
209+
WorkspaceTransition: transition,
210+
WorkspaceName: workspace.Name,
211+
WorkspaceOwner: owner.Username,
212+
WorkspaceOwnerEmail: owner.Email,
213+
WorkspaceOwnerOidcAccessToken: workspaceOwnerOIDCAccessToken,
214+
WorkspaceId: workspace.ID.String(),
215+
WorkspaceOwnerId: owner.ID.String(),
204216
},
205217
},
206218
}
@@ -1062,6 +1074,51 @@ func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid.
10621074
return nil
10631075
}
10641076

1077+
// obtainOIDCAccessToken returns a valid OpenID Connect access token
1078+
// for the user if it's able to obtain one, otherwise it returns an empty string.
1079+
func obtainOIDCAccessToken(ctx context.Context, db database.Store, oidcConfig httpmw.OAuth2Config, userID uuid.UUID) (string, error) {
1080+
link, err := db.GetUserLinkByUserIDLoginType(ctx, database.GetUserLinkByUserIDLoginTypeParams{
1081+
UserID: userID,
1082+
LoginType: database.LoginTypeOIDC,
1083+
})
1084+
if errors.Is(err, sql.ErrNoRows) {
1085+
err = nil
1086+
}
1087+
if err != nil {
1088+
return "", xerrors.Errorf("get owner oidc link: %w", err)
1089+
}
1090+
1091+
if link.OAuthExpiry.Before(database.Now()) && !link.OAuthExpiry.IsZero() && link.OAuthRefreshToken != "" {
1092+
token, err := oidcConfig.TokenSource(ctx, &oauth2.Token{
1093+
AccessToken: link.OAuthAccessToken,
1094+
RefreshToken: link.OAuthRefreshToken,
1095+
Expiry: link.OAuthExpiry,
1096+
}).Token()
1097+
if err != nil {
1098+
// If OIDC fails to refresh, we return an empty string and don't fail.
1099+
// There isn't a way to hard-opt in to OIDC from a template, so we don't
1100+
// want to fail builds if users haven't authenticated for a while or something.
1101+
return "", nil
1102+
}
1103+
link.OAuthAccessToken = token.AccessToken
1104+
link.OAuthRefreshToken = token.RefreshToken
1105+
link.OAuthExpiry = token.Expiry
1106+
1107+
link, err = db.UpdateUserLink(ctx, database.UpdateUserLinkParams{
1108+
UserID: userID,
1109+
LoginType: database.LoginTypeOIDC,
1110+
OAuthAccessToken: link.OAuthAccessToken,
1111+
OAuthRefreshToken: link.OAuthRefreshToken,
1112+
OAuthExpiry: link.OAuthExpiry,
1113+
})
1114+
if err != nil {
1115+
return "", xerrors.Errorf("update user link: %w", err)
1116+
}
1117+
}
1118+
1119+
return link.OAuthAccessToken, nil
1120+
}
1121+
10651122
func convertValidationTypeSystem(typeSystem sdkproto.ParameterSchema_TypeSystem) (database.ParameterTypeSystem, error) {
10661123
switch typeSystem {
10671124
case sdkproto.ParameterSchema_None:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package provisionerdserver
2+
3+
import (
4+
"context"
5+
"testing"
6+
"time"
7+
8+
"github.com/coder/coder/coderd/database"
9+
"github.com/coder/coder/coderd/database/dbfake"
10+
"github.com/coder/coder/coderd/database/dbgen"
11+
"github.com/google/uuid"
12+
"github.com/stretchr/testify/require"
13+
"golang.org/x/oauth2"
14+
)
15+
16+
func TestObtainOIDCAccessToken(t *testing.T) {
17+
t.Parallel()
18+
ctx := context.Background()
19+
t.Run("NoToken", func(t *testing.T) {
20+
t.Parallel()
21+
db := dbfake.New()
22+
_, err := obtainOIDCAccessToken(ctx, db, nil, uuid.Nil)
23+
require.NoError(t, err)
24+
})
25+
t.Run("InvalidConfig", func(t *testing.T) {
26+
// We still want OIDC to succeed even if exchanging the token fails.
27+
t.Parallel()
28+
db := dbfake.New()
29+
user := dbgen.User(t, db, database.User{})
30+
dbgen.UserLink(t, db, database.UserLink{
31+
UserID: user.ID,
32+
LoginType: database.LoginTypeOIDC,
33+
OAuthExpiry: database.Now().Add(-time.Hour),
34+
})
35+
_, err := obtainOIDCAccessToken(ctx, db, &oauth2.Config{}, user.ID)
36+
require.NoError(t, err)
37+
})
38+
t.Run("Exchange", func(t *testing.T) {
39+
t.Parallel()
40+
db := dbfake.New()
41+
user := dbgen.User(t, db, database.User{})
42+
dbgen.UserLink(t, db, database.UserLink{
43+
UserID: user.ID,
44+
LoginType: database.LoginTypeOIDC,
45+
OAuthExpiry: database.Now().Add(-time.Hour),
46+
})
47+
_, err := obtainOIDCAccessToken(ctx, db, &oauth2Config{
48+
tokenSource: func() (*oauth2.Token, error) {
49+
return &oauth2.Token{
50+
AccessToken: "token",
51+
}, nil
52+
},
53+
}, user.ID)
54+
require.NoError(t, err)
55+
link, err := db.GetUserLinkByUserIDLoginType(ctx, database.GetUserLinkByUserIDLoginTypeParams{
56+
UserID: user.ID,
57+
LoginType: database.LoginTypeOIDC,
58+
})
59+
require.NoError(t, err)
60+
require.Equal(t, "token", link.OAuthAccessToken)
61+
})
62+
}
63+
64+
type oauth2Config struct {
65+
tokenSource oauth2TokenSource
66+
}
67+
68+
func (o *oauth2Config) TokenSource(context.Context, *oauth2.Token) oauth2.TokenSource {
69+
return o.tokenSource
70+
}
71+
72+
func (*oauth2Config) AuthCodeURL(string, ...oauth2.AuthCodeOption) string {
73+
return ""
74+
}
75+
76+
func (*oauth2Config) Exchange(context.Context, string, ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
77+
return &oauth2.Token{}, nil
78+
}
79+
80+
type oauth2TokenSource func() (*oauth2.Token, error)
81+
82+
func (o oauth2TokenSource) Token() (*oauth2.Token, error) {
83+
return o()
84+
}

coderd/provisionerdserver/provisionerdserver_test.go

+16-7
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"github.com/google/uuid"
1313
"github.com/stretchr/testify/require"
14+
"golang.org/x/oauth2"
1415

1516
"cdr.dev/slog/sloggers/slogtest"
1617
"github.com/coder/coder/coderd/audit"
@@ -90,6 +91,12 @@ func TestAcquireJob(t *testing.T) {
9091
ctx := context.Background()
9192

9293
user := dbgen.User(t, srv.Database, database.User{})
94+
link := dbgen.UserLink(t, srv.Database, database.UserLink{
95+
LoginType: database.LoginTypeOIDC,
96+
UserID: user.ID,
97+
OAuthExpiry: database.Now().Add(time.Hour),
98+
OAuthAccessToken: "access-token",
99+
})
93100
template := dbgen.Template(t, srv.Database, database.Template{
94101
Name: "template",
95102
Provisioner: database.ProvisionerTypeEcho,
@@ -169,13 +176,14 @@ func TestAcquireJob(t *testing.T) {
169176
WorkspaceName: workspace.Name,
170177
ParameterValues: []*sdkproto.ParameterValue{},
171178
Metadata: &sdkproto.Provision_Metadata{
172-
CoderUrl: srv.AccessURL.String(),
173-
WorkspaceTransition: sdkproto.WorkspaceTransition_START,
174-
WorkspaceName: workspace.Name,
175-
WorkspaceOwner: user.Username,
176-
WorkspaceOwnerEmail: user.Email,
177-
WorkspaceId: workspace.ID.String(),
178-
WorkspaceOwnerId: user.ID.String(),
179+
CoderUrl: srv.AccessURL.String(),
180+
WorkspaceTransition: sdkproto.WorkspaceTransition_START,
181+
WorkspaceName: workspace.Name,
182+
WorkspaceOwner: user.Username,
183+
WorkspaceOwnerEmail: user.Email,
184+
WorkspaceId: workspace.ID.String(),
185+
WorkspaceOwnerId: user.ID.String(),
186+
WorkspaceOwnerOidcAccessToken: link.OAuthAccessToken,
179187
},
180188
},
181189
})
@@ -804,6 +812,7 @@ func setup(t *testing.T, ignoreLogErrors bool) *provisionerdserver.Server {
804812
return &provisionerdserver.Server{
805813
ID: uuid.New(),
806814
Logger: slogtest.Make(t, &slogtest.Options{IgnoreErrors: ignoreLogErrors}),
815+
OIDCConfig: &oauth2.Config{},
807816
AccessURL: &url.URL{},
808817
Provisioners: []database.ProvisionerType{database.ProvisionerTypeEcho},
809818
Database: db,

provisioner/terraform/provision.go

+1
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ func provisionEnv(config *proto.Provision_Config, params []*proto.ParameterValue
213213
"CODER_WORKSPACE_NAME="+config.Metadata.WorkspaceName,
214214
"CODER_WORKSPACE_OWNER="+config.Metadata.WorkspaceOwner,
215215
"CODER_WORKSPACE_OWNER_EMAIL="+config.Metadata.WorkspaceOwnerEmail,
216+
"CODER_WORKSPACE_OWNER_OIDC_ACCESS_TOKEN="+config.Metadata.WorkspaceOwnerOidcAccessToken,
216217
"CODER_WORKSPACE_ID="+config.Metadata.WorkspaceId,
217218
"CODER_WORKSPACE_OWNER_ID="+config.Metadata.WorkspaceOwnerId,
218219
)

0 commit comments

Comments
 (0)