-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
Conversation
There was a problem hiding this 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
unless i'm missing something, i can't access the yaml editor until after I am editing something 🤔 |
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 |
had to rebase, sorry. new commit here 3aa99c9 |
tokenId := r.Header.Get(HeaderTokenName) | ||
if tokenId == "" { | ||
return nil, trace.BadParameter("requires a token name to edit") | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
lib/web/apiserver.go
Outdated
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)) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
lib/web/ui/join_token.go
Outdated
AllowRules []string `json:"allowRules,omitempty"` | ||
// Allow is a list of allow rules | ||
Allow []*types.TokenRule `json:"allow,omitempty"` | ||
GCP *types.ProvisionTokenSpecV2GCP `json:"gcp,omitempty"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing godoc
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, | ||
})); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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'} |
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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>>; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great point, will 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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; |
lib/web/apiserver.go
Outdated
// 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)) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
smort
const newRules = [...rules].filter((rule, i) => { | ||
if (index === i) { | ||
return false; | ||
} | ||
return rule; | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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]
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
const newRules = [...rules].filter((rule, i) => { | ||
if (index === i) { | ||
return false; | ||
} | ||
return rule; | ||
}); |
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
label="Add Locatons" | |
label="Add Locations" |
{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> | ||
)} |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
placeholder="aws account ID" | |
placeholder="AWS Account ID" |
'Discovery', | ||
].map(role => ({ value: role as JoinRole, label: role as JoinRole })); | ||
|
||
const availableJoinMethods: Option<JoinMethod, JoinMethod>[] = [ |
There was a problem hiding this comment.
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')} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rule={requiredField('At least 1 projectID required')} | |
rule={requiredField('At least 1 project ID required')} |
I will backport this in a group with the rest of the previous supporting PRs |
There was a problem hiding this 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
lib/web/apiserver.go
Outdated
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)) |
There was a problem hiding this comment.
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)?
lib/web/apiserver.go
Outdated
@@ -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)) |
There was a problem hiding this comment.
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)
h.PUT("/webapi/token/yaml", h.WithAuth(h.editTokenYAML)) | |
h.PUT("/webapi/token/yaml", h.WithAuth(h.updateTokenYAML)) |
lib/web/join_tokens.go
Outdated
@@ -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.") |
There was a problem hiding this comment.
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
return nil, trace.BadParameter("renaming tokens is not supported.") | |
return nil, trace.BadParameter("renaming tokens is not supported") |
lib/web/join_tokens.go
Outdated
} | ||
|
||
if editing && tokenId != req.Name { | ||
return nil, trace.BadParameter("renaming tokens is not supported.") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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) |
There was a problem hiding this comment.
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"
There was a problem hiding this comment.
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[] { |
There was a problem hiding this comment.
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]" |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 = ({ |
There was a problem hiding this comment.
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 "?"`} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this 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" |
There was a problem hiding this comment.
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)
I'll add more to the sorry now that the storybook was updated and some tests |
There was a problem hiding this 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
true, added some more around adding/deleting rules and also updating select join roles |
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