Skip to content

feat(join-tokens): [44794] API changes for managing GitHub Join Tokens#54374

Merged
nicholasmarais1158 merged 11 commits intomasterfrom
nickmarais/feat/44794-github-join-tokens-api
May 1, 2025
Merged

feat(join-tokens): [44794] API changes for managing GitHub Join Tokens#54374
nicholasmarais1158 merged 11 commits intomasterfrom
nickmarais/feat/44794-github-join-tokens-api

Conversation

@nicholasmarais1158
Copy link
Copy Markdown
Contributor

@nicholasmarais1158 nicholasmarais1158 commented Apr 29, 2025

Description

This change prepares the Web API to support creating and editing GitHub Join Tokens from the Join Tokens page.

Issue: #44794

changelog: Fixed an issue causing Join Token expiries to be overwritten when editing a token

Changes

  • Expose github-specific fields from GET /webapi/tokens - this is required to populate the edit form
  • Support "token" in /webapi/yaml/parse/:kind - this is required to allow detection of unsupported fields
  • Use empty time.Time for token expiry in POST /webapi/tokens - also fixes a bug related to the expiry being overwritten on edit

@nicholasmarais1158 nicholasmarais1158 self-assigned this Apr 29, 2025
@nicholasmarais1158 nicholasmarais1158 added the no-changelog Indicates that a PR does not require a changelog entry label Apr 29, 2025
Copy link
Copy Markdown
Contributor Author

@nicholasmarais1158 nicholasmarais1158 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few questions.

Comment thread lib/web/join_tokens.go Outdated
Comment on lines +194 to +197
if r.Method == "PUT" {
// if using the PUT route, tokenId will be present
// in the X-Teleport-TokenName header
tokenId = r.Header.Get(HeaderTokenName)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a current pattern, or something legacy?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure - it strikes me as a little odd - @avatus may have more context?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we added it here specifically so someone could not get the secret token name from the request param in logs as JoinTokens are meant to be secret

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the context @avatus.

It makes sense for the delete endpoint, given the id is included in the path, but this endpoint doesn't use path params (POST|PUT /webapi/tokens).

Currently the Web UI doesn't use the PUT endpoint, should the header approach be retained or should we use a value from the request body instead?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request body is fine

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks.

Removed in 99dcad0.

Comment thread lib/web/join_tokens.go
Comment on lines +243 to +245
// If this is an edit, then overwrite the metadata to retain the existing fields
if existingToken != nil {
token.SetMetadata(existingToken.GetMetadata())
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a valid operation to perform? Is it safe to copy the metadata unchanged to the new token? Does it mess with the revision mechanism?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah - that's fine. The revision mechanism is actually designed to be used this way. When performing an action and providing the revision, the backend checks that the provided revision matches what is currently in the backend. You have introduced a change in behaviour here (e.g it'll now perform optimistic locking, which it didn't perform previously as the revision was wiped) but this is probably a good thing since it'll reject edits where the resource has changed since it was fetched.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't expect the revision to matter much if we are still calling UpsertToken below though.

Copy link
Copy Markdown
Contributor

@strideynet strideynet Apr 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't expect the revision to matter much if we are still calling UpsertToken below though.

Yeah - imho, we should eventually split this webapi endpoint to a seperate create/update and call the appropriate underlying Create or Update. At the moment, you can accidentally overwrite an existing token when creating a new one. But I think that's best left out of scope for this PR.

@nicholasmarais1158 nicholasmarais1158 removed the no-changelog Indicates that a PR does not require a changelog entry label Apr 29, 2025
@nicholasmarais1158 nicholasmarais1158 marked this pull request as ready for review April 29, 2025 09:36
@github-actions github-actions Bot requested review from klizhentas and zmb3 April 29, 2025 09:36
Comment thread lib/web/join_tokens_test.go Outdated
Comment on lines +482 to +485
// Skip enterprise-only methods
if method == types.JoinMethodTPM || method == types.JoinMethodSpacelift {
t.Skipf("Skipping %s, as it's enterprise-only", method)
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there an enterprise-mode that can be used to prevent having to skip enterprise-only features?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An error is returned when creating TMP and Spacelift tokens.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can do something similar to:

	modules.SetTestModules(t, &modules.TestModules{
		TestBuildType: modules.BuildEnterprise,
		TestFeatures: modules.Features{
			Cloud: true,
		},
	}

But since this modifies global state, it means that the test can't be marked parallel.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks.

Done in 2b90f08.

Copy link
Copy Markdown
Contributor

@strideynet strideynet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is nearly there - I've identified a few nits but I'm also waiting for some more context before approving.

Comment thread lib/web/join_tokens_test.go Outdated
}
}

func TestGetGithubTokens(t *testing.T) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to extend the existing TestGetTokens test table rather than introduce a new test here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in e7fd9a5.

Comment thread lib/web/join_tokens_test.go Outdated
Comment on lines +482 to +485
// Skip enterprise-only methods
if method == types.JoinMethodTPM || method == types.JoinMethodSpacelift {
t.Skipf("Skipping %s, as it's enterprise-only", method)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can do something similar to:

	modules.SetTestModules(t, &modules.TestModules{
		TestBuildType: modules.BuildEnterprise,
		TestFeatures: modules.Features{
			Cloud: true,
		},
	}

But since this modifies global state, it means that the test can't be marked parallel.

Comment thread lib/web/join_tokens.go Outdated

if editing && tokenId != req.Name {
return nil, trace.BadParameter("renaming tokens is not supported")
var tokenId string
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I'd simplify this by removing the else branch and initialising the variable with the value we'd set in the else.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 98c1c72.

require.Equal(t, map[string]string{
"test-key": "test-value",
}, editedToken.GetMetadata().Labels)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add an assertion that the expiry time isn't modified as well so we can prove that we've fixed the bug reported in #54306

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 1311d91.

@strideynet strideynet requested a review from avatus April 29, 2025 12:10
Comment thread lib/web/join_tokens.go Outdated
}

var existingToken types.ProvisionToken
if tokenId != "" {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a hunch that by this point, we definitely have a value for tokenId, so we can omit this if statement.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 98c1c72.

Copy link
Copy Markdown
Contributor Author

@nicholasmarais1158 nicholasmarais1158 Apr 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted in f0254a6. If req.Name is empty we want to avoid fetching an existing token.

@avatus
Copy link
Copy Markdown
Contributor

avatus commented Apr 29, 2025

will the updates to this endpoint support web UIs of version n-1 making this request still? for example, if this goes out in 17.5, will a webUI served by 17.4 fail when making this request to a 17.5 proxy?

@nicholasmarais1158
Copy link
Copy Markdown
Contributor Author

will the updates to this endpoint support web UIs of version n-1 making this request still? for example, if this goes out in 17.5, will a webUI served by 17.4 fail when making this request to a 17.5 proxy?

@avatus Yes, it's backwards compatible.

Out of interest, given the Proxy serves the Web UI, how would the situation of an older Web UI and new Proxy come about?

@avatus
Copy link
Copy Markdown
Contributor

avatus commented Apr 30, 2025

given the Proxy serves the Web UI, how would the situation of an older Web UI and new Proxy come about?

wonderful question. There are many users that do rolling upgrades on their clusters. For example, if i have 4 proxies and i upgrade 2 of them to N+1, if a user gets served the web UI from proxy N, their requests during their session lifetime may get routed to a proxy N api, or proxy N+1.

Its an edge case, but not as edge-y as it may sound. Sometimes, its unavoidable, but we want to keep it top of mind when we can to make sure as many features work between new/old proxy and new/old web UI.

You can read a bit more about it here

@nicholasmarais1158 nicholasmarais1158 added this pull request to the merge queue May 1, 2025
Merged via the queue into master with commit 8ba33a5 May 1, 2025
42 checks passed
@nicholasmarais1158 nicholasmarais1158 deleted the nickmarais/feat/44794-github-join-tokens-api branch May 1, 2025 13:49
@backport-bot-workflows
Copy link
Copy Markdown
Contributor

@nicholasmarais1158 See the table below for backport results.

Branch Result
branch/v16 Failed
branch/v17 Create PR

nicholasmarais1158 added a commit that referenced this pull request May 1, 2025
#54374)

* Return github-specific config from `GET /webapi/tokens`

* Support "token" in `/webapi/yaml/parse/:kind`

* Use empty time.Time for token expiry (`POST /webapi/tokens`)

* Cover enterprise token types in tests

* Cover github tokens in existing tests

* Tweak handling of `tokenId`

* Check expiry is not overwritten

* Revert removing tokenId check

* Remove use of `X-Teleport-TokenName` header
# Conflicts:
#	lib/web/join_tokens.go
#	lib/web/join_tokens_test.go
#	lib/web/yaml.go
github-merge-queue Bot pushed a commit that referenced this pull request May 2, 2025
…54454)

* feat(join-tokens): [44794] API changes for managing GitHub Join Tokens (#54374)

* Return github-specific config from `GET /webapi/tokens`

* Support "token" in `/webapi/yaml/parse/:kind`

* Use empty time.Time for token expiry (`POST /webapi/tokens`)

* Cover enterprise token types in tests

* Cover github tokens in existing tests

* Tweak handling of `tokenId`

* Check expiry is not overwritten

* Revert removing tokenId check

* Remove use of `X-Teleport-TokenName` header
# Conflicts:
#	lib/web/join_tokens.go
#	lib/web/join_tokens_test.go
#	lib/web/yaml.go

* nit: Fix comment
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants