mirror of
https://github.com/lejianwen/rustdesk-api.git
synced 2026-02-10 08:10:48 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c794e9d4b | ||
|
|
01f697d279 | ||
|
|
6cdc37333b | ||
|
|
ae32915565 |
1
.github/workflows/build.yml
vendored
1
.github/workflows/build.yml
vendored
@@ -203,6 +203,7 @@ jobs:
|
|||||||
- name: Build package for ${{ matrix.job.platform }} arch
|
- name: Build package for ${{ matrix.job.platform }} arch
|
||||||
run: |
|
run: |
|
||||||
mv ${{ matrix.job.platform }}/release/apimain debian-build/${{ matrix.job.platform }}/bin/rustdesk-api
|
mv ${{ matrix.job.platform }}/release/apimain debian-build/${{ matrix.job.platform }}/bin/rustdesk-api
|
||||||
|
mv ${{ matrix.job.platform }}/release/resources/admin resources
|
||||||
chmod -v a+x debian-build/${{ matrix.job.platform }}/bin/*
|
chmod -v a+x debian-build/${{ matrix.job.platform }}/bin/*
|
||||||
mkdir -p data
|
mkdir -p data
|
||||||
cp -vr debian systemd conf data resources runtime debian-build/${{ matrix.job.platform }}/
|
cp -vr debian systemd conf data resources runtime debian-build/${{ matrix.job.platform }}/
|
||||||
|
|||||||
@@ -58,3 +58,23 @@ oss:
|
|||||||
expire-time: 30
|
expire-time: 30
|
||||||
max-byte: 10240
|
max-byte: 10240
|
||||||
|
|
||||||
|
ldap:
|
||||||
|
enable: false
|
||||||
|
url: "ldap://ldap.example.com:389"
|
||||||
|
tls: false
|
||||||
|
tls-verify: false
|
||||||
|
base-dn: "dc=example,dc=com"
|
||||||
|
bind-dn: "cn=admin,dc=example,dc=com"
|
||||||
|
bind-password: "password"
|
||||||
|
|
||||||
|
user:
|
||||||
|
base-dn: "ou=users,dc=example,dc=com"
|
||||||
|
enable-attr: "" #The attribute name of the user for enabling, in AD it is "userAccountControl", empty means no enable attribute, all users are enabled
|
||||||
|
enable-attr-value: "" # The value of the enable attribute when the user is enabled. If you are using AD, just set random value, it will be ignored.
|
||||||
|
filter: "(cn=*)"
|
||||||
|
username: "uid" # The attribute name of the user for usernamem if you are using AD, it should be "sAMAccountName"
|
||||||
|
email: "mail"
|
||||||
|
first-name: "givenName"
|
||||||
|
last-name: "sn"
|
||||||
|
sync: false # If true, the user will be synchronized to the database when the user logs in. If false, the user will be synchronized to the database when the user be created.
|
||||||
|
admin-group: "cn=admin,dc=example,dc=com" # The group name of the admin group, if the user is in this group, the user will be an admin.
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ type Config struct {
|
|||||||
Jwt Jwt
|
Jwt Jwt
|
||||||
Rustdesk Rustdesk
|
Rustdesk Rustdesk
|
||||||
Proxy Proxy
|
Proxy Proxy
|
||||||
|
Ldap Ldap
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init 初始化配置
|
// Init 初始化配置
|
||||||
|
|||||||
36
config/ldap.go
Normal file
36
config/ldap.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type LdapUser struct {
|
||||||
|
BaseDn string `mapstructure:"base-dn"` // The base DN of the user for searching
|
||||||
|
EnableAttr string `mapstructure:"enable-attr"` // The attribute name of the user for enabling, in AD it is "userAccountControl", empty means no enable attribute, all users are enabled
|
||||||
|
EnableAttrValue string `mapstructure:"enable-attr-value"` // The value of the enable attribute when the user is enabled. If you are using AD, just leave it random str, it will be ignored.
|
||||||
|
Filter string `mapstructure:"filter"`
|
||||||
|
Username string `mapstructure:"username"`
|
||||||
|
Email string `mapstructure:"email"`
|
||||||
|
FirstName string `mapstructure:"first-name"`
|
||||||
|
LastName string `mapstructure:"last-name"`
|
||||||
|
Sync bool `mapstructure:"sync"` // Will sync the user's information to the internal database
|
||||||
|
AdminGroup string `mapstructure:"admin-group"` // Which group is the admin group
|
||||||
|
}
|
||||||
|
|
||||||
|
// type LdapGroup struct {
|
||||||
|
// BaseDn string `mapstructure:"base-dn"` // The base DN of the group for searching
|
||||||
|
// Name string `mapstructure:"name"` // The attribute name of the group
|
||||||
|
// Filter string `mapstructure:"filter"`
|
||||||
|
// Admin string `mapstructure:"admin"` // Which group is the admin group
|
||||||
|
// Member string `mapstructure:"member"` // How to get the member of the group: member, uniqueMember, or memberOf (default: member)
|
||||||
|
// Mode string `mapstructure:"mode"`
|
||||||
|
// Map map[string]string `mapstructure:"map"` // If mode is "map", map the LDAP group to the internal group
|
||||||
|
// }
|
||||||
|
|
||||||
|
type Ldap struct {
|
||||||
|
Enable bool `mapstructure:"enable"`
|
||||||
|
Url string `mapstructure:"url"`
|
||||||
|
TLS bool `mapstructure:"tls"`
|
||||||
|
TlsVerify bool `mapstructure:"tls-verify"`
|
||||||
|
BaseDn string `mapstructure:"base-dn"`
|
||||||
|
BindDn string `mapstructure:"bind-dn"`
|
||||||
|
BindPassword string `mapstructure:"bind-password"`
|
||||||
|
User LdapUser `mapstructure:"user"`
|
||||||
|
// Group LdapGroup `mapstructure:"group"`
|
||||||
|
}
|
||||||
13
go.mod
13
go.mod
@@ -13,7 +13,7 @@ require (
|
|||||||
github.com/go-playground/validator/v10 v10.11.2
|
github.com/go-playground/validator/v10 v10.11.2
|
||||||
github.com/go-redis/redis/v8 v8.11.4
|
github.com/go-redis/redis/v8 v8.11.4
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||||
github.com/google/uuid v1.1.2
|
github.com/google/uuid v1.6.0
|
||||||
github.com/nicksnyder/go-i18n/v2 v2.4.0
|
github.com/nicksnyder/go-i18n/v2 v2.4.0
|
||||||
github.com/sirupsen/logrus v1.8.1
|
github.com/sirupsen/logrus v1.8.1
|
||||||
github.com/spf13/cobra v1.8.1
|
github.com/spf13/cobra v1.8.1
|
||||||
@@ -22,13 +22,14 @@ require (
|
|||||||
github.com/swaggo/gin-swagger v1.6.0
|
github.com/swaggo/gin-swagger v1.6.0
|
||||||
github.com/swaggo/swag v1.16.3
|
github.com/swaggo/swag v1.16.3
|
||||||
golang.org/x/oauth2 v0.23.0
|
golang.org/x/oauth2 v0.23.0
|
||||||
golang.org/x/text v0.18.0
|
golang.org/x/text v0.21.0
|
||||||
gorm.io/driver/mysql v1.5.7
|
gorm.io/driver/mysql v1.5.7
|
||||||
gorm.io/driver/sqlite v1.5.6
|
gorm.io/driver/sqlite v1.5.6
|
||||||
gorm.io/gorm v1.25.7
|
gorm.io/gorm v1.25.7
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||||
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||||
@@ -37,6 +38,8 @@ require (
|
|||||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
|
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
|
||||||
|
github.com/go-ldap/ldap/v3 v3.4.10 // indirect
|
||||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||||
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||||
github.com/go-openapi/spec v0.20.4 // indirect
|
github.com/go-openapi/spec v0.20.4 // indirect
|
||||||
@@ -70,10 +73,10 @@ require (
|
|||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.9 // indirect
|
github.com/ugorji/go/codec v1.2.9 // indirect
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
|
||||||
golang.org/x/crypto v0.23.0 // indirect
|
golang.org/x/crypto v0.31.0 // indirect
|
||||||
golang.org/x/image v0.13.0 // indirect
|
golang.org/x/image v0.13.0 // indirect
|
||||||
golang.org/x/net v0.25.0 // indirect
|
golang.org/x/net v0.33.0 // indirect
|
||||||
golang.org/x/sys v0.25.0 // indirect
|
golang.org/x/sys v0.28.0 // indirect
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||||
google.golang.org/protobuf v1.33.0 // indirect
|
google.golang.org/protobuf v1.33.0 // indirect
|
||||||
gopkg.in/ini.v1 v1.63.2 // indirect
|
gopkg.in/ini.v1 v1.63.2 // indirect
|
||||||
|
|||||||
8
resources/web2/js/dist/index.js
vendored
8
resources/web2/js/dist/index.js
vendored
@@ -9649,7 +9649,7 @@ const hn = async (u, e) => {
|
|||||||
o = !1
|
o = !1
|
||||||
}
|
}
|
||||||
return o && i.push(K4(e, "")), i
|
return o && i.push(K4(e, "")), i
|
||||||
}, Ut = 21116, Lt = "rs-ny.rustdesk.com", tt = 100, w4 = "trust-this-device";
|
}, Ut = 21116, defaultIdServerPort = 21116, Lt = "rs-ny.rustdesk.com", tt = 100, w4 = "trust-this-device";
|
||||||
|
|
||||||
class Wt {
|
class Wt {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -11087,7 +11087,7 @@ function R4(u = !1) {
|
|||||||
return I4(e || Lt, u)
|
return I4(e || Lt, u)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getrUriFromRs(uri, isRelay = false, roffset = 0) {
|
function getUriFromRs(uri, isRelay = false, roffset = 0) {
|
||||||
const p = isHttps() ? "wss://" : "ws://"
|
const p = isHttps() ? "wss://" : "ws://"
|
||||||
const [domain, uriport] = uri.split(":")
|
const [domain, uriport] = uri.split(":")
|
||||||
if (isHttps() && (!uriport)) {
|
if (isHttps() && (!uriport)) {
|
||||||
@@ -11096,7 +11096,7 @@ function getrUriFromRs(uri, isRelay = false, roffset = 0) {
|
|||||||
if (uriport) {
|
if (uriport) {
|
||||||
const port = parseInt(uriport);
|
const port = parseInt(uriport);
|
||||||
uri = domain + ":" + (port + (isRelay ? roffset || 3 : 2))
|
uri = domain + ":" + (port + (isRelay ? roffset || 3 : 2))
|
||||||
} else uri += ":" + (Ut + (isRelay ? 3 : 2));
|
} else uri += ":" + (defaultIdServerPort + (isRelay ? 3 : 2));
|
||||||
return p + uri
|
return p + uri
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11105,7 +11105,7 @@ function isHttps() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function I4(u, e = !1, i = 0) {
|
function I4(u, e = !1, i = 0) {
|
||||||
return getrUriFromRs(u, e, i)
|
return getUriFromRs(u, e, i)
|
||||||
}
|
}
|
||||||
|
|
||||||
function wn() {
|
function wn() {
|
||||||
|
|||||||
425
service/ldap.go
Normal file
425
service/ldap.go
Normal file
@@ -0,0 +1,425 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/go-ldap/ldap/v3"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"Gwen/config"
|
||||||
|
"Gwen/global"
|
||||||
|
"Gwen/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LdapService is responsible for LDAP authentication and user synchronization.
|
||||||
|
type LdapService struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// LdapUser represents the user attributes retrieved from LDAP.
|
||||||
|
type LdapUser struct {
|
||||||
|
Dn string
|
||||||
|
Username string
|
||||||
|
Email string
|
||||||
|
FirstName string
|
||||||
|
LastName string
|
||||||
|
MemberOf []string
|
||||||
|
EnableAttrValue string
|
||||||
|
Enabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the full name of an LDAP user.
|
||||||
|
func (lu *LdapUser) Name() string {
|
||||||
|
return fmt.Sprintf("%s %s", lu.FirstName, lu.LastName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToUser merges the LdapUser data into a provided *model.User.
|
||||||
|
// If 'u' is nil, it creates and returns a new *model.User.
|
||||||
|
func (lu *LdapUser) ToUser(u *model.User) *model.User {
|
||||||
|
if u == nil {
|
||||||
|
u = &model.User{}
|
||||||
|
}
|
||||||
|
u.Username = lu.Username
|
||||||
|
u.Email = lu.Email
|
||||||
|
u.Nickname = lu.Name()
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// connectAndBind creates an LDAP connection, optionally starts TLS, and then binds using the provided credentials.
|
||||||
|
func (ls *LdapService) connectAndBind(cfg *config.Ldap, username, password string) (*ldap.Conn, error) {
|
||||||
|
conn, err := ldap.DialURL(cfg.Url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to dial LDAP: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.TLS {
|
||||||
|
// WARNING: InsecureSkipVerify: true is not recommended for production
|
||||||
|
if err = conn.StartTLS(&tls.Config{InsecureSkipVerify: !cfg.TlsVerify}); err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, fmt.Errorf("failed to start TLS: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind as the "service" user
|
||||||
|
if err = conn.Bind(username, password); err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, fmt.Errorf("failed to bind with service account: %w", err)
|
||||||
|
}
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// connectAndBindAdmin creates an LDAP connection, optionally starts TLS, and then binds using the admin credentials.
|
||||||
|
func (ls *LdapService) connectAndBindAdmin(cfg *config.Ldap) (*ldap.Conn, error) {
|
||||||
|
return ls.connectAndBind(cfg, cfg.BindDn, cfg.BindPassword)
|
||||||
|
}
|
||||||
|
|
||||||
|
// verifyCredentials checks the provided username and password against LDAP.
|
||||||
|
func (ls *LdapService) verifyCredentials(cfg *config.Ldap, username, password string) error {
|
||||||
|
ldapConn, err := ls.connectAndBind(cfg, username, password)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer ldapConn.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate checks the provided username and password against LDAP.
|
||||||
|
// Returns the corresponding *model.User if successful, or an error if not.
|
||||||
|
func (ls *LdapService) Authenticate(username, password string) (*model.User, error) {
|
||||||
|
cfg := &global.Config.Ldap
|
||||||
|
// 1. Use a service bind to search for the user DN
|
||||||
|
sr, err := ls.usernameSearchResult(cfg, username)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("LDAP search request failed: %w", err)
|
||||||
|
}
|
||||||
|
if len(sr.Entries) != 1 {
|
||||||
|
return nil, errors.New("user does not exist or too many entries returned")
|
||||||
|
}
|
||||||
|
entry := sr.Entries[0]
|
||||||
|
userDN := entry.DN
|
||||||
|
|
||||||
|
err = ls.verifyCredentials(cfg, userDN, password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("LDAP authentication failed: %w", err)
|
||||||
|
}
|
||||||
|
ldapUser := ls.userResultToLdapUser(cfg, entry)
|
||||||
|
if !ldapUser.Enabled {
|
||||||
|
return nil, errors.New("UserDisabledAtLdap")
|
||||||
|
}
|
||||||
|
user, err := ls.mapToLocalUser(cfg, ldapUser)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to map LDAP user to local user: %w", err)
|
||||||
|
}
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// mapToLocalUser checks whether the user exists locally; if not, creates one.
|
||||||
|
// If the user exists and Ldap.Sync is enabled, it updates local info.
|
||||||
|
func (ls *LdapService) mapToLocalUser(cfg *config.Ldap, lu *LdapUser) (*model.User, error) {
|
||||||
|
userService := &UserService{}
|
||||||
|
localUser := userService.InfoByUsername(lu.Username)
|
||||||
|
isAdmin := ls.isUserAdmin(cfg, lu)
|
||||||
|
// If the user doesn't exist in local DB, create a new one
|
||||||
|
if localUser.Id == 0 {
|
||||||
|
newUser := lu.ToUser(nil)
|
||||||
|
// Typically, you don’t store LDAP user passwords locally.
|
||||||
|
// If needed, you can set a random password here.
|
||||||
|
newUser.IsAdmin = &isAdmin
|
||||||
|
if err := global.DB.Create(newUser).Error; err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create new user: %w", err)
|
||||||
|
}
|
||||||
|
return userService.InfoByUsername(lu.Username), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user already exists and sync is enabled, update local info
|
||||||
|
if cfg.User.Sync {
|
||||||
|
originalEmail := localUser.Email
|
||||||
|
originalNickname := localUser.Nickname
|
||||||
|
originalIsAdmin := localUser.IsAdmin
|
||||||
|
lu.ToUser(localUser) // merges LDAP data into the existing user
|
||||||
|
localUser.IsAdmin = &isAdmin
|
||||||
|
if err := userService.Update(localUser); err != nil {
|
||||||
|
// If the update fails, revert to original data
|
||||||
|
localUser.Email = originalEmail
|
||||||
|
localUser.Nickname = originalNickname
|
||||||
|
localUser.IsAdmin = originalIsAdmin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return localUser, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsUsernameExists checks if a username exists in LDAP (can be useful for local registration checks).
|
||||||
|
func (ls *LdapService) IsUsernameExists(username string) bool {
|
||||||
|
|
||||||
|
cfg := &global.Config.Ldap
|
||||||
|
if !cfg.Enable {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
sr, err := ls.usernameSearchResult(cfg, username)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return len(sr.Entries) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEmailExists checks if an email exists in LDAP (can be useful for local registration checks).
|
||||||
|
func (ls *LdapService) IsEmailExists(email string) bool {
|
||||||
|
cfg := &global.Config.Ldap
|
||||||
|
if !cfg.Enable {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
sr, err := ls.emailSearchResult(cfg, email)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return len(sr.Entries) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// usernameSearchResult returns the search result for the given username.
|
||||||
|
func (ls *LdapService) usernameSearchResult(cfg *config.Ldap, username string) (*ldap.SearchResult, error) {
|
||||||
|
// Build the combined filter for the username
|
||||||
|
filter := ls.filterField(ls.fieldUsername(cfg), username)
|
||||||
|
// Create the *ldap.SearchRequest
|
||||||
|
searchRequest := ls.buildUserSearchRequest(cfg, filter)
|
||||||
|
return ls.searchResult(cfg, searchRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
// emailSearchResult returns the search result for the given email.
|
||||||
|
func (ls *LdapService) emailSearchResult(cfg *config.Ldap, email string) (*ldap.SearchResult, error) {
|
||||||
|
filter := ls.filterField(ls.fieldEmail(cfg), email)
|
||||||
|
searchRequest := ls.buildUserSearchRequest(cfg, filter)
|
||||||
|
return ls.searchResult(cfg, searchRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *LdapService) searchResult(cfg *config.Ldap, searchRequest *ldap.SearchRequest) (*ldap.SearchResult, error) {
|
||||||
|
ldapConn, err := ls.connectAndBindAdmin(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer ldapConn.Close()
|
||||||
|
return ldapConn.Search(searchRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildUserSearchRequest constructs an LDAP SearchRequest for users given a filter.
|
||||||
|
func (ls *LdapService) buildUserSearchRequest(cfg *config.Ldap, filter string) *ldap.SearchRequest {
|
||||||
|
baseDn := ls.baseDnUser(cfg) // user-specific base DN, or fallback
|
||||||
|
filterConfig := cfg.User.Filter
|
||||||
|
if filterConfig == "" {
|
||||||
|
filterConfig = "(cn=*)"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine the default filter with our field filter, e.g. (&(cn=*)(uid=jdoe))
|
||||||
|
combinedFilter := fmt.Sprintf("(&%s%s)", filterConfig, filter)
|
||||||
|
|
||||||
|
attributes := ls.buildUserAttributes(cfg)
|
||||||
|
|
||||||
|
return ldap.NewSearchRequest(
|
||||||
|
baseDn,
|
||||||
|
ldap.ScopeWholeSubtree,
|
||||||
|
ldap.NeverDerefAliases,
|
||||||
|
0, // unlimited search results
|
||||||
|
0, // no server-side time limit
|
||||||
|
false, // typesOnly
|
||||||
|
combinedFilter,
|
||||||
|
attributes,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildUserAttributes returns the list of attributes we want from LDAP user searches.
|
||||||
|
func (ls *LdapService) buildUserAttributes(cfg *config.Ldap) []string {
|
||||||
|
return []string{
|
||||||
|
"dn",
|
||||||
|
ls.fieldUsername(cfg),
|
||||||
|
ls.fieldEmail(cfg),
|
||||||
|
ls.fieldFirstName(cfg),
|
||||||
|
ls.fieldLastName(cfg),
|
||||||
|
ls.fieldMemberOf(),
|
||||||
|
ls.fieldUserEnableAttr(cfg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// userResultToLdapUser maps an *ldap.Entry to our LdapUser struct.
|
||||||
|
func (ls *LdapService) userResultToLdapUser(cfg *config.Ldap, entry *ldap.Entry) *LdapUser {
|
||||||
|
lu := &LdapUser{
|
||||||
|
Dn: entry.DN,
|
||||||
|
Username: entry.GetAttributeValue(ls.fieldUsername(cfg)),
|
||||||
|
Email: entry.GetAttributeValue(ls.fieldEmail(cfg)),
|
||||||
|
FirstName: entry.GetAttributeValue(ls.fieldFirstName(cfg)),
|
||||||
|
LastName: entry.GetAttributeValue(ls.fieldLastName(cfg)),
|
||||||
|
MemberOf: entry.GetAttributeValues(ls.fieldMemberOf()),
|
||||||
|
EnableAttrValue: entry.GetAttributeValue(ls.fieldUserEnableAttr(cfg)),
|
||||||
|
}
|
||||||
|
// Check if the user is enabled based on the LDAP configuration
|
||||||
|
ls.isUserEnabled(cfg, lu)
|
||||||
|
return lu
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterField helps build simple attribute filters, e.g. (uid=username).
|
||||||
|
func (ls *LdapService) filterField(field, value string) string {
|
||||||
|
return fmt.Sprintf("(%s=%s)", field, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fieldUsername returns the configured username attribute or "uid" if not set.
|
||||||
|
func (ls *LdapService) fieldUsername(cfg *config.Ldap) string {
|
||||||
|
if cfg.User.Username == "" {
|
||||||
|
return "uid"
|
||||||
|
}
|
||||||
|
return cfg.User.Username
|
||||||
|
}
|
||||||
|
|
||||||
|
// fieldEmail returns the configured email attribute or "mail" if not set.
|
||||||
|
func (ls *LdapService) fieldEmail(cfg *config.Ldap) string {
|
||||||
|
if cfg.User.Email == "" {
|
||||||
|
return "mail"
|
||||||
|
}
|
||||||
|
return cfg.User.Email
|
||||||
|
}
|
||||||
|
|
||||||
|
// fieldFirstName returns the configured first name attribute or "givenName" if not set.
|
||||||
|
func (ls *LdapService) fieldFirstName(cfg *config.Ldap) string {
|
||||||
|
if cfg.User.FirstName == "" {
|
||||||
|
return "givenName"
|
||||||
|
}
|
||||||
|
return cfg.User.FirstName
|
||||||
|
}
|
||||||
|
|
||||||
|
// fieldLastName returns the configured last name attribute or "sn" if not set.
|
||||||
|
func (ls *LdapService) fieldLastName(cfg *config.Ldap) string {
|
||||||
|
if cfg.User.LastName == "" {
|
||||||
|
return "sn"
|
||||||
|
}
|
||||||
|
return cfg.User.LastName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *LdapService) fieldMemberOf() string {
|
||||||
|
return "memberOf"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *LdapService) fieldUserEnableAttr(cfg *config.Ldap) string {
|
||||||
|
if cfg.User.EnableAttr == "" {
|
||||||
|
return "userAccountControl"
|
||||||
|
}
|
||||||
|
return cfg.User.EnableAttr
|
||||||
|
}
|
||||||
|
|
||||||
|
// baseDnUser returns the user-specific base DN or the global base DN if none is set.
|
||||||
|
func (ls *LdapService) baseDnUser(cfg *config.Ldap) string {
|
||||||
|
if cfg.User.BaseDn == "" {
|
||||||
|
return cfg.BaseDn
|
||||||
|
}
|
||||||
|
return cfg.User.BaseDn
|
||||||
|
}
|
||||||
|
|
||||||
|
// isUserAdmin checks if the user is a member of the admin group.
|
||||||
|
func (ls *LdapService) isUserAdmin(cfg *config.Ldap, ldapUser *LdapUser) bool {
|
||||||
|
// Check if the admin group is configured
|
||||||
|
adminGroup := cfg.User.AdminGroup
|
||||||
|
if adminGroup == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check "memberOf" directly
|
||||||
|
if len(ldapUser.MemberOf) > 0 {
|
||||||
|
for _, group := range ldapUser.MemberOf {
|
||||||
|
if group == adminGroup {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// For "member" attribute, perform a reverse search on the group
|
||||||
|
member := "member"
|
||||||
|
userDN := ldap.EscapeFilter(ldapUser.Dn)
|
||||||
|
adminGroupDn := ldap.EscapeFilter(adminGroup)
|
||||||
|
groupFilter := fmt.Sprintf("(%s=%s)", member, userDN)
|
||||||
|
|
||||||
|
// Create the LDAP search request
|
||||||
|
groupSearchRequest := ldap.NewSearchRequest(
|
||||||
|
adminGroupDn,
|
||||||
|
ldap.ScopeWholeSubtree,
|
||||||
|
ldap.NeverDerefAliases,
|
||||||
|
0, // Unlimited search results
|
||||||
|
0, // No time limit
|
||||||
|
false, // Return both attributes and DN
|
||||||
|
groupFilter,
|
||||||
|
[]string{"dn"},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Perform the group search
|
||||||
|
groupResult, err := ls.searchResult(cfg, groupSearchRequest)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// If any results are returned, the user is part of the admin group
|
||||||
|
if len(groupResult.Entries) > 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// isUserEnabled checks if the user is enabled based on the LDAP configuration.
|
||||||
|
// If no enable attribute or value is set, all users are considered enabled by default.
|
||||||
|
func (ls *LdapService) isUserEnabled(cfg *config.Ldap, ldapUser *LdapUser) bool {
|
||||||
|
// Retrieve the enable attribute and expected value from the configuration
|
||||||
|
enableAttr := cfg.User.EnableAttr
|
||||||
|
enableAttrValue := cfg.User.EnableAttrValue
|
||||||
|
|
||||||
|
// If no enable attribute or value is configured, consider all users as enabled
|
||||||
|
if enableAttr == "" || enableAttrValue == "" {
|
||||||
|
ldapUser.Enabled = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize the enable attribute for comparison
|
||||||
|
enableAttr = strings.ToLower(enableAttr)
|
||||||
|
|
||||||
|
// Handle Active Directory's userAccountControl attribute
|
||||||
|
if enableAttr == "useraccountcontrol" {
|
||||||
|
// Parse the userAccountControl value
|
||||||
|
userAccountControl, err := strconv.Atoi(ldapUser.EnableAttrValue)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("[ERROR] Invalid userAccountControl value: %v\n", err)
|
||||||
|
ldapUser.Enabled = false
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Account is disabled if the ACCOUNTDISABLE flag (0x2) is set
|
||||||
|
const ACCOUNTDISABLE = 0x2
|
||||||
|
ldapUser.Enabled = (userAccountControl&ACCOUNTDISABLE == 0)
|
||||||
|
return ldapUser.Enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other attributes, perform a direct comparison with the expected value
|
||||||
|
ldapUser.Enabled = (ldapUser.EnableAttrValue == enableAttrValue)
|
||||||
|
return ldapUser.Enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
// getAttrOfDn retrieves the value of an attribute for a given DN.
|
||||||
|
func (ls *LdapService) getAttrOfDn(cfg *config.Ldap, dn, attr string) string {
|
||||||
|
searchRequest := ldap.NewSearchRequest(
|
||||||
|
ldap.EscapeFilter(dn),
|
||||||
|
ldap.ScopeBaseObject,
|
||||||
|
ldap.NeverDerefAliases,
|
||||||
|
0, // unlimited search results
|
||||||
|
0, // no server-side time limit
|
||||||
|
false, // typesOnly
|
||||||
|
"(objectClass=*)",
|
||||||
|
[]string{attr},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
sr, err := ls.searchResult(cfg, searchRequest)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if len(sr.Entries) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return sr.Entries[0].GetAttributeValue(attr)
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ type Service struct {
|
|||||||
*AuditService
|
*AuditService
|
||||||
*ShareRecordService
|
*ShareRecordService
|
||||||
*ServerCmdService
|
*ServerCmdService
|
||||||
|
*LdapService
|
||||||
}
|
}
|
||||||
|
|
||||||
func New() *Service {
|
func New() *Service {
|
||||||
|
|||||||
@@ -46,6 +46,14 @@ func (us *UserService) InfoByOpenid(openid string) *model.User {
|
|||||||
|
|
||||||
// InfoByUsernamePassword 根据用户名密码取用户信息
|
// InfoByUsernamePassword 根据用户名密码取用户信息
|
||||||
func (us *UserService) InfoByUsernamePassword(username, password string) *model.User {
|
func (us *UserService) InfoByUsernamePassword(username, password string) *model.User {
|
||||||
|
if global.Config.Ldap.Enable {
|
||||||
|
u, err := AllService.LdapService.Authenticate(username, password)
|
||||||
|
if err == nil {
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
global.Logger.Error("LDAP authentication failed, %v", err)
|
||||||
|
global.Logger.Warn("Fallback to local database")
|
||||||
|
}
|
||||||
u := &model.User{}
|
u := &model.User{}
|
||||||
global.DB.Where("username = ? and password = ?", username, us.EncryptPassword(password)).First(u)
|
global.DB.Where("username = ? and password = ?", username, us.EncryptPassword(password)).First(u)
|
||||||
return u
|
return u
|
||||||
@@ -82,7 +90,7 @@ func (us *UserService) Login(u *model.User, llog *model.LoginLog) *model.UserTok
|
|||||||
Token: token,
|
Token: token,
|
||||||
DeviceUuid: llog.Uuid,
|
DeviceUuid: llog.Uuid,
|
||||||
DeviceId: llog.DeviceId,
|
DeviceId: llog.DeviceId,
|
||||||
ExpiredAt: time.Now().Add(time.Second * time.Duration(global.Config.App.TokenExpire)).Unix(),
|
ExpiredAt: us.UserTokenExpireTimestamp(),
|
||||||
}
|
}
|
||||||
global.DB.Create(ut)
|
global.DB.Create(ut)
|
||||||
llog.UserTokenId = ut.UserId
|
llog.UserTokenId = ut.UserId
|
||||||
@@ -156,6 +164,9 @@ func (us *UserService) CheckUserEnable(u *model.User) bool {
|
|||||||
// Create 创建
|
// Create 创建
|
||||||
func (us *UserService) Create(u *model.User) error {
|
func (us *UserService) Create(u *model.User) error {
|
||||||
// The initial username should be formatted, and the username should be unique
|
// The initial username should be formatted, and the username should be unique
|
||||||
|
if us.IsUsernameExists(u.Username) {
|
||||||
|
return errors.New("UsernameExists")
|
||||||
|
}
|
||||||
u.Username = us.formatUsername(u.Username)
|
u.Username = us.formatUsername(u.Username)
|
||||||
u.Password = us.EncryptPassword(u.Password)
|
u.Password = us.EncryptPassword(u.Password)
|
||||||
res := global.DB.Create(u).Error
|
res := global.DB.Create(u).Error
|
||||||
@@ -343,13 +354,10 @@ func (us *UserService) RegisterByOauth(oauthUser *model.OauthUser, op string) (e
|
|||||||
|
|
||||||
// GenerateUsernameByOauth 生成用户名
|
// GenerateUsernameByOauth 生成用户名
|
||||||
func (us *UserService) GenerateUsernameByOauth(name string) string {
|
func (us *UserService) GenerateUsernameByOauth(name string) string {
|
||||||
u := &model.User{}
|
for us.IsUsernameExists(name) {
|
||||||
global.DB.Where("username = ?", name).First(u)
|
name += strconv.Itoa(rand.Intn(10)) // Append a random digit (0-9)
|
||||||
if u.Id == 0 {
|
|
||||||
return name
|
|
||||||
}
|
}
|
||||||
name = name + strconv.FormatInt(rand.Int63n(10), 10)
|
return name
|
||||||
return us.GenerateUsernameByOauth(name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserThirdsByUserId
|
// UserThirdsByUserId
|
||||||
@@ -394,15 +402,18 @@ func (us *UserService) IsPasswordEmptyByUser(u *model.User) bool {
|
|||||||
return us.IsPasswordEmptyById(u.Id)
|
return us.IsPasswordEmptyById(u.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register 注册
|
// Register 注册, 如果用户名已存在则返回nil
|
||||||
func (us *UserService) Register(username string, email string, password string) *model.User {
|
func (us *UserService) Register(username string, email string, password string) *model.User {
|
||||||
u := &model.User{
|
u := &model.User{
|
||||||
Username: username,
|
Username: username,
|
||||||
Email: email,
|
Email: email,
|
||||||
Password: us.EncryptPassword(password),
|
Password: password,
|
||||||
GroupId: 1,
|
GroupId: 1,
|
||||||
}
|
}
|
||||||
global.DB.Create(u)
|
err := us.Create(u)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,8 +462,17 @@ func (us *UserService) getAdminUserCount() int64 {
|
|||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserTokenExpireTimestamp 生成用户token过期时间
|
||||||
|
func (us *UserService) UserTokenExpireTimestamp() int64 {
|
||||||
|
exp := global.Config.App.TokenExpire
|
||||||
|
if exp == 0 {
|
||||||
|
exp = 3600 * 24 * 7
|
||||||
|
}
|
||||||
|
return time.Now().Add(time.Second * time.Duration(exp)).Unix()
|
||||||
|
}
|
||||||
|
|
||||||
func (us *UserService) RefreshAccessToken(ut *model.UserToken) {
|
func (us *UserService) RefreshAccessToken(ut *model.UserToken) {
|
||||||
ut.ExpiredAt = time.Now().Add(time.Second * time.Duration(global.Config.App.TokenExpire)).Unix()
|
ut.ExpiredAt = us.UserTokenExpireTimestamp()
|
||||||
global.DB.Model(ut).Update("expired_at", ut.ExpiredAt)
|
global.DB.Model(ut).Update("expired_at", ut.ExpiredAt)
|
||||||
}
|
}
|
||||||
func (us *UserService) AutoRefreshAccessToken(ut *model.UserToken) {
|
func (us *UserService) AutoRefreshAccessToken(ut *model.UserToken) {
|
||||||
@@ -468,3 +488,11 @@ func (us *UserService) BatchDeleteUserToken(ids []uint) error {
|
|||||||
func (us *UserService) VerifyJWT(token string) (uint, error) {
|
func (us *UserService) VerifyJWT(token string) (uint, error) {
|
||||||
return global.Jwt.ParseToken(token)
|
return global.Jwt.ParseToken(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsUsernameExists 判断用户名是否存在, it will check the internal database and LDAP(if enabled)
|
||||||
|
func (us *UserService) IsUsernameExists(username string) bool {
|
||||||
|
u := &model.User{}
|
||||||
|
global.DB.Where("username = ?", username).First(u)
|
||||||
|
existsInLdap := AllService.LdapService.IsUsernameExists(username)
|
||||||
|
return u.Id != 0 || existsInLdap
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user