Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions lib/accessmonitoring/request_mapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ limitations under the License.
package accessmonitoring

import (
"slices"
"time"

"github.com/gravitational/trace"

"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/expression"
"github.com/gravitational/teleport/lib/utils/typical"
)
Expand All @@ -30,6 +32,7 @@ import (
type AccessRequestExpressionEnv struct {
Roles []string
SuggestedReviewers []string
RequestedResources []types.ResourceWithLabels
Annotations map[string][]string
User string
RequestReason string
Expand Down Expand Up @@ -87,10 +90,45 @@ func newRequestConditionParser() (*typical.Parser[AccessRequestExpressionEnv, an
return env.Expiry, nil
}),

"access_request.spec.resource_labels_union": typical.DynamicMap(func(env AccessRequestExpressionEnv) (expression.Dict, error) {
union := make(map[string][]string)
for _, resource := range env.RequestedResources {
for k, v := range resource.GetAllLabels() {
union[k] = append(union[k], v)
}
}
return expression.DictFromStringSliceMap(union), nil
}),
"access_request.spec.resource_labels_intersection": typical.DynamicMap(func(env AccessRequestExpressionEnv) (expression.Dict, error) {
if len(env.RequestedResources) == 0 {
return expression.Dict{}, nil
}

intersection := make(map[string][]string)

// Get first resource labels.
labels := env.RequestedResources[0].GetAllLabels()
for k, v := range labels {
intersection[k] = append(intersection[k], v)
}

// Remove non-intersecting labels.
for _, resource := range env.RequestedResources {
labels := resource.GetAllLabels()
for k, v := range intersection {
if label, ok := labels[k]; !ok || !slices.Contains(v, label) {
delete(intersection, k)
}
}
}
return expression.DictFromStringSliceMap(intersection), nil
}),

"user.traits": typical.DynamicMap(func(env AccessRequestExpressionEnv) (expression.Dict, error) {
return expression.DictFromStringSliceMap(env.UserTraits), nil
}),
}

defParserSpec := expression.DefaultParserSpec[AccessRequestExpressionEnv]()
defParserSpec.Variables = typicalEnvVar

Expand Down
97 changes: 97 additions & 0 deletions lib/accessmonitoring/request_mapping_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import (
"testing"

"github.com/stretchr/testify/require"

"github.com/gravitational/teleport/api/types"
)

func TestEvaluateCondition(t *testing.T) {
Expand Down Expand Up @@ -141,6 +143,101 @@ func TestEvaluateCondition(t *testing.T) {
},
match: false,
},
{
description: "(union) single resource has label",
condition: `
access_request.spec.resource_labels_union["env"].
contains("test")`,
env: AccessRequestExpressionEnv{
RequestedResources: []types.ResourceWithLabels{
&types.ServerV2{
Metadata: types.Metadata{
Labels: map[string]string{"env": "test"},
},
},
},
},
match: true,
},
{
description: "(union) multiple resources have label",
condition: `
access_request.spec.resource_labels_union["env"].
contains_all(set("test", "dev"))`,
env: AccessRequestExpressionEnv{
RequestedResources: []types.ResourceWithLabels{
&types.ServerV2{
Metadata: types.Metadata{
Labels: map[string]string{"env": "test"},
},
},
&types.ServerV2{
Metadata: types.Metadata{
Labels: map[string]string{"env": "dev"},
},
},
},
},
match: true,
},
{
description: "(intersection) single resource has label",
condition: `
access_request.spec.resource_labels_intersection["env"].
contains("test")`,
env: AccessRequestExpressionEnv{
RequestedResources: []types.ResourceWithLabels{
&types.ServerV2{
Metadata: types.Metadata{
Labels: map[string]string{"env": "test"},
},
},
},
},
match: true,
},
{
description: "(intersection) multiple resources have label",
condition: `
access_request.spec.resource_labels_intersection["env"].
contains("test")`,
env: AccessRequestExpressionEnv{
RequestedResources: []types.ResourceWithLabels{
&types.ServerV2{
Metadata: types.Metadata{
Labels: map[string]string{"env": "test"},
},
},
&types.ServerV2{
Metadata: types.Metadata{
Labels: map[string]string{"env": "test"},
},
},
},
},
match: true,
},
{
description: "(intersection) multiple resource labels do not intersect",
condition: `
access_request.spec.resource_labels_intersection["env"].
contains("test")`,
env: AccessRequestExpressionEnv{
RequestedResources: []types.ResourceWithLabels{
&types.ServerV2{
Metadata: types.Metadata{
Labels: map[string]string{"env": "test"},
},
},
&types.ServerV2{
Metadata: types.Metadata{
Labels: map[string]string{"env": "dev"},
},
},
},
},
match: false,
},
}

for _, test := range tests {
Expand Down
39 changes: 24 additions & 15 deletions lib/accessmonitoring/review/review.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import (
"github.com/gravitational/trace"

"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/accessrequest"
"github.com/gravitational/teleport/api/client"
accessmonitoringrulesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/accessmonitoringrules/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/accessmonitoring"
Expand All @@ -41,6 +43,7 @@ type Client interface {
SubmitAccessReview(ctx context.Context, params types.AccessReviewSubmission) (types.AccessRequest, error)
ListAccessMonitoringRulesWithFilter(ctx context.Context, req *accessmonitoringrulesv1.ListAccessMonitoringRulesWithFilterRequest) ([]*accessmonitoringrulesv1.AccessMonitoringRule, string, error)
GetUser(ctx context.Context, name string, withSecrets bool) (types.User, error)
client.ListResourcesClient
}

// Config specifies access review handler configuration.
Expand Down Expand Up @@ -190,18 +193,11 @@ func (handler *Handler) onPendingRequest(ctx context.Context, req types.AccessRe
"req_id", req.GetName(),
"user", req.GetUser())

// Automatic reviews are only supported with role requests.
if len(req.GetRequestedResourceIDs()) > 0 {
return trace.BadParameter("cannot automatically review access requests for resources other than 'roles'")
}

const withSecretsFalse = false
user, err := handler.Client.GetUser(ctx, req.GetUser(), withSecretsFalse)
env, err := handler.newExpressionEnv(ctx, req)
if err != nil {
return trace.Wrap(err)
}

env := getAccessRequestExpressionEnv(req, user.GetTraits())
reviewRule := handler.getMatchingRule(ctx, env)
if reviewRule == nil {
// This access request does not match any access monitoring rules.
Expand All @@ -211,7 +207,9 @@ func (handler *Handler) onPendingRequest(ctx context.Context, req types.AccessRe
review, err := newAccessReview(
req.GetUser(),
reviewRule.GetMetadata().GetName(),
reviewRule.GetSpec().GetAutomaticReview().GetDecision())
reviewRule.GetSpec().GetAutomaticReview().GetDecision(),
time.Now(),
)
if err != nil {
return trace.Wrap(err, "failed to create new access review")
}
Expand Down Expand Up @@ -267,7 +265,7 @@ func (handler *Handler) getMatchingRule(
return reviewRule
}

func newAccessReview(userName, ruleName, state string) (types.AccessReview, error) {
func newAccessReview(userName, ruleName, state string, created time.Time) (types.AccessReview, error) {
var proposedState types.RequestState
switch state {
case types.RequestState_APPROVED.String():
Expand All @@ -284,7 +282,7 @@ func newAccessReview(userName, ruleName, state string) (types.AccessReview, erro
Reason: fmt.Sprintf("Access request has been automatically %[4]s by %[1]q. "+
"User %[2]q is %[4]s by access_monitoring_rule %[3]q.",
teleport.SystemAccessApproverUserName, userName, ruleName, strings.ToLower(state)),
Created: time.Now(),
Created: created,
}, nil
}

Expand All @@ -296,16 +294,27 @@ func isAlreadyReviewedError(err error) bool {
return trace.IsAlreadyExists(err) || strings.HasSuffix(err.Error(), "has already reviewed this request")
}

// getAccessRequestExpressionEnv returns the expression env of the access request.
func getAccessRequestExpressionEnv(req types.AccessRequest, traits map[string][]string) accessmonitoring.AccessRequestExpressionEnv {
func (handler *Handler) newExpressionEnv(ctx context.Context, req types.AccessRequest) (accessmonitoring.AccessRequestExpressionEnv, error) {
const withSecretsFalse = false
user, err := handler.Client.GetUser(ctx, req.GetUser(), withSecretsFalse)
if err != nil {
return accessmonitoring.AccessRequestExpressionEnv{}, trace.Wrap(err)
}

requestedResources, err := accessrequest.GetResourcesByResourceIDs(ctx, handler.Client, req.GetRequestedResourceIDs())
if err != nil {
return accessmonitoring.AccessRequestExpressionEnv{}, trace.Wrap(err)
}

return accessmonitoring.AccessRequestExpressionEnv{
Roles: req.GetRoles(),
RequestedResources: requestedResources,
SuggestedReviewers: req.GetSuggestedReviewers(),
Annotations: req.GetSystemAnnotations(),
User: req.GetUser(),
RequestReason: req.GetRequestReason(),
CreationTime: req.GetCreationTime(),
Expiry: req.Expiry(),
UserTraits: traits,
}
UserTraits: user.GetTraits(),
}, nil
}
Loading
Loading