Skip to content

Commit 1d2ff0b

Browse files
committed
keys: add support for federated identities
Add support for creating and setting federated identities. Add support for setting OAuth clients. Updates tailscale/corp#33052 Signed-off-by: Mario Minardi <mario@tailscale.com>
1 parent 76abff4 commit 1d2ff0b

File tree

2 files changed

+237
-13
lines changed

2 files changed

+237
-13
lines changed

keys.go

Lines changed: 102 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -45,21 +45,71 @@ type createOAuthClientWithKeyTypeRequest struct {
4545
CreateOAuthClientRequest
4646
}
4747

48+
// SetOAuthClientRequest describes the definition of an existing OAuth client to
49+
// set (wholesale update) the configuration of.
50+
type SetOAuthClientRequest struct {
51+
Scopes []string `json:"scopes"`
52+
Tags []string `json:"tags"`
53+
Description string `json:"description"`
54+
}
55+
56+
type setOAuthClientWithKeyTypeRequest struct {
57+
KeyType string `json:"keyType"`
58+
SetOAuthClientRequest
59+
}
60+
61+
// CreateFederatedIdentityRequest describes the definition of a federated identity to create.
62+
type CreateFederatedIdentityRequest struct {
63+
Scopes []string `json:"scopes"`
64+
Tags []string `json:"tags"`
65+
Audience string `json:"audience"`
66+
Issuer string `json:"issuer"`
67+
Subject string `json:"subject"`
68+
CustomClaimRules map[string]string `json:"customClaimRules"`
69+
Description string `json:"description"`
70+
}
71+
72+
type createFederatedIdentityWithKeyTypeRequest struct {
73+
KeyType string `json:"keyType"`
74+
CreateFederatedIdentityRequest
75+
}
76+
77+
// SetFederatedIdentityRequest describes the definition of a federated identity to create.
78+
type SetFederatedIdentityRequest struct {
79+
Scopes []string `json:"scopes"`
80+
Tags []string `json:"tags"`
81+
Audience string `json:"audience"`
82+
Issuer string `json:"issuer"`
83+
Subject string `json:"subject"`
84+
CustomClaimRules map[string]string `json:"customClaimRules"`
85+
Description string `json:"description"`
86+
}
87+
88+
type setFederatedIdentityWithKeyTypeRequest struct {
89+
KeyType string `json:"keyType"`
90+
SetFederatedIdentityRequest
91+
}
92+
4893
// Key describes an authentication key within the tailnet.
4994
type Key struct {
50-
ID string `json:"id"`
51-
KeyType string `json:"keyType"`
52-
Key string `json:"key"`
53-
Description string `json:"description"`
54-
ExpirySeconds *time.Duration `json:"expirySeconds"`
55-
Created time.Time `json:"created"`
56-
Expires time.Time `json:"expires"`
57-
Revoked time.Time `json:"revoked"`
58-
Invalid bool `json:"invalid"`
59-
Capabilities KeyCapabilities `json:"capabilities"`
60-
Scopes []string `json:"scopes,omitempty"`
61-
Tags []string `json:"tags,omitempty"`
62-
UserID string `json:"userId"`
95+
ID string `json:"id"`
96+
KeyType string `json:"keyType"`
97+
Key string `json:"key"`
98+
Description string `json:"description"`
99+
ExpirySeconds *time.Duration `json:"expirySeconds"`
100+
Created time.Time `json:"created"`
101+
Updated time.Time `json:"updated"`
102+
Expires time.Time `json:"expires"`
103+
Revoked time.Time `json:"revoked"`
104+
Invalid bool `json:"invalid"`
105+
Capabilities KeyCapabilities `json:"capabilities"`
106+
Scopes []string `json:"scopes,omitempty"`
107+
Tags []string `json:"tags,omitempty"`
108+
UserID string `json:"userId"`
109+
Audience string `json:"audience"`
110+
Issuer string `json:"issuer"`
111+
Subject string `json:"subject"`
112+
CustomClaimRules map[string]string `json:"customClaimRules"`
63113
}
64114

65115
// Create creates a new authentication key. Returns the generated [Key] if successful.
@@ -91,6 +141,45 @@ func (kr *KeysResource) CreateOAuthClient(ctx context.Context, ckr CreateOAuthCl
91141
return body[Key](kr, req)
92142
}
93143

144+
// SetOAuthClient sets the configuration for an existing OAuth client. Returns the generated [Key] if successful.
145+
func (kr *KeysResource) SetOAuthClient(ctx context.Context, id string, skr SetOAuthClientRequest) (*Key, error) {
146+
req, err := kr.buildRequest(ctx, http.MethodPut, kr.buildTailnetURL("keys", id), requestBody(setOAuthClientWithKeyTypeRequest{
147+
KeyType: "client",
148+
SetOAuthClientRequest: skr,
149+
}))
150+
if err != nil {
151+
return nil, err
152+
}
153+
154+
return body[Key](kr, req)
155+
}
156+
157+
// CreateFederatedIdentity creates a new federated identity. Returns the generated [Key] if successful.
158+
func (kr *KeysResource) CreateFederatedIdentity(ctx context.Context, ckr CreateFederatedIdentityRequest) (*Key, error) {
159+
req, err := kr.buildRequest(ctx, http.MethodPost, kr.buildTailnetURL("keys"), requestBody(createFederatedIdentityWithKeyTypeRequest{
160+
KeyType: "federated",
161+
CreateFederatedIdentityRequest: ckr,
162+
}))
163+
if err != nil {
164+
return nil, err
165+
}
166+
167+
return body[Key](kr, req)
168+
}
169+
170+
// SetFederatedIdentity sets the configuration for an existing federated identity. Returns the generated [Key] if successful.
171+
func (kr *KeysResource) SetFederatedIdentity(ctx context.Context, id string, skr SetFederatedIdentityRequest) (*Key, error) {
172+
req, err := kr.buildRequest(ctx, http.MethodPut, kr.buildTailnetURL("keys", id), requestBody(setFederatedIdentityWithKeyTypeRequest{
173+
KeyType: "federated",
174+
SetFederatedIdentityRequest: skr,
175+
}))
176+
if err != nil {
177+
return nil, err
178+
}
179+
180+
return body[Key](kr, req)
181+
}
182+
94183
// Get returns all information on a [Key] whose identifier matches the one provided. This will not return the
95184
// authentication key itself, just the metadata.
96185
func (kr *KeysResource) Get(ctx context.Context, id string) (*Key, error) {

keys_test.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,141 @@ func TestClient_CreateOAuthClient(t *testing.T) {
184184
assert.EqualValues(t, "", actualReq.Description)
185185
}
186186

187+
func TestClient_SetOAuthClient(t *testing.T) {
188+
t.Parallel()
189+
190+
client, server := NewTestHarness(t)
191+
server.ResponseCode = http.StatusOK
192+
193+
expected := &Key{
194+
ID: "test",
195+
KeyType: "client",
196+
Key: "thisisatestclient",
197+
ExpirySeconds: nil,
198+
Created: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
199+
Expires: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
200+
Scopes: []string{"all:read"},
201+
Tags: []string{"tag:test"},
202+
Description: "",
203+
}
204+
205+
server.ResponseBody = expected
206+
207+
actual, err := client.Keys().SetOAuthClient(context.Background(), "test", SetOAuthClientRequest{
208+
Scopes: []string{"all:read"},
209+
Tags: []string{"tag:test"},
210+
})
211+
assert.NoError(t, err)
212+
assert.EqualValues(t, expected, actual)
213+
assert.Equal(t, http.MethodPut, server.Method)
214+
assert.Equal(t, "/api/v2/tailnet/example.com/keys/test", server.Path)
215+
216+
var actualReq createOAuthClientWithKeyTypeRequest
217+
assert.NoError(t, json.Unmarshal(server.Body.Bytes(), &actualReq))
218+
assert.EqualValues(t, "client", actualReq.KeyType)
219+
assert.EqualValues(t, 1, len(actualReq.Scopes))
220+
assert.EqualValues(t, "all:read", actualReq.Scopes[0])
221+
assert.EqualValues(t, 1, len(actualReq.Tags))
222+
assert.EqualValues(t, "tag:test", actualReq.Tags[0])
223+
assert.EqualValues(t, "", actualReq.Description)
224+
}
225+
226+
func TestClient_CreateFederatedIdentity(t *testing.T) {
227+
t.Parallel()
228+
229+
client, server := NewTestHarness(t)
230+
server.ResponseCode = http.StatusOK
231+
232+
expected := &Key{
233+
ID: "test",
234+
KeyType: "federated",
235+
Key: "thisisatestclient",
236+
ExpirySeconds: nil,
237+
Created: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
238+
Expires: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
239+
Scopes: []string{"all:read"},
240+
Tags: []string{"tag:test"},
241+
Description: "",
242+
Audience: "exampleaud",
243+
Subject: "examplesubject",
244+
CustomClaimRules: map[string]string{
245+
"foo": "bar",
246+
},
247+
}
248+
249+
server.ResponseBody = expected
250+
251+
actual, err := client.Keys().CreateFederatedIdentity(context.Background(), CreateFederatedIdentityRequest{
252+
Scopes: []string{"all:read"},
253+
Tags: []string{"tag:test"},
254+
Subject: "examplesubject",
255+
CustomClaimRules: map[string]string{
256+
"foo": "bar",
257+
},
258+
})
259+
assert.NoError(t, err)
260+
assert.EqualValues(t, expected, actual)
261+
assert.Equal(t, http.MethodPost, server.Method)
262+
assert.Equal(t, "/api/v2/tailnet/example.com/keys", server.Path)
263+
264+
var actualReq createFederatedIdentityWithKeyTypeRequest
265+
assert.NoError(t, json.Unmarshal(server.Body.Bytes(), &actualReq))
266+
assert.EqualValues(t, "federated", actualReq.KeyType)
267+
assert.EqualValues(t, 1, len(actualReq.Scopes))
268+
assert.EqualValues(t, "all:read", actualReq.Scopes[0])
269+
assert.EqualValues(t, 1, len(actualReq.Tags))
270+
assert.EqualValues(t, "tag:test", actualReq.Tags[0])
271+
assert.EqualValues(t, "", actualReq.Description)
272+
}
273+
274+
func TestClient_SetFederatedIdentity(t *testing.T) {
275+
t.Parallel()
276+
277+
client, server := NewTestHarness(t)
278+
server.ResponseCode = http.StatusOK
279+
280+
expected := &Key{
281+
ID: "test",
282+
KeyType: "federated",
283+
Key: "thisisatestclient",
284+
ExpirySeconds: nil,
285+
Created: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
286+
Expires: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
287+
Scopes: []string{"all:read"},
288+
Tags: []string{"tag:test"},
289+
Description: "",
290+
Audience: "exampleaud",
291+
Subject: "examplesubject",
292+
CustomClaimRules: map[string]string{
293+
"foo": "bar",
294+
},
295+
}
296+
297+
server.ResponseBody = expected
298+
299+
actual, err := client.Keys().SetFederatedIdentity(context.Background(), "test", SetFederatedIdentityRequest{
300+
Scopes: []string{"all:read"},
301+
Tags: []string{"tag:test"},
302+
Subject: "examplesubject",
303+
CustomClaimRules: map[string]string{
304+
"foo": "bar",
305+
},
306+
})
307+
assert.NoError(t, err)
308+
assert.EqualValues(t, expected, actual)
309+
assert.Equal(t, http.MethodPut, server.Method)
310+
assert.Equal(t, "/api/v2/tailnet/example.com/keys/test", server.Path)
311+
312+
var actualReq createFederatedIdentityWithKeyTypeRequest
313+
assert.NoError(t, json.Unmarshal(server.Body.Bytes(), &actualReq))
314+
assert.EqualValues(t, "federated", actualReq.KeyType)
315+
assert.EqualValues(t, 1, len(actualReq.Scopes))
316+
assert.EqualValues(t, "all:read", actualReq.Scopes[0])
317+
assert.EqualValues(t, 1, len(actualReq.Tags))
318+
assert.EqualValues(t, "tag:test", actualReq.Tags[0])
319+
assert.EqualValues(t, "", actualReq.Description)
320+
}
321+
187322
func TestClient_GetKey(t *testing.T) {
188323
t.Parallel()
189324

0 commit comments

Comments
 (0)