diff --git a/api/api.go b/api/api.go index eee98bb..925912c 100644 --- a/api/api.go +++ b/api/api.go @@ -254,6 +254,13 @@ type ApiMailcowGetRequest struct { xAPIKey *string } +type ApiMailcowGetAllRequest struct { + ctx context.Context + ApiService *ApiService + endpoint string + xAPIKey *string +} + func (r ApiMailcowGetRequest) XAPIKey(xAPIKey string) ApiMailcowGetRequest { r.xAPIKey = &xAPIKey return r @@ -351,6 +358,93 @@ func (a *ApiService) MailcowGetExecute(r ApiMailcowGetRequest) (*http.Response, return localVarHTTPResponse, nil } +func (a *ApiService) MailcowGetAllExecute(r ApiMailcowGetAllRequest) (*http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + formFiles []formFile + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "ApiService.MailcowGet") + if err != nil { + return nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + r.endpoint + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + if r.xAPIKey != nil { + localVarHeaderParams["X-API-Key"] = parameterToString(*r.xAPIKey, "") + } + if r.ctx != nil { + // API Key Authentication + if auth, ok := r.ctx.Value(ContextAPIKeys).(map[string]APIKey); ok { + if apiKey, ok := auth["ApiKeyAuth"]; ok { + var key string + if apiKey.Prefix != "" { + key = apiKey.Prefix + " " + apiKey.Key + } else { + key = apiKey.Key + } + localVarHeaderParams["X-API-Key"] = key + } + } + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarHTTPResponse, err + } + + localVarBody, err := ioutil.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = ioutil.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + var v ErrorResponse + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarHTTPResponse, newErr + } + newErr.model = v + return localVarHTTPResponse, newErr + } + + return localVarHTTPResponse, nil +} + type ApiMailcowUpdateRequest struct { ctx context.Context ApiService *ApiService diff --git a/api/model_alias_response.go b/api/model_alias_response.go index fc9b539..37db56c 100644 --- a/api/model_alias_response.go +++ b/api/model_alias_response.go @@ -5,13 +5,13 @@ import ( "fmt" ) -func (o *MailcowResponseArray) GetId() (error, *string) { +func (o *MailcowResponseArray) GetAliasId() (*string, error) { if !o.HasFinalMsgItem(0) || !o.HasFinalMsgItem(2) { - return errors.New(fmt.Sprint("msg error: ", o.GetFinalMsgs())), nil + return nil, errors.New(fmt.Sprint("msg error: ", o.GetFinalMsgs())) } receipt := *o.GetFinalMsgItem(0) if receipt != "alias_added" { - return errors.New(fmt.Sprint("msg error: ", receipt)), nil + return nil, errors.New(fmt.Sprint("msg error: ", receipt)) } - return nil, o.GetFinalMsgItem(2) + return o.GetFinalMsgItem(2), nil } diff --git a/api/model_create_request.go b/api/model_create_request.go index abd8818..cebef51 100644 --- a/api/model_create_request.go +++ b/api/model_create_request.go @@ -53,6 +53,14 @@ func NewCreateSyncjobRequest() *MailcowCreateRequest { return &this } +func NewCreateOAuth2ClientRequest() *MailcowCreateRequest { + this := MailcowCreateRequest{} + this.payload = make(map[string]interface{}) + this.endpoint = "/api/v1/add/oauth2-client" + this.ResourceName = "resourceOAuth2Client" + return &this +} + func (o *MailcowCreateRequest) Get(key string) interface{} { if !o.Has(key) { var ret bool @@ -129,7 +137,7 @@ func (o MailcowCreateRequest) MarshalJSON(requestSpec map[string]interface{}) ([ //if o.attr != nil { // toSerialize["attr"] = o.attr //} - for key, _ := range requestSpec { + for key := range requestSpec { //key := element.(map) if o.payload[key] != nil { toSerialize[key] = o.payload[key] diff --git a/api/model_delete_request.go b/api/model_delete_request.go index 7c9d0a8..1030758 100644 --- a/api/model_delete_request.go +++ b/api/model_delete_request.go @@ -47,6 +47,13 @@ func NewDeleteSyncjobRequest() *MailcowDeleteRequest { return &this } +func NewDeleteOAuth2ClientRequest() *MailcowDeleteRequest { + this := MailcowDeleteRequest{} + this.endpoint = "/api/v1/delete/oauth2-client" + this.ResourceName = "resourceOAuth2Client" + return &this +} + func (o *MailcowDeleteRequest) GetItem() *string { log.Print("[TRACE] GetItem") if !o.HasItem() { diff --git a/api/model_get_request.go b/api/model_get_request.go index d818227..f357b14 100644 --- a/api/model_get_request.go +++ b/api/model_get_request.go @@ -46,3 +46,20 @@ func (a *ApiService) MailcowGetSyncjob(ctx context.Context, id string) ApiMailco id: id, } } + +func (a *ApiService) MailcowGetOAuth2Client(ctx context.Context, id string) ApiMailcowGetRequest { + return ApiMailcowGetRequest{ + ApiService: a, + ctx: ctx, + endpoint: "/api/v1/get/oauth2-client/{id}", + id: id, + } +} + +func (a *ApiService) MailcowGetOAuth2Clients(ctx context.Context) ApiMailcowGetAllRequest { + return ApiMailcowGetAllRequest{ + ApiService: a, + ctx: ctx, + endpoint: "/api/v1/get/oauth2-client/all", + } +} diff --git a/docs/index.md b/docs/index.md index a21ba7c..954cfa0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -42,7 +42,7 @@ This is under development. You will certainly find bugs and limitations. In thos ## Schema -### Required +### Optional - `api_key` (String, Sensitive) The mailcow API key, can optionally be passed as `MAILCOW_API_KEY` environmental variable - `host_name` (String) The name of the mailcow host, can optionally be passed as `MAILCOW_HOST_NAME` environmental variable \ No newline at end of file diff --git a/docs/resources/oauth2_client.md b/docs/resources/oauth2_client.md new file mode 100644 index 0000000..9f9495b --- /dev/null +++ b/docs/resources/oauth2_client.md @@ -0,0 +1,37 @@ +--- +page_title: "mailcow_oauth2_client Resource - terraform-provider-mailcow" +subcategory: "" +description: |- +--- + +# mailcow_oauth2_client (Resource) + +Provides an OAuth2 client for mailcow. +This can be used to create and delete OAuth2 clients for mailcow. + +## Example Usage +```terraform +resource "mailcow_oauth2_client" "client" { + redirect_uri = "https:/redirect.uri" +} +``` + + +## Schema + +### Required + +- `redirect_uri` (String) + +### Read-Only + +- `client_id` (String) +- `client_secret` (String, Sensitive) +- `id` (String) The ID of this resource. +- `scope` (String) + +## Restriction + +The mailcow API does not return the id of the OAuth2 client as response for creation. +As workaround the redirect uri is used as identifier for the resource. +As consequence creation of an OAuth2 client with an existing redirect uri is prohibited. diff --git a/examples/resources/mailcow_oauth2_client/resource.tf b/examples/resources/mailcow_oauth2_client/resource.tf new file mode 100644 index 0000000..3907157 --- /dev/null +++ b/examples/resources/mailcow_oauth2_client/resource.tf @@ -0,0 +1,3 @@ +resource "mailcow_oauth2_client" "client" { + redirect_uri = "https:/redirect.uri" +} diff --git a/mailcow/provider.go b/mailcow/provider.go index 0c86712..54cbbfc 100644 --- a/mailcow/provider.go +++ b/mailcow/provider.go @@ -26,11 +26,12 @@ func Provider() *schema.Provider { }, }, ResourcesMap: map[string]*schema.Resource{ - "mailcow_alias": resourceAlias(), - "mailcow_domain": resourceDomain(), - "mailcow_mailbox": resourceMailbox(), - "mailcow_dkim": resourceDkim(), - "mailcow_syncjob": resourceSyncjob(), + "mailcow_alias": resourceAlias(), + "mailcow_domain": resourceDomain(), + "mailcow_mailbox": resourceMailbox(), + "mailcow_dkim": resourceDkim(), + "mailcow_syncjob": resourceSyncjob(), + "mailcow_oauth2_client": resourceOAuth2Client(), }, DataSourcesMap: map[string]*schema.Resource{ "mailcow_domain": dataSourceDomain(), diff --git a/mailcow/resource_alias.go b/mailcow/resource_alias.go index 803b5ca..98211e0 100644 --- a/mailcow/resource_alias.go +++ b/mailcow/resource_alias.go @@ -71,7 +71,7 @@ func resourceAliasCreate(ctx context.Context, d *schema.ResourceData, m interfac return diag.FromErr(err) } - err, id := response.GetId() + id, err := response.GetAliasId() if err != nil { return diag.FromErr(err) } @@ -99,6 +99,9 @@ func resourceAliasRead(ctx context.Context, d *schema.ResourceData, m interface{ } setResourceData(resourceAlias(), d, &alias, nil, nil) + if err != nil { + return diag.FromErr(err) + } d.SetId(fmt.Sprint(alias["id"].(float64))) diff --git a/mailcow/resource_domain_admin.gonot b/mailcow/resource_domain_admin.gonot new file mode 100644 index 0000000..93973e8 --- /dev/null +++ b/mailcow/resource_domain_admin.gonot @@ -0,0 +1,221 @@ +package mailcow + +import ( + "context" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/l-with/terraform-provider-mailcow/api" +) + +func resourceMailbox() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceMailboxCreate, + ReadContext: resourceMailboxRead, + UpdateContext: resourceMailboxUpdate, + DeleteContext: resourceMailboxDelete, + Schema: map[string]*schema.Schema{ + "active": { + Type: schema.TypeBool, + Description: "is alias active or not", + Default: true, + Optional: true, + }, + "domain": { + Type: schema.TypeString, + Description: "domain name", + Required: true, + ForceNew: true, + }, + "local_part": { + Type: schema.TypeString, + Description: "left part of email address", + Required: true, + ForceNew: true, + }, + "address": { + Type: schema.TypeString, + Description: "e-mail address", + Computed: true, + }, + "full_name": { + Type: schema.TypeString, + Description: "Full name of the mailbox user", + Required: true, + }, + "password": { + Type: schema.TypeString, + Description: "mailbox password", + Required: true, + Sensitive: true, + }, + "quota": { + Type: schema.TypeInt, + Description: "mailbox quota", + Optional: true, + }, + "force_pw_update": { + Type: schema.TypeBool, + Description: "forces the user to update its password on first login", + Default: true, + Optional: true, + }, + "tls_enforce_in": { + Type: schema.TypeBool, + Description: "force inbound email tls encryption", + Default: false, + Optional: true, + }, + "tls_enforce_out": { + Type: schema.TypeBool, + Description: "force outbound tmail tls encryption", + Default: false, + Optional: true, + }, + "sogo_access": { + Type: schema.TypeBool, + Description: "if direct login access to SOGo is granted", + Default: true, + Optional: true, + }, + "imap_access": { + Type: schema.TypeBool, + Description: "if 'IMAP' is an allowed protocol", + Default: true, + Optional: true, + }, + "pop3_access": { + Type: schema.TypeBool, + Description: "if 'POP3' is an allowed protocol", + Default: true, + Optional: true, + }, + "smtp_access": { + Type: schema.TypeBool, + Description: "if 'SMTP' is an allowed protocol", + Default: true, + Optional: true, + }, + "sieve_access": { + Type: schema.TypeBool, + Description: "if 'Sieve' is an allowed protocol", + Default: true, + Optional: true, + }, + //"relayhost": "0", + //"passwd_update": "2022-07-15 20:31:51", + //"mailbox_format": "maildir:", + //"quarantine_notification": "hourly", + //"quarantine_category": "reject" + }, + } +} + +func isMailboxAttribute(argument string) bool { + mailboxAttributes := []string{ + "force_pw_update", + "tls_enforce_in", + "tls_enforce_out", + "sogo_access", + "imap_access", + "pop3_access", + "smtp_access", + "sieve_access", + } + for _, attribute := range mailboxAttributes { + if argument == attribute { + return true + } + } + return false +} + +func resourceMailboxCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + var diags diag.Diagnostics + + c := m.(*APIClient) + + mailcowCreateRequest := api.NewCreateMailboxRequest() + + address := d.Get("local_part").(string) + "@" + d.Get("domain").(string) + err := d.Set("address", address) + if err != nil { + return diag.FromErr(err) + } + + mailcowCreateRequest.Set("password2", d.Get("password")) + + mapArguments := map[string]string{"full_name": "name"} + + err = mailcowCreate(ctx, resourceMailbox(), d, address, nil, &mapArguments, mailcowCreateRequest, c) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(address) + + return diags +} + +func resourceMailboxRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + var diags diag.Diagnostics + + c := m.(*APIClient) + id := d.Id() + + request := c.client.Api.MailcowGetMailbox(ctx, id) + + mailbox, err := readRequest(request) + if err != nil { + return diag.FromErr(err) + } + + exclude := []string{ + "password", + } + mailboxAttributes := []string{ + "force_pw_update", + "tls_enforce_in", + "tls_enforce_out", + "sogo_access", + "imap_access", + "pop3_access", + "smtp_access", + "sieve_access", + } + mailbox["address"] = id + mailbox["full_name"] = mailbox["name"] + excludeAndAttributes := append(exclude, mailboxAttributes...) + setResourceData(resourceMailbox(), d, &mailbox, &excludeAndAttributes, nil) + attributes := mailbox["attributes"].(map[string]interface{}) + setResourceData(resourceMailbox(), d, &attributes, &exclude, &mailboxAttributes) + + d.SetId(id) + + return diags +} + +func resourceMailboxUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(*APIClient) + + mailcowUpdateRequest := api.NewUpdateMailboxRequest() + + exclude := []string{ + "password", + } + mapArguments := map[string]string{ + "full_name": "name", + } + err := mailcowUpdate(ctx, resourceMailbox(), d, &exclude, &mapArguments, mailcowUpdateRequest, c) + if err != nil { + return diag.FromErr(err) + } + + return resourceMailboxRead(ctx, d, m) +} + +func resourceMailboxDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(*APIClient) + mailcowDeleteRequest := api.NewDeleteMailboxRequest() + diags, _ := mailcowDelete(ctx, d, mailcowDeleteRequest, c) + return diags +} diff --git a/mailcow/resource_oauth2_client.go b/mailcow/resource_oauth2_client.go new file mode 100644 index 0000000..453c368 --- /dev/null +++ b/mailcow/resource_oauth2_client.go @@ -0,0 +1,115 @@ +package mailcow + +import ( + "context" + "errors" + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/l-with/terraform-provider-mailcow/api" + "log" +) + +func resourceOAuth2Client() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceOAuth2ClientCreate, + ReadContext: resourceOAuth2ClientRead, + DeleteContext: resourceOAuth2ClientDelete, + Schema: map[string]*schema.Schema{ + "client_id": { + Type: schema.TypeString, + Computed: true, + }, + "client_secret": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + }, + "redirect_uri": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "scope": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceOAuth2ClientCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(*APIClient) + + redirectUri := d.Get("redirect_uri").(string) + log.Print("[TRACE] resourceOAuth2ClientCreate getId: ", redirectUri) + id, err := getId(ctx, c.client, redirectUri) + if err == nil { + log.Print("[TRACE] resourceOAuth2ClientCreate getId: ", redirectUri, " => ", *id) + log.Print("[TRACE] resourceOAuth2ClientCreate id: ", *id) + return diag.Errorf("redirect_uri exists: %s", redirectUri) + } + log.Print("[TRACE] resourceOAuth2ClientCreate getId: ", redirectUri, " => error") + + mailcowCreateRequest := api.NewCreateOAuth2ClientRequest() + + err = mailcowCreate(ctx, resourceOAuth2Client(), d, redirectUri, nil, nil, mailcowCreateRequest, c) + if err != nil { + return diag.FromErr(err) + } + + id, err = getId(ctx, c.client, redirectUri) + if err != nil { + return diag.FromErr(err) + } + d.SetId(*id) + + return resourceOAuth2ClientRead(ctx, d, m) +} + +func getId(ctx context.Context, client *api.APIClient, redirectUri string) (*string, error) { + request := client.Api.MailcowGetOAuth2Clients(ctx) + + oAuth2Clients, err := readAllRequest(request) + if err != nil { + return nil, err + } + + for _, oAuth2Client := range oAuth2Clients { + if oAuth2Client["redirect_uri"] == redirectUri { + id := fmt.Sprint(oAuth2Client["id"]) + return &id, nil + } + } + return nil, errors.New(fmt.Sprintf("redirect_uri not found: %s", redirectUri)) +} + +func resourceOAuth2ClientRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + var diags diag.Diagnostics + + c := m.(*APIClient) + id := d.Id() + + request := c.client.Api.MailcowGetOAuth2Client(ctx, id) + + oAuth2Client, err := readRequest(request) + if err != nil { + return diag.FromErr(err) + } + + err = setResourceData(resourceOAuth2Client(), d, &oAuth2Client, nil, nil) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(id) + + return diags +} + +func resourceOAuth2ClientDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(*APIClient) + mailcowDeleteRequest := api.NewDeleteOAuth2ClientRequest() + diags, _ := mailcowDelete(ctx, d, mailcowDeleteRequest, c) + return diags +} diff --git a/mailcow/resource_oauth2_client_test.go b/mailcow/resource_oauth2_client_test.go new file mode 100644 index 0000000..8019210 --- /dev/null +++ b/mailcow/resource_oauth2_client_test.go @@ -0,0 +1,53 @@ +package mailcow + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccResourceOAuth2Client(t *testing.T) { + redirectUri := "https:/redirect.uri" + resource.UnitTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testAccResourceOAuth2ClientSimple(redirectUri), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("mailcow_oauth2_client.client", "redirect_uri", redirectUri), + resource.TestCheckResourceAttr("mailcow_oauth2_client.client", "scope", "profile"), + resource.TestMatchResourceAttr("mailcow_oauth2_client.client", "client_id", regexp.MustCompile("^[a-z0-9]{12}$")), + resource.TestMatchResourceAttr("mailcow_oauth2_client.client", "client_secret", regexp.MustCompile("^[a-z0-9]{24}$")), + ), + }, + { + Config: testAccResourceOAuth2ClientError(redirectUri), + ExpectError: regexp.MustCompile("Error running apply"), + }, + }, + }) +} + +func testAccResourceOAuth2ClientSimple(redirectUri string) string { + return fmt.Sprintf(` +resource "mailcow_oauth2_client" "client" { + redirect_uri = "%[1]s" +} +`, redirectUri) +} + +func testAccResourceOAuth2ClientError(redirectUri string) string { + return fmt.Sprintf(` +resource "mailcow_oauth2_client" "client1" { + redirect_uri = "%[1]s" +} + +resource "mailcow_oauth2_client" "client2" { + depends_on = [ mailcow_oauth2_client.client1 ] + redirect_uri = "%[1]s" +} +`, redirectUri) +} diff --git a/mailcow/verbs.go b/mailcow/verbs.go index 5354abf..eb2bb2c 100644 --- a/mailcow/verbs.go +++ b/mailcow/verbs.go @@ -52,6 +52,19 @@ func readRequest(request api.ApiMailcowGetRequest) (map[string]interface{}, erro return result, nil } +func readAllRequest(request api.ApiMailcowGetAllRequest) ([]map[string]interface{}, error) { + response, err := request.ApiService.MailcowGetAllExecute(request) + if err != nil { + return nil, err + } + result := make([]map[string]interface{}, 0) + err = json.NewDecoder(response.Body).Decode(&result) + if err != nil { + return nil, err + } + return result, nil +} + func updateRequestSetAttr(mailcowUpdateRequest *api.MailcowUpdateRequest, res *schema.Resource, data *schema.ResourceData, exclude *[]string, mapArguments *map[string]string) { for argument := range (*res).Schema { log.Print("[TRACE] updateRequestSetAttr argument: ", argument) diff --git a/templates/resources/oauth2_client.md.tmpl b/templates/resources/oauth2_client.md.tmpl new file mode 100644 index 0000000..9e1251a --- /dev/null +++ b/templates/resources/oauth2_client.md.tmpl @@ -0,0 +1,21 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "" +description: |- +--- + +# {{.Name}} ({{.Type}}) + +Provides an OAuth2 client for mailcow. +This can be used to create and delete OAuth2 clients for mailcow. + +## Example Usage +{{ tffile (printf "examples/resources/%s/resource.tf" .Name)}} + +{{ .SchemaMarkdown | trimspace }} + +## Restriction + +The mailcow API does not return the id of the OAuth2 client as response for creation. +As workaround the redirect uri is used as identifier for the resource. +As consequence creation of an OAuth2 client with an existing redirect uri is prohibited.