Compare commits

..

5 Commits

Author SHA1 Message Date
DacongDA
d522247552 feat: fix countryCode param bug in MFA login (#3384) 2024-11-29 21:46:06 +08:00
DacongDA
79dbdab6c9 feat: fix "dest is missing" bug in MFA login (#3383)
* feat: support stateless mfa setup

* Revert "feat: support stateless mfa setup"

This reverts commit bd843b2ff3.

* feat: use new implement

* fix: missing set field on login
2024-11-29 19:59:30 +08:00
DacongDA
fe40910e3b feat: support stateless MFA setup (#3382) 2024-11-29 19:50:10 +08:00
Xinyu Ge
2d1736f13a feat: Add more data to the dashboard page chart #3365 (#3375)
* test

* feat: #3365 add more dada to the dashboard page chart

* feat: #3365 Add more data to the dashboard page chart
2024-11-26 09:16:35 +08:00
ming.zhang
12b4d1c7cd feat: change LDAP attribute from cn to title for correct username mapping (#3378) 2024-11-26 09:13:05 +08:00
10 changed files with 207 additions and 96 deletions

View File

@@ -22,13 +22,6 @@ import (
"github.com/google/uuid"
)
const (
MfaRecoveryCodesSession = "mfa_recovery_codes"
MfaCountryCodeSession = "mfa_country_code"
MfaDestSession = "mfa_dest"
MfaTotpSecretSession = "mfa_totp_secret"
)
// MfaSetupInitiate
// @Title MfaSetupInitiate
// @Tag MFA API
@@ -72,11 +65,6 @@ func (c *ApiController) MfaSetupInitiate() {
}
recoveryCode := uuid.NewString()
c.SetSession(MfaRecoveryCodesSession, recoveryCode)
if mfaType == object.TotpType {
c.SetSession(MfaTotpSecretSession, mfaProps.Secret)
}
mfaProps.RecoveryCodes = []string{recoveryCode}
resp := mfaProps
@@ -94,6 +82,9 @@ func (c *ApiController) MfaSetupInitiate() {
func (c *ApiController) MfaSetupVerify() {
mfaType := c.Ctx.Request.Form.Get("mfaType")
passcode := c.Ctx.Request.Form.Get("passcode")
secret := c.Ctx.Request.Form.Get("secret")
dest := c.Ctx.Request.Form.Get("dest")
countryCode := c.Ctx.Request.Form.Get("countryCode")
if mfaType == "" || passcode == "" {
c.ResponseError("missing auth type or passcode")
@@ -104,32 +95,28 @@ func (c *ApiController) MfaSetupVerify() {
MfaType: mfaType,
}
if mfaType == object.TotpType {
secret := c.GetSession(MfaTotpSecretSession)
if secret == nil {
if secret == "" {
c.ResponseError("totp secret is missing")
return
}
config.Secret = secret.(string)
config.Secret = secret
} else if mfaType == object.SmsType {
dest := c.GetSession(MfaDestSession)
if dest == nil {
if dest == "" {
c.ResponseError("destination is missing")
return
}
config.Secret = dest.(string)
countryCode := c.GetSession(MfaCountryCodeSession)
if countryCode == nil {
config.Secret = dest
if countryCode == "" {
c.ResponseError("country code is missing")
return
}
config.CountryCode = countryCode.(string)
config.CountryCode = countryCode
} else if mfaType == object.EmailType {
dest := c.GetSession(MfaDestSession)
if dest == nil {
if dest == "" {
c.ResponseError("destination is missing")
return
}
config.Secret = dest.(string)
config.Secret = dest
}
mfaUtil := object.GetMfaUtil(mfaType, config)
@@ -159,6 +146,10 @@ func (c *ApiController) MfaSetupEnable() {
owner := c.Ctx.Request.Form.Get("owner")
name := c.Ctx.Request.Form.Get("name")
mfaType := c.Ctx.Request.Form.Get("mfaType")
secret := c.Ctx.Request.Form.Get("secret")
dest := c.Ctx.Request.Form.Get("dest")
countryCode := c.Ctx.Request.Form.Get("secret")
recoveryCodes := c.Ctx.Request.Form.Get("recoveryCodes")
user, err := object.GetUser(util.GetId(owner, name))
if err != nil {
@@ -176,43 +167,39 @@ func (c *ApiController) MfaSetupEnable() {
}
if mfaType == object.TotpType {
secret := c.GetSession(MfaTotpSecretSession)
if secret == nil {
if secret == "" {
c.ResponseError("totp secret is missing")
return
}
config.Secret = secret.(string)
config.Secret = secret
} else if mfaType == object.EmailType {
if user.Email == "" {
dest := c.GetSession(MfaDestSession)
if dest == nil {
if dest == "" {
c.ResponseError("destination is missing")
return
}
user.Email = dest.(string)
user.Email = dest
}
} else if mfaType == object.SmsType {
if user.Phone == "" {
dest := c.GetSession(MfaDestSession)
if dest == nil {
if dest == "" {
c.ResponseError("destination is missing")
return
}
user.Phone = dest.(string)
countryCode := c.GetSession(MfaCountryCodeSession)
if countryCode == nil {
user.Phone = dest
if countryCode == "" {
c.ResponseError("country code is missing")
return
}
user.CountryCode = countryCode.(string)
user.CountryCode = countryCode
}
}
recoveryCodes := c.GetSession(MfaRecoveryCodesSession)
if recoveryCodes == nil {
if recoveryCodes == "" {
c.ResponseError("recovery codes is missing")
return
}
config.RecoveryCodes = []string{recoveryCodes.(string)}
config.RecoveryCodes = []string{recoveryCodes}
mfaUtil := object.GetMfaUtil(mfaType, config)
if mfaUtil == nil {
@@ -226,14 +213,6 @@ func (c *ApiController) MfaSetupEnable() {
return
}
c.DelSession(MfaRecoveryCodesSession)
if mfaType == object.TotpType {
c.DelSession(MfaTotpSecretSession)
} else {
c.DelSession(MfaCountryCodeSession)
c.DelSession(MfaDestSession)
}
c.ResponseOk(http.StatusText(http.StatusOK))
}

View File

@@ -246,8 +246,6 @@ func (c *ApiController) SendVerificationCode() {
if user != nil && util.GetMaskedEmail(mfaProps.Secret) == vform.Dest {
vform.Dest = mfaProps.Secret
}
} else if vform.Method == MfaSetupVerification {
c.SetSession(MfaDestSession, vform.Dest)
}
provider, err = application.GetEmailProvider(vform.Method)
@@ -282,11 +280,6 @@ func (c *ApiController) SendVerificationCode() {
vform.CountryCode = user.GetCountryCode(vform.CountryCode)
}
}
if vform.Method == MfaSetupVerification {
c.SetSession(MfaCountryCodeSession, vform.CountryCode)
c.SetSession(MfaDestSession, vform.Dest)
}
} else if vform.Method == MfaAuthVerification {
mfaProps := user.GetPreferredMfaProps(false)
if user != nil && util.GetMaskedPhone(mfaProps.Secret) == vform.Dest {

View File

@@ -142,7 +142,7 @@ func handleSearch(w ldap.ResponseWriter, m *ldap.Message) {
}
for _, attr := range attrs {
e.AddAttribute(message.AttributeDescription(attr), getAttribute(string(attr), user))
if string(attr) == "cn" {
if string(attr) == "title" {
e.AddAttribute(message.AttributeDescription(attr), getAttribute("title", user))
}
}

View File

@@ -25,6 +25,12 @@ type Dashboard struct {
ProviderCounts []int `json:"providerCounts"`
ApplicationCounts []int `json:"applicationCounts"`
SubscriptionCounts []int `json:"subscriptionCounts"`
RoleCounts []int `json:"roleCounts"`
GroupCounts []int `json:"groupCounts"`
ResourceCounts []int `json:"resourceCounts"`
CertCounts []int `json:"certCounts"`
PermissionCounts []int `json:"permissionCounts"`
TransactionCounts []int `json:"transactionCounts"`
}
func GetDashboard(owner string) (*Dashboard, error) {
@@ -38,6 +44,12 @@ func GetDashboard(owner string) (*Dashboard, error) {
ProviderCounts: make([]int, 31),
ApplicationCounts: make([]int, 31),
SubscriptionCounts: make([]int, 31),
RoleCounts: make([]int, 31),
GroupCounts: make([]int, 31),
ResourceCounts: make([]int, 31),
CertCounts: make([]int, 31),
PermissionCounts: make([]int, 31),
TransactionCounts: make([]int, 31),
}
organizations := []Organization{}
@@ -45,9 +57,15 @@ func GetDashboard(owner string) (*Dashboard, error) {
providers := []Provider{}
applications := []Application{}
subscriptions := []Subscription{}
roles := []Role{}
groups := []Group{}
resources := []Resource{}
certs := []Cert{}
permissions := []Permission{}
transactions := []Transaction{}
var wg sync.WaitGroup
wg.Add(5)
wg.Add(11)
go func() {
defer wg.Done()
if err := ormer.Engine.Find(&organizations, &Organization{Owner: owner}); err != nil {
@@ -86,6 +104,50 @@ func GetDashboard(owner string) (*Dashboard, error) {
panic(err)
}
}()
go func() {
defer wg.Done()
if err := ormer.Engine.Find(&roles, &Role{Owner: owner}); err != nil {
panic(err)
}
}()
go func() {
defer wg.Done()
if err := ormer.Engine.Find(&groups, &Group{Owner: owner}); err != nil {
panic(err)
}
}()
go func() {
defer wg.Done()
if err := ormer.Engine.Find(&resources, &Resource{Owner: owner}); err != nil {
panic(err)
}
}()
go func() {
defer wg.Done()
if err := ormer.Engine.Find(&certs, &Cert{Owner: owner}); err != nil {
panic(err)
}
}()
go func() {
defer wg.Done()
if err := ormer.Engine.Find(&permissions, &Permission{Owner: owner}); err != nil {
panic(err)
}
}()
go func() {
defer wg.Done()
if err := ormer.Engine.Find(&transactions, &Transaction{Owner: owner}); err != nil {
panic(err)
}
}()
wg.Wait()
nowTime := time.Now()
@@ -96,6 +158,12 @@ func GetDashboard(owner string) (*Dashboard, error) {
dashboard.ProviderCounts[30-i] = countCreatedBefore(providers, cutTime)
dashboard.ApplicationCounts[30-i] = countCreatedBefore(applications, cutTime)
dashboard.SubscriptionCounts[30-i] = countCreatedBefore(subscriptions, cutTime)
dashboard.RoleCounts[30-i] = countCreatedBefore(roles, cutTime)
dashboard.GroupCounts[30-i] = countCreatedBefore(groups, cutTime)
dashboard.ResourceCounts[30-i] = countCreatedBefore(resources, cutTime)
dashboard.CertCounts[30-i] = countCreatedBefore(certs, cutTime)
dashboard.PermissionCounts[30-i] = countCreatedBefore(permissions, cutTime)
dashboard.TransactionCounts[30-i] = countCreatedBefore(transactions, cutTime)
}
return dashboard, nil
}
@@ -138,6 +206,48 @@ func countCreatedBefore(objects interface{}, before time.Time) int {
count++
}
}
case []Role:
for _, r := range obj {
createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", r.CreatedTime)
if createdTime.Before(before) {
count++
}
}
case []Group:
for _, g := range obj {
createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", g.CreatedTime)
if createdTime.Before(before) {
count++
}
}
case []Resource:
for _, r := range obj {
createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", r.CreatedTime)
if createdTime.Before(before) {
count++
}
}
case []Cert:
for _, c := range obj {
createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", c.CreatedTime)
if createdTime.Before(before) {
count++
}
}
case []Permission:
for _, p := range obj {
createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", p.CreatedTime)
if createdTime.Before(before) {
count++
}
}
case []Transaction:
for _, t := range obj {
createdTime, _ := time.Parse("2006-01-02T15:04:05-07:00", t.CreatedTime)
if createdTime.Before(before) {
count++
}
}
}
return count
}

View File

@@ -179,8 +179,10 @@ class MfaSetupPage extends React.Component {
mfaProps={this.state.mfaProps}
application={this.state.application}
user={this.props.account}
onSuccess={() => {
onSuccess={(res) => {
this.setState({
dest: res.dest,
countryCode: res.countryCode,
current: this.state.current + 1,
});
}}
@@ -195,7 +197,7 @@ class MfaSetupPage extends React.Component {
);
case 2:
return (
<MfaEnableForm user={this.getUser()} mfaType={this.state.mfaType} recoveryCodes={this.state.mfaProps.recoveryCodes}
<MfaEnableForm user={this.getUser()} mfaType={this.state.mfaType} secret={this.state.mfaProps.secret} recoveryCodes={this.state.mfaProps.recoveryCodes} dest={this.state.dest} countryCode={this.state.countryCode}
onSuccess={() => {
Setting.showMessage("success", i18next.t("general:Enabled successfully"));
this.props.onfinish();

View File

@@ -3,11 +3,15 @@ import i18next from "i18next";
import React, {useState} from "react";
import * as MfaBackend from "../../backend/MfaBackend";
export function MfaEnableForm({user, mfaType, recoveryCodes, onSuccess, onFail}) {
export function MfaEnableForm({user, mfaType, secret, recoveryCodes, dest, countryCode, onSuccess, onFail}) {
const [loading, setLoading] = useState(false);
const requestEnableMfa = () => {
const data = {
mfaType,
secret,
recoveryCodes,
dest,
countryCode,
...user,
};
setLoading(true);

View File

@@ -26,11 +26,13 @@ export const mfaSetup = "mfaSetup";
export function MfaVerifyForm({mfaProps, application, user, onSuccess, onFail}) {
const [form] = Form.useForm();
const onFinish = ({passcode}) => {
const data = {passcode, mfaType: mfaProps.mfaType, ...user};
const onFinish = ({passcode, countryCode, dest}) => {
const data = {passcode, mfaType: mfaProps.mfaType, secret: mfaProps.secret, dest: dest, countryCode: countryCode, ...user};
MfaBackend.MfaSetupVerify(data)
.then((res) => {
if (res.status === "ok") {
res.dest = dest;
res.countryCode = countryCode;
onSuccess(res);
} else {
onFail(res);

View File

@@ -1,5 +1,5 @@
import {UserOutlined} from "@ant-design/icons";
import {Button, Form, Input} from "antd";
import {Button, Form, Input, Space} from "antd";
import i18next from "i18next";
import React, {useEffect} from "react";
import {CountryCodeSelect} from "../../common/select/CountryCodeSelect";
@@ -15,15 +15,18 @@ export const MfaVerifySmsForm = ({mfaProps, application, onFinish, method, user}
useEffect(() => {
if (method === mfaAuth) {
setDest(mfaProps.secret);
form.setFieldValue("dest", mfaProps.secret);
return;
}
if (mfaProps.mfaType === SmsMfaType) {
setDest(user.phone);
form.setFieldValue("dest", user.phone);
return;
}
if (mfaProps.mfaType === EmailMfaType) {
setDest(user.email);
form.setFieldValue("dest", user.email);
}
}, [mfaProps.mfaType]);
@@ -57,45 +60,44 @@ export const MfaVerifySmsForm = ({mfaProps, application, onFinish, method, user}
<div style={{marginBottom: 20, textAlign: "left", gap: 8}}>
{isEmail() ? i18next.t("mfa:Your email is") : i18next.t("mfa:Your phone is")} {dest}
</div> :
(<React.Fragment>
(
<p>{isEmail() ? i18next.t("mfa:Please bind your email first, the system will automatically uses the mail for multi-factor authentication") :
i18next.t("mfa:Please bind your phone first, the system automatically uses the phone for multi-factor authentication")}
</p>
<Input.Group compact style={{width: "300Px", marginBottom: "30px"}}>
{isEmail() ? null :
<Form.Item
name="countryCode"
noStyle
rules={[
{
required: false,
message: i18next.t("signup:Please select your country code!"),
},
]}
>
<CountryCodeSelect
initValue={mfaProps.countryCode}
style={{width: "30%"}}
countryCodes={application.organizationObj.countryCodes}
/>
</Form.Item>
}
<Form.Item
name="dest"
noStyle
rules={[{required: true, message: i18next.t("login:Please input your Email or Phone!")}]}
>
<Input
style={{width: isEmail() ? "100% " : "70%"}}
onChange={(e) => {setDest(e.target.value);}}
prefix={<UserOutlined />}
placeholder={isEmail() ? i18next.t("general:Email") : i18next.t("general:Phone")}
/>
</Form.Item>
</Input.Group>
</React.Fragment>
)
}
<Space.Compact style={{width: "300Px", marginBottom: "30px", display: isShowText() ? "none" : ""}}>
{isEmail() || isShowText() ? null :
<Form.Item
name="countryCode"
noStyle
rules={[
{
required: false,
message: i18next.t("signup:Please select your country code!"),
},
]}
>
<CountryCodeSelect
initValue={mfaProps.countryCode}
style={{width: "30%"}}
countryCodes={application.organizationObj.countryCodes}
/>
</Form.Item>
}
<Form.Item
name="dest"
noStyle
rules={[{required: true, message: i18next.t("login:Please input your Email or Phone!")}]}
>
<Input
style={{width: isEmail() ? "100% " : "70%"}}
onChange={(e) => {setDest(e.target.value);}}
prefix={<UserOutlined />}
placeholder={isEmail() ? i18next.t("general:Email") : i18next.t("general:Phone")}
/>
</Form.Item>
</Space.Compact>
<Form.Item
name="passcode"
rules={[{required: true, message: i18next.t("login:Please input your code!")}]}

View File

@@ -32,6 +32,9 @@ export function MfaSetupVerify(values) {
formData.append("name", values.name);
formData.append("mfaType", values.mfaType);
formData.append("passcode", values.passcode);
formData.append("secret", values.secret);
formData.append("dest", values.dest);
formData.append("countryCode", values.countryCode);
return fetch(`${Setting.ServerUrl}/api/mfa/setup/verify`, {
method: "POST",
credentials: "include",
@@ -44,6 +47,10 @@ export function MfaSetupEnable(values) {
formData.append("mfaType", values.mfaType);
formData.append("owner", values.owner);
formData.append("name", values.name);
formData.append("secret", values.secret);
formData.append("recoveryCodes", values.recoveryCodes);
formData.append("dest", values.dest);
formData.append("countryCode", values.countryCode);
return fetch(`${Setting.ServerUrl}/api/mfa/setup/enable`, {
method: "POST",
credentials: "include",

View File

@@ -135,6 +135,12 @@ const Dashboard = (props) => {
i18next.t("general:Applications"),
i18next.t("general:Organizations"),
i18next.t("general:Subscriptions"),
i18next.t("general:Roles"),
i18next.t("general:Groups"),
i18next.t("general:Resources"),
i18next.t("general:Certs"),
i18next.t("general:Permissions"),
i18next.t("general:Transactions"),
], top: "10%"},
grid: {left: "3%", right: "4%", bottom: "0", top: "25%", containLabel: true},
xAxis: {type: "category", boundaryGap: false, data: dateArray},
@@ -145,6 +151,12 @@ const Dashboard = (props) => {
{name: i18next.t("general:Providers"), type: "line", data: dashboardData.providerCounts},
{name: i18next.t("general:Applications"), type: "line", data: dashboardData.applicationCounts},
{name: i18next.t("general:Subscriptions"), type: "line", data: dashboardData.subscriptionCounts},
{name: i18next.t("general:Roles"), type: "line", data: dashboardData.roleCounts},
{name: i18next.t("general:Groups"), type: "line", data: dashboardData.groupCounts},
{name: i18next.t("general:Resources"), type: "line", data: dashboardData.resourceCounts},
{name: i18next.t("general:Certs"), type: "line", data: dashboardData.certCounts},
{name: i18next.t("general:Permissions"), type: "line", data: dashboardData.permissionCounts},
{name: i18next.t("general:Transactions"), type: "line", data: dashboardData.transactionCounts},
],
};
myChart.setOption(option);