Skip to content

Commit 6ebcb44

Browse files
authored
clients/v1: add service access token methods (#93)
Adds the RPC wrapper methods for the Service Access Token create/list/revoke RPCs. Updates the introspect wrapper to also return UserID. Part of CORE-980 ## Test plan Ran locally
1 parent 96ccfa2 commit 6ebcb44

File tree

5 files changed

+194
-38
lines changed

5 files changed

+194
-38
lines changed

clients/v1/clients.pb.go

Lines changed: 34 additions & 34 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

clients/v1/clients.proto

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -332,23 +332,23 @@ service ServiceAccessTokensService {
332332
// belong to the same service, e.g. "analytics::analytics::read" when the service is
333333
// "analytics".
334334
rpc CreateServiceAccessToken(CreateServiceAccessTokenRequest) returns (CreateServiceAccessTokenResponse) {
335-
option (sams_required_scopes) = "sams::service_access_token::write";
335+
option (sams_required_scopes) = "sams::service_access_tokens::write";
336336
}
337337
// ListServiceAccessTokens returns a list of service access tokens in reverse chronological
338338
// order by creation time. A client can only list service access tokens for services granted
339339
// via scopes, e.g. "sams::service_access_token.analytics::read" allows listing service
340340
// access tokens for the Sourcegraph Analytics service.
341341
rpc ListServiceAccessTokens(ListServiceAccessTokensRequest) returns (ListServiceAccessTokensResponse) {
342342
option idempotency_level = NO_SIDE_EFFECTS;
343-
option (sams_required_scopes) = "sams::service_access_token::read";
343+
option (sams_required_scopes) = "sams::service_access_tokens::read";
344344
}
345345
// RevokeServiceAccessToken revokes the specified service access token. A client can only revoke
346346
// service access tokens for services granted via scopes, e.g.
347347
// "sams::service_access_tokens.analytic::delete" allows revoking service access tokens for
348348
// the Sourcegraph Analytics service.
349349
rpc RevokeServiceAccessToken(RevokeServiceAccessTokenRequest) returns (RevokeServiceAccessTokenResponse) {
350350
option idempotency_level = IDEMPOTENT;
351-
option (sams_required_scopes) = "sams::service_access_token::delete";
351+
option (sams_required_scopes) = "sams::service_access_tokens::delete";
352352
}
353353
}
354354

clientv1.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,11 @@ func (c *ClientV1) Roles() *RolesServiceV1 {
175175
return &RolesServiceV1{client: c}
176176
}
177177

178+
// ServiceAccessTokens returns a client handler to interact with the ServiceAccessTokensServiceV1 API.
179+
func (c *ClientV1) ServiceAccessTokens() *ServiceAccessTokensServiceV1 {
180+
return &ServiceAccessTokensServiceV1{client: c}
181+
}
182+
178183
var (
179184
ErrNotFound = errors.New("not found")
180185
ErrRecordMismatch = errors.New("record mismatch")

clientv1_service_access_tokens.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package sams
2+
3+
import (
4+
"context"
5+
"time"
6+
7+
"connectrpc.com/connect"
8+
"google.golang.org/protobuf/types/known/timestamppb"
9+
10+
clientsv1 "github.com/sourcegraph/sourcegraph-accounts-sdk-go/clients/v1"
11+
"github.com/sourcegraph/sourcegraph-accounts-sdk-go/clients/v1/clientsv1connect"
12+
"github.com/sourcegraph/sourcegraph-accounts-sdk-go/scopes"
13+
"github.com/sourcegraph/sourcegraph-accounts-sdk-go/services"
14+
"github.com/sourcegraph/sourcegraph/lib/errors"
15+
"golang.org/x/oauth2"
16+
)
17+
18+
// ServiceAccessTokensServiceV1 provides client methods to interact with the
19+
// ServiceAccessTokensService API v1.
20+
type ServiceAccessTokensServiceV1 struct {
21+
client *ClientV1
22+
}
23+
24+
func (s *ServiceAccessTokensServiceV1) newClient(ctx context.Context) clientsv1connect.ServiceAccessTokensServiceClient {
25+
return clientsv1connect.NewServiceAccessTokensServiceClient(
26+
oauth2.NewClient(ctx, s.client.tokenSource),
27+
s.client.gRPCURL(),
28+
connect.WithInterceptors(s.client.defaultInterceptors...),
29+
)
30+
}
31+
32+
// CreateServiceAccessTokenOptions represents the optional parameters for creating a service access token.
33+
type CreateServiceAccessTokenOptions struct {
34+
// The human-friendly name of the token (optional).
35+
DisplayName string
36+
// The time the token will expire (optional, defaults to never expire).
37+
ExpiresAt *time.Time
38+
}
39+
40+
// CreateServiceAccessTokenResponse represents the response from creating a service access token.
41+
type CreateServiceAccessTokenResponse struct {
42+
Token *clientsv1.ServiceAccessToken
43+
Secret string
44+
}
45+
46+
// CreateServiceAccessToken creates a new service access token.
47+
//
48+
// Required scope: sams::service_access_tokens::write
49+
func (s *ServiceAccessTokensServiceV1) CreateServiceAccessToken(ctx context.Context, service services.Service, tokenScopes []scopes.Scope, userID string, opts CreateServiceAccessTokenOptions) (*CreateServiceAccessTokenResponse, error) {
50+
if service == "" {
51+
return nil, errors.New("service cannot be empty")
52+
}
53+
if len(tokenScopes) == 0 {
54+
return nil, errors.New("scopes cannot be empty")
55+
}
56+
if userID == "" {
57+
return nil, errors.New("user ID cannot be empty")
58+
}
59+
60+
token := &clientsv1.ServiceAccessToken{
61+
Service: string(service),
62+
Scopes: scopes.ToStrings(tokenScopes),
63+
UserId: userID,
64+
DisplayName: opts.DisplayName,
65+
}
66+
67+
if opts.ExpiresAt != nil {
68+
token.ExpireTime = timestamppb.New(*opts.ExpiresAt)
69+
}
70+
71+
req := &clientsv1.CreateServiceAccessTokenRequest{Token: token}
72+
client := s.newClient(ctx)
73+
resp, err := parseResponseAndError(client.CreateServiceAccessToken(ctx, connect.NewRequest(req)))
74+
if err != nil {
75+
return nil, err
76+
}
77+
78+
return &CreateServiceAccessTokenResponse{
79+
Token: resp.Msg.Token,
80+
Secret: resp.Msg.Secret,
81+
}, nil
82+
}
83+
84+
// ListServiceAccessTokensOptions represents the options for listing service access tokens.
85+
type ListServiceAccessTokensOptions struct {
86+
// Maximum number of results to return (optional).
87+
PageSize int32
88+
// Page token for pagination (optional).
89+
PageToken string
90+
// Service filter (optional).
91+
Service string
92+
// User ID filter (optional).
93+
UserID string
94+
// Whether to include expired tokens (optional).
95+
ShowExpired bool
96+
}
97+
98+
// ListServiceAccessTokens returns a list of service access tokens in reverse chronological
99+
// order by creation time.
100+
//
101+
// Required scope: sams::service_access_tokens::read
102+
func (s *ServiceAccessTokensServiceV1) ListServiceAccessTokens(ctx context.Context, opts ListServiceAccessTokensOptions) ([]*clientsv1.ServiceAccessToken, error) {
103+
req := &clientsv1.ListServiceAccessTokensRequest{
104+
PageSize: opts.PageSize,
105+
PageToken: opts.PageToken,
106+
}
107+
108+
// Build filters
109+
var filters []*clientsv1.ListServiceAccessTokensFilter
110+
if opts.Service != "" {
111+
filters = append(filters, &clientsv1.ListServiceAccessTokensFilter{
112+
Filter: &clientsv1.ListServiceAccessTokensFilter_Service{Service: opts.Service},
113+
})
114+
}
115+
if opts.UserID != "" {
116+
filters = append(filters, &clientsv1.ListServiceAccessTokensFilter{
117+
Filter: &clientsv1.ListServiceAccessTokensFilter_UserId{UserId: opts.UserID},
118+
})
119+
}
120+
if opts.ShowExpired {
121+
filters = append(filters, &clientsv1.ListServiceAccessTokensFilter{
122+
Filter: &clientsv1.ListServiceAccessTokensFilter_ShowExpired{ShowExpired: opts.ShowExpired},
123+
})
124+
}
125+
req.Filters = filters
126+
127+
client := s.newClient(ctx)
128+
resp, err := parseResponseAndError(client.ListServiceAccessTokens(ctx, connect.NewRequest(req)))
129+
if err != nil {
130+
return nil, err
131+
}
132+
return resp.Msg.GetTokens(), nil
133+
}
134+
135+
// RevokeServiceAccessToken revokes the specified service access token.
136+
//
137+
// Required scope: sams::service_access_tokens::delete
138+
func (s *ServiceAccessTokensServiceV1) RevokeServiceAccessToken(ctx context.Context, tokenID string) error {
139+
if tokenID == "" {
140+
return errors.New("token ID cannot be empty")
141+
}
142+
143+
req := &clientsv1.RevokeServiceAccessTokenRequest{Id: tokenID}
144+
client := s.newClient(ctx)
145+
_, err := parseResponseAndError(client.RevokeServiceAccessToken(ctx, connect.NewRequest(req)))
146+
return err
147+
}

clientv1_tokens.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,11 @@ type IntrospectTokenResponse struct {
4040
Scopes scopes.Scopes
4141
// ClientID is the identifier of the SAMS client that the token was issued to.
4242
ClientID string
43-
// ExpiresAt indicates when the token expires.
43+
// ExpiresAt indicates when the token expires. If the token has no expiry, this
44+
// will be the zero value.
4445
ExpiresAt time.Time
46+
// UserID if set is the identifier of the user that the Service Access Token was issued to.
47+
UserID string
4548
}
4649

4750
// IntrospectToken takes a SAMS access token and returns relevant metadata.
@@ -74,6 +77,7 @@ func (s *TokensServiceV1) IntrospectToken(ctx context.Context, token string) (*I
7477
Scopes: scopes.ToScopes(resp.Msg.Scopes),
7578
ClientID: resp.Msg.ClientId,
7679
ExpiresAt: resp.Msg.ExpiresAt.AsTime(),
80+
UserID: resp.Msg.UserId,
7781
}
7882
if s.introspectTokenCache != nil && tokenResponse.ExpiresAt.After(time.Now()) {
7983
_ = s.introspectTokenCache.Add(token, tokenResponse)

0 commit comments

Comments
 (0)