Skip to content

Commit 436bd5b

Browse files
Merge pull request #10 from systemli/feat/synapse_deprovision
Add support to deprovision matrix/synapse users
2 parents 53e9eb8 + 498cf31 commit 436bd5b

8 files changed

Lines changed: 179 additions & 5 deletions

File tree

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,6 @@ NEXTCLOUD_OIDC_PROVIDER_ID="1"
66
NEXTCLOUD_USER_DOMAIN="example.org"
77
NEXTCLOUD_ADMIN_USERNAME="admin"
88
NEXTCLOUD_ADMIN_PASSWORD="admin"
9+
SYNAPSE_USER_ADMIN_API_URL="https://matrix.example.org/_synapse/admin/v1"
10+
SYNAPSE_USER_DOMAIN="example.org"
11+
SYNAPSE_ADMIN_ACCESS_TOKEN="token"

config.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ type Config struct {
77
ListenAddr string
88
WebhookSecret string
99
Nextcloud *NextcloudConfig
10+
Synapse *SynapseConfig
1011
}
1112

1213
func BuildConfig() *Config {
1314
cfg := &Config{
1415
LogLevel: "info",
1516
ListenAddr: ":8080",
1617
Nextcloud: &NextcloudConfig{},
18+
Synapse: &SynapseConfig{},
1719
}
1820

1921
if os.Getenv("LOG_LEVEL") != "" {
@@ -30,6 +32,11 @@ func BuildConfig() *Config {
3032
ProviderID: getEnvOrFatal("NEXTCLOUD_OIDC_PROVIDER_ID"),
3133
Domain: getEnvOrFatal("NEXTCLOUD_USER_DOMAIN"),
3234
}
35+
cfg.Synapse = &SynapseConfig{
36+
ApiUrl: getEnvOrFatal("SYNAPSE_USER_ADMIN_API_URL"),
37+
AccessToken: getEnvOrFatal("SYNAPSE_ADMIN_ACCESS_TOKEN"),
38+
Domain: getEnvOrFatal("SYNAPSE_USER_DOMAIN"),
39+
}
3340

3441
return cfg
3542
}

config_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ func (s *ConfigSuite) TestBuildConfig() {
2020
os.Setenv("NEXTCLOUD_ADMIN_PASSWORD", "password")
2121
os.Setenv("NEXTCLOUD_OIDC_PROVIDER_ID", "1")
2222
os.Setenv("NEXTCLOUD_USER_DOMAIN", "example.com")
23+
os.Setenv("SYNAPSE_USER_ADMIN_API_URL", "https://example.com/_synapse/admin/v1")
24+
os.Setenv("SYNAPSE_ADMIN_ACCESS_TOKEN", "token")
25+
os.Setenv("SYNAPSE_USER_DOMAIN", "example.com")
2326

2427
cfg := BuildConfig()
2528

@@ -33,6 +36,11 @@ func (s *ConfigSuite) TestBuildConfig() {
3336
s.Equal("password", cfg.Nextcloud.Password)
3437
s.Equal("1", cfg.Nextcloud.ProviderID)
3538
s.Equal("example.com", cfg.Nextcloud.Domain)
39+
40+
s.NotNil(cfg.Synapse)
41+
s.Equal("https://example.com/_synapse/admin/v1", cfg.Synapse.ApiUrl)
42+
s.Equal("token", cfg.Synapse.AccessToken)
43+
s.Equal("example.com", cfg.Synapse.Domain)
3644
}
3745

3846
func TestConfig(t *testing.T) {

main.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ func main() {
3333
config := BuildConfig()
3434
logger.Info("Starting server", zap.String("listenAddr", config.ListenAddr))
3535
nc := NewNextcloud(config.Nextcloud)
36-
s := NewServer(config.WebhookSecret, nc)
36+
sy := NewSynapse(config.Synapse)
37+
s := NewServer(config.WebhookSecret, nc, sy)
3738
if err := s.Start(config.ListenAddr); err != nil {
3839
logger.Fatal("Failed to start server", zap.Error(err))
3940
}

server.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,16 @@ import (
1616
type Server struct {
1717
router *chi.Mux
1818
nextcloud *Nextcloud
19+
synapse *Synapse
1920
webhookSecret string
2021
}
2122

22-
func NewServer(webhookSecret string, nextcloud *Nextcloud) *Server {
23+
func NewServer(webhookSecret string, nextcloud *Nextcloud, synapse *Synapse) *Server {
2324
return &Server{
2425
router: chi.NewRouter(),
2526
webhookSecret: webhookSecret,
2627
nextcloud: nextcloud,
28+
synapse: synapse,
2729
}
2830
}
2931

@@ -75,6 +77,11 @@ func (s *Server) handleUserDeleted(event UserEvent) {
7577
if err != nil {
7678
logger.Error("Failed to deprovision user in Nextcloud")
7779
}
80+
81+
err = s.synapse.DeprovisionUser(event.Data.Email)
82+
if err != nil {
83+
logger.Error("Failed to deprovision user in Synapse")
84+
}
7885
}
7986

8087
func (s *Server) Authmiddleware(next http.Handler) http.Handler {

server_test.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,23 @@ func (s *ServerSuite) SetupTest() {
2323
gock.DisableNetworking()
2424
defer gock.Off()
2525

26-
config := &NextcloudConfig{
26+
ncConfig := &NextcloudConfig{
2727
ApiUrl: "https://example.com/ocs/v2.php/apps/user_oidc/api/v1/user",
2828
Username: "admin",
2929
Password: "password",
3030
ProviderID: "provider-id",
3131
Domain: "example.com",
3232
}
33-
nc := NewNextcloud(config)
33+
nc := NewNextcloud(ncConfig)
3434

35-
s.server = NewServer("secret", nc)
35+
syConfig := &SynapseConfig{
36+
ApiUrl: "https://example.com/_synapse/admin/v1",
37+
AccessToken: "token",
38+
Domain: "example.com",
39+
}
40+
sy := NewSynapse(syConfig)
41+
42+
s.server = NewServer("secret", nc, sy)
3643
}
3744

3845
func (s *ServerSuite) TestHandleWebhook() {
@@ -92,6 +99,10 @@ func (s *ServerSuite) TestHandleUserDeleted() {
9299
Delete("/ocs/v2.php/apps/user_oidc/api/v1/user/user").
93100
Reply(200)
94101

102+
gock.New("https://example.com").
103+
Post("/_synapse/admin/v1/deactivate/user").
104+
Reply(200)
105+
95106
event := UserEvent{
96107
Type: EventTypeUserDeleted,
97108
Data: struct {

synapse.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"net/http"
8+
"strings"
9+
)
10+
11+
type SynapseConfig struct {
12+
ApiUrl string
13+
AccessToken string
14+
Domain string
15+
}
16+
17+
type Synapse struct {
18+
client *http.Client
19+
config *SynapseConfig
20+
}
21+
22+
func NewSynapse(cfg *SynapseConfig) *Synapse {
23+
client := &http.Client{}
24+
25+
return &Synapse{
26+
client: client,
27+
config: cfg,
28+
}
29+
}
30+
31+
func (s *Synapse) DeprovisionUser(email string) error {
32+
// Extract the userId from the email
33+
userId := strings.Split(email, "@")[0]
34+
domain := strings.Split(email, "@")[1]
35+
36+
if domain != s.config.Domain {
37+
return fmt.Errorf("domain not allowed: %s", domain)
38+
}
39+
40+
body := map[string]any{
41+
"erase": true,
42+
}
43+
jsonData, err := json.Marshal(body)
44+
if err != nil {
45+
return fmt.Errorf("failed to marshal request body: %v", err)
46+
}
47+
48+
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/deactivate/@%s:%s", s.config.ApiUrl, userId, domain), bytes.NewBuffer(jsonData))
49+
if err != nil {
50+
return fmt.Errorf("failed to create request: %v", err)
51+
}
52+
53+
s.prepareRequest(req)
54+
res, err := s.client.Do(req)
55+
if err != nil {
56+
return fmt.Errorf("failed to deprovision user: %v", err)
57+
}
58+
defer res.Body.Close()
59+
60+
if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusNotFound {
61+
return fmt.Errorf("failed to deprovision user, status code: %d", res.StatusCode)
62+
}
63+
64+
return nil
65+
}
66+
67+
func (s *Synapse) prepareRequest(req *http.Request) {
68+
req.Header.Set("Authorization", fmt.Sprintf("Bearer: %s", s.config.AccessToken))
69+
req.Header.Set("Accept", "application/json")
70+
req.Header.Set("User-Agent", "UserliWebhookListener/1.0")
71+
req.Header.Set("ocs-apirequest", "true")
72+
}

synapse_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package main
2+
3+
import (
4+
"testing"
5+
6+
"github.com/h2non/gock"
7+
"github.com/stretchr/testify/suite"
8+
)
9+
10+
type SynapseSuite struct {
11+
suite.Suite
12+
13+
synapse *Synapse
14+
}
15+
16+
func (s *SynapseSuite) SetupTest() {
17+
gock.DisableNetworking()
18+
defer gock.Off()
19+
20+
config := &SynapseConfig{
21+
ApiUrl: "https://example.com/_synapse/admin/v1",
22+
AccessToken: "token",
23+
Domain: "example.com",
24+
}
25+
26+
s.synapse = NewSynapse(config)
27+
}
28+
29+
func (s *SynapseSuite) TestDeprovisionUser() {
30+
s.Run("happy path", func() {
31+
gock.New("https://example.com").
32+
Post("/_synapse/admin/v1/deactivate/@user:example.com").
33+
Reply(200)
34+
35+
err := s.synapse.DeprovisionUser("user@example.com")
36+
s.NoError(err)
37+
})
38+
39+
s.Run("wrong domain", func() {
40+
err := s.synapse.DeprovisionUser("user@example.org")
41+
s.Error(err)
42+
})
43+
44+
s.Run("user not found", func() {
45+
gock.New("https://example.com").
46+
Post("/_synapse/admin/v1/deactivate/@user:example.com").
47+
Reply(404)
48+
49+
err := s.synapse.DeprovisionUser("user@example.com")
50+
s.NoError(err)
51+
})
52+
53+
s.Run("error path", func() {
54+
gock.New("https://example.com").
55+
Delete("/_synapse/admin/v1/deactivate/user").
56+
Reply(500)
57+
58+
err := s.synapse.DeprovisionUser("user@example.com")
59+
s.Error(err)
60+
})
61+
}
62+
63+
func TestSynapseSuite(t *testing.T) {
64+
suite.Run(t, new(SynapseSuite))
65+
}

0 commit comments

Comments
 (0)