diff --git a/controllers/user.go b/controllers/user.go index c74d1546..77cbeaa2 100644 --- a/controllers/user.go +++ b/controllers/user.go @@ -528,3 +528,25 @@ func (c *ApiController) GetUserCount() { c.Data["json"] = count c.ServeJSON() } + +// AddUserkeys +// @Title AddUserkeys +// @router /add-user-keys [post] +// @Tag User API +func (c *ApiController) AddUserkeys() { + var user object.User + err := json.Unmarshal(c.Ctx.Input.RequestBody, &user) + if err != nil { + c.ResponseError(err.Error()) + return + } + + isAdmin := c.IsAdmin() + affected, err := object.AddUserkeys(&user, isAdmin) + if err != nil { + c.ResponseError(err.Error()) + return + } + + c.ResponseOk(affected) +} diff --git a/object/user.go b/object/user.go index 91adeb71..07357972 100644 --- a/object/user.go +++ b/object/user.go @@ -78,6 +78,8 @@ type User struct { Hash string `xorm:"varchar(100)" json:"hash"` PreHash string `xorm:"varchar(100)" json:"preHash"` Groups []string `xorm:"varchar(1000)" json:"groups"` + AccessKey string `xorm:"varchar(100)" json:"accessKey"` + SecretKey string `xorm:"varchar(100)" json:"secretKey"` CreatedIp string `xorm:"varchar(100)" json:"createdIp"` LastSigninTime string `xorm:"varchar(100)" json:"lastSigninTime"` @@ -422,6 +424,23 @@ func GetUserByUserId(owner string, userId string) (*User, error) { } } +func GetUserByAccessKey(accessKey string) (*User, error) { + if accessKey == "" { + return nil, nil + } + user := User{AccessKey: accessKey} + existed, err := adapter.Engine.Get(&user) + if err != nil { + return nil, err + } + + if existed { + return &user, nil + } else { + return nil, nil + } +} + func GetUser(id string) (*User, error) { owner, name := util.GetOwnerAndNameFromId(id) return getUser(owner, name) @@ -526,7 +545,7 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er "owner", "display_name", "avatar", "location", "address", "country_code", "region", "language", "affiliation", "title", "homepage", "bio", "tag", "language", "gender", "birthday", "education", "score", "karma", "ranking", "signup_application", "is_admin", "is_global_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts", - "signin_wrong_times", "last_signin_wrong_time", "groups", + "signin_wrong_times", "last_signin_wrong_time", "groups", "access_key", "secret_key", "github", "google", "qq", "wechat", "facebook", "dingtalk", "weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs", "baidu", "alipay", "casdoor", "infoflow", "apple", "azuread", "slack", "steam", "bilibili", "okta", "douyin", "line", "amazon", "auth0", "battlenet", "bitbucket", "box", "cloudfoundry", "dailymotion", "deezer", "digitalocean", "discord", "dropbox", @@ -928,3 +947,14 @@ func (user *User) GetPreferMfa(masked bool) *MfaProps { return user.MultiFactorAuths[0] } } + +func AddUserkeys(user *User, isAdmin bool) (bool, error) { + if user == nil { + return false, nil + } + + user.AccessKey = util.GenerateId() + user.SecretKey = util.GenerateId() + + return UpdateUser(user.GetId(), user, []string{}, isAdmin) +} diff --git a/routers/authz_filter.go b/routers/authz_filter.go index 2634b2c4..80139f07 100644 --- a/routers/authz_filter.go +++ b/routers/authz_filter.go @@ -26,8 +26,10 @@ import ( ) type Object struct { - Owner string `json:"owner"` - Name string `json:"name"` + Owner string `json:"owner"` + Name string `json:"name"` + AccessKey string `json:"accessKey"` + SecretKey string `json:"secretKey"` } func getUsername(ctx *context.Context) (username string) { @@ -43,6 +45,9 @@ func getUsername(ctx *context.Context) (username string) { username = getUsernameByClientIdSecret(ctx) } + if username == "" { + username = getUsernameByKeys(ctx) + } return } @@ -98,6 +103,30 @@ func getObject(ctx *context.Context) (string, string) { } } +func getKeys(ctx *context.Context) (string, string) { + method := ctx.Request.Method + + if method == http.MethodGet { + accessKey := ctx.Input.Query("accesskey") + secretKey := ctx.Input.Query("secretkey") + return accessKey, secretKey + } else { + body := ctx.Input.RequestBody + + if len(body) == 0 { + return ctx.Request.Form.Get("accesskey"), ctx.Request.Form.Get("secretkey") + } + + var obj Object + err := json.Unmarshal(body, &obj) + if err != nil { + return "", "" + } + + return obj.AccessKey, obj.SecretKey + } +} + func willLog(subOwner string, subName string, method string, urlPath string, objOwner string, objName string) bool { if subOwner == "anonymous" && subName == "anonymous" && method == "GET" && (urlPath == "/api/get-account" || urlPath == "/api/get-app-login") && objOwner == "" && objName == "" { return false diff --git a/routers/base.go b/routers/base.go index 716ba356..3b2b649a 100644 --- a/routers/base.go +++ b/routers/base.go @@ -84,6 +84,18 @@ func getUsernameByClientIdSecret(ctx *context.Context) string { return fmt.Sprintf("app/%s", application.Name) } +func getUsernameByKeys(ctx *context.Context) string { + accessKey, secretKey := getKeys(ctx) + user, err := object.GetUserByAccessKey(accessKey) + if err != nil { + panic(err) + } + if user != nil && secretKey == user.SecretKey { + return user.GetId() + } + return "" +} + func getSessionUser(ctx *context.Context) string { user := ctx.Input.CruSession.Get("username") if user == nil { diff --git a/routers/router.go b/routers/router.go index 94faae3a..ca661b0c 100644 --- a/routers/router.go +++ b/routers/router.go @@ -73,6 +73,7 @@ func initAPI() { beego.Router("/api/get-user-count", &controllers.ApiController{}, "GET:GetUserCount") beego.Router("/api/get-user", &controllers.ApiController{}, "GET:GetUser") beego.Router("/api/update-user", &controllers.ApiController{}, "POST:UpdateUser") + beego.Router("/api/add-user-keys", &controllers.ApiController{}, "POST:AddUserkeys") beego.Router("/api/add-user", &controllers.ApiController{}, "POST:AddUser") beego.Router("/api/delete-user", &controllers.ApiController{}, "POST:DeleteUser") beego.Router("/api/upload-users", &controllers.ApiController{}, "POST:UploadUsers") diff --git a/web/src/OrganizationListPage.js b/web/src/OrganizationListPage.js index 9d76914f..2439a4e2 100644 --- a/web/src/OrganizationListPage.js +++ b/web/src/OrganizationListPage.js @@ -60,6 +60,7 @@ class OrganizationListPage extends BaseListPage { {name: "Bio", visible: true, viewRule: "Public", modifyRule: "Self"}, {name: "Tag", visible: true, viewRule: "Public", modifyRule: "Admin"}, {name: "Signup application", visible: true, viewRule: "Public", modifyRule: "Admin"}, + {name: "API key", label: i18next.t("general:API key")}, {name: "Roles", visible: true, viewRule: "Public", modifyRule: "Immutable"}, {name: "Permissions", visible: true, viewRule: "Public", modifyRule: "Immutable"}, {name: "Groups", visible: true, viewRule: "Public", modifyRule: "Immutable"}, diff --git a/web/src/UserEditPage.js b/web/src/UserEditPage.js index c1f3af79..2a907f41 100644 --- a/web/src/UserEditPage.js +++ b/web/src/UserEditPage.js @@ -91,6 +91,17 @@ class UserEditPage extends React.Component { }); } + addUserKeys() { + UserBackend.addUserKeys(this.state.user) + .then((res) => { + if (res.status === "ok") { + this.getUser(); + } else { + Setting.showMessage("error", res.msg); + } + }); + } + getOrganizations() { OrganizationBackend.getOrganizations("admin") .then((res) => { @@ -266,6 +277,11 @@ class UserEditPage extends React.Component { } } + let isKeysGenerated = false; + if (this.state.user.accessKey !== "" && this.state.user.accessKey !== "") { + isKeysGenerated = true; + } + if (accountItem.name === "Organization") { return ( @@ -691,6 +707,37 @@ class UserEditPage extends React.Component { ); + } else if (accountItem.name === "API key") { + return ( + + + {Setting.getLabel(i18next.t("general:API key"), i18next.t("general:API key - Tooltip"))} : + + + + + {Setting.getLabel(i18next.t("general:Access key"), i18next.t("general:Access key - Tooltip"))} : + + + + + + + + {Setting.getLabel(i18next.t("general:Secret key"), i18next.t("general:Secret key - Tooltip"))} : + + + + + + + + + + + + + ); } else if (accountItem.name === "Roles") { return ( diff --git a/web/src/backend/UserBackend.js b/web/src/backend/UserBackend.js index eb6180c7..e4743ea3 100644 --- a/web/src/backend/UserBackend.js +++ b/web/src/backend/UserBackend.js @@ -45,6 +45,17 @@ export function getUser(owner, name) { }).then(res => res.json()); } +export function addUserKeys(user) { + return fetch(`${Setting.ServerUrl}/api/add-user-keys`, { + method: "POST", + credentials: "include", + body: JSON.stringify(user), + headers: { + "Accept-Language": Setting.getAcceptLanguage(), + }, + }).then(res => res.json()); +} + export function updateUser(owner, name, user) { const newUser = Setting.deepCopy(user); return fetch(`${Setting.ServerUrl}/api/update-user?id=${owner}/${encodeURIComponent(name)}`, { diff --git a/web/src/table/AccountTable.js b/web/src/table/AccountTable.js index 9aa9b3de..544efd1c 100644 --- a/web/src/table/AccountTable.js +++ b/web/src/table/AccountTable.js @@ -91,6 +91,7 @@ class AccountTable extends React.Component { {name: "Karma", label: i18next.t("user:Karma")}, {name: "Ranking", label: i18next.t("user:Ranking")}, {name: "Signup application", label: i18next.t("general:Signup application")}, + {name: "API key", label: i18next.t("general:API key")}, {name: "Roles", label: i18next.t("general:Roles")}, {name: "Permissions", label: i18next.t("general:Permissions")}, {name: "Groups", label: i18next.t("general:Groups")},