Repository: cloudflare/cloudflare-go
Pull request #1921 (2024-05-02 16:35:29)

AUTH-5816 adds scim_config to Access applications
## Description

Adds configuration for SCIM provisioning to Access applications.

## Has your change been tested?

Automated tests

## Types of changes

What sort of change does your code introduce/modify?

- [ ] New feature (non-breaking change which adds functionality)

## Checklist:

- [x] My code follows the code style of this project.
- [-] My change requires a change to the documentation.
- [x] I have updated the documentation accordingly.
- [x] I have added tests to cover my changes.
- [x] All new and existing tests passed.
- [x] This change is using publicly documented in [cloudflare/api-schemas](https://github.com/cloudflare/api-schemas) 
      and relies on stable APIs


github-actions[bot]: Oops! It looks like no changelog entry is attached to this PR. Please include a release note as described in https://github.com/cloudflare/cloudflare-go/blob/master/docs/changelog-process.md.

Example: 

~~~
```release-note:TYPE
Release note
```
~~~

If you do not require a release note to be included, please add the `workflow/skip-changelog-entry` label.
jacobbednarz: thanks @khiller-cf. are you able to point me at the public schema docs for this one? if they are in the staging pipeline, feel free to chat me a link instead.

Patch:
diff --git a/.changelog/1921.txt b/.changelog/1921.txt
new file mode 100644
index 000000000..a36bc61f6
--- /dev/null
+++ b/.changelog/1921.txt
@@ -0,0 +1,3 @@
+```release-note:enhancement
+access_application: add support for `scim_config`
+```
\ No newline at end of file
diff --git a/access_application.go b/access_application.go
index fa426643e..aeca100dd 100644
--- a/access_application.go
+++ b/access_application.go
@@ -2,6 +2,7 @@ package cloudflare
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"net/http"
 	"time"
@@ -56,6 +57,7 @@ type AccessApplication struct {
 	OptionsPreflightBypass   *bool                          `json:"options_preflight_bypass,omitempty"`
 	CustomPages              []string                       `json:"custom_pages,omitempty"`
 	Tags                     []string                       `json:"tags,omitempty"`
+	SCIMConfig               *AccessApplicationSCIMConfig   `json:"scim_config,omitempty"`
 	AccessAppLauncherCustomization
 }
 
@@ -76,6 +78,92 @@ type AccessApplicationCorsHeaders struct {
 	MaxAge           int      `json:"max_age,omitempty"`
 }
 
+// AccessApplicationSCIMConfig represents the configuration for provisioning to an Access Application via SCIM.
+type AccessApplicationSCIMConfig struct {
+	Enabled            *bool                                    `json:"enabled,omitempty"`
+	RemoteURI          string                                   `json:"remote_uri,omitempty"`
+	Authentication     *AccessApplicationScimAuthenticationJson `json:"authentication,omitempty"`
+	IdPUID             string                                   `json:"idp_uid,omitempty"`
+	DeactivateOnDelete *bool                                    `json:"deactivate_on_delete,omitempty"`
+	Mappings           []*AccessApplicationScimMapping          `json:"mappings,omitempty"`
+}
+
+type AccessApplicationScimAuthenticationScheme string
+
+const (
+	AccessApplicationScimAuthenticationSchemeHttpBasic        AccessApplicationScimAuthenticationScheme = "httpbasic"
+	AccessApplicationScimAuthenticationSchemeOauthBearerToken AccessApplicationScimAuthenticationScheme = "oauthbearertoken"
+	AccessApplicationScimAuthenticationSchemeOauth2           AccessApplicationScimAuthenticationScheme = "oauth2"
+)
+
+type AccessApplicationScimAuthenticationJson struct {
+	Value AccessApplicationScimAuthentication
+}
+
+func (a *AccessApplicationScimAuthenticationJson) UnmarshalJSON(buf []byte) error {
+	var scheme baseScimAuthentication
+	if err := json.Unmarshal(buf, &scheme); err != nil {
+		return err
+	}
+
+	switch scheme.Scheme {
+	case AccessApplicationScimAuthenticationSchemeHttpBasic:
+		a.Value = new(AccessApplicationScimAuthenticationHttpBasic)
+	case AccessApplicationScimAuthenticationSchemeOauthBearerToken:
+		a.Value = new(AccessApplicationScimAuthenticationOauthBearerToken)
+	case AccessApplicationScimAuthenticationSchemeOauth2:
+		a.Value = new(AccessApplicationScimAuthenticationOauth2)
+	default:
+		return errors.New("invalid authentication scheme")
+	}
+
+	return json.Unmarshal(buf, a.Value)
+}
+
+type AccessApplicationScimAuthentication interface {
+	isScimAuthentication()
+}
+
+type baseScimAuthentication struct {
+	Scheme AccessApplicationScimAuthenticationScheme `json:"scheme"`
+}
+
+func (baseScimAuthentication) isScimAuthentication() {}
+
+type AccessApplicationScimAuthenticationHttpBasic struct {
+	baseScimAuthentication
+	User     string `json:"user"`
+	Password string `json:"password"`
+}
+
+type AccessApplicationScimAuthenticationOauthBearerToken struct {
+	baseScimAuthentication
+	Token string `json:"token"`
+}
+
+type AccessApplicationScimAuthenticationOauth2 struct {
+	baseScimAuthentication
+	ClientID         string   `json:"client_id"`
+	ClientSecret     string   `json:"client_secret"`
+	AuthorizationURL string   `json:"authorization_url"`
+	TokenURL         string   `json:"token_url"`
+	Scopes           []string `json:"scopes,omitempty"`
+}
+
+type AccessApplicationScimMapping struct {
+	Schema           string                                  `json:"schema"`
+	Enabled          *bool                                   `json:"enabled,omitempty"`
+	Filter           string                                  `json:"filter,omitempty"`
+	TransformJsonata string                                  `json:"transform_jsonata,omitempty"`
+	Operations       *AccessApplicationScimMappingOperations `json:"operations,omitempty"`
+}
+
+type AccessApplicationScimMappingOperations struct {
+	Create *bool `json:"create,omitempty"`
+	Update *bool `json:"update,omitempty"`
+	Delete *bool `json:"delete,omitempty"`
+}
+
 // AccessApplicationListResponse represents the response from the list
 // access applications endpoint.
 type AccessApplicationListResponse struct {
@@ -155,6 +243,7 @@ type AccessLandingPageDesign struct {
 	ButtonColor     string `json:"button_color"`
 	ButtonTextColor string `json:"button_text_color"`
 }
+
 type ListAccessApplicationsParams struct {
 	ResultInfo
 }
@@ -187,6 +276,7 @@ type CreateAccessApplicationParams struct {
 	AllowAuthenticateViaWarp *bool                          `json:"allow_authenticate_via_warp,omitempty"`
 	CustomPages              []string                       `json:"custom_pages,omitempty"`
 	Tags                     []string                       `json:"tags,omitempty"`
+	SCIMConfig               *AccessApplicationSCIMConfig   `json:"scim_config,omitempty"`
 	AccessAppLauncherCustomization
 }
 
@@ -219,6 +309,7 @@ type UpdateAccessApplicationParams struct {
 	OptionsPreflightBypass   *bool                          `json:"options_preflight_bypass,omitempty"`
 	CustomPages              []string                       `json:"custom_pages,omitempty"`
 	Tags                     []string                       `json:"tags,omitempty"`
+	SCIMConfig               *AccessApplicationSCIMConfig   `json:"scim_config,omitempty"`
 	AccessAppLauncherCustomization
 }
 


Test patch:
diff --git a/access_application_test.go b/access_application_test.go
index c7f443381..7b8da1d63 100644
--- a/access_application_test.go
+++ b/access_application_test.go
@@ -1013,3 +1013,182 @@ func TestCreateApplicationWithAccessAppLauncherCustomization(t *testing.T) {
 		assert.Equal(t, fullAccessApplication, actual)
 	}
 }
+
+func TestCreateAccessApplicationWithSCIMProvisioning(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodPost, r.Method, "Expected method 'POST', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprintf(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": {
+				"id": "480f4f69-1a28-4fdd-9240-1ed29f0ac1db",
+				"created_at": "2014-01-01T05:20:00.12345Z",
+				"updated_at": "2014-01-01T05:20:00.12345Z",
+				"aud": "737646a56ab1df6ec9bddc7e5ca84eaf3b0768850f3ffb5d74f1534911fe3893",
+				"name": "Admin SCIM App",
+				"domain": "example.cloudflareaccess.com/cdn-cgi/access/sso/oidc/737646a56ab1df6ec9bddc7e5ca84eaf3b0768850f3ffb5d74f1534911fe3893",
+				"type": "saas",
+				"session_duration": "24h",
+				"allowed_idps": [],
+				"auto_redirect_to_identity": false,
+				"enable_binding_cookie": false,
+				"custom_deny_url": "https://www.example.com",
+				"custom_deny_message": "denied!",
+				"logo_url": "https://www.example.com/example.png",
+				"skip_interstitial": true,
+				"app_launcher_visible": true,
+				"service_auth_401_redirect": true,
+				"custom_non_identity_deny_url": "https://blocked.com",
+				"tags": ["engineers"],
+				"scim_config": {
+					"enabled": true,
+					"remote_uri": "https://scim.com",
+					"authentication": {
+						 "scheme": "oauthbearertoken",
+						 "token": "1234567890"
+					},
+					"idp_uid": "1234567",
+					"deactivate_on_delete": true,
+					"mappings": [
+						{
+							"schema": "urn:ietf:params:scim:schemas:core:2.0:User",
+							"enabled": true,
+							"filter": "title pr or userType eq \"Intern\"",
+							"transform_jsonata": "$merge([$, {'userName': $substringBefore($.userName, '@') & '+test@' & $substringAfter($.userName, '@')}])",
+							"operations": {
+								"create": true,
+								"update": true,
+								"delete": true
+							}
+						}
+					]
+				}
+			}
+		}
+		`)
+	}
+
+	createdAt, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z")
+	updatedAt, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z")
+	fullAccessApplication := AccessApplication{
+		ID:                       "480f4f69-1a28-4fdd-9240-1ed29f0ac1db",
+		Name:                     "Admin SCIM App",
+		Domain:                   "example.cloudflareaccess.com/cdn-cgi/access/sso/oidc/737646a56ab1df6ec9bddc7e5ca84eaf3b0768850f3ffb5d74f1534911fe3893",
+		Type:                     "saas",
+		SessionDuration:          "24h",
+		AUD:                      "737646a56ab1df6ec9bddc7e5ca84eaf3b0768850f3ffb5d74f1534911fe3893",
+		AllowedIdps:              []string{},
+		AutoRedirectToIdentity:   BoolPtr(false),
+		EnableBindingCookie:      BoolPtr(false),
+		AppLauncherVisible:       BoolPtr(true),
+		ServiceAuth401Redirect:   BoolPtr(true),
+		CustomDenyMessage:        "denied!",
+		CustomDenyURL:            "https://www.example.com",
+		LogoURL:                  "https://www.example.com/example.png",
+		SkipInterstitial:         BoolPtr(true),
+		CreatedAt:                &createdAt,
+		UpdatedAt:                &updatedAt,
+		CustomNonIdentityDenyURL: "https://blocked.com",
+		Tags:                     []string{"engineers"},
+		SCIMConfig: &AccessApplicationSCIMConfig{
+			Enabled:   BoolPtr(true),
+			RemoteURI: "https://scim.com",
+			Authentication: &AccessApplicationScimAuthenticationJson{
+				Value: &AccessApplicationScimAuthenticationOauthBearerToken{
+					Token:                  "1234567890",
+					baseScimAuthentication: baseScimAuthentication{Scheme: AccessApplicationScimAuthenticationSchemeOauthBearerToken},
+				},
+			},
+			IdPUID:             "1234567",
+			DeactivateOnDelete: BoolPtr(true),
+			Mappings: []*AccessApplicationScimMapping{
+				{
+					Schema:           "urn:ietf:params:scim:schemas:core:2.0:User",
+					Enabled:          BoolPtr(true),
+					Filter:           "title pr or userType eq \"Intern\"",
+					TransformJsonata: "$merge([$, {'userName': $substringBefore($.userName, '@') & '+test@' & $substringAfter($.userName, '@')}])",
+					Operations: &AccessApplicationScimMappingOperations{
+						Create: BoolPtr(true),
+						Update: BoolPtr(true),
+						Delete: BoolPtr(true),
+					},
+				},
+			},
+		},
+	}
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/access/apps", handler)
+
+	actual, err := client.CreateAccessApplication(context.Background(), AccountIdentifier(testAccountID), CreateAccessApplicationParams{
+		Name: "Admin Saas Site",
+		SCIMConfig: &AccessApplicationSCIMConfig{
+			Enabled:   BoolPtr(true),
+			RemoteURI: "https://scim.com",
+			Authentication: &AccessApplicationScimAuthenticationJson{
+				Value: &AccessApplicationScimAuthenticationOauthBearerToken{
+					Token:                  "1234567890",
+					baseScimAuthentication: baseScimAuthentication{Scheme: AccessApplicationScimAuthenticationSchemeOauthBearerToken},
+				},
+			},
+			IdPUID:             "1234567",
+			DeactivateOnDelete: BoolPtr(true),
+			Mappings: []*AccessApplicationScimMapping{
+				{
+					Schema:           "urn:ietf:params:scim:schemas:core:2.0:User",
+					Enabled:          BoolPtr(true),
+					Filter:           "title pr or userType eq \"Intern\"",
+					TransformJsonata: "$merge([$, {'userName': $substringBefore($.userName, '@') & '+test@' & $substringAfter($.userName, '@')}])",
+					Operations: &AccessApplicationScimMappingOperations{
+						Create: BoolPtr(true),
+						Update: BoolPtr(true),
+						Delete: BoolPtr(true),
+					},
+				},
+			},
+		},
+	})
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, fullAccessApplication, actual)
+	}
+
+	mux.HandleFunc("/zones/"+testZoneID+"/access/apps", handler)
+
+	actual, err = client.CreateAccessApplication(context.Background(), ZoneIdentifier(testZoneID), CreateAccessApplicationParams{
+		Name: "Admin SCIM Site",
+		SCIMConfig: &AccessApplicationSCIMConfig{
+			Enabled:   BoolPtr(true),
+			RemoteURI: "https://scim.com",
+			Authentication: &AccessApplicationScimAuthenticationJson{
+				Value: &AccessApplicationScimAuthenticationOauthBearerToken{
+					Token:                  "1234567890",
+					baseScimAuthentication: baseScimAuthentication{Scheme: AccessApplicationScimAuthenticationSchemeOauthBearerToken},
+				},
+			},
+			IdPUID:             "1234567",
+			DeactivateOnDelete: BoolPtr(true),
+			Mappings: []*AccessApplicationScimMapping{
+				{
+					Schema:           "urn:ietf:params:scim:schemas:core:2.0:User",
+					Enabled:          BoolPtr(true),
+					Filter:           "title pr or userType eq \"Intern\"",
+					TransformJsonata: "$merge([$, {'userName': $substringBefore($.userName, '@') & '+test@' & $substringAfter($.userName, '@')}])",
+					Operations: &AccessApplicationScimMappingOperations{
+						Create: BoolPtr(true),
+						Update: BoolPtr(true),
+						Delete: BoolPtr(true),
+					},
+				},
+			},
+		},
+	})
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, fullAccessApplication, actual)
+	}
+}