Fuseki & LDAP
My ToDos were already pretty chaotic, going to the UK for a week didn't help. The #FOAFRetrospective project is my top priority, but I know there's a piece of critical infrastructure that I need for this (and pretty much all my other code projects) : a SPARQL Server.
My go-to for this is Fuseki. I'm aware of, and have played with, many alternatives, some with notable benefits. But Fuseki has the core features I need, and I'm familiar with it. It's part of Apache Jena, which despite (because of?!) being unfashionably Java-based, is a very solid, mature, and well-supported platform. YMMV, but as far as I'm concerned, to all intents and purposes it's the reference implementation of the semweb specs.
A Docker image is available, but at this point in time a regular install suits me better, and basic installation is dead straightforward and Just Works out of the box.
Andy Seaborne was/is the lead on it, good bloke, not had contact for donkeys. One of many folks I must reconnect with. I see from his Twitter he's still active in the field. Ah, @andyseaborne@mastodon.green ...I guess Mastodon is probably how I should reconnect with people, once I've figured out how to use it...
I've already got Fuseki running on my server, but it's wide open. That hasn't caused any problems in the past, but after getting my fingers burnt for being lax with security on my last server setup, I want to take more care this time.
Nearby in my stuff there are various places where supporting user accounts on the server are kinda must-have. Last time around I was looking at using the semwebby Community Solid Server for ACL management, but this time I decided to try a more old-fashioned approach - LDAP. Coincidentally I saw a ref on Simon Willison’s Weblog the other day to a strategy (with essay) Choose Boring Technology. Yeah, this.
Years back I set up LDAP in a day job (network admin at a college), but it's so long ago I've forgotten the lot. But it turned out to be straightforward to get it running on my Ubuntu server. After a bit of trial & error I was able to get Apache Directory Studio on my desktop talking to the service.
To make sure I was on the right track, in the past few days, with the help of ChatGPT, I've hacked simple signup/login forms in with nodejs/express that talk to the LDAP server. While I should tidy the code at some point, it works well enough as a sanity check (code on GitHub).
So onto Fuseki.
Out of the box it uses Apache Shiro for auth, with configuration in a text file. But that's been around long enough to consider it Boring Technology, so hopefully it'll be straightforward to hook it into LDAP.
Docs : Security in Fuseki2
Requirements
I anticipate needing other tiers, but right now the users/access control requirements seem pretty simple, something like:
- LDAP admin
- Fuseki global admin group (can create/delete datasets)
- Per-dataset write access groups
Default I'll leave as open read access for now.
Current Configurations
/etc/systemd/system/fuseki.service
includes:
[Service]
Environment=FUSEKI_HOME=/home/services/fuseki
Environment=FUSEKI_BASE=/home/services/fuseki
Environment=JVM_ARGS=-Xmx1G
ExecStart=/home/services/fuseki/fuseki-server --port=3331
User=fuseki
Restart=on-abort
SuccessExitStatus=143
fuseki-server there appears to be the script in the distro unchanged.
/home/services/fuseki/shiro.ini
[main]
# Development
ssl.enabled = false
plainMatcher=org.apache.shiro.authc.credential.SimpleCredentialsMatcher
#iniRealm=org.apache.shiro.realm.text.IniRealm
iniRealm.credentialsMatcher = $plainMatcher
localhostFilter=org.apache.jena.fuseki.authz.LocalhostFilter
[users]
# Implicitly adds "iniRealm = org.apache.shiro.realm.text.IniRealm"
admin=PASSWORD
[roles]
[urls]
## Control functions open to anyone
/$/status = anon
/$/server = anon
/$/ping = anon
/$/metrics = anon
## and the rest are restricted to localhost.
/$/** = localhostFilter
## If you want simple, basic authentication user/password
## on the operations,
## 1 - set a better password in [users] above.
## 2 - comment out the "/$/** = localhost" line and use:
"/$/** = authcBasic,user[admin]"
## or to allow any access.
##/$/** = anon
# Everything else
/**=anon
/home/services/fuseki/config.ttl
@prefix : <#> .
@prefix fuseki: <http://jena.apache.org/fuseki#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix ja: <http://jena.hpl.hp.com/2005/11/Assembler#> .
[] rdf:type fuseki:Server ;
I asked ChatGPT, first-pass advice doesn't look entirely convincing, but gives me somewhere to start, it says to add :
shiro.ini
[main]
ldapRealm = org.apache.shiro.realm.ldap.JndiLdapRealm
ldapRealm.userDnTemplate = uid={0},ou=users,dc=example,dc=com
ldapRealm.contextFactory.url = ldap://localhost:389
securityManager.realms = $ldapRealm
[urls]
/ = authcBasic
fuseki-server.ttl
## Security configuration
[] ja:loadClass "org.apache.jena.fuseki.authz.FusekiSecurityFilter" .
Yeah, given there isn't a fuseki-server.ttl in the distro, I'm guessing that's a bit off. Maybe I won't start there. Time for a quick Google.
Ok, org.apache.shiro.realm.ldap.JndiLdapRealm is deprecated in favour of org.apache.shiro.realm.ldap.DefaultLdapRealm. That contains stuff.
These docs are for some unrelated project that uses Shiro, but they describe LDAP bits that correspond to those above.
Ew, ldapRealm.userDnTemplate gets ugly fast. I'd better start from the other end, users/groups in LDAP.
Wait. While Apache Directory Studio has everything, it would be convenient to get a simplified view.
Ok, locally :
sudo apt-get install ldapvi
ldapvi --out -h ldap://hyperdata.it -b "dc=hyperdata,dc=it"
Ok, that gives me straightforward blocks. But now I'm wondering : Have I secured the LDAP server? Eek!
Not an immediate issue - I can easily close the ports with ufw once I've done playing.
ChatGPT time again. Ok, keep it simple:
I wish to create the following groups in LDAP:
- service-admin group (one step below full admin)
- fuseki-admin group (can create/delete datasets, subgroup of service-admin)
- fuseki-foowiki write access group (will be a subgroup of fuseki-admin)
Ok, second pass - I primed it with a chunk of my current LDAP.
This looks promising
service-admin.ldif
dn: cn=service-admin,ou=groups,dc=hyperdata,dc=it
objectClass: groupOfNames
cn: service-admin
description: Service Admin Group
member: uid=danny,ou=agents,dc=hyperdata,dc=it
fuseki-admin.ldif
dn: cn=fuseki-admin,ou=groups,dc=hyperdata,dc=it
objectClass: groupOfNames
cn: fuseki-admin
description: Fuseki Admin Group
member: uid=danny,ou=agents,dc=hyperdata,dc=it
member: cn=service-admin,ou=groups,dc=hyperdata,dc=it
fuseki-foowiki.ldif
dn: cn=fuseki-foowiki,ou=groups,dc=hyperdata,dc=it
objectClass: groupOfNames
cn: fuseki-foowiki
description: Fuseki Foowiki Write Access Group
member: uid=danny,ou=agents,dc=hyperdata,dc=it
member: cn=fuseki-admin,ou=groups,dc=hyperdata,dc=it
I tried to do that with Apache Directory Studio (ADS), import ldif, but it gave:
#!RESULT ERROR
#!CONNECTION ldap://hyperdata.it:389
#!DATE 2024-04-18T13:07:47.783
#!ERROR [LDAP result code 50 - insufficientAccessRights]
dn: cn=config
changetype: modify
add: olcTLSCipherSuite
olcTLSCipherSuite: NORMAL
Ok, try on the server (I pasted them into files with nano in /home/services/ldap-config/):
root@hyperdata:/home/services/ldap-config# ldapadd -x -D "cn=admin,dc=hyperdata,dc=it" -W -f service-admin.ldif
...
Enter LDAP Password:
Ah, ok, at least it's password-protected.
Check in ADS - yay!
So far so good. Break time.
Back to ChatGPT:
I would like to configure Shiro in Fuseki to use these LDAP groups for access control: dn: cn=service-admin,ou=groups,dc=hyperdata,dc=it
- this has full access to Fuseki dn: cn=fuseki-admin,ou=groups,dc=hyperdata,dc=it
- this can create and delete datasets, full write access Fuseki dn: cn=fuseki-foowiki,ou=groups,dc=hyperdata,dc=it
- this has write access to the foowiki dataset any other user should have read access to the foowiki dataset
This produced:
[main]
# LDAP Realm Configuration
ldapRealm = org.apache.shiro.realm.ldap.JndiLdapRealm
ldapRealm.userDnTemplate = uid={0},ou=agents,dc=hyperdata,dc=it
ldapRealm.contextFactory.url = ldap://hyperdata.it:389
# Set the security manager to use the LDAP realm
securityManager.realms = $ldapRealm
# Caching and session management (optional but recommended)
cacheManager = org.apache.shiro.cache.MemoryConstrainedCacheManager
securityManager.cacheManager = $cacheManager
# Authorization configuration
[urls]
# Define access control for URLs
/dataset = authc, roles[service-admin, fuseki-admin]
/dataset/foowiki = authc, roles[service-admin, fuseki-admin, fuseki-foowiki]
/dataset/foowiki/** = authc, roles[service-admin, fuseki-admin, fuseki-foowiki]
/ = authcBasic
# Role to permission mapping
[roles]
service-admin = *
fuseki-admin = dataset:create, dataset:delete, dataset:write
fuseki-foowiki = dataset:foowiki:write
I would like to add the following LDAP users:
dn: uid=fuseki-foowiki-user,ou=agents,dc=hyperdata,dc=it
- this user should be a member of the fuseki-foowiki group
dn: uid=fuseki-admin-user,ou=agents,dc=hyperdata,dc=it
- this user should be a member of the fuseki-admin group
dn: uid=service-admin-user,ou=agents,dc=hyperdata,dc=it
- this user should be a member of the service-admin group
and give each a password they can use to log in.