8e6e53cf63
When running the test suite in CI (where requests are centralized from relatively few IPs), we'd occasionally hit Dockerhub's rate limits. Luckily Hashicorp runs a (limited) public mirror of the containers we need, so we can switch to them here in the tests. For consistency between developer and CI, we've opted to have the tests always pull from the Hashicorp mirror, rather than updating the CI runner to prefer the mirror. We exclude nomad and influxdb as we don't presently mirror these repos. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
359 lines
10 KiB
Go
359 lines
10 KiB
Go
package radius
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hashicorp/vault/helper/testhelpers/docker"
|
|
logicaltest "github.com/hashicorp/vault/helper/testhelpers/logical"
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
)
|
|
|
|
const (
|
|
testSysTTL = time.Hour * 10
|
|
testSysMaxTTL = time.Hour * 20
|
|
|
|
envRadiusRadiusHost = "RADIUS_HOST"
|
|
envRadiusPort = "RADIUS_PORT"
|
|
envRadiusSecret = "RADIUS_SECRET"
|
|
envRadiusUsername = "RADIUS_USERNAME"
|
|
envRadiusUserPass = "RADIUS_USERPASS"
|
|
)
|
|
|
|
func prepareRadiusTestContainer(t *testing.T) (func(), string, int) {
|
|
if os.Getenv(envRadiusRadiusHost) != "" {
|
|
port, _ := strconv.Atoi(os.Getenv(envRadiusPort))
|
|
return func() {}, os.Getenv(envRadiusRadiusHost), port
|
|
}
|
|
|
|
// Now allow any client to connect to this radiusd instance by writing our
|
|
// own clients.conf file.
|
|
//
|
|
// This is necessary because we lack control over the container's network
|
|
// IPs. We might be running in Circle CI (with variable IPs per new
|
|
// network) or in Podman (which uses an entirely different set of default
|
|
// ranges than Docker).
|
|
//
|
|
// See also: https://freeradius.org/radiusd/man/clients.conf.html
|
|
ctx := context.Background()
|
|
clientsConfig := `
|
|
client 0.0.0.0/1 {
|
|
ipaddr = 0.0.0.0/1
|
|
secret = testing123
|
|
shortname = all-clients-first
|
|
}
|
|
|
|
client 128.0.0.0/1 {
|
|
ipaddr = 128.0.0.0/1
|
|
secret = testing123
|
|
shortname = all-clients-second
|
|
}
|
|
`
|
|
|
|
containerfile := `
|
|
FROM docker.mirror.hashicorp.services/jumanjiman/radiusd:latest
|
|
|
|
COPY clients.conf /etc/raddb/clients.conf
|
|
`
|
|
|
|
bCtx := docker.NewBuildContext()
|
|
bCtx["clients.conf"] = docker.PathContentsFromBytes([]byte(clientsConfig))
|
|
|
|
imageName := "vault_radiusd_any_client"
|
|
imageTag := "latest"
|
|
|
|
runner, err := docker.NewServiceRunner(docker.RunOptions{
|
|
ImageRepo: imageName,
|
|
ImageTag: imageTag,
|
|
ContainerName: "radiusd",
|
|
Cmd: []string{"-f", "-l", "stdout", "-X"},
|
|
Ports: []string{"1812/udp"},
|
|
LogConsumer: func(s string) {
|
|
if t.Failed() {
|
|
t.Logf("container logs: %s", s)
|
|
}
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Could not provision docker service runner: %s", err)
|
|
}
|
|
|
|
output, err := runner.BuildImage(ctx, containerfile, bCtx,
|
|
docker.BuildRemove(true), docker.BuildForceRemove(true),
|
|
docker.BuildPullParent(true),
|
|
docker.BuildTags([]string{imageName + ":" + imageTag}))
|
|
if err != nil {
|
|
t.Fatalf("Could not build new image: %v", err)
|
|
}
|
|
|
|
t.Logf("Image build output: %v", string(output))
|
|
|
|
svc, err := runner.StartService(context.Background(), func(ctx context.Context, host string, port int) (docker.ServiceConfig, error) {
|
|
time.Sleep(2 * time.Second)
|
|
return docker.NewServiceHostPort(host, port), nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Could not start docker radiusd: %s", err)
|
|
}
|
|
|
|
pieces := strings.Split(svc.Config.Address(), ":")
|
|
port, _ := strconv.Atoi(pieces[1])
|
|
return svc.Cleanup, pieces[0], port
|
|
}
|
|
|
|
func TestBackend_Config(t *testing.T) {
|
|
b, err := Factory(context.Background(), &logical.BackendConfig{
|
|
Logger: nil,
|
|
System: &logical.StaticSystemView{
|
|
DefaultLeaseTTLVal: testSysTTL,
|
|
MaxLeaseTTLVal: testSysMaxTTL,
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Unable to create backend: %s", err)
|
|
}
|
|
|
|
configDataBasic := map[string]interface{}{
|
|
"host": "test.radius.hostname.com",
|
|
"secret": "test-secret",
|
|
}
|
|
|
|
configDataMissingRequired := map[string]interface{}{
|
|
"host": "test.radius.hostname.com",
|
|
}
|
|
|
|
configDataEmptyPort := map[string]interface{}{
|
|
"host": "test.radius.hostname.com",
|
|
"port": "",
|
|
"secret": "test-secret",
|
|
}
|
|
|
|
configDataInvalidPort := map[string]interface{}{
|
|
"host": "test.radius.hostname.com",
|
|
"port": "notnumeric",
|
|
"secret": "test-secret",
|
|
}
|
|
|
|
configDataInvalidBool := map[string]interface{}{
|
|
"host": "test.radius.hostname.com",
|
|
"secret": "test-secret",
|
|
"unregistered_user_policies": "test",
|
|
}
|
|
|
|
logicaltest.Test(t, logicaltest.TestCase{
|
|
AcceptanceTest: false,
|
|
// PreCheck: func() { testAccPreCheck(t) },
|
|
CredentialBackend: b,
|
|
Steps: []logicaltest.TestStep{
|
|
testConfigWrite(t, configDataBasic, false),
|
|
testConfigWrite(t, configDataMissingRequired, true),
|
|
testConfigWrite(t, configDataEmptyPort, true),
|
|
testConfigWrite(t, configDataInvalidPort, true),
|
|
testConfigWrite(t, configDataInvalidBool, true),
|
|
},
|
|
})
|
|
}
|
|
|
|
func TestBackend_users(t *testing.T) {
|
|
b, err := Factory(context.Background(), &logical.BackendConfig{
|
|
Logger: nil,
|
|
System: &logical.StaticSystemView{
|
|
DefaultLeaseTTLVal: testSysTTL,
|
|
MaxLeaseTTLVal: testSysMaxTTL,
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Unable to create backend: %s", err)
|
|
}
|
|
logicaltest.Test(t, logicaltest.TestCase{
|
|
CredentialBackend: b,
|
|
Steps: []logicaltest.TestStep{
|
|
testStepUpdateUser(t, "web", "foo"),
|
|
testStepUpdateUser(t, "web2", "foo"),
|
|
testStepUpdateUser(t, "web3", "foo"),
|
|
testStepUserList(t, []string{"web", "web2", "web3"}),
|
|
},
|
|
})
|
|
}
|
|
|
|
func TestBackend_acceptance(t *testing.T) {
|
|
b, err := Factory(context.Background(), &logical.BackendConfig{
|
|
Logger: nil,
|
|
System: &logical.StaticSystemView{
|
|
DefaultLeaseTTLVal: testSysTTL,
|
|
MaxLeaseTTLVal: testSysMaxTTL,
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Unable to create backend: %s", err)
|
|
}
|
|
|
|
cleanup, host, port := prepareRadiusTestContainer(t)
|
|
defer cleanup()
|
|
|
|
// These defaults are specific to the jumanjiman/radiusd docker image
|
|
username := os.Getenv(envRadiusUsername)
|
|
if username == "" {
|
|
username = "test"
|
|
}
|
|
|
|
password := os.Getenv(envRadiusUserPass)
|
|
if password == "" {
|
|
password = "test"
|
|
}
|
|
|
|
secret := os.Getenv(envRadiusSecret)
|
|
if len(secret) == 0 {
|
|
secret = "testing123"
|
|
}
|
|
|
|
configDataAcceptanceAllowUnreg := map[string]interface{}{
|
|
"host": host,
|
|
"port": strconv.Itoa(port),
|
|
"secret": secret,
|
|
"unregistered_user_policies": "policy1,policy2",
|
|
}
|
|
if configDataAcceptanceAllowUnreg["port"] == "" {
|
|
configDataAcceptanceAllowUnreg["port"] = "1812"
|
|
}
|
|
|
|
configDataAcceptanceNoAllowUnreg := map[string]interface{}{
|
|
"host": host,
|
|
"port": strconv.Itoa(port),
|
|
"secret": secret,
|
|
"unregistered_user_policies": "",
|
|
}
|
|
if configDataAcceptanceNoAllowUnreg["port"] == "" {
|
|
configDataAcceptanceNoAllowUnreg["port"] = "1812"
|
|
}
|
|
|
|
dataRealpassword := map[string]interface{}{
|
|
"password": password,
|
|
}
|
|
|
|
dataWrongpassword := map[string]interface{}{
|
|
"password": "wrongpassword",
|
|
}
|
|
|
|
logicaltest.Test(t, logicaltest.TestCase{
|
|
CredentialBackend: b,
|
|
PreCheck: testAccPreCheck(t, host, port),
|
|
Steps: []logicaltest.TestStep{
|
|
// Login with valid but unknown user will fail because unregistered_user_policies is empty
|
|
testConfigWrite(t, configDataAcceptanceNoAllowUnreg, false),
|
|
testAccUserLogin(t, username, dataRealpassword, true),
|
|
// Once the user is registered auth will succeed
|
|
testStepUpdateUser(t, username, ""),
|
|
testAccUserLoginPolicy(t, username, dataRealpassword, []string{"default"}, false),
|
|
|
|
testStepUpdateUser(t, username, "foopolicy"),
|
|
testAccUserLoginPolicy(t, username, dataRealpassword, []string{"default", "foopolicy"}, false),
|
|
testAccStepDeleteUser(t, username),
|
|
|
|
// When unregistered_user_policies is specified, an unknown user will be granted access and granted the listed policies
|
|
testConfigWrite(t, configDataAcceptanceAllowUnreg, false),
|
|
testAccUserLoginPolicy(t, username, dataRealpassword, []string{"default", "policy1", "policy2"}, false),
|
|
|
|
// More tests
|
|
testAccUserLogin(t, "nonexistinguser", dataRealpassword, true),
|
|
testAccUserLogin(t, username, dataWrongpassword, true),
|
|
testStepUpdateUser(t, username, "foopolicy"),
|
|
testAccUserLoginPolicy(t, username, dataRealpassword, []string{"default", "foopolicy"}, false),
|
|
testStepUpdateUser(t, username, "foopolicy, secondpolicy"),
|
|
testAccUserLoginPolicy(t, username, dataRealpassword, []string{"default", "foopolicy", "secondpolicy"}, false),
|
|
testAccUserLoginPolicy(t, username, dataRealpassword, []string{"default", "foopolicy", "secondpolicy", "thirdpolicy"}, true),
|
|
},
|
|
})
|
|
}
|
|
|
|
func testAccPreCheck(t *testing.T, host string, port int) func() {
|
|
return func() {
|
|
if host == "" {
|
|
t.Fatal("Host must be set for acceptance tests")
|
|
}
|
|
|
|
if port == 0 {
|
|
t.Fatal("Port must be non-zero for acceptance tests")
|
|
}
|
|
}
|
|
}
|
|
|
|
func testConfigWrite(t *testing.T, d map[string]interface{}, expectError bool) logicaltest.TestStep {
|
|
return logicaltest.TestStep{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "config",
|
|
Data: d,
|
|
ErrorOk: expectError,
|
|
}
|
|
}
|
|
|
|
func testAccStepDeleteUser(t *testing.T, n string) logicaltest.TestStep {
|
|
return logicaltest.TestStep{
|
|
Operation: logical.DeleteOperation,
|
|
Path: "users/" + n,
|
|
}
|
|
}
|
|
|
|
func testStepUserList(t *testing.T, users []string) logicaltest.TestStep {
|
|
return logicaltest.TestStep{
|
|
Operation: logical.ListOperation,
|
|
Path: "users",
|
|
Check: func(resp *logical.Response) error {
|
|
if resp.IsError() {
|
|
return fmt.Errorf("got error response: %#v", *resp)
|
|
}
|
|
|
|
if !reflect.DeepEqual(users, resp.Data["keys"].([]string)) {
|
|
return fmt.Errorf("expected:\n%#v\ngot:\n%#v\n", users, resp.Data["keys"])
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
}
|
|
|
|
func testStepUpdateUser(
|
|
t *testing.T, name string, policies string,
|
|
) logicaltest.TestStep {
|
|
return logicaltest.TestStep{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "users/" + name,
|
|
Data: map[string]interface{}{
|
|
"policies": policies,
|
|
},
|
|
}
|
|
}
|
|
|
|
func testAccUserLogin(t *testing.T, user string, data map[string]interface{}, expectError bool) logicaltest.TestStep {
|
|
return logicaltest.TestStep{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "login/" + user,
|
|
Data: data,
|
|
ErrorOk: expectError,
|
|
Unauthenticated: true,
|
|
}
|
|
}
|
|
|
|
func testAccUserLoginPolicy(t *testing.T, user string, data map[string]interface{}, policies []string, expectError bool) logicaltest.TestStep {
|
|
return logicaltest.TestStep{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "login/" + user,
|
|
Data: data,
|
|
ErrorOk: expectError,
|
|
Unauthenticated: true,
|
|
// Check: logicaltest.TestCheckAuth(policies),
|
|
Check: func(resp *logical.Response) error {
|
|
res := logicaltest.TestCheckAuth(policies)(resp)
|
|
if res != nil && expectError {
|
|
return nil
|
|
}
|
|
return res
|
|
},
|
|
}
|
|
}
|