This commit is contained in:
parent
18f9fd34e5
commit
065d8a5417
@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"git.pcux.cn/xuans/Smail/dto/response"
|
"git.pcux.cn/xuans/Smail/dto/response"
|
||||||
"git.pcux.cn/xuans/Smail/services/access"
|
"git.pcux.cn/xuans/Smail/services/access"
|
||||||
|
"git.pcux.cn/xuans/Smail/services/credentials"
|
||||||
appcontext "git.pcux.cn/xuans/Smail/utils/context"
|
appcontext "git.pcux.cn/xuans/Smail/utils/context"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -23,6 +24,10 @@ func TestSecurityHandlersRejectUnauthenticatedRequests(t *testing.T) {
|
|||||||
{name: "generate recovery codes", hand: GenerateRecoveryCodes, body: `{"count":5}`},
|
{name: "generate recovery codes", hand: GenerateRecoveryCodes, body: `{"count":5}`},
|
||||||
{name: "get recovery code status", hand: GetRecoveryCodeStatus},
|
{name: "get recovery code status", hand: GetRecoveryCodeStatus},
|
||||||
{name: "revoke recovery codes", hand: RevokeRecoveryCodes},
|
{name: "revoke recovery codes", hand: RevokeRecoveryCodes},
|
||||||
|
{name: "list protocol credentials", hand: ListProtocolCredentials},
|
||||||
|
{name: "set protocol enabled", hand: SetProtocolCredentialEnabled, body: `{"protocol":"smtp","enabled":true}`},
|
||||||
|
{name: "generate protocol credential", hand: GenerateProtocolCredential, body: `{"protocol":"smtp"}`},
|
||||||
|
{name: "revoke protocol credential", hand: RevokeProtocolCredential, body: `{"protocol":"smtp"}`},
|
||||||
{name: "request verification", hand: RequestVerification, body: `{"purpose":"login","account":"smail","email":"user@example.com"}`},
|
{name: "request verification", hand: RequestVerification, body: `{"purpose":"login","account":"smail","email":"user@example.com"}`},
|
||||||
{name: "verify verification", hand: VerifyVerification, body: `{"id":"challenge","account":"smail","email":"user@example.com","code":"12345678","purpose":"login"}`},
|
{name: "verify verification", hand: VerifyVerification, body: `{"id":"challenge","account":"smail","email":"user@example.com","code":"12345678","purpose":"login"}`},
|
||||||
{name: "list external emails", hand: ListExternalEmails},
|
{name: "list external emails", hand: ListExternalEmails},
|
||||||
@ -61,3 +66,114 @@ func TestSuperAdminCannotManageRecoveryCodesThroughWeb(t *testing.T) {
|
|||||||
t.Fatalf("error number = %d, want %d; body=%s", result.ErrorNo, response.NoAccessPrivileges, recorder.Body.String())
|
t.Fatalf("error number = %d, want %d; body=%s", result.ErrorNo, response.NoAccessPrivileges, recorder.Body.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestProtocolCredentialHandlersManageCurrentUserCredential(t *testing.T) {
|
||||||
|
_, _, engine, user, _ := newPublicSecurityFixture(t)
|
||||||
|
credentialService, err := credentials.NewService(engine)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
previous := securityRuntime
|
||||||
|
securityRuntime.credentials = credentialService
|
||||||
|
t.Cleanup(func() { securityRuntime = previous })
|
||||||
|
|
||||||
|
ctx := &appcontext.Context{UserID: user.ID, UserAccount: user.Account}
|
||||||
|
list := func() response.Response {
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
request := httptest.NewRequest(http.MethodGet, "/api/security/credentials/protocols", nil)
|
||||||
|
ListProtocolCredentials(ctx, recorder, request)
|
||||||
|
var result response.Response
|
||||||
|
if err := json.Unmarshal(recorder.Body.Bytes(), &result); err != nil {
|
||||||
|
t.Fatalf("invalid protocol list response: %v; body=%s", err, recorder.Body.String())
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
initial := list()
|
||||||
|
if initial.ErrorNo != 0 {
|
||||||
|
t.Fatalf("list protocol credentials failed: %+v", initial)
|
||||||
|
}
|
||||||
|
|
||||||
|
setRecorder := httptest.NewRecorder()
|
||||||
|
setRequest := httptest.NewRequest(
|
||||||
|
http.MethodPut,
|
||||||
|
"/api/security/credentials/protocols/enabled",
|
||||||
|
strings.NewReader(`{"protocol":"SMTP","enabled":true}`),
|
||||||
|
)
|
||||||
|
SetProtocolCredentialEnabled(ctx, setRecorder, setRequest)
|
||||||
|
var setResult response.Response
|
||||||
|
if err := json.Unmarshal(setRecorder.Body.Bytes(), &setResult); err != nil {
|
||||||
|
t.Fatalf("invalid protocol enable response: %v", err)
|
||||||
|
}
|
||||||
|
if setResult.ErrorNo != 0 {
|
||||||
|
t.Fatalf("enable protocol failed: %+v", setResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
generateRecorder := httptest.NewRecorder()
|
||||||
|
generateRequest := httptest.NewRequest(
|
||||||
|
http.MethodPost,
|
||||||
|
"/api/security/credentials/protocols/generate",
|
||||||
|
strings.NewReader(`{"protocol":"smtp"}`),
|
||||||
|
)
|
||||||
|
GenerateProtocolCredential(ctx, generateRecorder, generateRequest)
|
||||||
|
var generateResult response.Response
|
||||||
|
if err := json.Unmarshal(generateRecorder.Body.Bytes(), &generateResult); err != nil {
|
||||||
|
t.Fatalf("invalid protocol generate response: %v", err)
|
||||||
|
}
|
||||||
|
if generateResult.ErrorNo != 0 {
|
||||||
|
t.Fatalf("generate protocol credential failed: %+v", generateResult)
|
||||||
|
}
|
||||||
|
generated, ok := generateResult.Data.(map[string]any)
|
||||||
|
if !ok || generated["code"] == "" || generated["shown_once"] != true {
|
||||||
|
t.Fatalf("unexpected generated protocol response: %#v", generateResult.Data)
|
||||||
|
}
|
||||||
|
if strings.Contains(generateRecorder.Body.String(), "code_hash") {
|
||||||
|
t.Fatal("protocol response exposed code hash")
|
||||||
|
}
|
||||||
|
|
||||||
|
status := list()
|
||||||
|
statusData, ok := status.Data.([]any)
|
||||||
|
if !ok || len(statusData) != 3 {
|
||||||
|
t.Fatalf("unexpected protocol status response: %#v", status.Data)
|
||||||
|
}
|
||||||
|
foundSMTP := false
|
||||||
|
for _, item := range statusData {
|
||||||
|
itemMap, ok := item.(map[string]any)
|
||||||
|
if !ok || itemMap["protocol"] != "smtp" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
foundSMTP = true
|
||||||
|
if itemMap["enabled"] != true || itemMap["configured"] != true {
|
||||||
|
t.Fatalf("unexpected SMTP status: %#v", itemMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundSMTP {
|
||||||
|
t.Fatal("SMTP protocol status was not returned")
|
||||||
|
}
|
||||||
|
|
||||||
|
revokeRecorder := httptest.NewRecorder()
|
||||||
|
revokeRequest := httptest.NewRequest(
|
||||||
|
http.MethodPost,
|
||||||
|
"/api/security/credentials/protocols/revoke",
|
||||||
|
strings.NewReader(`{"protocol":"smtp"}`),
|
||||||
|
)
|
||||||
|
RevokeProtocolCredential(ctx, revokeRecorder, revokeRequest)
|
||||||
|
var revokeResult response.Response
|
||||||
|
if err := json.Unmarshal(revokeRecorder.Body.Bytes(), &revokeResult); err != nil {
|
||||||
|
t.Fatalf("invalid protocol revoke response: %v", err)
|
||||||
|
}
|
||||||
|
if revokeResult.ErrorNo != 0 {
|
||||||
|
t.Fatalf("revoke protocol credential failed: %+v", revokeResult)
|
||||||
|
}
|
||||||
|
status = list()
|
||||||
|
statusData, ok = status.Data.([]any)
|
||||||
|
if !ok || len(statusData) != 3 {
|
||||||
|
t.Fatalf("unexpected revoked protocol status response: %#v", status.Data)
|
||||||
|
}
|
||||||
|
for _, item := range statusData {
|
||||||
|
itemMap, ok := item.(map[string]any)
|
||||||
|
if ok && itemMap["protocol"] == "smtp" && itemMap["configured"] == true {
|
||||||
|
t.Fatal("revoked SMTP protocol credential remained configured")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -187,6 +187,10 @@ func TestFrontendV1RoutesAreRegistered(t *testing.T) {
|
|||||||
"/api/v1/security/credentials/recovery-codes",
|
"/api/v1/security/credentials/recovery-codes",
|
||||||
"/api/v1/security/credentials/recovery-codes/generate",
|
"/api/v1/security/credentials/recovery-codes/generate",
|
||||||
"/api/v1/security/credentials/recovery-codes/revoke",
|
"/api/v1/security/credentials/recovery-codes/revoke",
|
||||||
|
"/api/v1/security/credentials/protocols",
|
||||||
|
"/api/v1/security/credentials/protocols/enabled",
|
||||||
|
"/api/v1/security/credentials/protocols/generate",
|
||||||
|
"/api/v1/security/credentials/protocols/revoke",
|
||||||
"/api/v1/security/external-email/bind",
|
"/api/v1/security/external-email/bind",
|
||||||
"/api/v1/security/external-email/disable",
|
"/api/v1/security/external-email/disable",
|
||||||
"/api/v1/security/external-email/list",
|
"/api/v1/security/external-email/list",
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import (
|
|||||||
"git.pcux.cn/xuans/Smail/config"
|
"git.pcux.cn/xuans/Smail/config"
|
||||||
"git.pcux.cn/xuans/Smail/db"
|
"git.pcux.cn/xuans/Smail/db"
|
||||||
"git.pcux.cn/xuans/Smail/models"
|
"git.pcux.cn/xuans/Smail/models"
|
||||||
|
"git.pcux.cn/xuans/Smail/services/credentials"
|
||||||
"git.pcux.cn/xuans/Smail/utils/array"
|
"git.pcux.cn/xuans/Smail/utils/array"
|
||||||
pcontext "git.pcux.cn/xuans/Smail/utils/context"
|
pcontext "git.pcux.cn/xuans/Smail/utils/context"
|
||||||
"git.pcux.cn/xuans/Smail/utils/password"
|
"git.pcux.cn/xuans/Smail/utils/password"
|
||||||
@ -30,6 +31,7 @@ import (
|
|||||||
var clientUnLogin *imapclient.Client
|
var clientUnLogin *imapclient.Client
|
||||||
var clientLogin *imapclient.Client
|
var clientLogin *imapclient.Client
|
||||||
var imapTestAddr string
|
var imapTestAddr string
|
||||||
|
var imapTestProtocolCode string
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
wd, err := os.Getwd()
|
wd, err := os.Getwd()
|
||||||
@ -97,7 +99,7 @@ func TestMain(m *testing.M) {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = clientLogin.Login("testCase", "testCase").Wait()
|
err = clientLogin.Login("testCase@test.domain", imapTestProtocolCode).Wait()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@ -158,35 +160,67 @@ func seedIMAPTestData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var num int
|
var num int
|
||||||
_, err = db.Instance.Table(&models.UserEmail{}).Where("user_id=?", user.ID).Select("count(1)").Get(&num)
|
_, err = db.Instance.Table(&models.UserEmail{}).Where("user_id=? AND email_id > 0", user.ID).Select("count(1)").Get(&num)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
if num > 0 {
|
if num == 0 {
|
||||||
return
|
statuses := []int8{3}
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
statuses = append(statuses, 0)
|
||||||
|
}
|
||||||
|
for _, status := range statuses {
|
||||||
|
email := models.Email{
|
||||||
|
Subject: "test",
|
||||||
|
Status: status,
|
||||||
|
FromAddress: "sender@test.domain",
|
||||||
|
To: `[{"EmailAddress":"testCase@test.domain","Name":"testCase"}]`,
|
||||||
|
MsgID: "imap-test@test.domain",
|
||||||
|
SendDate: time.Now(),
|
||||||
|
}
|
||||||
|
if _, err = db.Instance.Insert(&email); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
ue := models.UserEmail{UserID: user.ID, EmailID: email.Id, Status: status}
|
||||||
|
if _, err = db.Instance.Insert(&ue); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
statuses := []int8{3}
|
canonicalAddress := "testcase@test.domain"
|
||||||
for i := 0; i < 10; i++ {
|
var loginBinding models.UserEmail
|
||||||
statuses = append(statuses, 0)
|
hasBinding, err := db.Instance.Table(&models.UserEmail{}).
|
||||||
|
Where("user_id = ? AND address_canonical = ?", user.ID, canonicalAddress).
|
||||||
|
Get(&loginBinding)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
}
|
}
|
||||||
for _, status := range statuses {
|
if !hasBinding {
|
||||||
email := models.Email{
|
verifiedAt := time.Now().UTC()
|
||||||
Subject: "test",
|
loginBinding = models.UserEmail{
|
||||||
Status: status,
|
UserID: user.ID,
|
||||||
FromAddress: "sender@test.domain",
|
Address: "testCase@test.domain",
|
||||||
To: `[{"EmailAddress":"testCase@test.domain","Name":"testCase"}]`,
|
AddressCanonical: &canonicalAddress,
|
||||||
MsgID: "imap-test@test.domain",
|
VerifiedAt: &verifiedAt,
|
||||||
SendDate: time.Now(),
|
IsPrimary: 1,
|
||||||
}
|
}
|
||||||
if _, err = db.Instance.Insert(&email); err != nil {
|
if _, err = db.Instance.Insert(&loginBinding); err != nil {
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
ue := models.UserEmail{UserID: user.ID, EmailID: email.Id, Status: status}
|
|
||||||
if _, err = db.Instance.Insert(&ue); err != nil {
|
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
credentialService, err := credentials.NewService(db.Instance)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if err = credentialService.SetProtocolEnabled(user.ID, credentials.ProtocolIMAP, true); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
imapTestProtocolCode, err = credentialService.GenerateProtocolCode(user.ID, credentials.ProtocolIMAP)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCapability(t *testing.T) {
|
func TestCapability(t *testing.T) {
|
||||||
@ -210,7 +244,7 @@ func TestCapability(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLogin(t *testing.T) {
|
func TestLogin(t *testing.T) {
|
||||||
err := clientUnLogin.Login("testCase", "testCaseasdfsadf").Wait()
|
err := clientUnLogin.Login("testCase@test.domain", "wrong-imap-code").Wait()
|
||||||
sErr := err.(*imap.Error)
|
sErr := err.(*imap.Error)
|
||||||
if sErr.Code != "AUTHENTICATIONFAILED" {
|
if sErr.Code != "AUTHENTICATIONFAILED" {
|
||||||
t.Error("Login Error")
|
t.Error("Login Error")
|
||||||
@ -495,19 +529,41 @@ func TestClose(t *testing.T) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
func TestExpunge(t *testing.T) {
|
func TestExpunge(t *testing.T) {
|
||||||
|
if _, err := clientLogin.Select("INBOX", &imap.SelectOptions{}).Wait(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
clientLogin.Select("INBOX", &imap.SelectOptions{}).Wait()
|
var targets []models.UserEmail
|
||||||
|
if err := db.Instance.Table("user_email").
|
||||||
res, err := clientLogin.UIDExpunge(imap.UIDSetNum(1, 2)).Collect()
|
Where("user_id = ? AND email_id > 0", 1).
|
||||||
|
OrderBy("id ASC").
|
||||||
|
Limit(2).
|
||||||
|
Find(&targets); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(targets) == 0 {
|
||||||
|
t.Fatal("no email relations available for expunge")
|
||||||
|
}
|
||||||
|
|
||||||
|
uids := make([]imap.UID, 0, len(targets))
|
||||||
|
for _, target := range targets {
|
||||||
|
uids = append(uids, imap.UID(target.ID))
|
||||||
|
}
|
||||||
|
res, err := clientLogin.UIDExpunge(imap.UIDSetNum(uids...)).Collect()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("%+v", err)
|
t.Errorf("%+v", err)
|
||||||
}
|
}
|
||||||
t.Logf("%+v", res)
|
t.Logf("%+v", res)
|
||||||
var ues []models.UserEmail
|
|
||||||
db.Instance.Table("user_email").Where("id=1 or id=2").Find(&ues)
|
for _, target := range targets {
|
||||||
if len(ues) > 0 {
|
var remaining models.UserEmail
|
||||||
t.Errorf("TestExpunge Error")
|
found, err := db.Instance.ID(target.ID).Get(&remaining)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if found {
|
||||||
|
t.Errorf("email relation %d was not expunged", target.ID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -620,6 +676,13 @@ func TestNoop(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
func TestIDLE(t *testing.T) {
|
func TestIDLE(t *testing.T) {
|
||||||
|
credentialService, err := credentials.NewService(db.Instance)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !credentialService.VerifyProtocolCode(1, credentials.ProtocolIMAP, imapTestProtocolCode) {
|
||||||
|
t.Fatalf("test IMAP authorization code was rejected before raw LOGIN: %q", imapTestProtocolCode)
|
||||||
|
}
|
||||||
conn, err := tls.Dial("tcp", imapTestAddr, &tls.Config{InsecureSkipVerify: true})
|
conn, err := tls.Dial("tcp", imapTestAddr, &tls.Config{InsecureSkipVerify: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@ -635,7 +698,7 @@ func TestIDLE(t *testing.T) {
|
|||||||
t.Fatalf("unexpected greeting: %s", line)
|
t.Fatalf("unexpected greeting: %s", line)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = conn.Write([]byte("a001 LOGIN testCase testCase\r\n")); err != nil {
|
if _, err = conn.Write([]byte("a001 LOGIN testCase@test.domain " + imapTestProtocolCode + "\r\n")); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if line, err = readTaggedLine(reader, "a001"); err != nil {
|
if line, err = readTaggedLine(reader, "a001"); err != nil {
|
||||||
@ -699,7 +762,7 @@ func TestIdleNotice(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
|
|
||||||
if err = client.Login("testCase", "testCase").Wait(); err != nil {
|
if err = client.Login("testCase@test.domain", imapTestProtocolCode).Wait(); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user