Files
jarl/internal/auth/auth_test.go
adamandClaude Opus 5.5 310ed8061e Permissions: a certificates role
A template for tokens and users that get certificates for other hosts
(jarl cert get): certificates:write, to narrow with names= in the web
UI's "Insert a template…", or --role certificates in the CLI. The token
form's template filter no longer counts "*" as covering certificates,
as the server doesn't.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 22:07:49 +01:00

167 lines
6.0 KiB
Go

package auth
import (
"strings"
"testing"
"jarl/internal/model"
)
func TestPasswords(t *testing.T) {
h, err := HashPassword("correct horse battery")
if err != nil {
t.Fatal(err)
}
if !strings.HasPrefix(h, "$argon2id$v=19$m=19456,t=2,p=1$") {
t.Errorf("hash format %q", h)
}
if !CheckPassword(h, "correct horse battery") || CheckPassword(h, "wrong horse battery") {
t.Error("CheckPassword wrong")
}
if CheckPassword("", "anything") || CheckPassword("garbage", "x") {
t.Error("bad hash accepted")
}
if _, err := HashPassword("short"); err == nil {
t.Error("short password accepted")
}
}
func TestTokens(t *testing.T) {
tok, pid, hash := NewToken()
if !strings.HasPrefix(tok, "jarl_") || len(tok) != len("jarl_")+16+1+32 {
t.Fatalf("token %q", tok)
}
p, secret, err := ParseToken(tok)
if err != nil || p != pid || !CheckSecret(secret, hash) {
t.Fatalf("ParseToken: %v %v", p, err)
}
if CheckSecret(secret+"x", hash) {
t.Error("wrong secret accepted")
}
for _, bad := range []string{"", "abc", "jarl_short_x", "Bearer " + tok} {
if _, _, err := ParseToken(bad); err == nil {
t.Errorf("ParseToken(%q) accepted", bad)
}
}
}
func TestParseGrant(t *testing.T) {
g, err := ParseGrant("records:write zones=Example.COM. names=_acme-challenge.* types=txt")
if err != nil {
t.Fatal(err)
}
if g.Zones[0] != "example.com" || g.Types[0] != "TXT" || g.Names[0] != "_acme-challenge.*" {
t.Errorf("grant = %+v", g)
}
if FormatGrant(g) != "records:write zones=example.com names=_acme-challenge.* types=TXT" {
t.Errorf("FormatGrant = %q", FormatGrant(g))
}
for _, bad := range []string{"", "records", "records:admin", "bogus:read", "acls:write names=x", "records:read color=red", "options:read zones=x"} {
if _, err := ParseGrant(bad); err == nil {
t.Errorf("ParseGrant(%q) accepted", bad)
}
}
}
func TestPermissions(t *testing.T) {
acme, _ := ParseGrant("records:write zones=example.com names=_acme-challenge.* types=TXT")
reader, _ := ParseGrant("*:read")
u := &model.User{Username: "bot", Grants: []model.Grant{acme, reader}}
p := &Principal{User: u}
allow := []Action{
{Resource: ResRecords, Write: true, Zone: "example.com.", Name: "_acme-challenge.www.example.com.", Type: "TXT"},
{Resource: ResRecords, Write: true, Zone: "example.com.", Name: "_acme-challenge.example.com.", Type: "txt"},
{Resource: ResRecords, Zone: "other.org.", Name: "www.other.org.", Type: "A"},
{Resource: ResACLs},
}
deny := []Action{
{Resource: ResRecords, Write: true, Zone: "example.com.", Name: "www.example.com.", Type: "TXT"},
{Resource: ResRecords, Write: true, Zone: "example.com.", Name: "_acme-challenge.example.com.", Type: "A"},
{Resource: ResRecords, Write: true, Zone: "other.org.", Name: "_acme-challenge.other.org.", Type: "TXT"},
{Resource: ResZones, Write: true, Zone: "example.com."},
{Resource: ResACLs, Write: true},
}
for _, a := range allow {
if !p.Can(a) {
t.Errorf("denied %s", a)
}
}
for _, a := range deny {
if p.Can(a) {
t.Errorf("allowed %s", a)
}
}
// A token narrows its user's permissions; it can't widen them.
tp := &Principal{User: u, Token: &model.Token{Grants: []model.Grant{acme}}}
if tp.Can(Action{Resource: ResACLs}) {
t.Error("token restriction ignored")
}
if !tp.Can(allow[0]) {
t.Error("token denied allowed action")
}
wide := &Principal{User: u, Token: &model.Token{Grants: []model.Grant{{Resource: ResAll, Access: Write}}}}
if wide.Can(Action{Resource: ResACLs, Write: true}) {
t.Error("token widened user's permissions")
}
// Admins can do everything, but not through restricted tokens.
admin := &model.User{Username: "root", Admin: true}
if !(&Principal{User: admin}).IsAdmin() || (&Principal{User: admin, Token: &model.Token{Grants: []model.Grant{reader}}}).IsAdmin() {
t.Error("admin via restricted token")
}
if (&Principal{User: &model.User{Admin: true, Disabled: true}}).Can(Action{Resource: ResACLs}) {
t.Error("disabled user allowed")
}
// View filters, with "_default" for zones outside views.
vg, _ := ParseGrant("zones:write views=external")
vp := &Principal{User: &model.User{Grants: []model.Grant{vg}}}
if !vp.Can(Action{Resource: ResZones, Write: true, Zone: "example.com.", View: "external"}) || vp.Can(Action{Resource: ResZones, Write: true, Zone: "example.com.", View: "internal"}) || vp.Can(Action{Resource: ResZones, Write: true, Zone: "example.com."}) {
t.Error("view filter wrong")
}
// CanSeeZone with a filtered records grant.
sp := &Principal{User: &model.User{Grants: []model.Grant{acme}}}
if !sp.CanSeeZone("example.com.", "") || sp.CanSeeZone("other.org.", "") {
t.Error("CanSeeZone wrong")
}
}
func TestRoleTemplatesValid(t *testing.T) {
for name, gs := range RoleTemplates {
for _, g := range gs {
gg := g
if err := ValidateGrant(&gg); err != nil {
t.Errorf("template %s: %v", name, err)
}
}
}
}
func TestCertificatesRole(t *testing.T) {
p := &Principal{User: &model.User{Grants: RoleTemplates["certificates"]}}
if !p.Can(Action{Resource: ResCerts, Write: true, Name: "web01.adbraun.lol"}) {
t.Error("the certificates role can't get a certificate")
}
if p.Can(Action{Resource: ResRecords, Write: true, Zone: "adbraun.lol."}) || p.Can(Action{Resource: ResZones, Zone: "adbraun.lol."}) {
t.Error("the certificates role reaches beyond certificates")
}
}
func TestCertificateGrants(t *testing.T) {
g, err := ParseGrant("certificates:write names=*.adbraun.lol")
if err != nil {
t.Fatal(err)
}
if !GrantAllows(g, Action{Resource: ResCerts, Write: true, Name: "mail.adbraun.lol"}) || GrantAllows(g, Action{Resource: ResCerts, Write: true, Name: "mail.example.org"}) {
t.Error("names filter")
}
if _, err := ParseGrant("certificates:read types=TXT"); err == nil {
t.Error("types on certificates accepted")
}
if _, err := ParseGrant("certificates:read zones=adbraun.lol"); err == nil {
t.Error("zones on certificates accepted")
}
// "*" never covers certificates: they hand out private keys.
if GrantAllows(model.Grant{Resource: ResAll, Access: Write}, Action{Resource: ResCerts, Name: "mail.adbraun.lol"}) {
t.Error("* covers certificates")
}
}