Skip to content

Commit 66ffd47

Browse files
committedMar 12, 2025·
Implement internal access monitoring service
1 parent e793e1e commit 66ffd47

File tree

11 files changed

+1510
-5
lines changed

11 files changed

+1510
-5
lines changed
 

‎integrations/access/accessmonitoring/access_monitoring_rules.go

+5-4
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232
"github.com/gravitational/teleport/integrations/access/common/teleport"
3333
"github.com/gravitational/teleport/integrations/lib/logger"
3434
"github.com/gravitational/teleport/integrations/lib/stringset"
35+
"github.com/gravitational/teleport/lib/accessmonitoring"
3536
)
3637

3738
const (
@@ -149,7 +150,7 @@ func (amrh *RuleHandler) RecipientsFromAccessMonitoringRules(ctx context.Context
149150
recipientSet := common.NewRecipientSet()
150151

151152
for _, rule := range amrh.getAccessMonitoringRules() {
152-
match, err := EvaluateCondition(rule.Spec.Condition, getAccessRequestExpressionEnv(req))
153+
match, err := accessmonitoring.EvaluateCondition(rule.Spec.Condition, getAccessRequestExpressionEnv(req))
153154
if err != nil {
154155
log.WarnContext(ctx, "Failed to parse access monitoring notification rule",
155156
"error", err,
@@ -176,7 +177,7 @@ func (amrh *RuleHandler) RawRecipientsFromAccessMonitoringRules(ctx context.Cont
176177
log := logger.Get(ctx)
177178
recipientSet := stringset.New()
178179
for _, rule := range amrh.getAccessMonitoringRules() {
179-
match, err := EvaluateCondition(rule.Spec.Condition, getAccessRequestExpressionEnv(req))
180+
match, err := accessmonitoring.EvaluateCondition(rule.Spec.Condition, getAccessRequestExpressionEnv(req))
180181
if err != nil {
181182
log.WarnContext(ctx, "Failed to parse access monitoring notification rule",
182183
"error", err,
@@ -242,8 +243,8 @@ func (amrh *RuleHandler) ruleApplies(amr *accessmonitoringrulesv1.AccessMonitori
242243
}
243244

244245
// getAccessRequestExpressionEnv returns the expression env of the access request.
245-
func getAccessRequestExpressionEnv(req types.AccessRequest) AccessRequestExpressionEnv {
246-
return AccessRequestExpressionEnv{
246+
func getAccessRequestExpressionEnv(req types.AccessRequest) accessmonitoring.AccessRequestExpressionEnv {
247+
return accessmonitoring.AccessRequestExpressionEnv{
247248
Roles: req.GetRoles(),
248249
SuggestedReviewers: req.GetSuggestedReviewers(),
249250
Annotations: req.GetSystemAnnotations(),
+279
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
/*
2+
* Teleport
3+
* Copyright (C) 2025 Gravitational, Inc.
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
package approval
20+
21+
import (
22+
"context"
23+
"fmt"
24+
"log/slog"
25+
"slices"
26+
"strings"
27+
"time"
28+
29+
"github.com/gravitational/trace"
30+
31+
"github.com/gravitational/teleport"
32+
accessmonitoringrulesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/accessmonitoringrules/v1"
33+
"github.com/gravitational/teleport/api/types"
34+
"github.com/gravitational/teleport/lib/accessmonitoring"
35+
)
36+
37+
const (
38+
// componentName specifies the access approval handler component name used for debugging.
39+
componentName = "access_approval_handler"
40+
41+
// stateApproved specifies the approved state.
42+
stateApproved = "approved"
43+
)
44+
45+
// Client aggregates the parts of Teleport API client interface
46+
// (as implemented by github.com/gravitational/teleport/api/client.Client)
47+
// that are used by the access approval handler.
48+
type Client interface {
49+
SubmitAccessReview(ctx context.Context, params types.AccessReviewSubmission) (types.AccessRequest, error)
50+
ListAccessMonitoringRulesWithFilter(ctx context.Context, req *accessmonitoringrulesv1.ListAccessMonitoringRulesWithFilterRequest) ([]*accessmonitoringrulesv1.AccessMonitoringRule, string, error)
51+
GetUser(ctx context.Context, name string, withSecrets bool) (types.User, error)
52+
}
53+
54+
// Config specifies approval handler configuration.
55+
type Config struct {
56+
// Logger is the logger for the handler.
57+
Logger *slog.Logger
58+
59+
// HandlerName specifies the handler name.
60+
HandlerName string
61+
62+
// Client is the auth service client interface.
63+
Client Client
64+
65+
// Cache is the access monitoring rules cache.
66+
Cache *accessmonitoring.Cache
67+
}
68+
69+
// CheckAndSetDefaults checks and sets default configuration.
70+
func (cfg *Config) CheckAndSetDefaults() error {
71+
if cfg.Logger == nil {
72+
cfg.Logger = slog.Default()
73+
}
74+
if cfg.HandlerName == "" {
75+
return trace.BadParameter("handler name is required")
76+
}
77+
if cfg.Client == nil {
78+
return trace.BadParameter("teleport client is required")
79+
}
80+
if cfg.Cache == nil {
81+
cfg.Cache = accessmonitoring.NewCache()
82+
}
83+
return nil
84+
}
85+
86+
// Handler handles automatic approvals of access requests.
87+
type Handler struct {
88+
Config
89+
90+
rules *accessmonitoring.Cache
91+
}
92+
93+
// NewHandler returns a new access approval handler.
94+
func NewHandler(cfg Config) (*Handler, error) {
95+
if err := cfg.CheckAndSetDefaults(); err != nil {
96+
return nil, trace.Wrap(err)
97+
}
98+
99+
return &Handler{
100+
Config: cfg,
101+
rules: cfg.Cache,
102+
}, nil
103+
}
104+
105+
// initialize the access monitoring rules cache.
106+
func (handler *Handler) initialize(ctx context.Context) error {
107+
err := handler.rules.Initialize(ctx, func(ctx context.Context, pageSize int64, pageToken string) (
108+
[]*accessmonitoringrulesv1.AccessMonitoringRule,
109+
string,
110+
error,
111+
) {
112+
req := &accessmonitoringrulesv1.ListAccessMonitoringRulesWithFilterRequest{
113+
PageSize: pageSize,
114+
PageToken: pageToken,
115+
Subjects: []string{types.KindAccessRequest},
116+
AutomaticApprovalName: handler.HandlerName,
117+
}
118+
page, next, err := handler.Client.ListAccessMonitoringRulesWithFilter(ctx, req)
119+
if err != nil {
120+
return nil, "", trace.Wrap(err)
121+
}
122+
123+
rules := []*accessmonitoringrulesv1.AccessMonitoringRule{}
124+
for _, rule := range page {
125+
if handler.ruleApplies(rule) {
126+
rules = append(rules, rule)
127+
}
128+
}
129+
return rules, next, nil
130+
})
131+
return trace.Wrap(err)
132+
}
133+
134+
// HandleAccessMonitoringRule handles access monitoring rule events.
135+
func (handler *Handler) HandleAccessMonitoringRule(ctx context.Context, event types.Event) error {
136+
switch event.Type {
137+
case types.OpInit:
138+
if err := handler.initialize(ctx); err != nil {
139+
return trace.Wrap(err)
140+
}
141+
case types.OpPut:
142+
e, ok := event.Resource.(types.Resource153Unwrapper)
143+
if !ok {
144+
return trace.BadParameter("expected Resource153Unwrapper resource type, got %T", event.Resource)
145+
}
146+
rule, ok := e.Unwrap().(*accessmonitoringrulesv1.AccessMonitoringRule)
147+
if !ok {
148+
return trace.BadParameter("expected AccessMonitoringRule resource type, got %T", event.Resource)
149+
}
150+
151+
// In the event an existing rule no longer applies we must remove it.
152+
if !handler.ruleApplies(rule) {
153+
handler.rules.Delete(rule.GetMetadata().GetName())
154+
return nil
155+
}
156+
handler.rules.Put(rule)
157+
case types.OpDelete:
158+
handler.rules.Delete(event.Resource.GetName())
159+
default:
160+
return trace.BadParameter("unexpected event operation %s", event.Type)
161+
}
162+
return nil
163+
}
164+
165+
// ruleApplies returns true if the rule applies to this handler.
166+
func (handler *Handler) ruleApplies(rule *accessmonitoringrulesv1.AccessMonitoringRule) bool {
167+
// Automatic approval rule is only applied if the desired state is "approved".
168+
if !slices.Contains(rule.GetSpec().GetStates(), stateApproved) {
169+
return false
170+
}
171+
if rule.GetSpec().GetAutomaticApproval().GetName() != handler.HandlerName {
172+
return false
173+
}
174+
return slices.Contains(rule.GetSpec().GetSubjects(), types.KindAccessRequest)
175+
}
176+
177+
// HandleAccessRequest handles access request events.
178+
func (handler *Handler) HandleAccessRequest(ctx context.Context, event types.Event) error {
179+
switch event.Type {
180+
case types.OpPut:
181+
req, ok := event.Resource.(types.AccessRequest)
182+
if !ok {
183+
return trace.BadParameter("unexpected resource type %T", event.Resource)
184+
}
185+
switch {
186+
case req.GetState().IsPending():
187+
return trace.Wrap(handler.onPendingRequest(ctx, req))
188+
case req.GetState().IsResolved():
189+
// Nothing to do when access request is resolved.
190+
return nil
191+
default:
192+
return trace.BadParameter("unknown request state")
193+
}
194+
case types.OpDelete:
195+
// Nothing to do when access request is deleted.
196+
return nil
197+
default:
198+
return trace.BadParameter("unexpected event operation %s", event.Type)
199+
}
200+
}
201+
202+
func (handler *Handler) onPendingRequest(ctx context.Context, req types.AccessRequest) error {
203+
log := handler.Logger.With(
204+
"req_id", req.GetName(),
205+
"user", req.GetUser())
206+
207+
const withSecretsFalse = false
208+
user, err := handler.Client.GetUser(ctx, req.GetUser(), withSecretsFalse)
209+
if err != nil {
210+
return trace.Wrap(err)
211+
}
212+
213+
for _, rule := range handler.rules.Get() {
214+
// Check if any access monitoring rule enables automatic approval for the access request.
215+
approved, err := accessmonitoring.EvaluateCondition(
216+
rule.GetSpec().GetCondition(),
217+
getAccessRequestExpressionEnv(req, user.GetTraits()))
218+
if err != nil {
219+
log.WarnContext(ctx, "Failed to evaluate access monitoring rule",
220+
"error", err,
221+
"rule", rule.GetMetadata().GetName(),
222+
)
223+
}
224+
225+
if !approved {
226+
continue
227+
}
228+
229+
// If the the request is pre-approved, then submimt an access request approval.
230+
_, err = handler.Client.SubmitAccessReview(ctx, types.AccessReviewSubmission{
231+
RequestID: req.GetName(),
232+
Review: newAccessReview(req.GetUser(), rule.GetMetadata().GetName()),
233+
})
234+
235+
switch {
236+
case isAlreadyReviewedError(err):
237+
log.DebugContext(ctx, "Already reviewed the request.", "error", err)
238+
return nil
239+
case err != nil:
240+
return trace.Wrap(err, "submitting access request")
241+
}
242+
243+
log.InfoContext(ctx, "Successfully submitted a request approval.")
244+
return nil
245+
}
246+
return nil
247+
}
248+
249+
func newAccessReview(userName, ruleName string) types.AccessReview {
250+
return types.AccessReview{
251+
Author: teleport.SystemAccessApproverUserName,
252+
ProposedState: types.RequestState_APPROVED,
253+
Reason: fmt.Sprintf("Access request has been automatically approved by %q. "+
254+
"User %q is pre-approved by access_monitoring_rule %q.",
255+
componentName, userName, ruleName),
256+
Created: time.Now(),
257+
}
258+
}
259+
260+
func isAlreadyReviewedError(err error) bool {
261+
if err == nil {
262+
return false
263+
}
264+
return strings.HasSuffix(err.Error(), "has already reviewed this request")
265+
}
266+
267+
// getAccessRequestExpressionEnv returns the expression env of the access request.
268+
func getAccessRequestExpressionEnv(req types.AccessRequest, traits map[string][]string) accessmonitoring.AccessRequestExpressionEnv {
269+
return accessmonitoring.AccessRequestExpressionEnv{
270+
Roles: req.GetRoles(),
271+
SuggestedReviewers: req.GetSuggestedReviewers(),
272+
Annotations: req.GetSystemAnnotations(),
273+
User: req.GetUser(),
274+
RequestReason: req.GetRequestReason(),
275+
CreationTime: req.GetCreationTime(),
276+
Expiry: req.Expiry(),
277+
UserTraits: traits,
278+
}
279+
}

0 commit comments

Comments
 (0)
Please sign in to comment.