Repository: l-with/terraform-provider-mailcow
Pull request #120: Add support for authsource attribute in mailbox resource
Author: login: madddi
Created: 2025-04-10T13:56:29Z | Merged: 2025-04-15T07:58:28Z

Add support for the `authsource` attribute added in Mailcow 2025-03


Diff:
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index 229cece..16392e4 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -2,14 +2,37 @@ name: Go
 
 on:
   push:
-    branches: [ "main" ]
+    branches: ["main"]
   pull_request:
 
 jobs:
-
   build:
     runs-on: ubuntu-latest
     steps:
+      - name: Ensure IdP config
+        shell: bash
+        env:
+          MAILCOW_HOST_NAME: ${{ secrets.MAILCOW_HOST_NAME }}
+          MAILCOW_API_KEY: ${{ secrets.MAILCOW_API_KEY }}
+        run: |
+          curl -k "https://$MAILCOW_HOST_NAME/api/v1/edit/identity-provider" \
+            -X POST \
+            -H "X-API-Key: $MAILCOW_API_KEY" \
+            -H 'Content-Type: application/json' \
+            -H 'Accept: application/json' --data \
+            '{
+              "items": ["identity-provider"],
+              "attr": {
+                "authsource": "keycloak",
+                "server_url": "https://auth.demo.mailcow.tld",
+                "realm": "mailcow",
+                "client_id": "mailcow_terraform",
+                "client_secret": "example",
+                "redirect_url": "https://demo.mailcow.tld",
+                "version": "26.1.3"
+              }
+            }'
+
       - uses: actions/checkout@v4
 
       - name: Set up Go
@@ -26,4 +49,4 @@ jobs:
           # These secrets will need to be configured for the repository:
           MAILCOW_HOST_NAME: ${{ secrets.MAILCOW_HOST_NAME }}
           MAILCOW_API_KEY: ${{ secrets.MAILCOW_API_KEY }}
-        run: go test -v ./...
\ No newline at end of file
+        run: go test -v ./...
diff --git a/README.md b/README.md
index 9f5ad08..1075619 100644
--- a/README.md
+++ b/README.md
@@ -9,16 +9,52 @@ terraform provider for [mailcow](https://github.com/mailcow/mailcow-dockerized)
 
 ## Disclaimer
 
-This is under development. You will certainly find bugs and limitations. 
+This is under development. You will certainly find bugs and limitations.
 In those cases, please report issues or, if you can, submit a pull-request.
 
 ### To change
 
-* The API of mailcow for tags always [adds the tags instead of replacing](https://github.com/mailcow/mailcow-dockerized/issues/4681). 
+* The API of mailcow for tags always [adds the tags instead of replacing](https://github.com/mailcow/mailcow-dockerized/issues/4681).
   Either the API has to be changed or extra resources  
   mailcow_domain_tags and mailcow_mailbox_tag have to be implemented.
 
 ### Blocked
 
 * There is no API to get user-acl, [but a merge request adding this](https://github.com/mailcow/mailcow-dockerized/pull/4690).
-  Without a possibility to read the user-acl a terraform-resource user-acl can not be implemented.
\ No newline at end of file
+  Without a possibility to read the user-acl a terraform-resource user-acl can not be implemented.
+
+## Testing
+
+To make all tests pass, an IdP must be configured. Set a sample config on your test environmnent:
+
+```bash
+curl -k 'https://demo.mailcow.email/api/v1/edit/identity-provider' \
+  -X POST \
+  -H "X-API-Key: 390448-22B69F-FA37D9-19701B-6F033F" \
+  -H 'Content-Type: application/json' \
+  -H 'Accept: application/json' --data \
+  '{
+    "items": ["identity-provider"],
+    "attr": {
+      "authsource": "keycloak",
+      "server_url": "https://auth.demo.mailcow.tld",
+      "realm": "mailcow",
+      "client_id": "mailcow_terraform",
+      "client_secret": "example",
+      "redirect_url": "https://demo.mailcow.email",
+      "version": "26.1.3"
+    }
+  }'
+```
+
+If no IdP is configured, the mailbox tests will fail with the following message:
+
+```code
+resource_mailbox_test.go:18: Step 4/7 error: Check failed: Check 1/1 error: mailcow_mailbox.mailbox: Attribute 'authsource' expected "keycloak", got "mailcow"
+```
+
+Then execute tests:
+
+```bash
+MAILCOW_HOST_NAME=demo.mailcow.email MAILCOW_API_KEY="390448-22B69F-FA37D9-19701B-6F033F" MAILCOW_INSECURE=true make testacc
+```
diff --git a/mailcow/data_source_mailbox.go b/mailcow/data_source_mailbox.go
index 5e8bde2..206864d 100644
--- a/mailcow/data_source_mailbox.go
+++ b/mailcow/data_source_mailbox.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
+
 	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
 )
@@ -32,6 +33,11 @@ func dataSourceMailbox() *schema.Resource {
 				Description: "left part of email address",
 				Computed:    true,
 			},
+			"authsource": {
+				Type:        schema.TypeString,
+				Description: "Authentication source",
+				Computed:    true,
+			},
 			"full_name": {
 				Type:        schema.TypeString,
 				Description: "Full name of the mailbox user",
diff --git a/mailcow/data_source_mailbox_test.go b/mailcow/data_source_mailbox_test.go
index 7e56e25..db90637 100644
--- a/mailcow/data_source_mailbox_test.go
+++ b/mailcow/data_source_mailbox_test.go
@@ -2,18 +2,17 @@ package mailcow
 
 import (
 	"fmt"
-	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
 	"regexp"
-)
-
-import (
 	"testing"
+
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
 )
 
 func TestAccDataSourceMailbox(t *testing.T) {
 	domain := fmt.Sprintf("with-ds-mailbox-%s.domain-%s.xyz", randomLowerCaseString(4), randomLowerCaseString(4))
 	localPart := fmt.Sprintf("with-ds-mailbox-%s", randomLowerCaseString(4))
 	fullName := "full name"
+	authSource := "mailcow"
 	resource.UnitTest(t, resource.TestCase{
 		PreCheck:          func() { testAccPreCheck(t) },
 		ProviderFactories: providerFactories,
@@ -24,6 +23,7 @@ func TestAccDataSourceMailbox(t *testing.T) {
 					resource.TestCheckResourceAttr("data.mailcow_mailbox.mailbox", "domain", domain),
 					resource.TestCheckResourceAttr("data.mailcow_mailbox.mailbox", "local_part", localPart),
 					resource.TestCheckResourceAttr("data.mailcow_mailbox.mailbox", "full_name", fullName),
+					resource.TestCheckResourceAttr("data.mailcow_mailbox.mailbox", "authsource", authSource),
 				),
 			},
 			{
diff --git a/mailcow/resource_mailbox.go b/mailcow/resource_mailbox.go
index 7bd3c7a..2dc7ec7 100644
--- a/mailcow/resource_mailbox.go
+++ b/mailcow/resource_mailbox.go
@@ -5,9 +5,17 @@ import (
 
 	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
 	"github.com/l-with/terraform-provider-mailcow/api"
 )
 
+const (
+	mailcowAuthsourceInternal = "mailcow"
+	mailcowAuthsourceKeycloak = "keycloak"
+	mailcowAuthsourceLdap     = "ldap"
+	mailcowAuthsourceOidc     = "generic-oidc"
+)
+
 func resourceMailbox() *schema.Resource {
 	return &schema.Resource{
 		CreateContext: resourceMailboxCreate,
@@ -38,6 +46,13 @@ func resourceMailbox() *schema.Resource {
 				Required:    true,
 				ForceNew:    true,
 			},
+			"authsource": {
+				Type:         schema.TypeString,
+				Description:  "Authentication source to use. One of: generic-oidc, mailcow, keycloak, ldap.",
+				Default:      mailcowAuthsourceInternal,
+				Optional:     true,
+				ValidateFunc: validation.StringInSlice([]string{mailcowAuthsourceInternal, mailcowAuthsourceKeycloak, mailcowAuthsourceLdap, mailcowAuthsourceOidc}, false),
+			},
 			"address": {
 				Type:        schema.TypeString,
 				Description: "e-mail address",
diff --git a/mailcow/resource_mailbox_test.go b/mailcow/resource_mailbox_test.go
index cdcb283..363ca09 100644
--- a/mailcow/resource_mailbox_test.go
+++ b/mailcow/resource_mailbox_test.go
@@ -24,6 +24,7 @@ func TestAccResourceMailbox(t *testing.T) {
 				Check: resource.ComposeTestCheckFunc(
 					resource.TestCheckResourceAttr("mailcow_mailbox.mailbox", "address", localPart+"@"+domain),
 					resource.TestCheckResourceAttr("mailcow_mailbox.mailbox", "tls_enforce_out", "true"),
+					resource.TestCheckResourceAttr("mailcow_mailbox.mailbox", "authsource", mailcowAuthsourceInternal),
 				),
 			},
 			{
@@ -42,6 +43,20 @@ func TestAccResourceMailbox(t *testing.T) {
 					resource.TestCheckResourceAttr("mailcow_mailbox.mailbox", "quota", strconv.Itoa(quota+1024)),
 				),
 			},
+			{
+				Config: testAccResourceMailboxChangeAuthsource(domain, domainMaxQuota, localPart, fullName, quota+1024),
+				Check: resource.ComposeTestCheckFunc(
+					resource.TestCheckResourceAttr("mailcow_mailbox.mailbox", "authsource", mailcowAuthsourceKeycloak),
+				),
+			},
+			{
+				Config: testAccResourceMailboxAuthsource(domain, localPart),
+				Check: resource.ComposeTestCheckFunc(
+					resource.TestCheckResourceAttr("mailcow_mailbox.mailbox_sso", "address", localPart+"-sso@"+domain),
+					resource.TestCheckResourceAttr("mailcow_mailbox.mailbox_sso", "tls_enforce_out", "true"),
+					resource.TestCheckResourceAttr("mailcow_mailbox.mailbox_sso", "authsource", mailcowAuthsourceKeycloak),
+				),
+			},
 			{
 				Config:      testAccResourceMailboxUpdate(domain, domainMaxQuota, localPart, fullName, domainMaxQuota+1024),
 				ExpectError: regexp.MustCompile("danger"),
@@ -88,6 +103,44 @@ resource "mailcow_mailbox" "mailbox" {
 `, domain, domainMaxquota, localPart, fullName, quota)
 }
 
+func testAccResourceMailboxChangeAuthsource(domain string, domainMaxquota int, localPart string, fullName string, quota int) string {
+	return fmt.Sprintf(`
+resource "mailcow_domain" "domain" {
+  domain = "%[1]s"
+  maxquota = %[2]d
+}
+
+resource "mailcow_mailbox" "mailbox" {
+  local_part      = "%[3]s"
+  domain          = mailcow_domain.domain.id
+  password        = "secret-password"
+	force_pw_update = false
+	authsource      = "keycloak"
+  full_name       = "%[4]s"
+  tls_enforce_out = true
+  quota           = %[5]d
+}
+`, domain, domainMaxquota, localPart, fullName, quota)
+}
+
+func testAccResourceMailboxAuthsource(domain string, localPart string) string {
+	return fmt.Sprintf(`
+resource "mailcow_domain" "domain" {
+  domain = "%[1]s"
+}
+
+resource "mailcow_mailbox" "mailbox_sso" {
+  local_part      = "%[2]s-sso"
+  domain          = mailcow_domain.domain.id
+  password        = "secret-password"
+	force_pw_update = false
+	authsource      = "keycloak"
+  full_name       = "initial full name"
+  tls_enforce_out = true
+}
+`, domain, localPart)
+}
+
 func testAccResourceMailboxCreateError(domain string) string {
 	return fmt.Sprintf(`
 resource "mailcow_mailbox" "mailbox-create" {


Reviews:
user: login: l-with
body: Thank you very much. Great work, I suggested one improvement (using standard functions).
state: CHANGES_REQUESTED
author_association: OWNER
submitted_at: 2025-04-12T05:18:25Z
user: login: madddi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-12T18:09:35Z

Review comments:
diff_hunk: @@ -38,6 +40,21 @@ func resourceMailbox() *schema.Resource {
 				Required:    true,
 				ForceNew:    true,
 			},
+			"authsource": {
+				Type:        schema.TypeString,
+				Description: "Authentication source to use. One of: generic-oidc, mailcow, keycloak, ldap.",
+				Default:     "mailcow",
+				Optional:    true,
+				ValidateFunc: func(val any, key string) ([]string, []error) {
path: mailcow/resource_mailbox.go
user: login: l-with
body: please use [StringInSlice](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-sdk/v2@v2.36.1/helper/validation#StringInSlice) 
created_at: 2025-04-12T05:13:34Z
author_association: OWNER
diff_hunk: @@ -38,6 +40,21 @@ func resourceMailbox() *schema.Resource {
 				Required:    true,
 				ForceNew:    true,
 			},
+			"authsource": {
+				Type:        schema.TypeString,
+				Description: "Authentication source to use. One of: generic-oidc, mailcow, keycloak, ldap.",
+				Default:     "mailcow",
+				Optional:    true,
+				ValidateFunc: func(val any, key string) ([]string, []error) {
path: mailcow/resource_mailbox.go
user: login: madddi
body: Sure, that's much easier... 
created_at: 2025-04-12T18:09:34Z
author_association: CONTRIBUTOR

PR comments:
user: login: l-with
created_at: 2025-04-10T16:30:06Z
body: Thank you very much for the contribution!
author_association: OWNER
user: login: l-with
created_at: 2025-04-10T16:33:34Z
body: Please add the new attribute to the tests in resource_mailbox_test.go
author_association: OWNER
user: login: madddi
created_at: 2025-04-10T17:14:12Z
body: Sure, I've also added it to the data source. Please have a good look - to be honest it's been a while since I've worked on Terraform providers :-)  
author_association: CONTRIBUTOR
user: login: madddi
created_at: 2025-04-12T18:10:36Z
body: I don't know how I missed that, but `authsource` is not a mailbox attribute, so I fixed that too and made sure the tests pass.

Edit: we could add a test for the authsource update, but this needs a configured identity provider on the test system. I could do that using `keycloak` on my test system. But if you don't have at least a simple keycloak config on your test env, this will break. So let me know if you'd like me to add a test.
author_association: CONTRIBUTOR
user: login: l-with
created_at: 2025-04-12T19:45:45Z
body: > I don't know how I missed that, but `authsource` is not a mailbox attribute, so I fixed that too and made sure the tests pass.
> 
> Edit: we could add a test for the authsource update, but this needs a configured identity provider on the test system. I could do that using `keycloak` on my test system. But if you don't have at least a simple keycloak config on your test env, this will break. So let me know if you'd like me to add a test.

My tests use the mailcow test instance https://demo.mailcow.email 
I checked it, there is only mailcow as identity provider in this instance.

The Swagger url lists only the POST verb for /api/v1/edit/identity-provider. I tried out
```bash
curl -H "X-API-Key: 390448-22B69F-FA37D9-19701B-6F033F" https://demo.mailcow.email/api/v1/get/identity-provider
```
and it worked.
Thus a terraform resource and data source for the identity_provider could be implemented. The documentation of the api is very bad.
After inspecting the code https://github.com/mailcow/mailcow-dockerized/blob/master/data/web/json_api.php I am very sure that also /api/v1/delete/identity-provider will work without any payload.
That also means that there is no id, an interesting situation for terraform. It should be mentioned in the documentation. Shurely the api will fail if one creates a second resource with a different name.

It would be great if you implement the resource and data source identity_provider too. Then this could be integrated in the test.



author_association: OWNER
user: login: madddi
created_at: 2025-04-13T19:04:40Z
body: Sure, I could have a look at that. Would you say this is blocking this PR?
author_association: CONTRIBUTOR
user: login: l-with
created_at: 2025-04-14T06:26:15Z
body: > Sure, I could have a look at that. Would you say this is blocking this PR?

No, but a test would be fine.
author_association: OWNER
user: login: madddi
created_at: 2025-04-14T07:29:40Z
body: > No, but a test would be fine.

A complete test would only work would only work if you'd set up a second IdP on the demo instance first though. If this is fine for you, I'll add the test.
author_association: CONTRIBUTOR
user: login: madddi
created_at: 2025-04-14T08:56:04Z
body: I went ahead and added the tests along with some documentation. Have a look :-)
author_association: CONTRIBUTOR
user: login: l-with
created_at: 2025-04-14T11:43:04Z
body: > I went ahead and added the tests along with some documentation. Have a look :-)

Nice, thanks, I need this command in go.yml , otherwise the workflow will fail.
author_association: OWNER
user: login: madddi
created_at: 2025-04-14T12:20:24Z
body: Done, I hope it works this way.
author_association: CONTRIBUTOR
user: login: l-with
created_at: 2025-04-14T12:21:46Z
body: Thanks, let's try
author_association: OWNER
user: login: l-with
created_at: 2025-04-14T12:23:22Z
body: <img width="958" alt="image" src="https://github.com/user-attachments/assets/afa18c5a-771a-4013-874a-84b395f66e6f" />
perhaps ${...} is not supported
author_association: OWNER
user: login: madddi
created_at: 2025-04-14T13:00:50Z
body: Let's see, I've changed it to `$MAILCOW_HOST_NAME`
author_association: CONTRIBUTOR
user: login: l-with
created_at: 2025-04-14T13:22:16Z
body: <img width="946" alt="image" src="https://github.com/user-attachments/assets/47c61631-a856-4e82-a7e3-b0d61b6da347" />
Hmmm
author_association: OWNER
user: login: madddi
created_at: 2025-04-14T13:32:33Z
body: I've explicitly set the shell to bash now. Following these docs it should work: https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions#example-using-bash

author_association: CONTRIBUTOR
user: login: l-with
created_at: 2025-04-14T13:58:09Z
body: <img width="947" alt="image" src="https://github.com/user-attachments/assets/2ae8e7f3-193e-4ae3-adc9-4522737db3fb" />

author_association: OWNER
user: login: madddi
created_at: 2025-04-14T15:24:41Z
body: ![failure](https://github.com/user-attachments/assets/6b0c8cd2-cc1a-4213-ba4b-682d00331720)

So it looks like `$MAILCOW_HOST_NAME` is just empty, resulting in `curl https:///api/...`. Bash substitution obviously works but the env vars are empty. Is there maybe a restriction on secrets availability?
author_association: CONTRIBUTOR
user: login: l-with
created_at: 2025-04-14T15:27:05Z
body: <img width="781" alt="image" src="https://github.com/user-attachments/assets/4adf2f1e-3a79-4316-834e-c9599d645f3a" />

I'll search for a solution
author_association: OWNER
user: login: l-with
created_at: 2025-04-14T15:51:03Z
body: Please try setting the "secrets" in your fork repository:

MAILCOW_HOST_NAME=demo.mailcow.email
MAILCOW_API_KEY=390448-22B69F-FA37D9-19701B-6F033F

the API-Key is public accessible by login in the [demo instance](https://mailcow.email/posts/2022/our-new-demos/)

author_association: OWNER
user: login: madddi
created_at: 2025-04-14T17:25:34Z
body: You could maybe try the workflow in #121, this PR doesn't require any special config.
author_association: CONTRIBUTOR
user: login: l-with
created_at: 2025-04-14T17:32:24Z
body: > You could maybe try the workflow in #121, this PR doesn't require any special config.

I started the workflow:

<img width="670" alt="image" src="https://github.com/user-attachments/assets/d3bb4d9c-d12d-44cd-adf8-cf5f34b9c530" />

The secrets are not available for a pull request from forked repository, please set them in your fork.

<img width="1121" alt="image" src="https://github.com/user-attachments/assets/0683ba4d-5390-46ae-875a-cc0e55f665dd" />

author_association: OWNER
user: login: madddi
created_at: 2025-04-14T18:02:32Z
body: Ahh, sorry I didn't get you at first. Secrets are configured in my fork now.
author_association: CONTRIBUTOR
user: login: l-with
created_at: 2025-04-14T18:07:32Z
body: <img width="940" alt="image" src="https://github.com/user-attachments/assets/cd4ad20e-2291-4519-9bfe-57838a26615f" />
author_association: OWNER
user: login: madddi
created_at: 2025-04-14T18:16:06Z
body: I don't understand this. Just to troubleshoot it, I've opened a PR for this branch against my own forked `main` branch: https://github.com/madddi/terraform-provider-mailcow/pull/2

Tests including the curl command work completely fine there - also proving that secrets in my repo work, I'd say. Running out of ideas...
author_association: CONTRIBUTOR
user: login: l-with
created_at: 2025-04-15T06:07:31Z
body: > I don't understand this. Just to troubleshoot it, I've opened a PR for this branch against my own forked `main` branch: [madddi#2](https://github.com/madddi/terraform-provider-mailcow/pull/2)
> 
> Tests including the curl command work completely fine there - also proving that secrets in my repo work, I'd say. Running out of ideas...

I also have no idea except that I invited you as collaborator.
author_association: OWNER
user: login: madddi
created_at: 2025-04-15T07:51:54Z
body: > I invited you as collaborator

Thanks!

> They are not passed to workflows that are triggered by a pull request from a fork.

It seems that this is just the case without exceptions. I pushed my branch to your repo directly and filed a PR (#122). They still fail here... :-/
author_association: CONTRIBUTOR
user: login: l-with
created_at: 2025-04-15T07:59:21Z
body: @madddi Thank you very much!
author_association: OWNER