mirror of
https://github.com/gravitational/teleport.git
synced 2026-11-01 21:52:02 +08:00
757 lines
24 KiB
Go
757 lines
24 KiB
Go
/*
|
|
* Teleport
|
|
* Copyright (C) 2023 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 services
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gravitational/trace"
|
|
"github.com/vulcand/predicate"
|
|
"github.com/vulcand/predicate/builder"
|
|
|
|
"github.com/gravitational/teleport"
|
|
"github.com/gravitational/teleport/api/types"
|
|
"github.com/gravitational/teleport/api/types/events"
|
|
"github.com/gravitational/teleport/api/types/wrappers"
|
|
"github.com/gravitational/teleport/lib/session"
|
|
logutils "github.com/gravitational/teleport/lib/utils/log"
|
|
"github.com/gravitational/teleport/lib/utils/typical"
|
|
)
|
|
|
|
// RuleContext specifies context passed to the
|
|
// rule processing matcher, and contains information
|
|
// about current session, e.g. current user
|
|
type RuleContext interface {
|
|
// GetIdentifier returns identifier defined in a context
|
|
GetIdentifier(fields []string) (interface{}, error)
|
|
// GetResource returns resource if specified in the context,
|
|
// if unspecified, returns error.
|
|
GetResource() (types.Resource, error)
|
|
}
|
|
|
|
var (
|
|
// ResourceNameExpr is the identifier that specifies resource name.
|
|
ResourceNameExpr = builder.Identifier("resource.metadata.name")
|
|
// CertAuthorityTypeExpr is a function call that returns
|
|
// cert authority type.
|
|
CertAuthorityTypeExpr = builder.Identifier(`system.catype()`)
|
|
)
|
|
|
|
// predicateAllEndWith is a custom function to test if a string ends with a
|
|
// particular suffix. If given a `[]string` as the first argument, all values
|
|
// must have the given suffix (2nd argument).
|
|
func predicateAllEndWith(a interface{}, b interface{}) predicate.BoolPredicate {
|
|
return func() bool {
|
|
// bval is the suffix and must always be a plain string.
|
|
bval, ok := b.(string)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
switch aval := a.(type) {
|
|
case string:
|
|
return strings.HasSuffix(aval, bval)
|
|
case []string:
|
|
for _, val := range aval {
|
|
if !strings.HasSuffix(val, bval) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
// predicateAllEqual is a custom function to test if all entries in a []string
|
|
// are equal to a certain value. This is primarily useful for comparing string
|
|
// fields that are only expected to contain a single, specific value.
|
|
func predicateAllEqual(a interface{}, b interface{}) predicate.BoolPredicate {
|
|
return func() bool {
|
|
// bval is the suffix and must always be a plain string.
|
|
bval, ok := b.(string)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
switch aval := a.(type) {
|
|
case string:
|
|
return aval == bval
|
|
case []string:
|
|
for _, val := range aval {
|
|
if val != bval {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
// predicateIsSubset determines if the first parameter is contained within the
|
|
// variadic args. The first argument may either by `string` or `[]string`, and
|
|
// the variadic args may only be `string`.
|
|
func predicateIsSubset(a interface{}, b ...interface{}) predicate.BoolPredicate {
|
|
return func() bool {
|
|
// Populate the set.
|
|
set := map[string]bool{}
|
|
for _, bval := range b {
|
|
s, ok := bval.(string)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
set[s] = true
|
|
}
|
|
|
|
switch aval := a.(type) {
|
|
case string:
|
|
return set[aval]
|
|
case []string:
|
|
for _, v := range aval {
|
|
if !set[v] {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
// NewWhereParser returns standard parser for `where` section in access rules.
|
|
func NewWhereParser(ctx RuleContext) (predicate.Parser, error) {
|
|
return predicate.NewParser(predicate.Def{
|
|
Operators: predicate.Operators{
|
|
AND: predicate.And,
|
|
OR: predicate.Or,
|
|
NOT: predicate.Not,
|
|
},
|
|
Functions: map[string]interface{}{
|
|
"equals": predicate.Equals,
|
|
"contains": predicate.Contains,
|
|
"all_end_with": predicateAllEndWith,
|
|
"all_equal": predicateAllEqual,
|
|
"is_subset": predicateIsSubset,
|
|
// system.catype is a function that returns cert authority type,
|
|
// it returns empty values for unrecognized values to
|
|
// pass static rule checks.
|
|
"system.catype": func() (interface{}, error) {
|
|
resource, err := ctx.GetResource()
|
|
if err != nil {
|
|
if trace.IsNotFound(err) {
|
|
return "", nil
|
|
}
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
ca, ok := resource.(types.CertAuthority)
|
|
if !ok {
|
|
return "", nil
|
|
}
|
|
return string(ca.GetType()), nil
|
|
},
|
|
},
|
|
GetIdentifier: ctx.GetIdentifier,
|
|
GetProperty: GetStringMapValue,
|
|
})
|
|
}
|
|
|
|
// GetStringMapValue is a helper function that returns property
|
|
// from map[string]string or map[string][]string
|
|
// the function returns empty value in case if key not found
|
|
// In case if map is nil, returns empty value as well
|
|
func GetStringMapValue(mapVal, keyVal interface{}) (interface{}, error) {
|
|
key, ok := keyVal.(string)
|
|
if !ok {
|
|
return nil, trace.BadParameter("only string keys are supported")
|
|
}
|
|
switch m := mapVal.(type) {
|
|
case map[string][]string:
|
|
if len(m) == 0 {
|
|
// to return nil with a proper type
|
|
var n []string
|
|
return n, nil
|
|
}
|
|
return m[key], nil
|
|
case wrappers.Traits:
|
|
if len(m) == 0 {
|
|
// to return nil with a proper type
|
|
var n []string
|
|
return n, nil
|
|
}
|
|
return m[key], nil
|
|
case map[string]string:
|
|
if len(m) == 0 {
|
|
return "", nil
|
|
}
|
|
return m[key], nil
|
|
default:
|
|
_, ok := mapVal.(map[string][]string)
|
|
return nil, trace.BadParameter("type %T is not supported, but %v %#v", m, ok, mapVal)
|
|
}
|
|
}
|
|
|
|
// NewActionsParser returns standard parser for 'actions' section in access rules
|
|
func NewActionsParser(ctx RuleContext) (predicate.Parser, error) {
|
|
return predicate.NewParser(predicate.Def{
|
|
Operators: predicate.Operators{},
|
|
Functions: map[string]interface{}{
|
|
"log": NewLogActionFn(ctx),
|
|
},
|
|
GetIdentifier: ctx.GetIdentifier,
|
|
GetProperty: predicate.GetStringMapValue,
|
|
})
|
|
}
|
|
|
|
// NewLogActionFn creates logger functions
|
|
func NewLogActionFn(ctx RuleContext) interface{} {
|
|
l := &LogAction{ctx: ctx}
|
|
|
|
return l.Log
|
|
}
|
|
|
|
// LogAction represents action that will emit log entry
|
|
// when specified in the actions of a matched rule
|
|
type LogAction struct {
|
|
ctx RuleContext
|
|
}
|
|
|
|
// Log logs with specified level and message string and attributes
|
|
func (l *LogAction) Log(level, msg string, args ...any) predicate.BoolPredicate {
|
|
return func() bool {
|
|
slevel := slog.LevelDebug
|
|
switch strings.ToLower(level) {
|
|
case "error":
|
|
slevel = slog.LevelError
|
|
case "warn", "warning":
|
|
slevel = slog.LevelWarn
|
|
case "info":
|
|
slevel = slog.LevelInfo
|
|
case "debug":
|
|
slevel = slog.LevelDebug
|
|
case "trace":
|
|
slevel = logutils.TraceLevel
|
|
}
|
|
|
|
ctx := context.Background()
|
|
// Expicitly check whether logging is enabled for the level
|
|
// to avoid formatting the message if the log won't be sampled.
|
|
if !slog.Default().Handler().Enabled(ctx, slevel) {
|
|
//nolint:sloglint // msg cannot be constant
|
|
slog.Log(context.Background(), slevel, fmt.Sprintf(msg, args...))
|
|
}
|
|
|
|
return true
|
|
}
|
|
}
|
|
|
|
// Context is a default rule context used in teleport
|
|
type Context struct {
|
|
// User is currently authenticated user
|
|
User UserState
|
|
// Resource is an optional resource, in case if the rule
|
|
// checks access to the resource
|
|
Resource types.Resource
|
|
// Session is an optional session.end or windows.desktop.session.end event.
|
|
// These events hold information about session recordings.
|
|
Session events.AuditEvent
|
|
// SSHSession is an optional (active) SSH session.
|
|
SSHSession *session.Session
|
|
// HostCert is an optional host certificate.
|
|
HostCert *HostCertContext
|
|
// SessionTracker is an optional session tracker, in case if the rule checks access to the tracker.
|
|
SessionTracker types.SessionTracker
|
|
}
|
|
|
|
// String returns user friendly representation of this context
|
|
func (ctx *Context) String() string {
|
|
return fmt.Sprintf("user %v, resource: %v", ctx.User, ctx.Resource)
|
|
}
|
|
|
|
const (
|
|
// UserIdentifier represents user registered identifier in the rules
|
|
UserIdentifier = "user"
|
|
// ResourceIdentifier represents resource registered identifier in the rules
|
|
ResourceIdentifier = "resource"
|
|
// ResourceLabelsIdentifier refers to the static and dynamic labels in a resource.
|
|
ResourceLabelsIdentifier = "labels"
|
|
// ResourceNameIdentifier refers to two different fields depending on the kind of resource:
|
|
// - KindNode will refer to its resource.spec.hostname field
|
|
// - All other kinds will refer to its resource.metadata.name field
|
|
// It refers to two different fields because the way this shorthand is being used,
|
|
// implies it will return the name of the resource where users identifies nodes
|
|
// by its hostname and all other resources that can be `ls` queried is identified
|
|
// by its metadata name.
|
|
ResourceNameIdentifier = "name"
|
|
// SessionIdentifier refers to a session (recording) in the rules.
|
|
SessionIdentifier = "session"
|
|
// SSHSessionIdentifier refers to an (active) SSH session in the rules.
|
|
SSHSessionIdentifier = "ssh_session"
|
|
// ImpersonateRoleIdentifier is a role to impersonate
|
|
ImpersonateRoleIdentifier = "impersonate_role"
|
|
// ImpersonateUserIdentifier is a user to impersonate
|
|
ImpersonateUserIdentifier = "impersonate_user"
|
|
// HostCertIdentifier refers to a host certificate being created.
|
|
HostCertIdentifier = "host_cert"
|
|
// SessionTrackerIdentifier refers to a session tracker in the rules.
|
|
SessionTrackerIdentifier = "session_tracker"
|
|
)
|
|
|
|
// GetResource returns resource specified in the context,
|
|
// returns error if not specified.
|
|
func (ctx *Context) GetResource() (types.Resource, error) {
|
|
if ctx.Resource == nil {
|
|
return nil, trace.NotFound("resource is not set in the context")
|
|
}
|
|
return ctx.Resource, nil
|
|
}
|
|
|
|
// GetIdentifier returns identifier defined in a context
|
|
func (ctx *Context) GetIdentifier(fields []string) (interface{}, error) {
|
|
switch fields[0] {
|
|
case UserIdentifier:
|
|
var user UserState
|
|
if ctx.User == nil {
|
|
user = emptyUser
|
|
} else {
|
|
user = ctx.User
|
|
}
|
|
return predicate.GetFieldByTag(user, teleport.JSON, fields[1:])
|
|
case ResourceIdentifier:
|
|
var resource types.Resource
|
|
if ctx.Resource == nil {
|
|
resource = emptyResource
|
|
} else {
|
|
resource = ctx.Resource
|
|
}
|
|
return predicate.GetFieldByTag(resource, teleport.JSON, fields[1:])
|
|
case SessionIdentifier:
|
|
var session events.AuditEvent = &events.SessionEnd{}
|
|
switch ctx.Session.(type) {
|
|
case *events.SessionEnd, *events.WindowsDesktopSessionEnd:
|
|
session = ctx.Session
|
|
}
|
|
return predicate.GetFieldByTag(session, teleport.JSON, fields[1:])
|
|
case SSHSessionIdentifier:
|
|
// Do not expose the original session.Session, instead transform it into a
|
|
// ctxSession so the exposed fields match our desired API.
|
|
return predicate.GetFieldByTag(toCtxSession(ctx.SSHSession), teleport.JSON, fields[1:])
|
|
case HostCertIdentifier:
|
|
var hostCert *HostCertContext
|
|
if ctx.HostCert == nil {
|
|
hostCert = emptyHostCert
|
|
} else {
|
|
hostCert = ctx.HostCert
|
|
}
|
|
return predicate.GetFieldByTag(hostCert, teleport.JSON, fields[1:])
|
|
case SessionTrackerIdentifier:
|
|
return predicate.GetFieldByTag(toCtxTracker(ctx.SessionTracker), teleport.JSON, fields[1:])
|
|
default:
|
|
return nil, trace.NotFound("%v is not defined", strings.Join(fields, "."))
|
|
}
|
|
}
|
|
|
|
// ctxSession represents the public contract of a session.Session, as exposed
|
|
// to a Context rule.
|
|
// See RFD 82: https://github.com/gravitational/teleport/blob/master/rfd/0082-session-tracker-resource-rbac.md
|
|
type ctxTracker struct {
|
|
SessionID string `json:"session_id"`
|
|
Kind string `json:"kind"`
|
|
Participants []string `json:"participants"`
|
|
State string `json:"state"`
|
|
Hostname string `json:"hostname"`
|
|
Address string `json:"address"`
|
|
Login string `json:"login"`
|
|
Cluster string `json:"cluster"`
|
|
KubeCluster string `json:"kube_cluster"`
|
|
HostUser string `json:"host_user"`
|
|
HostRoles []string `json:"host_roles"`
|
|
}
|
|
|
|
func toCtxTracker(t types.SessionTracker) ctxTracker {
|
|
if t == nil {
|
|
return ctxTracker{}
|
|
}
|
|
|
|
getParticipants := func(s types.SessionTracker) []string {
|
|
participants := s.GetParticipants()
|
|
names := make([]string, len(participants))
|
|
for i, participant := range participants {
|
|
names[i] = participant.User
|
|
}
|
|
|
|
return names
|
|
}
|
|
|
|
getHostRoles := func(s types.SessionTracker) []string {
|
|
policySets := s.GetHostPolicySets()
|
|
roles := make([]string, len(policySets))
|
|
for i, policySet := range policySets {
|
|
roles[i] = policySet.Name
|
|
}
|
|
|
|
return roles
|
|
}
|
|
|
|
return ctxTracker{
|
|
SessionID: t.GetSessionID(),
|
|
Kind: t.GetKind(),
|
|
Participants: getParticipants(t),
|
|
State: string(t.GetState()),
|
|
Hostname: t.GetHostname(),
|
|
Address: t.GetAddress(),
|
|
Login: t.GetLogin(),
|
|
Cluster: t.GetClusterName(),
|
|
KubeCluster: t.GetKubeCluster(),
|
|
HostUser: t.GetHostUser(),
|
|
HostRoles: getHostRoles(t),
|
|
}
|
|
}
|
|
|
|
// ctxSession represents the public contract of a session.Session, as exposed
|
|
// to a Context rule.
|
|
// See RFD 45:
|
|
// https://github.com/gravitational/teleport/blob/master/rfd/0045-ssh_session-where-condition.md#replacing-parties-by-usernames.
|
|
type ctxSession struct {
|
|
// Namespace is a session namespace, separating sessions from each other.
|
|
Namespace string `json:"namespace"`
|
|
// Login is a login used by all parties joining the session.
|
|
Login string `json:"login"`
|
|
// Created records the information about the time when session was created.
|
|
Created time.Time `json:"created"`
|
|
// LastActive holds the information about when the session was last active.
|
|
LastActive time.Time `json:"last_active"`
|
|
// ServerID of session.
|
|
ServerID string `json:"server_id"`
|
|
// ServerHostname of session.
|
|
ServerHostname string `json:"server_hostname"`
|
|
// ServerAddr of session.
|
|
ServerAddr string `json:"server_addr"`
|
|
// ClusterName is the name of cluster that this session belongs to.
|
|
ClusterName string `json:"cluster_name"`
|
|
// Participants is a list of session participants expressed as usernames.
|
|
Participants []string `json:"participants"`
|
|
}
|
|
|
|
func toCtxSession(s *session.Session) ctxSession {
|
|
if s == nil {
|
|
return ctxSession{}
|
|
}
|
|
return ctxSession{
|
|
Namespace: s.Namespace,
|
|
Login: s.Login,
|
|
Created: s.Created,
|
|
LastActive: s.LastActive,
|
|
ServerID: s.ServerID,
|
|
ServerHostname: s.ServerHostname,
|
|
ServerAddr: s.ServerAddr,
|
|
ClusterName: s.ClusterName,
|
|
Participants: s.Participants(),
|
|
}
|
|
}
|
|
|
|
// HostCertContext is used to evaluate the `where` condition on a `host_cert`
|
|
// pseudo-resource. These resources only exist for RBAC purposes and do not
|
|
// exist in the database.
|
|
type HostCertContext struct {
|
|
// HostID is the host ID in the cert request.
|
|
HostID string `json:"host_id"`
|
|
// NodeName is the node name in the cert request.
|
|
NodeName string `json:"node_name"`
|
|
// Principals is the list of requested certificate principals.
|
|
Principals []string `json:"principals"`
|
|
// ClusterName is the name of the cluster for which the certificate should
|
|
// be issued.
|
|
ClusterName string `json:"cluster_name"`
|
|
// Role is the name of the Teleport role for which the cert should be
|
|
// issued.
|
|
Role types.SystemRole `json:"role"`
|
|
// TTL is the requested certificate TTL.
|
|
TTL time.Duration `json:"ttl"`
|
|
}
|
|
|
|
// emptyResource is used when no resource is specified
|
|
var emptyResource = &EmptyResource{}
|
|
|
|
// emptyUser is used when no user is specified
|
|
var emptyUser = &types.UserV2{}
|
|
|
|
// emptyHostCert is an empty host certificate used when no host cert is
|
|
// specified
|
|
var emptyHostCert = &HostCertContext{}
|
|
|
|
// EmptyResource is used to represent a use case when no resource
|
|
// is specified in the rules matcher
|
|
type EmptyResource struct {
|
|
// Kind is a resource kind
|
|
Kind string `json:"kind"`
|
|
// SubKind is a resource sub kind
|
|
SubKind string `json:"sub_kind,omitempty"`
|
|
// Version is a resource version
|
|
Version string `json:"version"`
|
|
// Metadata is Role metadata
|
|
Metadata types.Metadata `json:"metadata"`
|
|
}
|
|
|
|
// GetVersion returns resource version
|
|
func (r *EmptyResource) GetVersion() string {
|
|
return r.Version
|
|
}
|
|
|
|
// GetSubKind returns resource sub kind
|
|
func (r *EmptyResource) GetSubKind() string {
|
|
return r.SubKind
|
|
}
|
|
|
|
// SetSubKind sets resource subkind
|
|
func (r *EmptyResource) SetSubKind(s string) {
|
|
r.SubKind = s
|
|
}
|
|
|
|
// GetKind returns resource kind
|
|
func (r *EmptyResource) GetKind() string {
|
|
return r.Kind
|
|
}
|
|
|
|
// GetRevision returns the revision
|
|
func (r *EmptyResource) GetRevision() string {
|
|
return r.Metadata.GetRevision()
|
|
}
|
|
|
|
// SetRevision sets the revision
|
|
func (r *EmptyResource) SetRevision(rev string) {
|
|
r.Metadata.SetRevision(rev)
|
|
}
|
|
|
|
// SetExpiry sets expiry time for the object.
|
|
func (r *EmptyResource) SetExpiry(expires time.Time) {
|
|
r.Metadata.SetExpiry(expires)
|
|
}
|
|
|
|
// Expiry returns the expiry time for the object.
|
|
func (r *EmptyResource) Expiry() time.Time {
|
|
return r.Metadata.Expiry()
|
|
}
|
|
|
|
// SetName sets the role name and is a shortcut for SetMetadata().Name.
|
|
func (r *EmptyResource) SetName(s string) {
|
|
r.Metadata.Name = s
|
|
}
|
|
|
|
// GetName gets the role name and is a shortcut for GetMetadata().Name.
|
|
func (r *EmptyResource) GetName() string {
|
|
return r.Metadata.Name
|
|
}
|
|
|
|
// GetMetadata returns role metadata.
|
|
func (r *EmptyResource) GetMetadata() types.Metadata {
|
|
return r.Metadata
|
|
}
|
|
|
|
func (r *EmptyResource) CheckAndSetDefaults() error { return nil }
|
|
|
|
// newParserForIdentifierSubcondition returns a parser customized for
|
|
// extracting the largest admissible subexpression of a `where` condition that
|
|
// involves the given identifier.
|
|
//
|
|
// For example, consider the `where` condition
|
|
// `contains(session.participants, "user") && equals(user.metadata.name, "user")`.
|
|
// Given a RuleContext where user.metadata.name is equal to "user", its largest
|
|
// admissible subcondition involving the identifier "session" is
|
|
// `contains(session.participants, "user")`. With another RuleContext the
|
|
// largest such subcondition is the empty expression.
|
|
func newParserForIdentifierSubcondition(ctx RuleContext, identifier string) (predicate.Parser, error) {
|
|
binaryPred := func(predFn func(a, b interface{}) predicate.BoolPredicate, exprFn func(a, b types.WhereExpr) types.WhereExpr) func(a, b interface{}) types.WhereExpr {
|
|
return func(a, b interface{}) types.WhereExpr {
|
|
an, aOK := a.(types.WhereExpr)
|
|
if !aOK {
|
|
an = types.WhereExpr{Literal: a}
|
|
}
|
|
bn, bOK := b.(types.WhereExpr)
|
|
if !bOK {
|
|
bn = types.WhereExpr{Literal: b}
|
|
}
|
|
if an.Literal != nil && bn.Literal != nil {
|
|
return types.WhereExpr{Literal: predFn(an.Literal, bn.Literal)()}
|
|
}
|
|
return exprFn(an, bn)
|
|
}
|
|
}
|
|
return predicate.NewParser(predicate.Def{
|
|
Operators: predicate.Operators{
|
|
AND: func(a, b types.WhereExpr) types.WhereExpr {
|
|
aVal, aOK := a.Literal.(bool)
|
|
bVal, bOK := b.Literal.(bool)
|
|
switch {
|
|
case aOK && bOK:
|
|
return types.WhereExpr{Literal: aVal && bVal}
|
|
case aVal:
|
|
return b
|
|
case bVal:
|
|
return a
|
|
case aOK || bOK:
|
|
return types.WhereExpr{Literal: false}
|
|
default:
|
|
return types.WhereExpr{And: types.WhereExpr2{L: &a, R: &b}}
|
|
}
|
|
},
|
|
OR: func(a, b types.WhereExpr) types.WhereExpr {
|
|
aVal, aOK := a.Literal.(bool)
|
|
bVal, bOK := b.Literal.(bool)
|
|
switch {
|
|
case aOK && bOK:
|
|
return types.WhereExpr{Literal: aVal || bVal}
|
|
case aVal || bVal:
|
|
return types.WhereExpr{Literal: true}
|
|
case aOK:
|
|
return b
|
|
case bOK:
|
|
return a
|
|
default:
|
|
return types.WhereExpr{Or: types.WhereExpr2{L: &a, R: &b}}
|
|
}
|
|
},
|
|
NOT: func(expr types.WhereExpr) types.WhereExpr {
|
|
if val, ok := expr.Literal.(bool); ok {
|
|
return types.WhereExpr{Literal: !val}
|
|
}
|
|
return types.WhereExpr{Not: &expr}
|
|
},
|
|
},
|
|
Functions: map[string]interface{}{
|
|
"equals": binaryPred(predicate.Equals, func(a, b types.WhereExpr) types.WhereExpr {
|
|
return types.WhereExpr{Equals: types.WhereExpr2{L: &a, R: &b}}
|
|
}),
|
|
"contains": binaryPred(predicate.Contains, func(a, b types.WhereExpr) types.WhereExpr {
|
|
return types.WhereExpr{Contains: types.WhereExpr2{L: &a, R: &b}}
|
|
}),
|
|
},
|
|
GetIdentifier: func(fields []string) (interface{}, error) {
|
|
if fields[0] == identifier {
|
|
// TODO: Session events have only one level of attributes. Support for
|
|
// more nested levels may be added when needed for other objects.
|
|
if len(fields) != 2 {
|
|
return nil, trace.BadParameter("only exactly two fields are supported with identifier %q, got %d: %v", identifier, len(fields), fields)
|
|
}
|
|
return types.WhereExpr{Field: fields[1]}, nil
|
|
}
|
|
lit, err := ctx.GetIdentifier(fields)
|
|
return types.WhereExpr{Literal: lit}, trace.Wrap(err)
|
|
},
|
|
GetProperty: func(mapVal, keyVal interface{}) (interface{}, error) {
|
|
mapExpr, mapOK := mapVal.(types.WhereExpr)
|
|
if !mapOK {
|
|
mapExpr = types.WhereExpr{Literal: mapVal}
|
|
}
|
|
keyExpr, keyOK := keyVal.(types.WhereExpr)
|
|
if !keyOK {
|
|
keyExpr = types.WhereExpr{Literal: keyVal}
|
|
}
|
|
if mapExpr.Literal == nil || keyExpr.Literal == nil {
|
|
// TODO: Add support for general WhereExpr.
|
|
return nil, trace.BadParameter("GetProperty is implemented only for literals")
|
|
}
|
|
return GetStringMapValue(mapExpr.Literal, keyExpr.Literal)
|
|
},
|
|
})
|
|
}
|
|
|
|
// NewResourceExpression returns a [typical.Expression] that is to be evaluated against a
|
|
// [types.ResourceWithLabels]. It is customized to allow short identifiers common in all
|
|
// resources:
|
|
// - shorthand `name` refers to `resource.spec.hostname` for node resources, or it refers
|
|
// to `resource.metadata.name` for all other resources eg: `name == "app-name-jenkins"`
|
|
// - shorthand `labels` refers to resource `resource.metadata.labels + resource.spec.dynamic_labels`
|
|
// eg: `labels.env == "prod"`
|
|
//
|
|
// All other fields can be referenced by starting expression with identifier `resource`
|
|
// followed by the names of the json fields ie: `resource.spec.public_addr`.
|
|
func NewResourceExpression(expression string) (typical.Expression[types.ResourceWithLabels, bool], error) {
|
|
parser, err := typical.NewParser[types.ResourceWithLabels, bool](typical.ParserSpec[types.ResourceWithLabels]{
|
|
Variables: map[string]typical.Variable{
|
|
"resource.metadata.labels": typical.DynamicVariable(func(r types.ResourceWithLabels) (map[string]string, error) {
|
|
return r.GetStaticLabels(), nil
|
|
}),
|
|
"resource.metadata.name": typical.DynamicVariable(func(r types.ResourceWithLabels) (string, error) {
|
|
return r.GetName(), nil
|
|
}),
|
|
"labels": typical.DynamicMapFunction(func(r types.ResourceWithLabels, key string) (string, error) {
|
|
val, _ := r.GetLabel(key)
|
|
return val, nil
|
|
}),
|
|
"name": typical.DynamicVariable(func(r types.ResourceWithLabels) (string, error) {
|
|
// For nodes, the resource "name" that user expects is the
|
|
// nodes hostname, not its UUID. Currently, for other resources,
|
|
// the metadata.name returns the name as expected.
|
|
if server, ok := r.(types.Server); ok {
|
|
return server.GetHostname(), nil
|
|
}
|
|
|
|
return r.GetName(), nil
|
|
}),
|
|
},
|
|
Functions: map[string]typical.Function{
|
|
"hasPrefix": typical.BinaryFunction[types.ResourceWithLabels](func(s, suffix string) (bool, error) {
|
|
return strings.HasPrefix(s, suffix), nil
|
|
}),
|
|
"equals": typical.BinaryFunction[types.ResourceWithLabels](func(a, b string) (bool, error) {
|
|
return strings.Compare(a, b) == 0, nil
|
|
}),
|
|
"search": typical.UnaryVariadicFunctionWithEnv(func(r types.ResourceWithLabels, v ...string) (bool, error) {
|
|
return r.MatchSearch(v), nil
|
|
}),
|
|
"exists": typical.UnaryFunction[types.ResourceWithLabels](func(value string) (bool, error) {
|
|
return value != "", nil
|
|
}),
|
|
"split": typical.BinaryFunction[types.ResourceWithLabels](func(value string, delimiter string) ([]string, error) {
|
|
return strings.Split(value, delimiter), nil
|
|
}),
|
|
"contains": typical.BinaryFunction[types.ResourceWithLabels](func(list []string, value string) (bool, error) {
|
|
return slices.Contains(list, value), nil
|
|
}),
|
|
},
|
|
GetUnknownIdentifier: func(env types.ResourceWithLabels, fields []string) (any, error) {
|
|
if fields[0] == ResourceIdentifier {
|
|
if f, err := predicate.GetFieldByTag(env, teleport.JSON, fields[1:]); err == nil {
|
|
return f, nil
|
|
}
|
|
}
|
|
|
|
identifier := strings.Join(fields, ".")
|
|
return nil, trace.BadParameter("identifier %s is not defined", identifier)
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
expr, err := parser.Parse(expression)
|
|
return expr, trace.Wrap(err)
|
|
}
|