policy: add ssh PPL criteria (#5658)

Add five new PPL criteria for use with SSH, matching username and public
key. Username can be matched against a String Matcher, user's email 
address, or a custom claim from the IdP claims. Public key can be 
matched against a list of keys or a trusted CA.
This commit is contained in:
Kenneth Jenkins 2025-06-25 09:42:29 -07:00 committed by GitHub
parent 9363457849
commit 93ff662e1f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 679 additions and 1 deletions

View file

@ -37,8 +37,9 @@ type Request struct {
IsInternal bool
Policy *config.Policy
HTTP RequestHTTP
Session RequestSession
SSH RequestSSH
MCP RequestMCP
Session RequestSession
EnvoyRouteChecksum uint64
EnvoyRouteID string
}
@ -129,6 +130,11 @@ func getClientCertificateInfo(
return c
}
type RequestSSH struct {
Username string `json:"username"`
PublicKey []byte `json:"publickey"`
}
// RequestSession is the session field in the request.
type RequestSession struct {
ID string `json:"id"`
@ -374,6 +380,7 @@ func (e *Evaluator) evaluatePolicy(ctx context.Context, req *Request) (*PolicyRe
return policyEvaluator.Evaluate(ctx, &PolicyRequest{
HTTP: req.HTTP,
SSH: req.SSH,
MCP: req.MCP,
Session: req.Session,
IsValidClientCertificate: isValidClientCertificate,

View file

@ -21,6 +21,7 @@ import (
// PolicyRequest is the input to policy evaluation.
type PolicyRequest struct {
HTTP RequestHTTP `json:"http"`
SSH RequestSSH `json:"ssh"`
MCP RequestMCP `json:"mcp"`
Session RequestSession `json:"session"`
IsValidClientCertificate bool `json:"is_valid_client_certificate"`
@ -161,6 +162,7 @@ func NewPolicyEvaluator(
rego.Query("result = data.pomerium.policy"),
rego.EnablePrintStatements(true),
getGoogleCloudServerlessHeadersRegoOption,
criteria.SSHVerifyUserCert,
store.GetDataBrokerRecordOption(),
)
@ -173,6 +175,7 @@ func NewPolicyEvaluator(
rego.Query("result = data.pomerium.policy"),
rego.EnablePrintStatements(true),
getGoogleCloudServerlessHeadersRegoOption,
criteria.SSHVerifyUserCert,
store.GetDataBrokerRecordOption(),
)
q, err = r.PrepareForEval(ctx)

View file

@ -31,6 +31,7 @@ var testingNow = time.Date(2021, 5, 11, 13, 43, 0, 0, time.Local)
type (
Input struct {
HTTP InputHTTP `json:"http"`
SSH InputSSH `json:"ssh"`
Session InputSession `json:"session"`
MCP InputMCP `json:"mcp"`
IsValidClientCertificate bool `json:"is_valid_client_certificate"`
@ -41,6 +42,10 @@ type (
Headers map[string][]string `json:"headers"`
ClientCertificate ClientCertificateInfo `json:"client_certificate"`
}
InputSSH struct {
Username string `json:"username"`
PublicKey []byte `json:"publickey"`
}
InputSession struct {
ID string `json:"id"`
}
@ -150,6 +155,7 @@ func evaluate(t *testing.T,
return nil, nil
}),
SSHVerifyUserCert,
rego.Input(input),
rego.SetRegoVersion(ast.RegoV1),
)

View file

@ -39,6 +39,10 @@ const (
ReasonPomeriumRoute = "pomerium-route"
ReasonReject = "reject"
ReasonRouteNotFound = "route-not-found"
ReasonSSHPublickeyOK = "ssh-publickey-ok"
ReasonSSHPublickeyUnauthorized = "ssh-publickey-unauthorized"
ReasonSSHUsernameOK = "ssh-username-ok"
ReasonSSHUsernameUnauthorized = "ssh-username-unauthorized"
ReasonUserOK = "user-ok"
ReasonUserUnauthenticated = "user-unauthenticated" // user needs to log in
ReasonUserUnauthorized = "user-unauthorized" // user does not have access

View file

@ -0,0 +1,196 @@
package criteria
import (
"encoding/base64"
"fmt"
"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/rego"
"github.com/open-policy-agent/opa/types"
"golang.org/x/crypto/ssh"
"github.com/pomerium/pomerium/pkg/policy/generator"
"github.com/pomerium/pomerium/pkg/policy/parser"
)
var SSHVerifyUserCert = rego.Function3(&rego.Function{
Name: "ssh_verify_user_cert",
Decl: types.NewFunction(
types.Args(types.S, types.S, types.NewArray(nil, types.S)),
types.B,
),
}, func(_ rego.BuiltinContext, op1, op2, op3 *ast.Term) (*ast.Term, error) {
// The first argument should be an ssh principal name.
principal, ok := op1.Value.(ast.String)
if !ok {
return nil, fmt.Errorf("expected string value, got %T", op1.Value)
}
// The second argument should be a base64-encoded ssh certificate in the wire format.
s, ok := op2.Value.(ast.String)
if !ok {
return nil, fmt.Errorf("expected string value, got %T", op2.Value)
}
keyBytes, err := base64.StdEncoding.DecodeString(string(s))
if err != nil {
return nil, err
}
key, err := ssh.ParsePublicKey(keyBytes)
if err != nil {
return nil, err
}
cert, ok := key.(*ssh.Certificate)
if !ok {
return ast.BooleanTerm(false), nil // not an ssh certificate
}
// The third argument should be an array of CAs, also base64-encoded in the wire format.
arr, ok := op3.Value.(*ast.Array)
if !ok {
return nil, fmt.Errorf("expected array value, got: %T", op3.Value)
}
userCAs := make(map[string]struct{})
if arr.Iter(func(t *ast.Term) error {
s, ok := t.Value.(ast.String)
if !ok {
return fmt.Errorf("expected string value, got: %T", t.Value)
}
userCABytes, err := base64.StdEncoding.DecodeString(string(s))
if err != nil {
return err
}
userCAs[string(userCABytes)] = struct{}{}
return nil
}) != nil {
return nil, err
}
checker := ssh.CertChecker{
IsUserAuthority: func(auth ssh.PublicKey) bool {
_, ok := userCAs[string(auth.Marshal())]
return ok
},
}
_, err = checker.Authenticate(usernameConnMetadata{username: string(principal)}, cert)
return ast.BooleanTerm(err == nil), nil
})
type usernameConnMetadata struct {
ssh.ConnMetadata
username string
}
func (m usernameConnMetadata) User() string {
return m.username
}
type sshPublicKeyCriterion struct {
g *Generator
}
func (sshPublicKeyCriterion) DataType() generator.CriterionDataType {
return generator.CriterionDataTypeUnknown
}
func (sshPublicKeyCriterion) Name() string {
return "ssh_publickey"
}
// Converts a single ssh key from the authorized_keys format to base64-encoded wire format.
// Returns an error if the input is not a [parser.String] or the key could not be parsed.
func parseAuthorizedKey(v parser.Value) (*ast.Term, error) {
s, ok := v.(parser.String)
if !ok {
return nil, fmt.Errorf("expected string value, got: %T", v)
}
key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(s)) //nolint:dogsled
if err != nil {
return nil, err
}
return ast.StringTerm(base64.StdEncoding.EncodeToString(key.Marshal())), nil
}
// Converts a single ssh key or a list of ssh keys from the authorized_keys format to
// an array of keys in the base64-encoded wire format.
func parseAuthorizedKeys(data parser.Value) (*ast.Term, error) {
var arr parser.Array
switch v := data.(type) {
case parser.String:
arr = parser.Array{v}
case parser.Array:
arr = v
default:
return nil, fmt.Errorf("expected string or array of strings, got: %T", data)
}
// Convert each key to ssh wire format for comparison.
keys := make([]*ast.Term, len(arr))
for i, v := range arr {
key, err := parseAuthorizedKey(v)
if err != nil {
return nil, err
}
keys[i] = key
}
return ast.NewTerm(ast.NewArray(keys...)), nil
}
func (c sshPublicKeyCriterion) GenerateRule(_ string, data parser.Value) (*ast.Rule, []*ast.Rule, error) {
keys, err := parseAuthorizedKeys(data)
if err != nil {
return nil, nil, err
}
body := ast.Body{
ast.Member.Expr(ast.VarTerm("input.ssh.publickey"), keys),
}
rule := NewCriterionRule(c.g, c.Name(),
ReasonSSHPublickeyOK, ReasonSSHPublickeyUnauthorized,
body)
return rule, nil, nil
}
func SSHPublicKey(generator *Generator) Criterion {
return sshPublicKeyCriterion{g: generator}
}
type sshUserCACriterion struct {
g *Generator
}
func (sshUserCACriterion) DataType() generator.CriterionDataType {
return generator.CriterionDataTypeUnknown
}
func (sshUserCACriterion) Name() string {
return "ssh_ca"
}
func (c sshUserCACriterion) GenerateRule(_ string, data parser.Value) (*ast.Rule, []*ast.Rule, error) {
keys, err := parseAuthorizedKeys(data)
if err != nil {
return nil, nil, err
}
body := ast.Body{
ast.Assign.Expr(ast.VarTerm("userCAs"), keys),
ast.MustParseExpr("ssh_verify_user_cert(input.ssh.username, input.ssh.publickey, userCAs)"),
}
rule := NewCriterionRule(c.g, c.Name(),
ReasonSSHPublickeyOK, ReasonSSHPublickeyUnauthorized,
body)
return rule, nil, nil
}
func SSHUserCA(generator *Generator) Criterion {
return sshUserCACriterion{g: generator}
}
func init() {
Register(SSHPublicKey)
Register(SSHUserCA)
}

View file

@ -0,0 +1,126 @@
package criteria
import (
"encoding/base64"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/pomerium/pomerium/pkg/grpc/databroker"
)
func TestSSHPublicKey(t *testing.T) {
key1, _ := base64.StdEncoding.DecodeString("AAAAC3NzaC1lZDI1NTE5AAAAIIeAQ7VbiYJdPaxsMYTW/q5QpqtyHMtHHRBUJOcQMaLE")
t.Run("single ok", func(t *testing.T) {
res, err := evaluate(t, `
allow:
and:
- ssh_publickey: 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIeAQ7VbiYJdPaxsMYTW/q5QpqtyHMtHHRBUJOcQMaLE key-1'
`, []*databroker.Record{}, Input{SSH: InputSSH{PublicKey: key1}})
require.NoError(t, err)
assert.Equal(t, A{true, A{ReasonSSHPublickeyOK}, M{}}, res["allow"])
assert.Equal(t, A{false, A{}}, res["deny"])
})
t.Run("single unauthorized", func(t *testing.T) {
res, err := evaluate(t, `
allow:
and:
- ssh_publickey: 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPzK2WQZ0NU52W8Bk/8po+4V4oUEdCklf3GtQmiYQB/9 key-2'
`, []*databroker.Record{}, Input{SSH: InputSSH{PublicKey: key1}})
require.NoError(t, err)
assert.Equal(t, A{false, A{ReasonSSHPublickeyUnauthorized}, M{}}, res["allow"])
assert.Equal(t, A{false, A{}}, res["deny"])
})
t.Run("multiple ok", func(t *testing.T) {
res, err := evaluate(t, `
allow:
and:
- ssh_publickey:
- 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIeAQ7VbiYJdPaxsMYTW/q5QpqtyHMtHHRBUJOcQMaLE key-1'
- 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPzK2WQZ0NU52W8Bk/8po+4V4oUEdCklf3GtQmiYQB/9 key-2'
`, []*databroker.Record{}, Input{SSH: InputSSH{PublicKey: key1}})
require.NoError(t, err)
assert.Equal(t, A{true, A{ReasonSSHPublickeyOK}, M{}}, res["allow"])
assert.Equal(t, A{false, A{}}, res["deny"])
})
t.Run("multiple unauthorized", func(t *testing.T) {
res, err := evaluate(t, `
allow:
and:
- ssh_publickey:
- 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPzK2WQZ0NU52W8Bk/8po+4V4oUEdCklf3GtQmiYQB/9 key-2'
- 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMC+uqlilwtfsI5koXGKSBU4vQRZUrse8w4+ea9BsK2v key-3'
`, []*databroker.Record{}, Input{SSH: InputSSH{PublicKey: key1}})
require.NoError(t, err)
assert.Equal(t, A{false, A{ReasonSSHPublickeyUnauthorized}, M{}}, res["allow"])
assert.Equal(t, A{false, A{}}, res["deny"])
})
}
func TestSSHUserCA(t *testing.T) {
user1key, _ := base64.StdEncoding.DecodeString(
"AAAAC3NzaC1lZDI1NTE5AAAAICRpwMbj13mXdSMzHJBiMLln0Wx0iCepff5N/W8vi0ta")
user1cert, _ := base64.StdEncoding.DecodeString(
"AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIKNKPd0B1q9Jx7h/" +
"tnFFBtbMybwlSmxGoE308BkWuJIrAAAAICRpwMbj13mXdSMzHJBiMLln0Wx0iCepff5N" +
"/W8vi0taAAAAAAAAAAAAAAABAAAABXVzZXIxAAAAAAAAAAAAAAAA//////////8AAAAA" +
"AAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1m" +
"b3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJt" +
"aXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQy" +
"NTUxOQAAACBIntPLN0pEWLpPTktfLuUeKloK97ysMJRvf0f1FigxJQAAAFMAAAALc3No" +
"LWVkMjU1MTkAAABABkLudnDsTEw3aPbgqP5NvuAtZrqzknCadFMjIL+hXoFXFitJq+u9" +
"cAl9KIE+2ZQTf2ISbrQDh8Vw+5pivxGZBA==")
t.Run("single ok", func(t *testing.T) {
res, err := evaluate(t, `
allow:
and:
- ssh_ca: 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEie08s3SkRYuk9OS18u5R4qWgr3vKwwlG9/R/UWKDEl ca_user_key'
`, []*databroker.Record{}, Input{SSH: InputSSH{
PublicKey: user1cert,
}})
require.NoError(t, err)
assert.Equal(t, A{true, A{ReasonSSHPublickeyOK}, M{}}, res["allow"])
assert.Equal(t, A{false, A{}}, res["deny"])
})
t.Run("single unauthorized", func(t *testing.T) {
res, err := evaluate(t, `
allow:
and:
- ssh_ca: 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJjxHDM2CpDj4QfY5NxhDPQaOtAebNXzzFsn61kX0LCF other_key'
`, []*databroker.Record{}, Input{SSH: InputSSH{
PublicKey: user1cert,
}})
require.NoError(t, err)
assert.Equal(t, A{false, A{ReasonSSHPublickeyUnauthorized}, M{}}, res["allow"])
assert.Equal(t, A{false, A{}}, res["deny"])
})
t.Run("not a cert", func(t *testing.T) {
res, err := evaluate(t, `
allow:
and:
- ssh_ca: 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJjxHDM2CpDj4QfY5NxhDPQaOtAebNXzzFsn61kX0LCF other_key'
`, []*databroker.Record{}, Input{SSH: InputSSH{
PublicKey: user1key,
}})
require.NoError(t, err)
assert.Equal(t, A{false, A{ReasonSSHPublickeyUnauthorized}, M{}}, res["allow"])
assert.Equal(t, A{false, A{}}, res["deny"])
})
t.Run("multiple ok", func(t *testing.T) {
res, err := evaluate(t, `
allow:
and:
- ssh_ca:
- 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJjxHDM2CpDj4QfY5NxhDPQaOtAebNXzzFsn61kX0LCF other_key'
- 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEie08s3SkRYuk9OS18u5R4qWgr3vKwwlG9/R/UWKDEl ca_user_key'
`, []*databroker.Record{}, Input{SSH: InputSSH{
PublicKey: user1cert,
}})
require.NoError(t, err)
assert.Equal(t, A{true, A{ReasonSSHPublickeyOK}, M{}}, res["allow"])
assert.Equal(t, A{false, A{}}, res["deny"])
})
}

View file

@ -0,0 +1,120 @@
package criteria
import (
"fmt"
"github.com/open-policy-agent/opa/ast"
"github.com/pomerium/pomerium/pkg/policy/generator"
"github.com/pomerium/pomerium/pkg/policy/parser"
"github.com/pomerium/pomerium/pkg/policy/rules"
)
type sshUsernameCriterion struct {
g *Generator
}
func (sshUsernameCriterion) DataType() generator.CriterionDataType {
return CriterionDataTypeStringMatcher
}
func (sshUsernameCriterion) Name() string {
return "ssh_username"
}
func (c sshUsernameCriterion) GenerateRule(_ string, data parser.Value) (*ast.Rule, []*ast.Rule, error) {
var body ast.Body
err := matchString(&body, ast.VarTerm("input.ssh.username"), data)
if err != nil {
return nil, nil, err
}
rule := NewCriterionRule(c.g, c.Name(),
ReasonSSHUsernameOK, ReasonSSHUsernameUnauthorized,
body)
return rule, nil, nil
}
func SSHUsername(generator *Generator) Criterion {
return sshUsernameCriterion{g: generator}
}
type sshUsernameMatchesEmailCriterion struct {
g *Generator
}
func (sshUsernameMatchesEmailCriterion) DataType() generator.CriterionDataType {
return generator.CriterionDataTypeUnused
}
func (sshUsernameMatchesEmailCriterion) Name() string {
return "ssh_username_matches_email"
}
func (c sshUsernameMatchesEmailCriterion) GenerateRule(_ string, _ parser.Value) (*ast.Rule, []*ast.Rule, error) {
var body ast.Body
body = append(body, emailBody...)
body = append(body, ast.MustParseExpr(`username := split(email, "@")[0]`))
body = append(body, ast.Equal.Expr(ast.VarTerm("input.ssh.username"), ast.VarTerm("username")))
rule := NewCriterionSessionRule(c.g, c.Name(),
ReasonSSHUsernameOK, ReasonSSHUsernameUnauthorized,
body)
return rule, []*ast.Rule{
rules.GetSession(),
rules.GetUser(),
rules.GetUserEmail(),
rules.GetDirectoryUser(),
}, nil
}
func SSHUsernameMatchesEmail(generator *Generator) Criterion {
return sshUsernameMatchesEmailCriterion{g: generator}
}
type sshUsernameMatchesClaimCriterion struct {
g *Generator
}
func (sshUsernameMatchesClaimCriterion) DataType() generator.CriterionDataType {
return generator.CriterionDataTypeUnknown
}
func (sshUsernameMatchesClaimCriterion) Name() string {
return "ssh_username_matches_claim"
}
func (c sshUsernameMatchesClaimCriterion) GenerateRule(_ string, data parser.Value) (*ast.Rule, []*ast.Rule, error) {
claimName, ok := data.(parser.String)
if !ok {
return nil, nil, fmt.Errorf("expected string value, got: %T", data)
}
var body ast.Body
body = append(body, ast.Assign.Expr(ast.VarTerm("rule_path"), ast.NewTerm(ast.String(claimName))))
body = append(body, claimBody...)
body = append(body, ast.Member.Expr(ast.VarTerm("input.ssh.username"), ast.VarTerm("values")))
rule := NewCriterionSessionRule(c.g, c.Name(),
ReasonSSHUsernameOK, ReasonSSHUsernameUnauthorized,
body)
return rule, []*ast.Rule{
rules.GetSession(),
rules.GetUser(),
rules.ObjectGet(),
}, nil
}
func SSHUsernameMatchesClaim(generator *Generator) Criterion {
return sshUsernameMatchesClaimCriterion{g: generator}
}
func init() {
Register(SSHUsername)
Register(SSHUsernameMatchesEmail)
Register(SSHUsernameMatchesClaim)
}

View file

@ -0,0 +1,216 @@
package criteria
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/structpb"
"github.com/pomerium/datasource/pkg/directory"
"github.com/pomerium/pomerium/pkg/grpc/databroker"
"github.com/pomerium/pomerium/pkg/grpc/session"
"github.com/pomerium/pomerium/pkg/grpc/user"
)
func TestSSHUsername(t *testing.T) {
t.Run("ok", func(t *testing.T) {
res, err := evaluate(t, `
allow:
and:
- ssh_username:
is: example-username
`, []*databroker.Record{}, Input{SSH: InputSSH{Username: "example-username"}})
require.NoError(t, err)
assert.Equal(t, A{true, A{ReasonSSHUsernameOK}, M{}}, res["allow"])
assert.Equal(t, A{false, A{}}, res["deny"])
})
t.Run("shorthand", func(t *testing.T) {
res, err := evaluate(t, `
allow:
and:
- ssh_username: example-username
`, []*databroker.Record{}, Input{SSH: InputSSH{Username: "example-username"}})
require.NoError(t, err)
assert.Equal(t, A{true, A{ReasonSSHUsernameOK}, M{}}, res["allow"])
assert.Equal(t, A{false, A{}}, res["deny"])
})
t.Run("multiple ok", func(t *testing.T) {
res, err := evaluate(t, `
allow:
and:
- ssh_username:
in: [user1, user2, user3]
`, []*databroker.Record{}, Input{SSH: InputSSH{Username: "user2"}})
require.NoError(t, err)
assert.Equal(t, A{true, A{ReasonSSHUsernameOK}, M{}}, res["allow"])
assert.Equal(t, A{false, A{}}, res["deny"])
})
t.Run("multiple unauthorized", func(t *testing.T) {
res, err := evaluate(t, `
allow:
and:
- ssh_username:
in: [user1, user2, user3]
`, []*databroker.Record{}, Input{SSH: InputSSH{Username: "user4"}})
require.NoError(t, err)
assert.Equal(t, A{false, A{ReasonSSHUsernameUnauthorized}, M{}}, res["allow"])
assert.Equal(t, A{false, A{}}, res["deny"])
})
}
func TestSSHUsernameFromEmail(t *testing.T) {
policy := `
allow:
and:
- ssh_username_matches_email:`
t.Run("matches email from user", func(t *testing.T) {
res, err := evaluate(t, policy,
[]*databroker.Record{
makeRecord(&session.Session{
Id: "SESSION_ID",
UserId: "USER_ID",
}),
makeRecord(&user.User{
Id: "USER_ID",
Email: "my-user@example.com",
}),
},
Input{
SSH: InputSSH{Username: "my-user"},
Session: InputSession{ID: "SESSION_ID"},
})
require.NoError(t, err)
assert.Equal(t, A{true, A{ReasonSSHUsernameOK}, M{}}, res["allow"])
assert.Equal(t, A{false, A{}}, res["deny"])
})
t.Run("matches email from directory", func(t *testing.T) {
res, err := evaluate(t, policy,
[]*databroker.Record{
makeRecord(&session.Session{
Id: "SESSION_ID",
UserId: "USER_ID",
}),
makeStructRecord(directory.UserRecordType, "USER_ID", map[string]any{
"email": "my-user@example.com",
}),
},
Input{
SSH: InputSSH{Username: "my-user"},
Session: InputSession{ID: "SESSION_ID"},
})
require.NoError(t, err)
assert.Equal(t, A{true, A{ReasonSSHUsernameOK}, M{}}, res["allow"])
assert.Equal(t, A{false, A{}}, res["deny"])
})
t.Run("does not match", func(t *testing.T) {
res, err := evaluate(t, policy,
[]*databroker.Record{
makeRecord(&session.Session{
Id: "SESSION_ID",
UserId: "USER_ID",
}),
makeStructRecord(directory.UserRecordType, "USER_ID", map[string]any{
"email": "my-user@example.com",
}),
},
Input{
SSH: InputSSH{Username: "not-my-user"},
Session: InputSession{ID: "SESSION_ID"},
})
require.NoError(t, err)
assert.Equal(t, A{false, A{ReasonSSHUsernameUnauthorized}, M{}}, res["allow"])
assert.Equal(t, A{false, A{}}, res["deny"])
})
}
func TestSSHUsernameFromClaim(t *testing.T) {
t.Run("session claim ok", func(t *testing.T) {
res, err := evaluate(t, `
allow:
and:
- ssh_username_matches_claim: username
`,
[]*databroker.Record{
makeRecord(&session.Session{
Id: "SESSION_ID",
UserId: "USER_ID",
Claims: map[string]*structpb.ListValue{
"username": {Values: []*structpb.Value{
structpb.NewStringValue("root"),
structpb.NewStringValue("admin"),
}},
},
}),
makeRecord(&user.User{
Id: "USER_ID",
Email: "test@example.com",
}),
},
Input{
SSH: InputSSH{Username: "admin"},
Session: InputSession{ID: "SESSION_ID"},
},
)
require.NoError(t, err)
assert.Equal(t, A{true, A{ReasonSSHUsernameOK}, M{}}, res["allow"])
assert.Equal(t, A{false, A{}}, res["deny"])
})
t.Run("session claim unauthorized", func(t *testing.T) {
res, err := evaluate(t, `
allow:
and:
- ssh_username_matches_claim: username
`,
[]*databroker.Record{
makeRecord(&session.Session{
Id: "SESSION_ID",
UserId: "USER_ID",
Claims: map[string]*structpb.ListValue{
"username": {Values: []*structpb.Value{
structpb.NewStringValue("root"),
structpb.NewStringValue("admin"),
}},
},
}),
makeRecord(&user.User{
Id: "USER_ID",
Email: "test@example.com",
}),
},
Input{
SSH: InputSSH{Username: "other-username"},
Session: InputSession{ID: "SESSION_ID"},
},
)
require.NoError(t, err)
assert.Equal(t, A{false, A{ReasonSSHUsernameUnauthorized}, M{}}, res["allow"])
assert.Equal(t, A{false, A{}}, res["deny"])
})
t.Run("claim missing", func(t *testing.T) {
res, err := evaluate(t, `
allow:
and:
- ssh_username_matches_claim: username
`,
[]*databroker.Record{
makeRecord(&session.Session{
Id: "SESSION_ID",
UserId: "USER_ID",
}),
makeRecord(&user.User{
Id: "USER_ID",
Email: "test@example.com",
}),
},
Input{
SSH: InputSSH{Username: "admin"},
Session: InputSession{ID: "SESSION_ID"},
},
)
require.NoError(t, err)
assert.Equal(t, A{false, A{ReasonSSHUsernameUnauthorized}, M{}}, res["allow"])
assert.Equal(t, A{false, A{}}, res["deny"])
})
}