mirror of
https://github.com/pomerium/pomerium.git
synced 2025-08-06 02:09:15 +02:00
ppl: add new client certificate criterion (#4448)
Add a new client_certificate criterion that accepts a "Certificate Matcher" object. Start with two certificate match conditions: fingerprint and SPKI hash, each of which can accept either a single string or an array of strings. Add new "client-certificate-ok" and "client-certificate-unauthorized" reason strings.
This commit is contained in:
parent
f7e0b61c03
commit
ac475f4c5d
5 changed files with 400 additions and 27 deletions
164
pkg/policy/criteria/client_certificate.go
Normal file
164
pkg/policy/criteria/client_certificate.go
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
package criteria
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/open-policy-agent/opa/ast"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/pkg/policy/generator"
|
||||||
|
"github.com/pomerium/pomerium/pkg/policy/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
var clientCertificateBaseBody = ast.MustParseBody(`
|
||||||
|
cert := crypto.x509.parse_certificates(trim_space(input.http.client_certificate.leaf))[0]
|
||||||
|
fingerprint := crypto.sha256(base64.decode(cert.Raw))
|
||||||
|
spki_hash := base64.encode(hex.decode(
|
||||||
|
crypto.sha256(base64.decode(cert.RawSubjectPublicKeyInfo))))
|
||||||
|
`)
|
||||||
|
|
||||||
|
type clientCertificateCriterion struct {
|
||||||
|
g *Generator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (clientCertificateCriterion) DataType() generator.CriterionDataType {
|
||||||
|
return CriterionDataTypeCertificateMatcher
|
||||||
|
}
|
||||||
|
|
||||||
|
func (clientCertificateCriterion) Name() string {
|
||||||
|
return "client_certificate"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c clientCertificateCriterion) GenerateRule(
|
||||||
|
_ string, data parser.Value,
|
||||||
|
) (*ast.Rule, []*ast.Rule, error) {
|
||||||
|
body := append(ast.Body(nil), clientCertificateBaseBody...)
|
||||||
|
|
||||||
|
obj, ok := data.(parser.Object)
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, fmt.Errorf("expected object for certificate matcher, got: %T", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range obj {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch k {
|
||||||
|
case "fingerprint":
|
||||||
|
err = addCertFingerprintCondition(&body, v)
|
||||||
|
case "spki_hash":
|
||||||
|
err = addCertSPKIHashCondition(&body, v)
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("unsupported certificate matcher condition: %s", k)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rule := NewCriterionRule(c.g, c.Name(),
|
||||||
|
ReasonClientCertificateOK, ReasonClientCertificateUnauthorized,
|
||||||
|
body)
|
||||||
|
|
||||||
|
return rule, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addCertFingerprintCondition(body *ast.Body, data parser.Value) error {
|
||||||
|
var pa parser.Array
|
||||||
|
switch v := data.(type) {
|
||||||
|
case parser.Array:
|
||||||
|
pa = v
|
||||||
|
case parser.String:
|
||||||
|
pa = parser.Array{data}
|
||||||
|
default:
|
||||||
|
return errors.New("certificate fingerprint condition expects a string or array of strings")
|
||||||
|
}
|
||||||
|
|
||||||
|
ra := ast.NewArray()
|
||||||
|
for _, v := range pa {
|
||||||
|
f, err := canonicalCertFingerprint(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ra = ra.Append(ast.NewTerm(f))
|
||||||
|
}
|
||||||
|
|
||||||
|
*body = append(*body,
|
||||||
|
ast.Assign.Expr(ast.VarTerm("allowed_fingerprints"), ast.NewTerm(ra)),
|
||||||
|
ast.Equal.Expr(ast.VarTerm("fingerprint"), ast.VarTerm("allowed_fingerprints[_]")))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// The long certificate fingerprint format is 32 uppercase hex-encoded bytes
|
||||||
|
// separated by colons.
|
||||||
|
var longCertFingerprintRE = regexp.MustCompile("^[0-9A-F]{2}(:[0-9A-F]{2}){31}$")
|
||||||
|
|
||||||
|
// The short certificate fingerprint format is 32 lowercase hex-encoded bytes.
|
||||||
|
var shortCertFingerprintRE = regexp.MustCompile("^[0-9a-f]{64}$")
|
||||||
|
|
||||||
|
// canonicalCertFingeprint converts a single fingerprint value into the format
|
||||||
|
// that our Rego logic generates.
|
||||||
|
func canonicalCertFingerprint(data parser.Value) (ast.Value, error) {
|
||||||
|
s, ok := data.(parser.String)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("certificate fingerprint must be a string (was %v)", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
f := string(s)
|
||||||
|
if f == "" {
|
||||||
|
return nil, errors.New("certificate fingerprint must not be empty")
|
||||||
|
} else if shortCertFingerprintRE.MatchString(f) {
|
||||||
|
return ast.String(f), nil
|
||||||
|
} else if longCertFingerprintRE.MatchString(f) {
|
||||||
|
f = strings.ToLower(strings.ReplaceAll(f, ":", ""))
|
||||||
|
return ast.String(f), nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unsupported certificate fingerprint format (%s)", f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addCertSPKIHashCondition(body *ast.Body, data parser.Value) error {
|
||||||
|
var pa parser.Array
|
||||||
|
switch v := data.(type) {
|
||||||
|
case parser.Array:
|
||||||
|
pa = v
|
||||||
|
case parser.String:
|
||||||
|
pa = parser.Array{data}
|
||||||
|
default:
|
||||||
|
return errors.New("certificate SPKI hash condition expects a string or array of strings")
|
||||||
|
}
|
||||||
|
|
||||||
|
ra := ast.NewArray()
|
||||||
|
for _, v := range pa {
|
||||||
|
s, ok := v.(parser.String)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("certificate SPKI hash must be a string (was %v)", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
h := string(s)
|
||||||
|
if h == "" {
|
||||||
|
return errors.New("certificate SPKI hash must not be empty")
|
||||||
|
} else if b, err := base64.StdEncoding.DecodeString(h); err != nil || len(b) != 32 {
|
||||||
|
return fmt.Errorf("certificate SPKI hash must be a base64-encoded SHA-256 hash "+
|
||||||
|
"(was %s)", h)
|
||||||
|
}
|
||||||
|
|
||||||
|
ra = ra.Append(ast.NewTerm(ast.String(h)))
|
||||||
|
}
|
||||||
|
|
||||||
|
*body = append(*body,
|
||||||
|
ast.Assign.Expr(ast.VarTerm("allowed_spki_hashes"), ast.NewTerm(ra)),
|
||||||
|
ast.Equal.Expr(ast.VarTerm("spki_hash"), ast.VarTerm("allowed_spki_hashes[_]")))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClientCertificate returns a Criterion on a client certificate.
|
||||||
|
func ClientCertificate(generator *Generator) Criterion {
|
||||||
|
return clientCertificateCriterion{g: generator}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(ClientCertificate)
|
||||||
|
}
|
203
pkg/policy/criteria/client_certificate_test.go
Normal file
203
pkg/policy/criteria/client_certificate_test.go
Normal file
|
@ -0,0 +1,203 @@
|
||||||
|
package criteria
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/open-policy-agent/opa/ast"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/pkg/policy/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
const testCert = `
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIBYTCCAQigAwIBAgICEAEwCgYIKoZIzj0EAwIwGjEYMBYGA1UEAxMPVHJ1c3Rl
|
||||||
|
ZCBSb290IENBMCAYDzAwMDEwMTAxMDAwMDAwWhcNMzMwNzMxMTUzMzE5WjAeMRww
|
||||||
|
GgYDVQQDExN0cnVzdGVkIGNsaWVudCBjZXJ0MFkwEwYHKoZIzj0CAQYIKoZIzj0D
|
||||||
|
AQcDQgAEfAYP3ZwiKJgk9zXpR/CMHYlAxjweJaMJihIS2FTA5gb0xBcTEe5AGpNF
|
||||||
|
CHWPk4YCB25VeHg9GmY9Q1+qDD1hdqM4MDYwEwYDVR0lBAwwCgYIKwYBBQUHAwIw
|
||||||
|
HwYDVR0jBBgwFoAUXep6D8FTP6+5ZdR/HjP3pYfmxkwwCgYIKoZIzj0EAwIDRwAw
|
||||||
|
RAIgProROtxpvKS/qjrjonSvacnhdU0JwoXj2DgYvF/qjrUCIAXlHkdEzyXmTLuu
|
||||||
|
/YxuOibV35vlaIzj21GRj4pYmVR1
|
||||||
|
-----END CERTIFICATE-----`
|
||||||
|
|
||||||
|
func TestClientCertificate(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
label string
|
||||||
|
policy string
|
||||||
|
cert string
|
||||||
|
expected A
|
||||||
|
}{
|
||||||
|
{"no certificate",
|
||||||
|
`allow:
|
||||||
|
or:
|
||||||
|
- client_certificate:
|
||||||
|
fingerprint: 17859273e8a980631d367b2d5a6a6635412b0f22835f69e47b3f65624546a704`,
|
||||||
|
"",
|
||||||
|
A{false, A{ReasonClientCertificateUnauthorized}, M{}},
|
||||||
|
},
|
||||||
|
{"no fingerprint match",
|
||||||
|
`allow:
|
||||||
|
or:
|
||||||
|
- client_certificate:
|
||||||
|
fingerprint: df6ff72fe9116521268f6f2dd4966f51df479883fe7037b39f75916ac3049d1a`,
|
||||||
|
testCert,
|
||||||
|
A{false, A{ReasonClientCertificateUnauthorized}, M{}},
|
||||||
|
},
|
||||||
|
{"fingerprint match",
|
||||||
|
`allow:
|
||||||
|
or:
|
||||||
|
- client_certificate:
|
||||||
|
fingerprint: 17859273e8a980631d367b2d5a6a6635412b0f22835f69e47b3f65624546a704`,
|
||||||
|
testCert,
|
||||||
|
A{true, A{ReasonClientCertificateOK}, M{}},
|
||||||
|
},
|
||||||
|
{"fingerprint list match",
|
||||||
|
`allow:
|
||||||
|
or:
|
||||||
|
- client_certificate:
|
||||||
|
fingerprint:
|
||||||
|
- 17859273e8a980631d367b2d5a6a6635412b0f22835f69e47b3f65624546a704
|
||||||
|
- df6ff72fe9116521268f6f2dd4966f51df479883fe7037b39f75916ac3049d1a`,
|
||||||
|
testCert,
|
||||||
|
A{true, A{ReasonClientCertificateOK}, M{}},
|
||||||
|
},
|
||||||
|
{"spki hash match",
|
||||||
|
`allow:
|
||||||
|
or:
|
||||||
|
- client_certificate:
|
||||||
|
spki_hash: FsDbM0rUYIiL3V339eIKqiz6HPSB+Pz2WeAWhqlqh8U=`,
|
||||||
|
testCert,
|
||||||
|
A{true, A{ReasonClientCertificateOK}, M{}},
|
||||||
|
},
|
||||||
|
{"spki hash list match",
|
||||||
|
`allow:
|
||||||
|
or:
|
||||||
|
- client_certificate:
|
||||||
|
spki_hash:
|
||||||
|
- FsDbM0rUYIiL3V339eIKqiz6HPSB+Pz2WeAWhqlqh8U=
|
||||||
|
- NvqYIYSbgK2vCJpQhObf77vv+bQWtc5ek5RIOwPiC9A=`,
|
||||||
|
testCert,
|
||||||
|
A{true, A{ReasonClientCertificateOK}, M{}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range cases {
|
||||||
|
c := cases[i]
|
||||||
|
t.Run(c.label, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
input := Input{
|
||||||
|
HTTP: InputHTTP{
|
||||||
|
ClientCertificate: ClientCertificateInfo{
|
||||||
|
Leaf: c.cert,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
res, err := evaluate(t, c.policy, nil, input)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, c.expected, res["allow"])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCanonicalCertFingerprint(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
label string
|
||||||
|
input string
|
||||||
|
output string
|
||||||
|
err string
|
||||||
|
}{
|
||||||
|
{"object",
|
||||||
|
`{}`, "", "certificate fingerprint must be a string (was {})",
|
||||||
|
},
|
||||||
|
{"empty",
|
||||||
|
`""`, "", "certificate fingerprint must not be empty",
|
||||||
|
},
|
||||||
|
{"SHA-1 fingerprint",
|
||||||
|
`"B1:E6:A2:DC:DD:6B:87:A4:9B:C5:7C:3B:7C:7F:1C:74:9A:DB:88:36"`,
|
||||||
|
"", "unsupported certificate fingerprint format (B1:E6:A2:DC:DD:6B:87:A4:9B:C5:7C:3B:7C:7F:1C:74:9A:DB:88:36)",
|
||||||
|
},
|
||||||
|
{"uppercase short",
|
||||||
|
`"DF6FF72FE9116521268F6F2DD4966F51DF479883FE7037B39F75916AC3049D1A"`,
|
||||||
|
"", "unsupported certificate fingerprint format (DF6FF72FE9116521268F6F2DD4966F51DF479883FE7037B39F75916AC3049D1A)",
|
||||||
|
},
|
||||||
|
{"valid short",
|
||||||
|
`"df6ff72fe9116521268f6f2dd4966f51df479883fe7037b39f75916ac3049d1a"`,
|
||||||
|
"df6ff72fe9116521268f6f2dd4966f51df479883fe7037b39f75916ac3049d1a", "",
|
||||||
|
},
|
||||||
|
{"lowercase long",
|
||||||
|
`"df:6f:f7:2f:e9:11:65:21:26:8f:6f:2d:d4:96:6f:51:df:47:98:83:fe:70:37:b3:9f:75:91:6a:c3:04:9d:1a"`,
|
||||||
|
"", "unsupported certificate fingerprint format (df:6f:f7:2f:e9:11:65:21:26:8f:6f:2d:d4:96:6f:51:df:47:98:83:fe:70:37:b3:9f:75:91:6a:c3:04:9d:1a)",
|
||||||
|
},
|
||||||
|
{"valid long",
|
||||||
|
`"DF:6F:F7:2F:E9:11:65:21:26:8F:6F:2D:D4:96:6F:51:DF:47:98:83:FE:70:37:B3:9F:75:91:6A:C3:04:9D:1A"`,
|
||||||
|
"df6ff72fe9116521268f6f2dd4966f51df479883fe7037b39f75916ac3049d1a", "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range cases {
|
||||||
|
c := cases[i]
|
||||||
|
t.Run(c.label, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
value, err := parser.ParseValue(strings.NewReader(c.input))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
f, err := canonicalCertFingerprint(value)
|
||||||
|
if c.err == "" {
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, ast.String(c.output), f)
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, c.err, err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSPKIHashFormatErrors(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
label string
|
||||||
|
input string
|
||||||
|
err string
|
||||||
|
}{
|
||||||
|
{"object",
|
||||||
|
`{}`, "certificate SPKI hash condition expects a string or array of strings",
|
||||||
|
},
|
||||||
|
{"not base64",
|
||||||
|
`"not%valid%base64%data"`, "certificate SPKI hash must be a base64-encoded SHA-256 hash (was not%valid%base64%data)",
|
||||||
|
},
|
||||||
|
{"SHA-1 hash",
|
||||||
|
`"VYby3BAoHawLLtsyckwo5Q=="`, "certificate SPKI hash must be a base64-encoded SHA-256 hash (was VYby3BAoHawLLtsyckwo5Q==)",
|
||||||
|
},
|
||||||
|
{"valid",
|
||||||
|
`"FsDbM0rUYIiL3V339eIKqiz6HPSB+Pz2WeAWhqlqh8U="`, "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range cases {
|
||||||
|
c := cases[i]
|
||||||
|
t.Run(c.label, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
value, err := parser.ParseValue(strings.NewReader(c.input))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var body ast.Body
|
||||||
|
err = addCertSPKIHashCondition(&body, value)
|
||||||
|
if c.err == "" {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, c.err, err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -45,6 +45,9 @@ func Register(criterionConstructor CriterionConstructor) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// CriterionDataTypeCertificateMatcher indicates the expected data type is
|
||||||
|
// a certificate matcher.
|
||||||
|
CriterionDataTypeCertificateMatcher CriterionDataType = "certificate_matcher"
|
||||||
// CriterionDataTypeStringListMatcher indicates the expected data type is a string list matcher.
|
// CriterionDataTypeStringListMatcher indicates the expected data type is a string list matcher.
|
||||||
CriterionDataTypeStringListMatcher CriterionDataType = "string_list_matcher"
|
CriterionDataTypeStringListMatcher CriterionDataType = "string_list_matcher"
|
||||||
// CriterionDataTypeStringMatcher indicates the expected data type is a string matcher.
|
// CriterionDataTypeStringMatcher indicates the expected data type is a string matcher.
|
||||||
|
|
|
@ -43,7 +43,8 @@ type (
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
}
|
}
|
||||||
ClientCertificateInfo struct {
|
ClientCertificateInfo struct {
|
||||||
Presented bool `json:"presented"`
|
Presented bool `json:"presented"`
|
||||||
|
Leaf string `json:"leaf"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -7,32 +7,34 @@ type Reason string
|
||||||
|
|
||||||
// Well-known reasons.
|
// Well-known reasons.
|
||||||
const (
|
const (
|
||||||
ReasonAccept = "accept"
|
ReasonAccept = "accept"
|
||||||
ReasonClaimOK = "claim-ok"
|
ReasonClaimOK = "claim-ok"
|
||||||
ReasonClaimUnauthorized = "claim-unauthorized"
|
ReasonClaimUnauthorized = "claim-unauthorized"
|
||||||
ReasonClientCertificateRequired = "client-certificate-required"
|
ReasonClientCertificateOK = "client-certificate-ok"
|
||||||
ReasonCORSRequest = "cors-request"
|
ReasonClientCertificateUnauthorized = "client-certificate-unauthorized"
|
||||||
ReasonDeviceOK = "device-ok"
|
ReasonClientCertificateRequired = "client-certificate-required"
|
||||||
ReasonDeviceUnauthenticated = "device-unauthenticated"
|
ReasonCORSRequest = "cors-request"
|
||||||
ReasonDeviceUnauthorized = "device-unauthorized"
|
ReasonDeviceOK = "device-ok"
|
||||||
ReasonDomainOK = "domain-ok"
|
ReasonDeviceUnauthenticated = "device-unauthenticated"
|
||||||
ReasonDomainUnauthorized = "domain-unauthorized"
|
ReasonDeviceUnauthorized = "device-unauthorized"
|
||||||
ReasonEmailOK = "email-ok"
|
ReasonDomainOK = "domain-ok"
|
||||||
ReasonEmailUnauthorized = "email-unauthorized"
|
ReasonDomainUnauthorized = "domain-unauthorized"
|
||||||
ReasonHTTPMethodOK = "http-method-ok"
|
ReasonEmailOK = "email-ok"
|
||||||
ReasonHTTPMethodUnauthorized = "http-method-unauthorized"
|
ReasonEmailUnauthorized = "email-unauthorized"
|
||||||
ReasonHTTPPathOK = "http-path-ok"
|
ReasonHTTPMethodOK = "http-method-ok"
|
||||||
ReasonHTTPPathUnauthorized = "http-path-unauthorized"
|
ReasonHTTPMethodUnauthorized = "http-method-unauthorized"
|
||||||
ReasonInvalidClientCertificate = "invalid-client-certificate"
|
ReasonHTTPPathOK = "http-path-ok"
|
||||||
ReasonNonCORSRequest = "non-cors-request"
|
ReasonHTTPPathUnauthorized = "http-path-unauthorized"
|
||||||
ReasonNonPomeriumRoute = "non-pomerium-route"
|
ReasonInvalidClientCertificate = "invalid-client-certificate"
|
||||||
ReasonPomeriumRoute = "pomerium-route"
|
ReasonNonCORSRequest = "non-cors-request"
|
||||||
ReasonReject = "reject"
|
ReasonNonPomeriumRoute = "non-pomerium-route"
|
||||||
ReasonRouteNotFound = "route-not-found"
|
ReasonPomeriumRoute = "pomerium-route"
|
||||||
ReasonUserOK = "user-ok"
|
ReasonReject = "reject"
|
||||||
ReasonUserUnauthenticated = "user-unauthenticated" // user needs to log in
|
ReasonRouteNotFound = "route-not-found"
|
||||||
ReasonUserUnauthorized = "user-unauthorized" // user does not have access
|
ReasonUserOK = "user-ok"
|
||||||
ReasonValidClientCertificate = "valid-client-certificate"
|
ReasonUserUnauthenticated = "user-unauthenticated" // user needs to log in
|
||||||
|
ReasonUserUnauthorized = "user-unauthorized" // user does not have access
|
||||||
|
ReasonValidClientCertificate = "valid-client-certificate"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Reasons is a collection of reasons.
|
// Reasons is a collection of reasons.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue