Commit 65ab4547 by Geoff Simmons

Auth config uses the same conditions array as for ACLs.

Closes #22
parent 6af60a42
......@@ -79,15 +79,28 @@ spec:
type: string
utf8:
type: boolean
condition:
type: object
properties:
url-match:
type: string
minLength: 1
host-match:
type: string
minLength: 1
conditions:
type: array
minItems: 1
items:
type: object
required:
- comparand
- value
properties:
comparand:
type: string
pattern: "^req\\.(url|http\\.[a-zA-Z0-9!#$%&'*+.^_`|~-]+)$"
compare:
enum:
- equal
- not-equal
- match
- not-match
type: string
value:
type: string
minLength: 1
acl:
type: array
minItems: 1
......
......@@ -191,26 +191,56 @@ These fields in the elements of ``auth`` are optional:
encoding is used for the username/password (see
[RFC 7617 2.1](https://tools.ietf.org/html/rfc7617#section-2.1)).
By default, ``charset`` is ``false``.
* ``condition``: conditions under which the authentication protocol is
* ``conditions``: conditions under which the authentication protocol is
to be executed.
If the ``condition`` object is present, it may have either or both of
these fields:
If the ``conditions`` array is present, then it MUST have at least one
element, and each element must specify at least these two fields:
* ``url-match`` (regular expression): pattern to match against the
URL path of the request
* ``host-match`` (regular expression): pattern to match against the
``Host`` request header
* ``comparand`` (string): either ``req.url`` or ``req.http.$HEADER``,
where ``$HEADER`` is the name of a client request header.
* ``value`` (string): the value against which the ``comparand``
is compared
If either or both of these two fields are present, then the
authentication protocol is executed for matching requests. If the
``condition`` is left out, then the authentication is required for
every client request. The patterns in ``url-match`` and
``host-match`` are implemented as
``conditions`` may also have this optional field:
* ``compare``: one of the following (default ``equal``):
* ``equal`` for string equality
* ``not-equal`` for string inequality
* ``match`` for regex match
* ``not-match`` for regex non-match
If ``compare`` is ``equal`` or ``not-equal``, then ``value`` is
interpreted as a fixed string, and ``comparand`` is tested for
(in)equality with ``value``. Otherwise, ``value`` is interpreted as a
regular expression, and the ``comparand`` is tested for
(non-)match. Regexen are implemented as
[VCL regular expressions](https://varnish-cache.org/docs/6.1/reference/vcl.html#regular-expressions),
and hence have the syntax and semantics of
[PCRE](https://www.pcre.org/original/doc/html/).
The authentication protocol is executed only if all of the
``conditions`` succeed; in other words, the ``conditions`` are the
boolean AND of all of the match terms.
For example, these ``conditions`` specify that authentication is
executed when the URL begins with "/tea", and the Host header is
exactly "cafe.example.com":
```
conditions:
- comparand: req.url
compare: match
value: ^/tea(/|$)
- comparand: req.http.Host
value: cafe.example.com
```
Validation for ``VarnishConfig`` reports errors at apply time if:
* the ``auth`` array is empty
......@@ -218,10 +248,10 @@ Validation for ``VarnishConfig`` reports errors at apply time if:
* any of the string fields are empty
* ``type`` has an illegal value (neither of ``basic`` or ``proxy``)
Other errors, in particular illegal regex syntax for ``url-match`` or
``host-match``, are not reported until VCL load time. Check the
controller log and Events generated for the Varnish Service for error
messages from the VCL compiler.
Other errors, in particular illegal regex syntax for ``conditions``,
are not reported until VCL load time. Check the controller log and
Events generated for the Varnish Service for error messages from the
VCL compiler.
Examples:
```
......@@ -237,9 +267,13 @@ spec:
secretName: coffee-creds
type: basic
utf8: true
condition:
host-match: ^cafe\.example\.com$
url-match: ^/coffee($|/)
conditions:
- comparand: req.http.Host
value: cafe.example.com
compare: equal
- comparand: req.url
value: ^/coffee($|/)
compare: match
# For the tea Service, require authentication for the realm "tea"
# when the Host is "cafe.example.com" and the URL path begins with
......@@ -248,9 +282,13 @@ spec:
# left out.
- realm: tea
secretName: tea-creds
condition:
host-match: ^cafe\.example\.com$
url-match: ^/tea($|/)
conditions:
- comparand: req.http.Host
value: cafe.example.com
compare: equal
- comparand: req.url
value: ^/tea($|/)
compare: match
```
```
spec:
......@@ -288,8 +326,10 @@ Optional fields for ``acl`` are:
is matched, as detailed below; default ``client.ip``
* ``conditions``: array of conditions under which the ACL match is
executed, as detailed below. By default, ``conditions`` is empty,
in which case the match is executed for every client request.
executed. The ``conditions`` field has the same syntax and semantics
as specified above for ``spec.auth`` (Basic and Proxy
Authentication). By default, ``conditions`` is empty, in which case
the match is executed for every client request.
* ``result-header``: specifies a client request header and values to
set for the header when the failure status is or is not invoked for
......@@ -414,52 +454,10 @@ X-Forwarded-For: 192.0.2.47, 203.0.113.11
``xff-2ndlast`` specifies a match against 203.0.113.11.
If ``conditions`` are specified for an ACL, they define restrictions
for executing the match. Each element of ``conditions`` must specify
these two required fields:
* ``comparand`` (string): either ``req.url`` or ``req.http.$HEADER``,
where ``$HEADER`` is the name of a client request header.
* ``value`` (string): the value against which the ``comparand``
is compared
``conditions`` may also have this optional field:
* ``compare``: one of the following (default ``equal``):
* ``equal`` for string equality
* ``not-equal`` for string inequality
* ``match`` for regex match
* ``not-match`` for regex non-match
If ``compare`` is ``equal`` or ``not-equal``, then ``value`` is
interpreted as a fixed string, and ``comparand`` is tested for
(in)equality with ``value``. Otherwise, ``value`` is interpreted as a
regular expression, and the ``comparand`` is tested for
(non-)match. Regexen are implemented as
[VCL regular expressions](https://varnish-cache.org/docs/6.1/reference/vcl.html#regular-expressions),
and hence have the syntax and semantics of
[PCRE](https://www.pcre.org/original/doc/html/).
The ACL match is executed only if all of the ``conditions`` succeed;
in other words, the ``conditions`` are the boolean AND of all of the
match terms.
For example, these ``conditions`` specify that the match is executed
when the URL begins with "/tea", and the Host header is exactly
"cafe.example.com":
```
conditions:
- comparand: req.url
compare: match
value: ^/tea(/|$)
- comparand: req.http.Host
value: cafe.example.com
```
for executing the match; the ACL match is executed only if all of the
``conditions`` succeed. The ``conditions`` field for ACLs has the same
syntax and semantics as specified above for Basic and Proxy
Authentication.
The ``result-header`` field specifies a client request header that is
set with a value for the "fail" or "success" results of the ACL
......
......@@ -69,15 +69,22 @@ from the ``coffee-creds`` Secret:
secretName: coffee-creds
type: basic
utf8: true
condition:
host-match: ^cafe\.example\.com$
url-match: ^/coffee($|/)
```
``type: basic`` specifies Basic Authentication, and the ``host-match``
and ``url-match`` fields require authentication in the "coffee" realm
when the Host is exactly equal to "cafe.example.com", and the URL path
begins with "/coffee".
conditions:
- comparand: req.http.Host
value: cafe.example.com
compare: equal
- comparand: req.url
value: ^/coffee($|/)
compare: match
```
``type: basic`` specifies Basic Authentication, and the ``conditions``
array requires authentication when a request is routed to the
coffee-svc Service. The first element of ``comparand`` specifies that
the Host header is exactly equal to "cafe.example.com", and the second
specifies that the URL path begins with "/coffee". The Basic
Authentication protocol configured here is only executed when all of
the ``conditions`` are met.
The ``utf8: true`` setting means that the field ``charset="UTF-8"``
field is appended to the ``WWW-Authenticate`` response header when
......@@ -93,9 +100,13 @@ from the ``tea-creds`` Secret when the URL path begins with "/tea":
```
- realm: tea
secretName: tea-creds
condition:
host-match: ^cafe\.example\.com$
url-match: ^/tea($|/)
conditions:
- comparand: req.http.Host
value: cafe.example.com
compare: equal
- comparand: req.url
value: ^/tea($|/)
compare: match
```
Not that ``type: basic`` was left out here, since ``basic`` is the
......@@ -175,9 +186,9 @@ unconditionally to all requests:
```
As with Basic Authentication, it is also possible to use the
``condition.host-match`` and ``condition.url-match`` fields to
restrict the requests for which the authentication is required (but
Proxy Authentication typically applies to all requests).
``conditions`` array to restrict the requests for which the
authentication is required (but Proxy Authentication typically applies
to all requests).
To verify with curl, we use the ``-x`` (or ``--proxy``) argument to
specify ``$ADDR:$PORT`` as the proxy, and send the request with an
......
......@@ -22,9 +22,13 @@ spec:
secretName: coffee-creds
type: basic
utf8: true
condition:
host-match: ^cafe\.example\.com$
url-match: ^/coffee($|/)
conditions:
- comparand: req.http.Host
value: cafe.example.com
compare: equal
- comparand: req.url
value: ^/coffee($|/)
compare: match
# For the tea Service, require authentication for the realm "tea"
# when the Host is "cafe.example.com" and the URL path begins with
......@@ -33,6 +37,10 @@ spec:
# left out.
- realm: tea
secretName: tea-creds
condition:
host-match: ^cafe\.example\.com$
url-match: ^/tea($|/)
conditions:
- comparand: req.http.Host
value: cafe.example.com
compare: equal
- comparand: req.url
value: ^/tea($|/)
compare: match
......@@ -79,11 +79,11 @@ type ProbeSpec struct {
// AuthSpec specifies authentication (basic or proxy).
type AuthSpec struct {
Realm string `json:"realm"`
SecretName string `json:"secretName"`
Type AuthType `json:"type,omitempty"`
Condition *AuthCondition `json:"condition,omitempty"`
UTF8 bool `json:"utf8,omitempty"`
Realm string `json:"realm"`
SecretName string `json:"secretName"`
Type AuthType `json:"type,omitempty"`
UTF8 bool `json:"utf8,omitempty"`
Conditions []Condition `json:"conditions,omitempty"`
}
// AuthType classifies the protocol for an AuthSpec.
......@@ -96,14 +96,6 @@ const (
Proxy = "proxy"
)
// AuthCondition specifies a condition under which an authentication
// protocol must be executed -- the URL path or the Host must match a
// pattern (or both).
type AuthCondition struct {
URLRegex string `json:"url-match,omitempty"`
HostRegex string `json:"host-match,omitempty"`
}
// ACLSpec specifies whitelisting or blacklisting IP addresses against
// an access control list.
type ACLSpec struct {
......
......@@ -94,28 +94,12 @@ func (in *ACLSpec) DeepCopy() *ACLSpec {
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AuthCondition) DeepCopyInto(out *AuthCondition) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthCondition.
func (in *AuthCondition) DeepCopy() *AuthCondition {
if in == nil {
return nil
}
out := new(AuthCondition)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AuthSpec) DeepCopyInto(out *AuthSpec) {
*out = *in
if in.Condition != nil {
in, out := &in.Condition, &out.Condition
*out = new(AuthCondition)
**out = **in
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]Condition, len(*in))
copy(*out, *in)
}
return
}
......
......@@ -326,6 +326,33 @@ func (worker *NamespaceWorker) configSharding(spec *vcl.Spec,
return nil
}
func configConditions(vclConds []vcl.MatchTerm,
vcfgConds []vcr_v1alpha1.Condition) {
if len(vclConds) != len(vcfgConds) {
panic("configConditions: unequal slice lengths")
}
for i, cond := range vcfgConds {
vclMatch := vcl.MatchTerm{
Comparand: cond.Comparand,
Value: cond.Value,
}
switch cond.Compare {
case vcr_v1alpha1.Equal:
vclMatch.Compare = vcl.Equal
case vcr_v1alpha1.NotEqual:
vclMatch.Compare = vcl.NotEqual
case vcr_v1alpha1.Match:
vclMatch.Compare = vcl.Match
case vcr_v1alpha1.NotMatch:
vclMatch.Compare = vcl.NotMatch
default:
vclMatch.Compare = vcl.Equal
}
vclConds[i] = vclMatch
}
}
func (worker *NamespaceWorker) configAuth(spec *vcl.Spec,
vcfg *vcr_v1alpha1.VarnishConfig) error {
......@@ -357,6 +384,7 @@ func (worker *NamespaceWorker) configAuth(spec *vcl.Spec,
vclAuth := vcl.Auth{
Realm: auth.Realm,
Credentials: make([]string, 0, len(secret.Data)),
Conditions: make([]vcl.MatchTerm, len(auth.Conditions)),
UTF8: auth.UTF8,
}
if auth.Type == "" || auth.Type == vcr_v1alpha1.Basic {
......@@ -372,10 +400,7 @@ func (worker *NamespaceWorker) configAuth(spec *vcl.Spec,
vcfg.Name, cred, vclAuth.Realm)
vclAuth.Credentials = append(vclAuth.Credentials, cred)
}
if auth.Condition != nil {
vclAuth.Condition.URLRegex = auth.Condition.URLRegex
vclAuth.Condition.HostRegex = auth.Condition.HostRegex
}
configConditions(vclAuth.Conditions, auth.Conditions)
worker.log.Debugf("VarnishConfig %s/%s add VCL auth config: "+
"%+v", vcfg.Namespace, vcfg.Name, vclAuth)
spec.Auths = append(spec.Auths, vclAuth)
......@@ -426,23 +451,7 @@ func (worker *NamespaceWorker) configACL(spec *vcl.Spec,
}
vclACL.Addresses[j] = vclAddr
}
for j, cond := range acl.Conditions {
vclMatch := vcl.MatchTerm{
Comparand: cond.Comparand,
Value: cond.Value,
}
switch cond.Compare {
case vcr_v1alpha1.Equal:
vclMatch.Compare = vcl.Equal
case vcr_v1alpha1.NotEqual:
vclMatch.Compare = vcl.NotEqual
case vcr_v1alpha1.Match:
vclMatch.Compare = vcl.Match
case vcr_v1alpha1.NotMatch:
vclMatch.Compare = vcl.NotMatch
}
vclACL.Conditions[j] = vclMatch
}
configConditions(vclACL.Conditions, acl.Conditions)
if acl.ResultHdr != nil {
worker.log.Debugf("ACL %s: ResultHdr=%+v", acl.Name,
*acl.ResultHdr)
......
......@@ -14,11 +14,8 @@ sub vcl_init {
sub vcl_recv {
{{- range .Auths}}
if (
{{- if ne .Condition.HostRegex ""}}
req.http.Host ~ "{{.Condition.HostRegex}}" &&
{{- end}}
{{- if ne .Condition.URLRegex ""}}
req.url ~ "{{.Condition.URLRegex}}" &&
{{- range $cond := .Conditions}}
{{$cond.Comparand}} {{cmpRelation .Compare}} "{{.Value}}" &&
{{- end}}
{{- if eq .Status 401}}
!{{credsMatcher .Realm}}.match(req.http.Authorization)
......
......@@ -242,16 +242,6 @@ func (shard ShardCluster) hash(hash hash.Hash) {
hash.Write([]byte(shard.MaxSecondaryTTL))
}
// Condition specifies conditions under which an authentication
// protocols must be executed -- the URL path or the Host must match
// patterns, the request must be received from a TLS offloader, or any
// combination of the three.
type Condition struct {
URLRegex string
HostRegex string
TLS bool
}
// AuthStatus is the response code to be sent for authentication
// failures, and serves to distinguish the protocols.
type AuthStatus uint16
......@@ -266,10 +256,10 @@ const (
// Auth specifies Basic or Proxy Authentication, derived from an
// AuthSpec in a VarnishConfig resource.
type Auth struct {
Realm string
Conditions []MatchTerm
Credentials []string
Realm string
Status AuthStatus
Condition Condition
UTF8 bool
}
......@@ -281,10 +271,8 @@ func (auth Auth) hash(hash hash.Hash) {
statusBytes := make([]byte, 2)
binary.BigEndian.PutUint16(statusBytes, uint16(auth.Status))
hash.Write(statusBytes)
hash.Write([]byte(auth.Condition.URLRegex))
hash.Write([]byte(auth.Condition.HostRegex))
if auth.Condition.TLS {
hash.Write([]byte("TLS"))
for _, cond := range auth.Conditions {
cond.hash(hash)
}
if auth.UTF8 {
hash.Write([]byte("UTF8"))
......
......@@ -42,7 +42,7 @@ sub vcl_recv {
return(synth(60000 + 407));
}
if (
req.http.Host ~ "^baz\.com$" &&
req.http.Host == "baz.com" &&
!vk8s_baz_auth.match(req.http.Authorization)
) {
set req.http.VK8S-Authenticate =
......@@ -58,7 +58,7 @@ sub vcl_recv {
return(synth(60000 + 407));
}
if (
req.http.Host ~ "^url\.regex\.org$" &&
req.http.Host == "url.regex.org" &&
req.url ~ "^/secret/path" &&
!vk8s_urlhost_auth.match(req.http.Authorization)
) {
......
......@@ -244,9 +244,11 @@ var auths = Spec{
"dXNlcjpwYXNzd29yZDE=",
"bmFtZTpzZWNyZXQ=",
},
Condition: Condition{
HostRegex: `^baz\.com$`,
},
Conditions: []MatchTerm{{
Comparand: "req.http.Host",
Value: `baz.com`,
Compare: Equal,
}},
UTF8: true,
},
{
......@@ -256,9 +258,11 @@ var auths = Spec{
"YmVudXR6ZXI6Z2VoZWlt",
"QWxiZXJ0IEFkZGluOm9wZW4gc2V6IG1l",
},
Condition: Condition{
URLRegex: "^/baz/quux",
},
Conditions: []MatchTerm{{
Comparand: "req.url",
Value: "^/baz/quux",
Compare: Match,
}},
UTF8: true,
},
{
......@@ -268,9 +272,17 @@ var auths = Spec{
"dXJsOmhvc3Q=",
"YWRtaW46c3VwZXJwb3dlcnM=",
},
Condition: Condition{
HostRegex: `^url\.regex\.org$`,
URLRegex: "^/secret/path",
Conditions: []MatchTerm{
{
Comparand: "req.http.Host",
Value: "url.regex.org",
Compare: Equal,
},
{
Comparand: "req.url",
Value: "^/secret/path",
Compare: Match,
},
},
},
},
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment