Commit 5be22808 authored by Geoff Simmons's avatar Geoff Simmons

Add support for Basic and Proxy authentication.

parent ac720f7a
......@@ -57,6 +57,37 @@ spec:
type: integer
minimum: 0
maximum: 64
auth:
type: array
minItems: 1
items:
type: object
required:
- realm
- secretName
properties:
realm:
type: string
minLength: 1
secretName:
type: string
minLength: 1
type:
enum:
- basic
- proxy
type: string
utf8:
type: boolean
condition:
type: object
properties:
url-match:
type: string
minLength: 1
host-match:
type: string
minLength: 1
status:
acceptedNames:
kind: VarnishConfig
......
......@@ -156,3 +156,104 @@ spec:
window: 4
threshold: 3
```
### ``spec.auth``
The ``auth`` object is optional, and if present it contains a
non-empty array of specifications for authentication protocols (Basic
or Proxy) to be implemented by Varnish Services listed in the
``services`` array. See [RFC7235](https://tools.ietf.org/html/rfc7235)
for the HTTP Authentication standard.
For each element of ``auth``, these two fields are required:
* ``realm``: string identifying the realm or "protection space" for
authentication
* ``secretName``: the name of a Secret in the same namespace as the
VarnishConfig resource and Varnish Services that contains the
username/password credentials for authentication
The key-value pairs in the Secret are the username-password pairs to
be used for authentication.
These fields in the elements of ``auth`` are optional:
* ``type`` (string): one of the values ``basic`` or ``proxy`` to
specify the authentication protocol, ``basic`` by default
* ``utf8`` (boolean): if ``true``, then the ``charset="UTF-8"``
field is added to the ``*-Authenticate`` response header
(``WWW-Authentcate`` or ``Proxy-Authenticate``) in the case of
authentication failures, to advise clients that UTF-8 character
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
to be executed.
If the ``condition`` object is present, it may have either or both of
these 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
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
[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/).
Validation for ``VarnishConfig`` reports errors at apply time if:
* the ``auth`` array is empty
* either of the fields ``realm`` or ``secretName`` is left out
* 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.
Examples:
```
spec:
# Require Basic Authentication for both the coffee and tea Services.
auth:
# For the coffee Service, require authentication for the realm
# "coffee" when the Host is "cafe.example.com" and the URL path
# begins with "/coffee". Username/password pairs are taken from
# the Secret "coffee-creds" in the same namespace, and clients
# are advised that they are encoded with UTF-8.
- realm: coffee
secretName: coffee-creds
type: basic
utf8: true
condition:
host-match: ^cafe\.example\.com$
url-match: ^/coffee($|/)
# For the tea Service, require authentication for the realm "tea"
# when the Host is "cafe.example.com" and the URL path begins with
# "/tea", with usernames/passwords from the Secret
# "tea-creds". Note that the "type" defaults to basic and can be
# left out.
- realm: tea
secretName: tea-creds
condition:
host-match: ^cafe\.example\.com$
url-match: ^/tea($|/)
```
```
spec:
# Require Proxy Authentication for the realm "ingress" for every
# request, using usernames/passwords from the Secret "proxy-creds".
auth:
- realm: ingress
secretName: proxy-creds
type: proxy
```
......@@ -51,6 +51,7 @@ type VarnishConfig struct {
type VarnishConfigSpec struct {
Services []string `json:"services,omitempty"`
SelfSharding *SelfShardSpec `json:"self-sharding,omitempty"`
Auth []AuthSpec `json:"auth,omitempty"`
}
// SelfShardSpec specifies self-sharding in a Varnish cluster.
......@@ -70,6 +71,33 @@ type ProbeSpec struct {
Threshold *int32 `json:"threshold,omitempty"`
}
// 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"`
}
// AuthType classifies the protocol for an AuthSpec.
type AuthType string
const (
// Basic Authentication
Basic AuthType = "basic"
// Proxy Authentication
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"`
}
// VarnishConfigStatus is the status for a VarnishConfig resource
// type VarnishConfigStatus struct {
// AvailableReplicas int32 `json:"availableReplicas"`
......
......@@ -34,6 +34,43 @@ import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// 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
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthSpec.
func (in *AuthSpec) DeepCopy() *AuthSpec {
if in == nil {
return nil
}
out := new(AuthSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ProbeSpec) DeepCopyInto(out *ProbeSpec) {
*out = *in
......@@ -155,6 +192,13 @@ func (in *VarnishConfigSpec) DeepCopyInto(out *VarnishConfigSpec) {
*out = new(SelfShardSpec)
(*in).DeepCopyInto(*out)
}
if in.Auth != nil {
in, out := &in.Auth, &out.Auth
*out = make([]AuthSpec, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
......
......@@ -31,6 +31,7 @@ package controller
// Methods for syncing Ingresses
import (
"encoding/base64"
"fmt"
"strconv"
......@@ -161,26 +162,9 @@ func (worker *NamespaceWorker) ing2VCLSpec(
}
func (worker *NamespaceWorker) configSharding(spec *vcl.Spec,
svc *api_v1.Service) error {
vcfg *vcr_v1alpha1.VarnishConfig, svc *api_v1.Service) error {
var vcfg *vcr_v1alpha1.VarnishConfig
vcfgs, err := worker.vcfg.List(labels.Everything())
if err != nil {
return err
}
worker.log.Debugf("Listing VarnishConfigs in namespace %s",
worker.namespace)
for _, v := range vcfgs {
worker.log.Debugf("VarnishConfig: %s/%s: %+v", v.Namespace,
v.Name, v)
for _, svcName := range v.Spec.Services {
if svcName == svc.Name {
vcfg = v
break
}
}
}
if vcfg == nil || vcfg.Spec.SelfSharding == nil {
if vcfg.Spec.SelfSharding == nil {
worker.log.Debugf("No cluster shard configuration for Service "+
"%s/%s", svc.Namespace, svc.Name)
return nil
......@@ -268,6 +252,63 @@ func (worker *NamespaceWorker) configSharding(spec *vcl.Spec,
return nil
}
func (worker *NamespaceWorker) configAuth(spec *vcl.Spec,
vcfg *vcr_v1alpha1.VarnishConfig) error {
if len(vcfg.Spec.Auth) == 0 {
worker.log.Infof("No Auth spec found for VarnishConfig %s/%s",
vcfg.Namespace, vcfg.Name)
return nil
}
worker.log.Debugf("VarnishConfig %s/%s: configure %d VCL auths",
vcfg.Namespace, vcfg.Name, len(vcfg.Spec.Auth))
spec.Auths = make([]vcl.Auth, 0, len(vcfg.Spec.Auth))
for _, auth := range vcfg.Spec.Auth {
worker.log.Debugf("VarnishConfig %s/%s configuring VCL auth "+
"from: %+v", vcfg.Namespace, vcfg.Name, auth)
secret, err := worker.secr.Get(auth.SecretName)
if err != nil {
return err
}
if len(secret.Data) == 0 {
worker.log.Warnf("No secrets found in Secret %s/%s "+
"for realm %s in VarnishConfig %s/%s, ignoring",
secret.Namespace, secret.Name, auth.Realm,
vcfg.Namespace, vcfg.Name)
continue
}
worker.log.Debugf("VarnishConfig %s/%s configure %d "+
"credentials for realm %s", vcfg.Namespace, vcfg.Name,
len(secret.Data), auth.Realm)
vclAuth := vcl.Auth{
Realm: auth.Realm,
Credentials: make([]string, 0, len(secret.Data)),
UTF8: auth.UTF8,
}
if auth.Type == "" || auth.Type == vcr_v1alpha1.Basic {
vclAuth.Status = vcl.Basic
} else {
vclAuth.Status = vcl.Proxy
}
for user, pass := range secret.Data {
str := user + ":" + string(pass)
cred := base64.StdEncoding.EncodeToString([]byte(str))
worker.log.Debugf("VarnishConfig %s/%s: add cred %s "+
"for realm %s to VCL config", vcfg.Namespace,
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
}
worker.log.Debugf("VarnishConfig %s/%s add VCL auth config: "+
"%+v", vcfg.Namespace, vcfg.Name, vclAuth)
spec.Auths = append(spec.Auths, vclAuth)
}
return nil
}
func (worker *NamespaceWorker) hasIngress(svc *api_v1.Service,
ing *extensions.Ingress, spec vcl.Spec) bool {
......@@ -299,9 +340,37 @@ func (worker *NamespaceWorker) addOrUpdateIng(ing *extensions.Ingress) error {
return err
}
if err = worker.configSharding(&vclSpec, svc); err != nil {
var vcfg *vcr_v1alpha1.VarnishConfig
worker.log.Debugf("Listing VarnishConfigs in namespace %s",
worker.namespace)
vcfgs, err := worker.vcfg.List(labels.Everything())
if err != nil {
return err
}
for _, v := range vcfgs {
worker.log.Debugf("VarnishConfig: %s/%s: %+v", v.Namespace,
v.Name, v)
for _, svcName := range v.Spec.Services {
if svcName == svc.Name {
vcfg = v
break
}
}
}
if vcfg != nil {
worker.log.Infof("Found VarnishConfig %s/%s for Varnish "+
"Service %s/%s", vcfg.Namespace, vcfg.Name,
svc.Namespace, svc.Name)
if err = worker.configSharding(&vclSpec, vcfg, svc); err != nil {
return err
}
if err = worker.configAuth(&vclSpec, vcfg); err != nil {
return err
}
} else {
worker.log.Infof("Found no VarnishConfigs for Varnish Service "+
"%s/%s", svc.Namespace, svc.Name)
}
worker.log.Debugf("Check if Ingress is loaded: key=%s uuid=%s hash=%0x",
ingKey, string(ing.UID), vclSpec.Canonical().DeepHash())
......
......@@ -31,7 +31,10 @@ package controller
import (
"fmt"
vcr_v1alpha1 "code.uplex.de/uplex-varnish/k8s-ingress/pkg/apis/varnishingress/v1alpha1"
api_v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/labels"
)
// XXX make this configurable
......@@ -73,6 +76,47 @@ func (worker *NamespaceWorker) getVarnishSvcsForSecret(
return secrSvcs, nil
}
func (worker *NamespaceWorker) updateVcfgsForSecret(secrName string) error {
var vcfgs []*vcr_v1alpha1.VarnishConfig
vs, err := worker.vcfg.List(labels.Everything())
if err != nil {
return err
}
for _, v := range vs {
for _, auth := range v.Spec.Auth {
if auth.SecretName == secrName {
vcfgs = append(vcfgs, v)
}
}
}
if len(vcfgs) == 0 {
worker.log.Infof("No VarnishConfigs found for secret: "+
"%s/%s", worker.namespace, secrName)
return nil
}
for _, vcfg := range vcfgs {
worker.log.Infof("Requeuing VarnishConfig %s/%s "+
"after update for secret %s/%s",
vcfg.Namespace, vcfg.Name, worker.namespace, secrName)
worker.queue.Add(vcfg)
}
return nil
}
func (worker *NamespaceWorker) updateVarnishSvcsForSecret(
svcs []*api_v1.Service, secretKey string) error {
for _, svc := range svcs {
svcKey := svc.Namespace + "/" + svc.Name
if err := worker.vController.
UpdateSvcForSecret(svcKey, secretKey); err != nil {
return err
}
}
return nil
}
func (worker *NamespaceWorker) syncSecret(key string) error {
worker.log.Infof("Syncing Secret: %s/%s", worker.namespace, key)
secret, err := worker.secr.Get(key)
......@@ -82,18 +126,10 @@ func (worker *NamespaceWorker) syncSecret(key string) error {
app, ok := secret.Labels[labelKey]
if !ok || app != labelVal {
worker.log.Infof("Not a Varnish admin secret, ignoring: %s/%s",
worker.log.Infof("Not a Varnish secret: %s/%s",
secret.Namespace, secret.Name)
return nil
}
secretData, exists := secret.Data[admSecretKey]
if !exists {
return fmt.Errorf("Secret %s/%s does not have key %s",
secret.Namespace, secret.Name, admSecretKey)
}
secretKey := secret.Namespace + "/" + secret.Name
worker.log.Debugf("Setting secret %s", secretKey)
worker.vController.SetAdmSecret(secretKey, secretData)
svcs, err := worker.getVarnishSvcsForSecret(secret.Name)
if err != nil {
......@@ -101,35 +137,40 @@ func (worker *NamespaceWorker) syncSecret(key string) error {
}
worker.log.Debugf("Found Varnish services for secret %s/%s: %v",
secret.Namespace, secret.Name, svcs)
for _, svc := range svcs {
svcKey := svc.Namespace + "/" + svc.Name
if err = worker.vController.
UpdateSvcForSecret(svcKey, secretKey); err != nil {
if len(svcs) == 0 {
worker.log.Infof("No Varnish services with admin secret: %s/%s",
secret.Namespace, secret.Name)
return worker.updateVcfgsForSecret(secret.Name)
}
return err
}
secretData, exists := secret.Data[admSecretKey]
if !exists {
return fmt.Errorf("Secret %s/%s does not have key %s",
secret.Namespace, secret.Name, admSecretKey)
}
return nil
secretKey := secret.Namespace + "/" + secret.Name
worker.log.Debugf("Setting secret %s", secretKey)
worker.vController.SetAdmSecret(secretKey, secretData)
return worker.updateVarnishSvcsForSecret(svcs, secretKey)
}
func (worker *NamespaceWorker) deleteSecret(key string) error {
worker.log.Infof("Deleting Secret: %s", key)
worker.log.Infof("Deleting Secret: %s/%s", worker.namespace, key)
svcs, err := worker.getVarnishSvcsForSecret(key)
if err != nil {
return err
}
worker.log.Debugf("Found Varnish services for secret %s/%s: %v",
worker.namespace, key, svcs)
if len(svcs) == 0 {
worker.log.Infof("No Varnish services with admin secret: %s/%s",
worker.namespace, key)
return worker.updateVcfgsForSecret(key)
}
secretKey := worker.namespace + "/" + key
worker.vController.DeleteAdmSecret(secretKey)
for _, svc := range svcs {
svcKey := svc.Namespace + "/" + svc.Name
if err = worker.vController.
UpdateSvcForSecret(svcKey, secretKey); err != nil {
return err
}
}
return nil
return worker.updateVarnishSvcsForSecret(svcs, secretKey)
}
......@@ -39,6 +39,9 @@ import (
// the Timeout, Interval and Initial fields, and that Window and
// Threshold have been checked for permitted ranges.
func validateSharding(spec *vcr_v1alpha1.SelfShardSpec) error {
if spec == nil {
return nil
}
if spec.Probe.Window != nil && spec.Probe.Threshold != nil &&
*spec.Probe.Threshold > *spec.Probe.Window {
return fmt.Errorf("Threshold (%d) may not be greater than "+
......@@ -62,11 +65,6 @@ func (worker *NamespaceWorker) syncVcfg(key string) error {
"ignoring", vcfg.Namespace, vcfg.Name)
return nil
}
if vcfg.Spec.SelfSharding == nil {
worker.log.Infof("VarnishConfig %s/%s: no config defined, "+
"ignoring", vcfg.Namespace, vcfg.Name)
return nil
}
if err = validateSharding(vcfg.Spec.SelfSharding); err != nil {
return fmt.Errorf("VarnishConfig %s/%s invalid sharding "+
......
import re2;
sub vcl_init {
{{- range $auth := .Auths}}
new vk8s_{{vclMangle .Realm}}_auth = re2.set(anchor=both);
{{- range $cred := .Credentials}}
vk8s_{{vclMangle $auth.Realm}}_auth.add("\s*Basic\s+\Q{{$cred}}\E\s*");
{{- end}}
vk8s_{{vclMangle .Realm}}_auth.compile();
{{end -}}
}
sub vcl_recv {
{{- range .Auths}}
if (
{{- if ne .Condition.HostRegex ""}}
req.http.Host ~ "{{.Condition.HostRegex}}" &&
{{- end}}
{{- if ne .Condition.URLRegex ""}}
req.url ~ "{{.Condition.URLRegex}}" &&
{{- end}}
{{- if eq .Status 401}}
!vk8s_{{vclMangle .Realm}}_auth.match(req.http.Authorization)
{{- else}}
!vk8s_{{vclMangle .Realm}}_auth.match(req.http.Proxy-Authorization)
{{- end}}
) {
{{- if .UTF8 }}
set req.http.VK8S-Authenticate =
{"Basic realm="{{.Realm}}", charset="UTF-8""};
{{- else}}
set req.http.VK8S-Authenticate = {"Basic realm="{{.Realm}}""};
{{- end}}
return(synth(60000 + {{.Status}}));
}
{{- end}}
}
sub vcl_synth {
if (resp.status == 60401) {
set resp.http.WWW-Authenticate = req.http.VK8S-Authenticate;
return(deliver);
}
if (resp.status == 60407) {
set resp.http.Proxy-Authenticate = req.http.VK8S-Authenticate;
return(deliver);
}
}
import re2;
sub vcl_init {
new vk8s_foo_auth = re2.set(anchor=both);
vk8s_foo_auth.add("\s*Basic\s+\QQWxhZGRpbjpvcGVuIHNlc2FtZQ==\E\s*");
vk8s_foo_auth.add("\s*Basic\s+\QQWxhZGRpbjpPcGVuU2VzYW1l\E\s*");
vk8s_foo_auth.compile();
new vk8s_bar_auth = re2.set(anchor=both);
vk8s_bar_auth.add("\s*Basic\s+\QZm9vOmJhcg==\E\s*");
vk8s_bar_auth.add("\s*Basic\s+\QYmF6OnF1dXg=\E\s*");
vk8s_bar_auth.compile();
new vk8s_baz_auth = re2.set(anchor=both);
vk8s_baz_auth.add("\s*Basic\s+\QdXNlcjpwYXNzd29yZDE=\E\s*");
vk8s_baz_auth.add("\s*Basic\s+\QbmFtZTpzZWNyZXQ=\E\s*");
vk8s_baz_auth.compile();
new vk8s_quux_auth = re2.set(anchor=both);
vk8s_quux_auth.add("\s*Basic\s+\QYmVudXR6ZXI6Z2VoZWlt\E\s*");
vk8s_quux_auth.add("\s*Basic\s+\QQWxiZXJ0IEFkZGluOm9wZW4gc2V6IG1l\E\s*");
vk8s_quux_auth.compile();
new vk8s_urlhost_auth = re2.set(anchor=both);
vk8s_urlhost_auth.add("\s*Basic\s+\QdXJsOmhvc3Q=\E\s*");
vk8s_urlhost_auth.add("\s*Basic\s+\QYWRtaW46c3VwZXJwb3dlcnM=\E\s*");
vk8s_urlhost_auth.compile();
}
sub vcl_recv {
if (
!vk8s_foo_auth.match(req.http.Authorization)
) {
set req.http.VK8S-Authenticate = {"Basic realm="foo""};
return(synth(60000 + 401));
}
if (
!vk8s_bar_auth.match(req.http.Proxy-Authorization)
) {
set req.http.VK8S-Authenticate = {"Basic realm="bar""};
return(synth(60000 + 407));
}
if (
req.http.Host ~ "^baz\.com$" &&
!vk8s_baz_auth.match(req.http.Authorization)
) {
set req.http.VK8S-Authenticate =
{"Basic realm="baz", charset="UTF-8""};
return(synth(60000 + 401));
}
if (
req.url ~ "^/baz/quux" &&
!vk8s_quux_auth.match(req.http.Proxy-Authorization)
) {
set req.http.VK8S-Authenticate =
{"Basic realm="quux", charset="UTF-8""};
return(synth(60000 + 407));
}
if (
req.http.Host ~ "^url\.regex\.org$" &&
req.url ~ "^/secret/path" &&
!vk8s_urlhost_auth.match(req.http.Authorization)
) {
set req.http.VK8S-Authenticate = {"Basic realm="urlhost""};
return(synth(60000 + 401));
}
}
sub vcl_synth {
if (resp.status == 60401) {
set resp.http.WWW-Authenticate = req.http.VK8S-Authenticate;
return(deliver);
}
if (resp.status == 60407) {
set resp.http.Proxy-Authenticate = req.http.VK8S-Authenticate;
return(deliver);
}
}
......@@ -157,6 +157,62 @@ 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
const (
// Basic Authentication
Basic AuthStatus = 401
// Proxy Authentication
Proxy = 407
)
// Auth specifies Basic or Proxy Authentication, derived from an
// AuthSpec in a VarnishConfig resource.
type Auth struct {
Realm string
Credentials []string
Status AuthStatus
Condition Condition
UTF8 bool
}
func (auth Auth) hash(hash hash.Hash) {
hash.Write([]byte(auth.Realm))
for _, cred := range auth.Credentials {
hash.Write([]byte(cred))
}
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"))
}
if auth.UTF8 {
hash.Write([]byte("UTF8"))
}
}
// interface for sorting []Auth
type byRealm []Auth
func (a byRealm) Len() int { return len(a) }
func (a byRealm) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byRealm) Less(i, j int) bool { return a[i].Realm < a[j].Realm }
// Spec is the specification for a VCL configuration derived from
// Ingresses and VarnishConfig Custom Resources. This abstracts the
// VCL to be loaded by all instances of a Varnish Service.
......@@ -174,6 +230,10 @@ type Spec struct {
// ShardCluster is derived from the self-sharding
// specification in a VarnishConfig resource.
ShardCluster ShardCluster
// Auths is a list of specifications for Basic or Proxy
// Authentication, derived from the Auth section of a
// VarnishConfig.
Auths []Auth
}
// DeepHash computes a 64-bit hash value from a Spec such that if two
......@@ -196,6 +256,9 @@ func (spec Spec) DeepHash() uint64 {
spec.AllServices[svc].hash(hash)
}
spec.ShardCluster.hash(hash)
for _, auth := range spec.Auths {
auth.hash(hash)
}
return hash.Sum64()
}
......@@ -209,6 +272,7 @@ func (spec Spec) Canonical() Spec {
Rules: make([]Rule, len(spec.Rules)),
AllServices: make(map[string]Service, len(spec.AllServices)),
ShardCluster: spec.ShardCluster,
Auths: make([]Auth, len(spec.Auths)),
}
copy(canon.DefaultService.Addresses, spec.DefaultService.Addresses)
sort.Stable(byIPPort(canon.DefaultService.Addresses))
......@@ -227,6 +291,11 @@ func (spec Spec) Canonical() Spec {
for _, node := range canon.ShardCluster.Nodes {
sort.Stable(byIPPort(node.Addresses))
}
copy(canon.Auths, spec.Auths)
sort.Stable(byRealm(canon.Auths))
for _, auth := range canon.Auths {
sort.Strings(auth.Credentials)
}
return canon
}
......@@ -247,11 +316,13 @@ var fMap = template.FuncMap{
const (
ingTmplSrc = "vcl.tmpl"
shardTmplSrc = "self-shard.tmpl"
authTmplSrc = "auth.tmpl"
)
var (
ingressTmpl *template.Template
shardTmpl *template.Template
authTmpl *template.Template
symPattern = regexp.MustCompile("^[[:alpha:]][[:word:]-]*$")
first = regexp.MustCompile("[[:alpha:]]")
restIllegal = regexp.MustCompile("[^[:word:]-]+")
......@@ -262,6 +333,7 @@ func InitTemplates(tmplDir string) error {
var err error
ingTmplPath := path.Join(tmplDir, ingTmplSrc)
shardTmplPath := path.Join(tmplDir, shardTmplSrc)
authTmplPath := path.Join(tmplDir, authTmplSrc)
ingressTmpl, err = template.New(ingTmplSrc).
Funcs(fMap).ParseFiles(ingTmplPath)
if err != nil {
......@@ -272,6 +344,11 @@ func InitTemplates(tmplDir string) error {
if err != nil {
return err
}
authTmpl, err = template.New(authTmplSrc).
Funcs(fMap).ParseFiles(authTmplPath)
if err != nil {
return err
}
return nil
}
......@@ -295,6 +372,11 @@ func (spec Spec) GetSrc() (string, error) {
return "", err
}
}
if len(spec.Auths) > 0 {
if err := authTmpl.Execute(&buf, spec); err != nil {
return "", err
}
}
return buf.String(), nil
}
......
......@@ -312,3 +312,88 @@ func TestGetSrc(t *testing.T) {
}
}
}
var auths = Spec{
Auths: []Auth{
{
Realm: "foo",
Status: Basic,
Credentials: []string{
"QWxhZGRpbjpvcGVuIHNlc2FtZQ==",
"QWxhZGRpbjpPcGVuU2VzYW1l",
},
},
{
Realm: "bar",
Status: Proxy,
Credentials: []string{
"Zm9vOmJhcg==",
"YmF6OnF1dXg=",
},
UTF8: false,
},
{
Realm: "baz",
Status: Basic,
Credentials: []string{
"dXNlcjpwYXNzd29yZDE=",
"bmFtZTpzZWNyZXQ=",
},
Condition: Condition{
HostRegex: `^baz\.com$`,
},
UTF8: true,
},
{
Realm: "quux",
Status: Proxy,
Credentials: []string{
"YmVudXR6ZXI6Z2VoZWlt",
"QWxiZXJ0IEFkZGluOm9wZW4gc2V6IG1l",
},
Condition: Condition{
URLRegex: "^/baz/quux",
},
UTF8: true,
},
{
Realm: "urlhost",
Status: Basic,
Credentials: []string{
"dXJsOmhvc3Q=",
"YWRtaW46c3VwZXJwb3dlcnM=",
},
Condition: Condition{
HostRegex: `^url\.regex\.org$`,
URLRegex: "^/secret/path",
},
},
},
}
func TestAuthTemplate(t *testing.T) {
var buf bytes.Buffer
gold := "auth.golden"
tmplName := "auth.tmpl"
tmpl, err := template.New(tmplName).Funcs(fMap).ParseFiles(tmplName)
if err != nil {
t.Error("Cannot parse auth template:", err)
return
}
if err := tmpl.Execute(&buf, auths); err != nil {
t.Error("auths template Execute():", err)
return
}
ok, err := cmpGold(buf.Bytes(), gold)
if err != nil {
t.Fatalf("Reading %s: %v", gold, err)
}
if !ok {
t.Errorf("Generated VCL for authorization does not match gold "+
"file: %s", gold)
if testing.Verbose() {
t.Logf("Generated: %s", buf.String())
}
}
}
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