Skip to content

Commit

Permalink
Merge branch 'branch/v16' into timothyb89/v16/bot-instances
Browse files Browse the repository at this point in the history
  • Loading branch information
timothyb89 authored Aug 26, 2024
2 parents a2d02ac + e5c0f0e commit 518e15d
Show file tree
Hide file tree
Showing 63 changed files with 3,203 additions and 61 deletions.
4 changes: 4 additions & 0 deletions api/types/matchers_aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ const (
AWSMatcherMemoryDB = "memorydb"
// AWSMatcherOpenSearch is the AWS matcher type for OpenSearch databases.
AWSMatcherOpenSearch = "opensearch"
// AWSMatcherDocumentDB is the AWS matcher type for DocumentDB databases.
AWSMatcherDocumentDB = "docdb"
)

// SupportedAWSMatchers is list of AWS services currently supported by the
Expand All @@ -81,6 +83,7 @@ var SupportedAWSDatabaseMatchers = []string{
AWSMatcherElastiCache,
AWSMatcherMemoryDB,
AWSMatcherOpenSearch,
AWSMatcherDocumentDB,
}

// RequireAWSIAMRolesAsUsersMatchers is a list of the AWS databases that
Expand All @@ -91,6 +94,7 @@ var SupportedAWSDatabaseMatchers = []string{
var RequireAWSIAMRolesAsUsersMatchers = []string{
AWSMatcherRedshiftServerless,
AWSMatcherOpenSearch,
AWSMatcherDocumentDB,
}

// GetTypes gets the types that the matcher can match.
Expand Down
23 changes: 19 additions & 4 deletions api/types/provisioning.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,16 @@ type ProvisionToken interface {
GetAllowRules() []*TokenRule
// SetAllowRules sets the allow rules
SetAllowRules([]*TokenRule)
// GetGCPRules will return the GCP rules within this token.
GetGCPRules() *ProvisionTokenSpecV2GCP
// GetAWSIIDTTL returns the TTL of EC2 IIDs
GetAWSIIDTTL() Duration
// GetJoinMethod returns joining method that must be used with this token.
GetJoinMethod() JoinMethod
// GetBotName returns the BotName field which must be set for joining bots.
GetBotName() string

// IsStatic returns true if the token is statically configured
IsStatic() bool
// GetSuggestedLabels returns the set of labels that the resource should add when adding itself to the cluster
GetSuggestedLabels() Labels

Expand Down Expand Up @@ -384,6 +387,11 @@ func (p *ProvisionTokenV2) SetAllowRules(rules []*TokenRule) {
p.Spec.Allow = rules
}

// GetGCPRules will return the GCP rules within this token.
func (p *ProvisionTokenV2) GetGCPRules() *ProvisionTokenSpecV2GCP {
return p.Spec.GCP
}

// GetAWSIIDTTL returns the TTL of EC2 IIDs
func (p *ProvisionTokenV2) GetAWSIIDTTL() Duration {
return p.Spec.AWSIIDTTL
Expand All @@ -394,6 +402,11 @@ func (p *ProvisionTokenV2) GetJoinMethod() JoinMethod {
return p.Spec.JoinMethod
}

// IsStatic returns true if the token is statically configured
func (p *ProvisionTokenV2) IsStatic() bool {
return p.Origin() == OriginConfigFile
}

// GetBotName returns the BotName field which must be set for joining bots.
func (p *ProvisionTokenV2) GetBotName() string {
return p.Spec.BotName
Expand Down Expand Up @@ -535,14 +548,16 @@ func ProvisionTokensToV1(in []ProvisionToken) []ProvisionTokenV1 {
return out
}

// ProvisionTokensFromV1 converts V1 provision tokens to resource list
func ProvisionTokensFromV1(in []ProvisionTokenV1) []ProvisionToken {
// ProvisionTokensFromStatic converts static tokens to resource list
func ProvisionTokensFromStatic(in []ProvisionTokenV1) []ProvisionToken {
if in == nil {
return nil
}
out := make([]ProvisionToken, len(in))
for i := range in {
out[i] = in[i].V2()
tok := in[i].V2()
tok.SetOrigin(OriginConfigFile)
out[i] = tok
}
return out
}
Expand Down
2 changes: 1 addition & 1 deletion api/types/statictokens.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ func (c *StaticTokensV2) SetStaticTokens(s []ProvisionToken) {

// GetStaticTokens gets the list of static tokens used to provision nodes.
func (c *StaticTokensV2) GetStaticTokens() []ProvisionToken {
return ProvisionTokensFromV1(c.Spec.StaticTokens)
return ProvisionTokensFromStatic(c.Spec.StaticTokens)
}

// setStaticFields sets static resource header and metadata fields.
Expand Down
5 changes: 5 additions & 0 deletions docs/pages/includes/plugins/rbac-with-friendly-name.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ spec:
- resources: ['access_list']
verbs: ['list', 'read']

# Optional: To display logins permitted by roles, the plugin also needs
# permission to read the role resource.
- resources: ['role']
verbs: ['read']

# Optional: To display user-friendly names in resource-based Access
# Requests instead of resource IDs, the plugin also needs permission
# to list the resources being requested. Include this along with the
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/usage-billing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ This will be displayed only for those on usage-based plans. Users will need perm
1. Click on "Summary" under "Usage and Billing" at the left-hand side.
1. Usage data for the current billing cycle will be displayed. Example:

[Billing Cycle](../img/webui_billing_cycle.png)
![Billing Cycle](../img/webui_billing_cycle.png)

### Monthly Active Users

Expand Down
22 changes: 22 additions & 0 deletions integrations/access/accessrequest/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,12 +234,20 @@ func (a *App) onPendingRequest(ctx context.Context, req types.AccessRequest) err
return trace.Wrap(err)
}

loginsByRole, err := a.getLoginsByRole(ctx, req)
if trace.IsAccessDenied(err) {
log.Warnf("Missing permissions to get logins by role. Please add role.read to the associated role. error: %s", err)
} else if err != nil {
return trace.Wrap(err)
}

reqData := pd.AccessRequestData{
User: req.GetUser(),
Roles: req.GetRoles(),
RequestReason: req.GetRequestReason(),
SystemAnnotations: req.GetSystemAnnotations(),
Resources: resourceNames,
LoginsByRole: loginsByRole,
}

_, err = a.pluginData.Create(ctx, reqID, PluginData{AccessRequestData: reqData})
Expand Down Expand Up @@ -488,6 +496,20 @@ func (a *App) updateMessages(ctx context.Context, reqID string, tag pd.Resolutio
return nil
}

func (a *App) getLoginsByRole(ctx context.Context, req types.AccessRequest) (map[string][]string, error) {
loginsByRole := make(map[string][]string, len(req.GetRoles()))

for _, role := range req.GetRoles() {
currentRole, err := a.apiClient.GetRole(ctx, role)
if err != nil {
return nil, trace.Wrap(err)
}
loginsByRole[role] = currentRole.GetLogins(types.Allow)
}

return loginsByRole, nil
}

func (a *App) getResourceNames(ctx context.Context, req types.AccessRequest) ([]string, error) {
resourceNames := make([]string, 0, len(req.GetRequestedResourceIDs()))
resourcesByCluster := accessrequest.GetResourceIDsByCluster(req)
Expand Down
84 changes: 84 additions & 0 deletions integrations/access/accessrequest/app_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Teleport
* Copyright (C) 2024 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package accessrequest

import (
"context"
"testing"

"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"

"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/integrations/access/common/teleport"
)

type mockTeleportClient struct {
mock.Mock
teleport.Client
}

func (m *mockTeleportClient) GetRole(ctx context.Context, name string) (types.Role, error) {
args := m.Called(ctx, name)
return args.Get(0).(types.Role), args.Error(1)
}

func TestGetLoginsByRole(t *testing.T) {
teleportClient := &mockTeleportClient{}
teleportClient.On("GetRole", mock.Anything, "admin").Return(&types.RoleV6{
Spec: types.RoleSpecV6{
Allow: types.RoleConditions{
Logins: []string{"root", "foo", "bar"},
},
},
}, (error)(nil))
teleportClient.On("GetRole", mock.Anything, "foo").Return(&types.RoleV6{
Spec: types.RoleSpecV6{
Allow: types.RoleConditions{
Logins: []string{"foo"},
},
},
}, (error)(nil))
teleportClient.On("GetRole", mock.Anything, "dev").Return(&types.RoleV6{
Spec: types.RoleSpecV6{
Allow: types.RoleConditions{
Logins: []string{},
},
},
}, (error)(nil))

app := App{
apiClient: teleportClient,
}
ctx := context.Background()
loginsByRole, err := app.getLoginsByRole(ctx, &types.AccessRequestV3{
Spec: types.AccessRequestSpecV3{
Roles: []string{"admin", "foo", "dev"},
},
})
require.NoError(t, err)

expected := map[string][]string{
"admin": {"root", "foo", "bar"},
"foo": {"foo"},
"dev": {},
}
require.Equal(t, expected, loginsByRole)
teleportClient.AssertNumberOfCalls(t, "GetRole", 3)
}
3 changes: 1 addition & 2 deletions integrations/access/accessrequest/bot.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import (

"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/integrations/access/common"
"github.com/gravitational/teleport/integrations/lib/plugindata"
pd "github.com/gravitational/teleport/integrations/lib/plugindata"
)

Expand All @@ -39,5 +38,5 @@ type MessagingBot interface {
// This is used to change the access-request status and number of required approval remaining
UpdateMessages(ctx context.Context, reqID string, data pd.AccessRequestData, messageData SentMessages, reviews []types.AccessReview) error
// NotifyUser notifies the user if their access request status has changed
NotifyUser(ctx context.Context, reqID string, ard plugindata.AccessRequestData) error
NotifyUser(ctx context.Context, reqID string, ard pd.AccessRequestData) error
}
45 changes: 40 additions & 5 deletions integrations/access/accessrequest/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package accessrequest
import (
"fmt"
"net/url"
"slices"
"strings"
"text/template"
"time"
Expand All @@ -36,7 +37,8 @@ import (
// for message section texts, so we truncate all reasons to a generous but
// conservative limit
const (
requestReasonLimit = 500
requestInlineLimit = 500
requestReasonLimit
resolutionReasonLimit
ReviewReasonLimit
)
Expand Down Expand Up @@ -70,21 +72,36 @@ func MsgStatusText(tag pd.ResolutionTag, reason string) string {
return statusText
}

// MsgFields constructs and returns the Access Request message. List values are
// constructed in sorted order.
func MsgFields(reqID string, reqData pd.AccessRequestData, clusterName string, webProxyURL *url.URL) string {
var builder strings.Builder
builder.Grow(128)

msgFieldToBuilder(&builder, "ID", reqID)
msgFieldToBuilder(&builder, "Cluster", clusterName)

sortedRoles := sortList(reqData.Roles)

if len(reqData.User) > 0 {
msgFieldToBuilder(&builder, "User", reqData.User)
}
if len(reqData.Roles) > 0 {
msgFieldToBuilder(&builder, "Role(s)", strings.Join(reqData.Roles, ","))
if len(reqData.LoginsByRole) > 0 {
for _, role := range sortedRoles {
sortedLogins := sortList(reqData.LoginsByRole[role])
loginStr := "-"
if len(sortedLogins) > 0 {
loginStr = strings.Join(sortedLogins, ", ")
}
msgFieldToBuilder(&builder, "Role", lib.MarkdownEscapeInLine(role, requestInlineLimit),
"Login(s)", lib.MarkdownEscapeInLine(loginStr, requestInlineLimit))
}
} else if len(reqData.Roles) > 0 {
msgFieldToBuilder(&builder, "Role(s)", lib.MarkdownEscapeInLine(strings.Join(sortedRoles, ","), requestInlineLimit))
}
if len(reqData.Resources) > 0 {
msgFieldToBuilder(&builder, "Resource(s)", strings.Join(reqData.Resources, ","))
sortedResources := sortList(reqData.Resources)
msgFieldToBuilder(&builder, "Resource(s)", lib.MarkdownEscapeInLine(strings.Join(sortedResources, ","), requestInlineLimit))
}
if reqData.RequestReason != "" {
msgFieldToBuilder(&builder, "Reason", lib.MarkdownEscape(reqData.RequestReason, requestReasonLimit))
Expand Down Expand Up @@ -134,10 +151,28 @@ func MsgReview(review types.AccessReview) (string, error) {
return builder.String(), nil
}

func msgFieldToBuilder(b *strings.Builder, field, value string) {
func msgFieldToBuilder(b *strings.Builder, field, value string, additionalFields ...string) {
b.WriteString("*")
b.WriteString(field)
b.WriteString("*: ")
b.WriteString(value)

for i := 0; i < len(additionalFields)-1; i += 2 {
field := additionalFields[i]
value := additionalFields[i+1]
b.WriteString(" *")
b.WriteString(field)
b.WriteString("*: ")
b.WriteString(value)
}

b.WriteString("\n")
}

// sortedList returns a sorted copy of the src.
func sortList(src []string) []string {
sorted := make([]string, len(src))
copy(sorted, src)
slices.Sort(sorted)
return sorted
}
Loading

0 comments on commit 518e15d

Please sign in to comment.