feat: enhance LDAP integration with new configuration options and browsing capabilities
This commit is contained in:
@@ -31,14 +31,20 @@ type Query {
|
||||
|
||||
# List images next to a document.
|
||||
images(slug: String!): [ImageFile!]!
|
||||
|
||||
# Browse LDAP tree (admin only)
|
||||
ldapBrowse(url: String, adminUser: String, adminPass: String): LDAPTree!
|
||||
|
||||
# Fetch permissions for a path
|
||||
acl(path: String!): [ACLEntry!]!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
# First-run setup.
|
||||
setup(input: SetupInput!): Boolean!
|
||||
|
||||
# Test an LDAP configuration before saving.
|
||||
testLdapConnection(input: LDAPInput!): LDAPTestResult!
|
||||
# Test an LDAP configuration.
|
||||
testLdapConnection(url: String!, adminUser: String, adminPass: String): LDAPTestResult!
|
||||
|
||||
# Authenticate and receive a bearer token.
|
||||
login(username: String!, password: String!): String!
|
||||
@@ -55,6 +61,13 @@ type Mutation {
|
||||
# Modify LDAP Configuration (admin only).
|
||||
updateLdapConfig(input: LDAPInput): Boolean!
|
||||
|
||||
# Sync users/groups from LDAP
|
||||
importLdapSubject(type: String!, name: String!): Boolean!
|
||||
|
||||
# ACL Mutations
|
||||
setAcl(input: ACLInput!): Boolean!
|
||||
removeAcl(id: Int!): Boolean!
|
||||
|
||||
# Modify Storage Path (admin only).
|
||||
updateStoragePath(path: String!): Boolean!
|
||||
|
||||
@@ -87,10 +100,27 @@ type AppConfig {
|
||||
}
|
||||
|
||||
type LDAPConfig {
|
||||
host: String!
|
||||
port: Int!
|
||||
baseDN: String!
|
||||
bindDN: String!
|
||||
url: String!
|
||||
adminUser: String!
|
||||
}
|
||||
|
||||
type LDAPTree {
|
||||
users: [String!]!
|
||||
groups: [String!]!
|
||||
}
|
||||
|
||||
type ACLEntry {
|
||||
id: Int!
|
||||
path: String!
|
||||
subjectType: String!
|
||||
subjectId: Int!
|
||||
canSearch: Boolean!
|
||||
canView: Boolean!
|
||||
canRead: Boolean!
|
||||
canEdit: Boolean!
|
||||
canCreate: Boolean!
|
||||
canDelete: Boolean!
|
||||
canMove: Boolean!
|
||||
}
|
||||
|
||||
type ServerDirectory {
|
||||
@@ -148,11 +178,22 @@ input SetupInput {
|
||||
}
|
||||
|
||||
input LDAPInput {
|
||||
host: String!
|
||||
port: Int!
|
||||
baseDN: String!
|
||||
bindDN: String!
|
||||
bindPassword: String!
|
||||
url: String!
|
||||
adminUser: String!
|
||||
adminPass: String!
|
||||
}
|
||||
|
||||
input ACLInput {
|
||||
path: String!
|
||||
subjectType: String!
|
||||
subjectId: Int!
|
||||
canSearch: Boolean!
|
||||
canView: Boolean!
|
||||
canRead: Boolean!
|
||||
canEdit: Boolean!
|
||||
canCreate: Boolean!
|
||||
canDelete: Boolean!
|
||||
canMove: Boolean!
|
||||
}
|
||||
|
||||
input SaveDocumentInput {
|
||||
|
||||
@@ -232,6 +232,21 @@ func (s *Server) dispatchAuthenticated(
|
||||
case strings.Contains(q, "moveDocument"):
|
||||
s.handleMoveDocument(w, req, sess)
|
||||
|
||||
case strings.Contains(q, "ldapBrowse"):
|
||||
s.handleLdapBrowse(w, req, sess)
|
||||
|
||||
case strings.Contains(q, "importLdapSubject"):
|
||||
s.handleImportLdapSubject(w, req, sess)
|
||||
|
||||
case strings.Contains(q, "acl(") || strings.Contains(q, "acl "):
|
||||
s.handleAcl(w, req, sess)
|
||||
|
||||
case strings.Contains(q, "setAcl"):
|
||||
s.handleSetAcl(w, req, sess)
|
||||
|
||||
case strings.Contains(q, "removeAcl"):
|
||||
s.handleRemoveAcl(w, req, sess)
|
||||
|
||||
case strings.Contains(q, "folders"):
|
||||
s.handleFolders(w, sess)
|
||||
|
||||
@@ -813,16 +828,10 @@ func (s *Server) handleUpdateLdapConfig(w http.ResponseWriter, req gqlRequest, s
|
||||
|
||||
newCfg := *cfg // copy
|
||||
if hasInput && input != nil {
|
||||
port := 389
|
||||
if p, ok := input["port"].(float64); ok {
|
||||
port = int(p)
|
||||
}
|
||||
newCfg.LDAP = config.LDAPConfig{
|
||||
Host: strVal(input, "host"),
|
||||
Port: port,
|
||||
BaseDN: strVal(input, "baseDN"),
|
||||
BindDN: strVal(input, "bindDN"),
|
||||
BindPassword: strVal(input, "bindPassword"),
|
||||
Url: strVal(input, "url"),
|
||||
AdminUser: strVal(input, "adminUser"),
|
||||
AdminPass: strVal(input, "adminPass"),
|
||||
}
|
||||
} else {
|
||||
// Disable LDAP
|
||||
@@ -863,12 +872,10 @@ func (s *Server) handleConfig(w http.ResponseWriter, sess *auth.Session) {
|
||||
}
|
||||
|
||||
var ldap map[string]interface{}
|
||||
if cfg.LDAP.Host != "" {
|
||||
if cfg.LDAP.Url != "" {
|
||||
ldap = map[string]interface{}{
|
||||
"host": cfg.LDAP.Host,
|
||||
"port": cfg.LDAP.Port,
|
||||
"baseDN": cfg.LDAP.BaseDN,
|
||||
"bindDN": cfg.LDAP.BindDN,
|
||||
"url": cfg.LDAP.Url,
|
||||
"adminUser": cfg.LDAP.AdminUser,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1005,16 +1012,10 @@ func (s *Server) handleSetup(w http.ResponseWriter, req gqlRequest) {
|
||||
}
|
||||
|
||||
if ldapRaw, ok := input["ldap"].(map[string]interface{}); ok && ldapRaw != nil {
|
||||
port := 389
|
||||
if p, ok := ldapRaw["port"].(float64); ok {
|
||||
port = int(p)
|
||||
}
|
||||
cfg.LDAP = config.LDAPConfig{
|
||||
Host: strVal(ldapRaw, "host"),
|
||||
Port: port,
|
||||
BaseDN: strVal(ldapRaw, "baseDN"),
|
||||
BindDN: strVal(ldapRaw, "bindDN"),
|
||||
BindPassword: strVal(ldapRaw, "bindPassword"),
|
||||
Url: strVal(ldapRaw, "url"),
|
||||
AdminUser: strVal(ldapRaw, "adminUser"),
|
||||
AdminPass: strVal(ldapRaw, "adminPass"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1081,34 +1082,24 @@ func (s *Server) handleSetup(w http.ResponseWriter, req gqlRequest) {
|
||||
// ── LDAP test handler ─────────────────────────────────────────────────────────
|
||||
|
||||
func (s *Server) handleTestLdapConnection(w http.ResponseWriter, req gqlRequest) {
|
||||
input, _ := req.Variables["i"].(map[string]interface{})
|
||||
if input == nil {
|
||||
writeGQLError(w, "missing LDAP input")
|
||||
return
|
||||
}
|
||||
url_ := strVal(req.Variables, "url")
|
||||
adminUser := strVal(req.Variables, "adminUser")
|
||||
adminPass := strVal(req.Variables, "adminPass")
|
||||
|
||||
host := strVal(input, "host")
|
||||
port := 389
|
||||
if p, ok := input["port"].(float64); ok {
|
||||
port = int(p)
|
||||
}
|
||||
bindDN := strVal(input, "bindDN")
|
||||
bindPassword := strVal(input, "bindPassword")
|
||||
|
||||
if host == "" {
|
||||
if url_ == "" {
|
||||
writeJSONObj(w, map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"testLdapConnection": map[string]interface{}{
|
||||
"success": false, "message": "host is required",
|
||||
"success": false, "message": "url is required",
|
||||
},
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
err := auth.TestLDAP(host, port, bindDN, bindPassword)
|
||||
err := auth.TestLDAP(url_, adminUser, adminPass)
|
||||
if err != nil {
|
||||
log.Printf("[ldap] test failed (%s:%d): %v", host, port, err)
|
||||
log.Printf("[ldap] test failed (%s): %v", url_, err)
|
||||
writeJSONObj(w, map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"testLdapConnection": map[string]interface{}{
|
||||
@@ -1119,7 +1110,7 @@ func (s *Server) handleTestLdapConnection(w http.ResponseWriter, req gqlRequest)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[ldap] test succeeded (%s:%d)", host, port)
|
||||
log.Printf("[ldap] test succeeded (%s)", url_)
|
||||
writeJSONObj(w, map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"testLdapConnection": map[string]interface{}{
|
||||
@@ -1246,10 +1237,203 @@ func firstWord(s string) string {
|
||||
}
|
||||
|
||||
func strVal(m map[string]interface{}, key string) string {
|
||||
if m == nil {
|
||||
return ""
|
||||
}
|
||||
v, _ := m[key].(string)
|
||||
return v
|
||||
}
|
||||
|
||||
// ── LDAP & ACL Additions ──────────────────────────────────────────────────────
|
||||
|
||||
func (s *Server) handleLdapBrowse(w http.ResponseWriter, req gqlRequest, sess *auth.Session) {
|
||||
if sess == nil || sess.Role != "admin" {
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
|
||||
url_ := strVal(req.Variables, "url")
|
||||
adminUser := strVal(req.Variables, "adminUser")
|
||||
adminPass := strVal(req.Variables, "adminPass")
|
||||
|
||||
if url_ == "" {
|
||||
s.mu.RLock()
|
||||
if s.cfg != nil && s.cfg.LDAP.Url != "" {
|
||||
url_ = s.cfg.LDAP.Url
|
||||
adminUser = s.cfg.LDAP.AdminUser
|
||||
adminPass = s.cfg.LDAP.AdminPass
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
}
|
||||
|
||||
users, groups, err := auth.BrowseLDAP(url_, adminUser, adminPass)
|
||||
if err != nil {
|
||||
writeGQLError(w, fmt.Sprintf("LDAP traverse error: %v", err))
|
||||
return
|
||||
}
|
||||
if users == nil {
|
||||
users = []string{}
|
||||
}
|
||||
if groups == nil {
|
||||
groups = []string{}
|
||||
}
|
||||
|
||||
writeJSONObj(w, map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"ldapBrowse": map[string]interface{}{
|
||||
"users": users,
|
||||
"groups": groups,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleImportLdapSubject(w http.ResponseWriter, req gqlRequest, sess *auth.Session) {
|
||||
if sess == nil || sess.Role != "admin" {
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
|
||||
typ := strVal(req.Variables, "type")
|
||||
name := strVal(req.Variables, "name")
|
||||
|
||||
s.mu.RLock()
|
||||
database := s.database
|
||||
s.mu.RUnlock()
|
||||
|
||||
var err error
|
||||
if typ == "user" {
|
||||
err = database.CreateOrUpdateLDAPUser(name)
|
||||
} else if typ == "group" {
|
||||
err = database.CreateOrUpdateGroup(name, true)
|
||||
} else {
|
||||
writeGQLError(w, "Invalid subject type")
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
writeGQLError(w, fmt.Sprintf("DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, `{"data":{"importLdapSubject":true}}`)
|
||||
}
|
||||
|
||||
func (s *Server) handleSetAcl(w http.ResponseWriter, req gqlRequest, sess *auth.Session) {
|
||||
if sess == nil || sess.Role != "admin" {
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
|
||||
input, _ := req.Variables["input"].(map[string]interface{})
|
||||
if input == nil {
|
||||
writeGQLError(w, "Missing input")
|
||||
return
|
||||
}
|
||||
|
||||
s.mu.RLock()
|
||||
database := s.database
|
||||
s.mu.RUnlock()
|
||||
|
||||
var entry db.ACLEntry
|
||||
entry.Path = strVal(input, "path")
|
||||
entry.SubjectType = strVal(input, "subjectType")
|
||||
if idF, ok := input["subjectId"].(float64); ok {
|
||||
entry.SubjectID = int64(idF)
|
||||
} else {
|
||||
writeGQLError(w, "Missing subjectId")
|
||||
return
|
||||
}
|
||||
|
||||
entry.CanSearch, _ = input["canSearch"].(bool)
|
||||
entry.CanView, _ = input["canView"].(bool)
|
||||
entry.CanRead, _ = input["canRead"].(bool)
|
||||
entry.CanEdit, _ = input["canEdit"].(bool)
|
||||
entry.CanCreate, _ = input["canCreate"].(bool)
|
||||
entry.CanDelete, _ = input["canDelete"].(bool)
|
||||
entry.CanMove, _ = input["canMove"].(bool)
|
||||
|
||||
if err := database.SetACL(entry); err != nil {
|
||||
writeGQLError(w, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, `{"data":{"setAcl":true}}`)
|
||||
}
|
||||
|
||||
func (s *Server) handleRemoveAcl(w http.ResponseWriter, req gqlRequest, sess *auth.Session) {
|
||||
if sess == nil || sess.Role != "admin" {
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
|
||||
idF, ok := req.Variables["id"].(float64)
|
||||
if !ok {
|
||||
writeGQLError(w, "Missing id")
|
||||
return
|
||||
}
|
||||
|
||||
s.mu.RLock()
|
||||
database := s.database
|
||||
s.mu.RUnlock()
|
||||
|
||||
if err := database.RemoveACL(int64(idF)); err != nil {
|
||||
writeGQLError(w, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, `{"data":{"removeAcl":true}}`)
|
||||
}
|
||||
|
||||
func (s *Server) handleAcl(w http.ResponseWriter, req gqlRequest, sess *auth.Session) {
|
||||
if sess == nil {
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
|
||||
path := strVal(req.Variables, "path")
|
||||
if path == "" {
|
||||
writeGQLError(w, "Missing path")
|
||||
return
|
||||
}
|
||||
|
||||
// Example: Allow everyone to see ACLs for now, or restrict to admin
|
||||
s.mu.RLock()
|
||||
database := s.database
|
||||
s.mu.RUnlock()
|
||||
|
||||
acls, err := database.GetACLsForPath(path)
|
||||
if err != nil {
|
||||
writeGQLError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var aclData []map[string]interface{}
|
||||
for _, a := range acls {
|
||||
aclData = append(aclData, map[string]interface{}{
|
||||
"id": a.ID,
|
||||
"path": a.Path,
|
||||
"subjectType": a.SubjectType,
|
||||
"subjectId": a.SubjectID,
|
||||
"canSearch": a.CanSearch,
|
||||
"canView": a.CanView,
|
||||
"canRead": a.CanRead,
|
||||
"canEdit": a.CanEdit,
|
||||
"canCreate": a.CanCreate,
|
||||
"canDelete": a.CanDelete,
|
||||
"canMove": a.CanMove,
|
||||
})
|
||||
}
|
||||
|
||||
if aclData == nil {
|
||||
aclData = []map[string]interface{}{}
|
||||
}
|
||||
|
||||
writeJSONObj(w, map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"acl": aclData,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func dirOf(path string) string {
|
||||
idx := strings.LastIndexAny(path, "/\\")
|
||||
if idx < 0 {
|
||||
@@ -1564,64 +1748,62 @@ func (s *Server) handleFolders(w http.ResponseWriter, sess *auth.Session) {
|
||||
}
|
||||
|
||||
func (s *Server) handleImages(w http.ResponseWriter, req gqlRequest, sess *auth.Session) {
|
||||
if sess == nil {
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
slug, _ := req.Variables["slug"].(string)
|
||||
if slug == "" {
|
||||
writeGQLError(w, "slug is required")
|
||||
return
|
||||
}
|
||||
if sess == nil {
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
slug, _ := req.Variables["slug"].(string)
|
||||
if slug == "" {
|
||||
writeGQLError(w, "slug is required")
|
||||
return
|
||||
}
|
||||
|
||||
s.mu.RLock()
|
||||
store := s.store
|
||||
s.mu.RUnlock()
|
||||
s.mu.RLock()
|
||||
store := s.store
|
||||
s.mu.RUnlock()
|
||||
|
||||
if store == nil {
|
||||
writeGQLError(w, "storage not initialized")
|
||||
return
|
||||
}
|
||||
if store == nil {
|
||||
writeGQLError(w, "storage not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
images, err := store.ListImages(slug)
|
||||
if err != nil {
|
||||
writeGQLError(w, fmt.Sprintf("failed to list images: %v", err))
|
||||
return
|
||||
}
|
||||
images, err := store.ListImages(slug)
|
||||
if err != nil {
|
||||
writeGQLError(w, fmt.Sprintf("failed to list images: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
writeJSONObj(w, map[string]interface{}{"data": map[string]interface{}{"images": images}})
|
||||
writeJSONObj(w, map[string]interface{}{"data": map[string]interface{}{"images": images}})
|
||||
}
|
||||
|
||||
func (s *Server) handleDeleteImage(w http.ResponseWriter, req gqlRequest, sess *auth.Session) {
|
||||
if sess == nil {
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
if sess == nil {
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
|
||||
slug, _ := req.Variables["slug"].(string)
|
||||
filename, _ := req.Variables["filename"].(string)
|
||||
|
||||
if slug == "" || filename == "" {
|
||||
writeGQLError(w, "slug and filename are required")
|
||||
return
|
||||
}
|
||||
|
||||
s.mu.RLock()
|
||||
store := s.store
|
||||
s.mu.RUnlock()
|
||||
|
||||
if store == nil {
|
||||
writeGQLError(w, "storage not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
if err := store.DeleteImage(slug, filename); err != nil {
|
||||
writeGQLError(w, fmt.Sprintf("failed to delete image: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[storage] deleted image %q from %q by %s", filename, slug, sess.Username)
|
||||
writeJSON(w, `{"data":{"deleteImage":true}}`)
|
||||
}
|
||||
|
||||
slug, _ := req.Variables["slug"].(string)
|
||||
filename, _ := req.Variables["filename"].(string)
|
||||
|
||||
if slug == "" || filename == "" {
|
||||
writeGQLError(w, "slug and filename are required")
|
||||
return
|
||||
}
|
||||
|
||||
s.mu.RLock()
|
||||
store := s.store
|
||||
s.mu.RUnlock()
|
||||
|
||||
if store == nil {
|
||||
writeGQLError(w, "storage not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
if err := store.DeleteImage(slug, filename); err != nil {
|
||||
writeGQLError(w, fmt.Sprintf("failed to delete image: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[storage] deleted image %q from %q by %s", filename, slug, sess.Username)
|
||||
writeJSON(w, `{"data":{"deleteImage":true}}`)
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user