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 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") } }