Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add token create forms to Join Tokens UI #44408

Merged
merged 1 commit into from
Jul 26, 2024
Merged

Add token create forms to Join Tokens UI #44408

merged 1 commit into from
Jul 26, 2024

Conversation

avatus
Copy link
Contributor

@avatus avatus commented Jul 18, 2024

This adds the create tokens forms to our Join Tokens UI. The forms only support IAM, GCP and tokens for now (preferred) and allow a yaml editor for other types. You can also edit IAM and GCP and "token" in yaml if you wish but its purposefully kinda "hidden".

This adds the feature to the navigation under "Join Tokens".

closes #31671

@avatus avatus added the no-changelog Indicates that a PR does not require a changelog entry label Jul 18, 2024
@avatus avatus requested review from kimlisa and rudream July 18, 2024 18:35
@github-actions github-actions bot requested a review from ryanclark July 18, 2024 18:36
Copy link
Contributor

@kimlisa kimlisa left a comment

Choose a reason for hiding this comment

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

some other UX issues:

  • when the dialog is open (viewing a token), if i press the create button or try viewing other tokens, my current opened dialog does not update the state (btw you can trigger a component re-mount by defining a key attribute)
  • when i delete the token that i am viewing, it does not close the dialog

web/packages/teleport/src/services/joinToken/joinToken.ts Outdated Show resolved Hide resolved
lib/web/apiserver.go Outdated Show resolved Hide resolved
lib/web/apiserver.go Outdated Show resolved Hide resolved
lib/web/join_tokens.go Outdated Show resolved Hide resolved
@kimlisa
Copy link
Contributor

kimlisa commented Jul 19, 2024

The forms only support IAM, GCP and tokens for now (preferred) and allow a yaml editor for other types

unless i'm missing something, i can't access the yaml editor until after I am editing something 🤔

@avatus
Copy link
Contributor Author

avatus commented Jul 19, 2024

We don't allow a YAML editor to create at all and push towards creating only IAM or GCP. So you are correct that you will only see it while editing

@avatus avatus force-pushed the avatus/joinforms branch from dc1cb8b to 3aa99c9 Compare July 22, 2024 23:27
@avatus
Copy link
Contributor Author

avatus commented Jul 22, 2024

had to rebase, sorry. new commit here 3aa99c9

@avatus avatus requested a review from kimlisa July 22, 2024 23:28
Comment on lines +151 to +154
tokenId := r.Header.Get(HeaderTokenName)
if tokenId == "" {
return nil, trace.BadParameter("requires a token name to edit")
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this make the endpoint not backwards compatible? Should this use a new endpoint instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

These are new endpoints (I'm editing this one but it isn't actually used/released yet, just from my last PR) so no worry about backward compatibility here.

h.POST("/webapi/token", h.WithAuth(h.createTokenHandle))
h.PUT("/webapi/token/yaml", h.WithAuth(h.editTokenYAML))
// used for creating a new token
h.POST("/webapi/token/new", h.WithAuth(h.upsertTokenHandle))
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we have /new in the URL? I don't see any other endpoint that uses this convention

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can think of something better than /new, sure

Copy link
Contributor Author

Choose a reason for hiding this comment

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

i just made it plural "/webapi/tokens", as posting to a REST api should end in the plural of the resource anyway.

AllowRules []string `json:"allowRules,omitempty"`
// Allow is a list of allow rules
Allow []*types.TokenRule `json:"allow,omitempty"`
GCP *types.ProvisionTokenSpecV2GCP `json:"gcp,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing godoc

Comment on lines 55 to 71
const joinRoleOptions: Option<JoinRole, JoinRole>[] = [
'App',
'Node',
'Db',
'Kube',
'Bot',
'WindowsDesktop',
'Discovery',
].map(role => ({ value: role as JoinRole, label: role as JoinRole }));

const availableJoinMethods: Option<JoinMethod, JoinMethod>[] = [
'iam',
'gcp',
].map(method => ({
value: method as JoinMethod,
label: method as JoinMethod,
}));
Copy link
Contributor

Choose a reason for hiding this comment

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

Should the labels for these options be something a bit more user friendly?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I chose this to match what's in the documentation and YAML for these join methods. I think in this case particularly this would be the most friendly/understood.

</ButtonIcon>
</HoverTooltip>
<Text typography="h3" fontWeight={400}>
{editToken ? `Edit Token` : 'Create a New Join Token'}
Copy link
Contributor

Choose a reason for hiding this comment

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

Here the button would be "Create a New Join Token" and elsewhere there's a button that's "Create new token" - we should be consistent on capitalisation

editTokenWithYAML(editToken.id);
}}
>
<Text color="buttons.link.default">Use YAML editor instead</Text>
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe "instead" isn't needed here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wanted to imply "one or the other" here but, i'll just remove "instead" and see if any user complains. thank you!

cluster. This will remove the ability for any new resources to join
with this token and any non-renewable resource from renewing.
cluster. This will remove the ability for any new resources to join or
renew with this token and any non-renewable resource from renewing.
Copy link
Contributor

Choose a reason for hiding this comment

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

"to join or renew with this token" - the "or renew with this token" part sounds a bit weird to me

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll remove as "renew" is kind of a join anyway

setTokenState,
}: {
rules: NewJoinTokenGCPState[];
setTokenState: React.Dispatch<React.SetStateAction<NewJoinTokenState>>;
Copy link
Contributor

Choose a reason for hiding this comment

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

I've never really been a fan of passing the React.Dispatch method to child components - I think components should care about themselves and offer a onXXX prop, then the parent component updates its own state instead of passing down the setState method

https://medium.com/@christopherthai/why-you-shouldnt-pass-react-s-setstate-as-a-prop-a-deep-dive-8a3dcd74bec8

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great point, will change!

Comment on lines 367 to 370
padding-top: ${props => props.theme.space[3]}px;
padding-bottom: ${props => props.theme.space[3]}px;
padding-left: ${props => props.theme.space[3]}px;
padding-right: ${props => props.theme.space[3]}px;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
padding-top: ${props => props.theme.space[3]}px;
padding-bottom: ${props => props.theme.space[3]}px;
padding-left: ${props => props.theme.space[3]}px;
padding-right: ${props => props.theme.space[3]}px;
padding: ${props => props.theme.space[3]}px;

@avatus avatus requested a review from ryanclark July 23, 2024 16:08
@avatus avatus force-pushed the avatus/joinforms branch from 7a4ad95 to efc405f Compare July 24, 2024 16:15
// used for creating a new token
h.POST("/webapi/tokens", h.WithAuth(h.upsertTokenHandle))
// used for updating a token
h.PUT("/webapi/token", h.WithAuth(h.upsertTokenHandle))
Copy link
Contributor

Choose a reason for hiding this comment

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

Normally a REST API would be something like PUT /webapi/token/name for editing, and not rely on an arbitrary header for specifying the token name - thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We've chosen to do the header for tokens to obfuscate a potentially secret token name from request logs

Copy link
Contributor

Choose a reason for hiding this comment

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

smort

Comment on lines 380 to 385
const newRules = [...rules].filter((rule, i) => {
if (index === i) {
return false;
}
return rule;
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
const newRules = [...rules].filter((rule, i) => {
if (index === i) {
return false;
}
return rule;
});
const newRules = rules.filter((rule, i) => index !== i);

filter expects a boolean so we should be returning true or false, and .filter creates a new copy of the array so there's no need for [...rules]

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah I think this was originally a map and then a filter and then map and then ended up with this monstrosity. Thanks for the catch

))}
<ButtonText onClick={addNewRule}>
<Plus size={16} mr={2} />
Add another AWS Account
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be "Add another AWS rule" or something?

Comment on lines 477 to 482
const newRules = [...rules].filter((rule, i) => {
if (index === i) {
return false;
}
return rule;
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Same as above

updateRuleField(index, 'locations', opts as OptionGCP[])
}
value={rule.locations}
label="Add Locatons"
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
label="Add Locatons"
label="Add Locations"

Comment on lines 434 to 438
{index > 0 && ( // at least one rule is required, so lets not allow the user to remove it
<ButtonIcon onClick={() => removeRule(index)}>
<Trash size={16} color="text.muted" />
</ButtonIcon>
)}
Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment about rules.length > 1

>
<Flex alignItems="center" justifyContent="space-between">
<Text fontWeight={700} mb={2}>
AWS Account
Copy link
Contributor

Choose a reason for hiding this comment

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

"AWS Rule"?

<FieldInput
label="Account ID"
rule={requiredField('Account ID is required')}
placeholder="aws account ID"
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
placeholder="aws account ID"
placeholder="AWS Account ID"

'Discovery',
].map(role => ({ value: role as JoinRole, label: role as JoinRole }));

const availableJoinMethods: Option<JoinMethod, JoinMethod>[] = [
Copy link
Contributor

Choose a reason for hiding this comment

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

As these are typed as OptionJoinMethod and OptionJoinRole below, this should probably use those types

}
value={rule.project_ids}
label="Add Project ID(s)"
rule={requiredField('At least 1 projectID required')}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
rule={requiredField('At least 1 projectID required')}
rule={requiredField('At least 1 project ID required')}

@avatus avatus requested a review from ryanclark July 25, 2024 16:21
@avatus
Copy link
Contributor Author

avatus commented Jul 25, 2024

I will backport this in a group with the rest of the previous supporting PRs

Copy link
Contributor

@kimlisa kimlisa left a comment

Choose a reason for hiding this comment

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

overall lgtm, there is one change that will make creating integrations break that needs to be addressed

Comment on lines 774 to 778
h.PUT("/webapi/token/yaml", h.WithAuth(h.editTokenYAML))
// used for creating a new token
h.POST("/webapi/tokens", h.WithAuth(h.upsertTokenHandle))
// used for updating a token
h.PUT("/webapi/token", h.WithAuth(h.upsertTokenHandle))
Copy link
Contributor

Choose a reason for hiding this comment

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

should all these be /webapi/tokens (plura)?

@@ -771,8 +771,13 @@ func (h *Handler) bindDefaultEndpoints() {
h.GET("/webapi/auth/export", h.authExportPublic)

// join token handlers
h.PUT("/webapi/token/yaml", h.WithAuth(h.upsertTokenContent))
h.POST("/webapi/token", h.WithAuth(h.createTokenHandle))
h.PUT("/webapi/token/yaml", h.WithAuth(h.editTokenYAML))
Copy link
Contributor

Choose a reason for hiding this comment

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

nit. the UI tends use the word edit but backend uses update (afaik)

Suggested change
h.PUT("/webapi/token/yaml", h.WithAuth(h.editTokenYAML))
h.PUT("/webapi/token/yaml", h.WithAuth(h.updateTokenYAML))

@@ -158,6 +163,10 @@ func (h *Handler) upsertTokenContent(w http.ResponseWriter, r *http.Request, par
return nil, trace.Wrap(err)
}

if tokenId != extractedRes.Metadata.Name {
return nil, trace.BadParameter("renaming tokens is not supported.")
Copy link
Contributor

Choose a reason for hiding this comment

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

can't remember... i think error messaging only has proper period stuff if the msg starts with a capital

Suggested change
return nil, trace.BadParameter("renaming tokens is not supported.")
return nil, trace.BadParameter("renaming tokens is not supported")

}

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

Choose a reason for hiding this comment

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

Suggested change
return nil, trace.BadParameter("renaming tokens is not supported.")
return nil, trace.BadParameter("renaming tokens is not supported")

expires := time.Now().UTC().Add(defaults.NodeJoinTokenTTL)
// IAM and GCP tokens should never expire
if req.JoinMethod == types.JoinMethodGCP || req.JoinMethod == types.JoinMethodIAM {
expires = time.Now().UTC().AddDate(1000, 0, 0)
Copy link
Contributor

Choose a reason for hiding this comment

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

is there a const for 1000? to consistently set a "never expires"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We could add a constant but the time package doesn't have Year (at least my editor doesn't seem to think so) so it'd end up being only a const of 1000 which might not get across that its a "time" so probably best to leave out

@@ -76,8 +81,7 @@ export const JoinTokens = () => {
{ join_token: '' } // we are only editing for now, so template can be empty
);

async function handleSave(content: string): Promise<void> {
const token = await ctx.joinTokenService.upsertJoinToken({ content });
function updateTokenList(token: JoinToken): JoinToken[] {
Copy link
Contributor

Choose a reason for hiding this comment

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

does line 97 still apply? (since we prevent user from changing the name on edit now)

labelTip="Allows regions and/or zones."
/>
<FieldSelectCreatable
placeholder="[email protected]"
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe a shorter email example? b/c i can't see the entire text in the input box

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think its in a specific format according to the docs, but i'll remove _NUMBER and that'll shorten it enough i think

padding: ${props => props.theme.space[3]}px;
`;

const JoinTokenIAMForm = ({
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: i would pull these forms (JoinTokenIAMForm and JoinTokenGCPForm) out into its own file?

/>
<FieldInput
label="ARN"
toolTipContent={`The joining nodes must match this ARN. Supports wildcards "*" and "?"`}
Copy link
Contributor

Choose a reason for hiding this comment

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

does this need to be a specific role arn format? can we maybe provide example? like how do you use the question mark in the arn?

eg (this is IAM role arn format)

  • arn:aws:iam::account-id:role/role-name
  • arn:aws:iam::account-id:role/*
  • question mark?

Copy link
Contributor

Choose a reason for hiding this comment

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

rename to UpsertXXX maybe? since it does both create and edit

@avatus avatus requested a review from kimlisa July 26, 2024 12:11
Copy link
Contributor

@ryanclark ryanclark left a comment

Choose a reason for hiding this comment

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

Can you add some tests?

)}
</Flex>
<FieldSelectCreatable
placeholder="Type a project ID"
Copy link
Contributor

Choose a reason for hiding this comment

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

We should be consistent on capitalisation (project ID vs Project ID in the required rule)

@avatus
Copy link
Contributor Author

avatus commented Jul 26, 2024

I'll add more to the sorry now that the storybook was updated and some tests

@avatus avatus requested a review from ryanclark July 26, 2024 16:22
Copy link
Contributor

@ryanclark ryanclark left a comment

Choose a reason for hiding this comment

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

LGTM. might be worth adding some more tests in the future around the rule configuration

@avatus
Copy link
Contributor Author

avatus commented Jul 26, 2024

might be worth adding some more tests in the future around the rule configuration

true, added some more around adding/deleting rules and also updating select join roles

@avatus avatus force-pushed the avatus/joinforms branch from f849632 to b118038 Compare July 26, 2024 17:25
@avatus
Copy link
Contributor Author

avatus commented Jul 26, 2024

Rebased. @kimlisa the feedback for your comments starts with 8f919a3

@avatus avatus mentioned this pull request Jul 26, 2024
@public-teleport-github-review-bot public-teleport-github-review-bot bot removed the request for review from rudream July 26, 2024 18:30
@avatus avatus force-pushed the avatus/joinforms branch from 332f29e to 21fe53a Compare July 26, 2024 18:31
@avatus avatus enabled auto-merge July 26, 2024 18:33
@avatus avatus added this pull request to the merge queue Jul 26, 2024
Merged via the queue into master with commit 8e9cfc6 Jul 26, 2024
43 checks passed
@avatus avatus deleted the avatus/joinforms branch July 26, 2024 19:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
no-changelog Indicates that a PR does not require a changelog entry size/lg ui
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Provide a way to manage Teleport join tokens via web UI
3 participants