Skip to content

Commit

Permalink
Add token list/delete endpoints
Browse files Browse the repository at this point in the history
This PR implements list/delete for tokens in the api the same way as
tctl interacts with them. This does not include pagination yet but is
currently being used while the feature is in development. Pagination can
come in the future in the form of a sort cache, but the endpoints won't
change
  • Loading branch information
avatus committed Jun 4, 2024
1 parent 39e4c0d commit 004e95b
Show file tree
Hide file tree
Showing 5 changed files with 369 additions and 2 deletions.
10 changes: 9 additions & 1 deletion lib/auth/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -374,9 +374,17 @@ func NewTestAuthServer(cfg TestAuthServerConfig) (*TestAuthServer, error) {
return nil, trace.Wrap(err)
}

token, err := types.NewProvisionTokenFromSpec("static-token", time.Time{}, types.ProvisionTokenSpecV2{
Roles: types.SystemRoles{types.RoleNode},
})
if err != nil {
return nil, trace.Wrap(err)
}
// set static tokens
staticTokens, err := types.NewStaticTokens(types.StaticTokensSpecV2{
StaticTokens: []types.ProvisionTokenV1{},
StaticTokens: []types.ProvisionTokenV1{
*token.V1(),
},
})
if err != nil {
return nil, trace.Wrap(err)
Expand Down
4 changes: 3 additions & 1 deletion lib/web/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -803,8 +803,10 @@ func (h *Handler) bindDefaultEndpoints() {
h.GET("/webapi/sites/:site/auth/export", h.authExportPublic)
h.GET("/webapi/auth/export", h.authExportPublic)

// token generation
// join token handlers
h.POST("/webapi/token", h.WithAuth(h.createTokenHandle))
h.GET("/webapi/tokens", h.WithAuth(h.getTokens))
h.DELETE("/webapi/tokens/:id", h.WithAuth(h.deleteToken))

// join scripts
h.GET("/scripts/:token/install-node.sh", h.WithLimiter(h.getNodeJoinScriptHandle))
Expand Down
45 changes: 45 additions & 0 deletions lib/web/join_tokens.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,51 @@ func automaticUpgrades(features proto.Features) bool {
return features.AutomaticUpgrades && features.Cloud
}

// Currently we aren't paginating this endpoint as we don't
// expect many tokens to exist at a time. I'm leaving it in a "paginated" form
// without a nextKey for now so implementing pagination won't change the response shape
// TODO (avatus) implement pagination

// GetTokensResponse returns a list of JoinTokens.
type GetTokensResponse struct {
Items []ui.JoinToken `json:"items"`
}

func (h *Handler) getTokens(w http.ResponseWriter, r *http.Request, params httprouter.Params, ctx *SessionContext) (interface{}, error) {
clt, err := ctx.GetClient()
if err != nil {
return nil, trace.Wrap(err)
}

tokens, err := clt.GetTokens(r.Context())
if err != nil {
return nil, trace.Wrap(err)
}

return GetTokensResponse{
Items: ui.MakeJoinTokens(tokens),
}, nil
}

func (h *Handler) deleteToken(_ http.ResponseWriter, r *http.Request, params httprouter.Params, ctx *SessionContext) (interface{}, error) {
token := params.ByName("id")
if token == "" {
return nil, trace.BadParameter("requires a token to delete")
}

clt, err := ctx.GetClient()
if err != nil {
return nil, trace.Wrap(err)
}

err = clt.DeleteToken(r.Context(), token)
if err != nil {
return nil, trace.Wrap(err)
}

return OK(), nil
}

func (h *Handler) createTokenHandle(w http.ResponseWriter, r *http.Request, params httprouter.Params, ctx *SessionContext) (interface{}, error) {
var req types.ProvisionTokenSpecV2
if err := httplib.ReadJSON(r, &req); err != nil {
Expand Down
239 changes: 239 additions & 0 deletions lib/web/join_tokens_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@ package web
import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
"net/url"
"regexp"
"testing"
"time"

"github.com/gravitational/trace"
"github.com/stretchr/testify/require"
Expand All @@ -32,8 +35,12 @@ import (
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/utils"
"github.com/gravitational/teleport/lib/auth/authclient"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/fixtures"
"github.com/gravitational/teleport/lib/modules"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/web/ui"
)

func TestGenerateIAMTokenName(t *testing.T) {
Expand Down Expand Up @@ -74,6 +81,238 @@ func TestGenerateIAMTokenName(t *testing.T) {
require.NotEqual(t, hash1, hash2)
}

type tokenData struct {
name string
roles types.SystemRoles
expiry time.Time
}

func Test_getTokens(t *testing.T) {
t.Parallel()
username := "[email protected]"
ctx := context.Background()
expiry := time.Now().UTC().Add(30 * time.Minute)

staticUiToken := ui.JoinToken{
Id: "static-token",
SafeName: "************",
Roles: types.SystemRoles{types.RoleNode},
IsStatic: true,
Method: types.JoinMethodToken,
}

tt := []struct {
name string
tokenData []tokenData
expected []ui.JoinToken
noAccess bool
includeUserToken bool
}{
{
name: "no access",
tokenData: []tokenData{},
noAccess: true,
expected: []ui.JoinToken{},
},
{
name: "only static tokens exist",
tokenData: []tokenData{},
expected: []ui.JoinToken{
staticUiToken,
},
},
{
name: "static and sign up tokens",
tokenData: []tokenData{},
expected: []ui.JoinToken{
staticUiToken,
},
includeUserToken: true,
},
{
name: "all tokens",
tokenData: []tokenData{
{
name: "test-token",
roles: types.SystemRoles{
types.RoleNode,
},
expiry: expiry,
},
{
name: "test-token-2",
roles: types.SystemRoles{
types.RoleNode,
types.RoleDatabase,
},
expiry: expiry,
},
{
name: "test-token-3-and-super-duper-long",
roles: types.SystemRoles{
types.RoleNode,
types.RoleKube,
types.RoleDatabase,
},
expiry: expiry,
},
},
expected: []ui.JoinToken{
staticUiToken,
{
Id: "test-token",
SafeName: "**********",
IsStatic: false,
Expires: expiry,
Roles: types.SystemRoles{
types.RoleNode,
},
Method: types.JoinMethodToken,
},
{
Id: "test-token-2",
SafeName: "************",
IsStatic: false,
Expires: expiry,
Roles: types.SystemRoles{
types.RoleNode,
types.RoleDatabase,
},
Method: types.JoinMethodToken,
},
{
Id: "test-token-3-and-super-duper-long",
SafeName: "************************uper-long",
IsStatic: false,
Expires: expiry,
Roles: types.SystemRoles{
types.RoleNode,
types.RoleKube,
types.RoleDatabase,
},
Method: types.JoinMethodToken,
},
},
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
env := newWebPack(t, 1)
proxy := env.proxies[0]
pack := proxy.authPack(t, username, nil /* roles */)

if tc.noAccess {
noAccessRole, err := types.NewRole(services.RoleNameForUser("[email protected]"), types.RoleSpecV6{})
require.NoError(t, err)
noAccessPack := proxy.authPack(t, "[email protected]", []types.Role{noAccessRole})
endpoint := noAccessPack.clt.Endpoint("webapi", "tokens")
_, err = noAccessPack.clt.Get(ctx, endpoint, url.Values{})
require.Error(t, err)
return
}

if tc.includeUserToken {
passwordToken, err := env.server.Auth().CreateResetPasswordToken(ctx, authclient.CreateUserTokenRequest{
Name: username,
TTL: defaults.MaxSignupTokenTTL,
Type: authclient.UserTokenTypeResetPasswordInvite,
})
require.NoError(t, err)
userToken, err := types.NewProvisionToken(passwordToken.GetName(), types.SystemRoles{types.RoleSignup}, passwordToken.Expiry())
require.NoError(t, err)

userUiToken := ui.JoinToken{
Id: userToken.GetName(),
SafeName: userToken.GetSafeName(),
IsStatic: false,
Expires: userToken.Expiry(),
Roles: userToken.GetRoles(),
Method: types.JoinMethodToken,
}
tc.expected = append(tc.expected, userUiToken)
}

for _, td := range tc.tokenData {
token, err := types.NewProvisionTokenFromSpec(td.name, td.expiry, types.ProvisionTokenSpecV2{
Roles: td.roles,
})
require.NoError(t, err)
err = env.server.Auth().CreateToken(ctx, token)
require.NoError(t, err)
}

endpoint := pack.clt.Endpoint("webapi", "tokens")
re, err := pack.clt.Get(ctx, endpoint, url.Values{})
require.NoError(t, err)

resp := GetTokensResponse{}
require.NoError(t, json.Unmarshal(re.Bytes(), &resp))
require.Len(t, resp.Items, len(tc.expected))
require.ElementsMatch(t, resp.Items, tc.expected)
})
}
}

func Test_deleteToken(t *testing.T) {
ctx := context.Background()
username := "[email protected]"
env := newWebPack(t, 1)
proxy := env.proxies[0]
pack := proxy.authPack(t, username, nil /* roles */)
endpoint := pack.clt.Endpoint("webapi", "tokens")
staticUiToken := ui.JoinToken{
Id: "static-token",
SafeName: "************",
Roles: types.SystemRoles{types.RoleNode},
IsStatic: true,
Method: types.JoinMethodToken,
}

// create join token
token, err := types.NewProvisionTokenFromSpec("my-token", time.Now().UTC().Add(30*time.Minute), types.ProvisionTokenSpecV2{
Roles: types.SystemRoles{
types.RoleNode,
types.RoleDatabase,
},
})
require.NoError(t, err)
err = env.server.Auth().CreateToken(ctx, token)
require.NoError(t, err)

// create password reset token
passwordToken, err := env.server.Auth().CreateResetPasswordToken(ctx, authclient.CreateUserTokenRequest{
Name: username,
TTL: defaults.MaxSignupTokenTTL,
Type: authclient.UserTokenTypeResetPasswordInvite,
})
require.NoError(t, err)
userToken, err := types.NewProvisionToken(passwordToken.GetName(), types.SystemRoles{types.RoleSignup}, passwordToken.Expiry())
require.NoError(t, err)

// should have static token + a signup token now
re, err := pack.clt.Get(ctx, endpoint, url.Values{})
require.NoError(t, err)
resp := GetTokensResponse{}
require.NoError(t, json.Unmarshal(re.Bytes(), &resp))
require.Len(t, resp.Items, 3 /* static + sign up + join */)

// delete
_, err = pack.clt.Delete(ctx, pack.clt.Endpoint("webapi", "tokens", userToken.GetName()))
require.NoError(t, err)
// and delete again
_, err = pack.clt.Delete(ctx, pack.clt.Endpoint("webapi", "tokens", token.GetName()))
require.NoError(t, err)

re, err = pack.clt.Get(ctx, endpoint, url.Values{})
require.NoError(t, err)
resp = GetTokensResponse{}
require.NoError(t, json.Unmarshal(re.Bytes(), &resp))
require.Len(t, resp.Items, 1 /* only static again */)
require.ElementsMatch(t, resp.Items, []ui.JoinToken{
staticUiToken,
})
}

func TestGenerateAzureTokenName(t *testing.T) {
t.Parallel()
rule1 := types.ProvisionTokenSpecV2Azure_Rule{
Expand Down
Loading

0 comments on commit 004e95b

Please sign in to comment.