From 5ff51c45239ee9762a256b545d235fcafb2d0554 Mon Sep 17 00:00:00 2001 From: Akemi Davisson Date: Tue, 1 Aug 2023 09:37:24 -0500 Subject: [PATCH 1/3] AUTH-5315 adds new conditional_access_enabled attr to access IDP, auth contexts --- access_group.go | 9 +++ access_identity_provider.go | 111 +++++++++++++++++++------ access_identity_provider_test.go | 131 ++++++++++++++++++++++++++++-- access_policy_test.go | 135 +++++++++++++++++++++++++++++++ 4 files changed, 358 insertions(+), 28 deletions(-) diff --git a/access_group.go b/access_group.go index c21a80eebef..90ee26193cf 100644 --- a/access_group.go +++ b/access_group.go @@ -153,6 +153,15 @@ type AccessGroupSAML struct { } `json:"saml"` } +// AccessGroupAzureAuthContext is used to configure access based on Azure auth contexts. +type AccessGroupAzureAuthContext struct { + AuthContext struct { + ID string `json:"id"` + IdentityProviderID string `json:"identity_provider_id"` + ACID string `json:"ac_id"` + } `json:"auth_context"` +} + // AccessGroupAuthMethod is used for managing access by the "amr" // (Authentication Methods References) identifier. For example, an // application may want to require that users authenticate using a hardware diff --git a/access_identity_provider.go b/access_identity_provider.go index c248bc8969c..daf7caa85c1 100644 --- a/access_identity_provider.go +++ b/access_identity_provider.go @@ -22,29 +22,30 @@ type AccessIdentityProvider struct { // // API reference: https://developers.cloudflare.com/access/configuring-identity-providers/ type AccessIdentityProviderConfiguration struct { - APIToken string `json:"api_token,omitempty"` - AppsDomain string `json:"apps_domain,omitempty"` - Attributes []string `json:"attributes,omitempty"` - AuthURL string `json:"auth_url,omitempty"` - CentrifyAccount string `json:"centrify_account,omitempty"` - CentrifyAppID string `json:"centrify_app_id,omitempty"` - CertsURL string `json:"certs_url,omitempty"` - ClientID string `json:"client_id,omitempty"` - ClientSecret string `json:"client_secret,omitempty"` - Claims []string `json:"claims,omitempty"` - Scopes []string `json:"scopes,omitempty"` - DirectoryID string `json:"directory_id,omitempty"` - EmailAttributeName string `json:"email_attribute_name,omitempty"` - IdpPublicCert string `json:"idp_public_cert,omitempty"` - IssuerURL string `json:"issuer_url,omitempty"` - OktaAccount string `json:"okta_account,omitempty"` - OneloginAccount string `json:"onelogin_account,omitempty"` - RedirectURL string `json:"redirect_url,omitempty"` - SignRequest bool `json:"sign_request,omitempty"` - SsoTargetURL string `json:"sso_target_url,omitempty"` - SupportGroups bool `json:"support_groups,omitempty"` - TokenURL string `json:"token_url,omitempty"` - PKCEEnabled *bool `json:"pkce_enabled,omitempty"` + APIToken string `json:"api_token,omitempty"` + AppsDomain string `json:"apps_domain,omitempty"` + Attributes []string `json:"attributes,omitempty"` + AuthURL string `json:"auth_url,omitempty"` + CentrifyAccount string `json:"centrify_account,omitempty"` + CentrifyAppID string `json:"centrify_app_id,omitempty"` + CertsURL string `json:"certs_url,omitempty"` + ClientID string `json:"client_id,omitempty"` + ClientSecret string `json:"client_secret,omitempty"` + Claims []string `json:"claims,omitempty"` + Scopes []string `json:"scopes,omitempty"` + DirectoryID string `json:"directory_id,omitempty"` + EmailAttributeName string `json:"email_attribute_name,omitempty"` + IdpPublicCert string `json:"idp_public_cert,omitempty"` + IssuerURL string `json:"issuer_url,omitempty"` + OktaAccount string `json:"okta_account,omitempty"` + OneloginAccount string `json:"onelogin_account,omitempty"` + RedirectURL string `json:"redirect_url,omitempty"` + SignRequest bool `json:"sign_request,omitempty"` + SsoTargetURL string `json:"sso_target_url,omitempty"` + SupportGroups bool `json:"support_groups,omitempty"` + TokenURL string `json:"token_url,omitempty"` + PKCEEnabled *bool `json:"pkce_enabled,omitempty"` + ConditionalAccessEnabled bool `json:"conditional_access_enabled,omitempty"` } type AccessIdentityProviderScimConfiguration struct { @@ -89,6 +90,22 @@ type UpdateAccessIdentityProviderParams struct { ScimConfig AccessIdentityProviderScimConfiguration `json:"scim_config"` } +// AccessAuthContext represents an Access Azure Identity Provider Auth Context +type AccessAuthContext struct { + ID string `json:"id"` + UID string `json:"uid"` + ACID string `json:"ac_id"` + DisplayName string `json:"display_name"` + Description string `json:"description"` +} + +// AccessAuthContextsListResponse represents the response from the list +// Access Auth Contexts endpoint. +type AccessAuthContextsListResponse struct { + Result []AccessAuthContext `json:"result"` + Response +} + // ListAccessIdentityProviders returns all Access Identity Providers for an // account or zone. // @@ -235,3 +252,51 @@ func (api *API) DeleteAccessIdentityProvider(ctx context.Context, rc *ResourceCo return accessIdentityProviderResponse.Result, nil } + +// ListAccessIdentityProviderAuthContexts returns an identity provider's auth contexts +// AzureAD only +// Account API Reference: https://developers.cloudflare.com/api/operations/access-identity-providers-get-an-access-identity-provider +// Zone API Reference: https://developers.cloudflare.com/api/operations/zone-level-access-identity-providers-get-an-access-identity-provider +func (api *API) ListAccessIdentityProviderAuthContexts(ctx context.Context, rc *ResourceContainer, identityProviderID string) ([]AccessAuthContext, error) { + uri := fmt.Sprintf("/%s/%s/access/identity_providers/%s/auth_context", rc.Level, rc.Identifier, identityProviderID) + + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return []AccessAuthContext{}, err + } + + var accessAuthContextListResponse AccessAuthContextsListResponse + err = json.Unmarshal(res, &accessAuthContextListResponse) + if err != nil { + return []AccessAuthContext{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + + return accessAuthContextListResponse.Result, nil +} + +// UpdateAccessIdentityProvider updates an existing Access Identity +// Provider. +// AzureAD only +// Account API Reference: https://developers.cloudflare.com/api/operations/access-identity-providers-update-an-access-identity-provider +// Zone API Reference: https://developers.cloudflare.com/api/operations/zone-level-access-identity-providers-update-an-access-identity-provider +func (api *API) UpdateAccessIdentityProviderAuthContexts(ctx context.Context, rc *ResourceContainer, identityProviderID string) (AccessIdentityProvider, error) { + uri := fmt.Sprintf( + "/%s/%s/access/identity_providers/%s/auth_context", + rc.Level, + rc.Identifier, + identityProviderID, + ) + + res, err := api.makeRequestContext(ctx, http.MethodPut, uri, nil) + if err != nil { + return AccessIdentityProvider{}, err + } + + var accessIdentityProviderResponse AccessIdentityProviderResponse + err = json.Unmarshal(res, &accessIdentityProviderResponse) + if err != nil { + return AccessIdentityProvider{}, fmt.Errorf("%s: %w", errUnmarshalError, err) + } + + return accessIdentityProviderResponse.Result, nil +} diff --git a/access_identity_provider_test.go b/access_identity_provider_test.go index ad9b6935b9f..e9285415176 100644 --- a/access_identity_provider_test.go +++ b/access_identity_provider_test.go @@ -140,7 +140,8 @@ func TestCreateAccessIdentityProvider(t *testing.T) { "type": "github", "config": { "client_id": "example_id", - "client_secret": "a-secret-key" + "client_secret": "a-secret-key", + "conditional_access_enabled": true } } } @@ -151,8 +152,9 @@ func TestCreateAccessIdentityProvider(t *testing.T) { Name: "Widget Corps OTP", Type: "github", Config: AccessIdentityProviderConfiguration{ - ClientID: "example_id", - ClientSecret: "a-secret-key", + ClientID: "example_id", + ClientSecret: "a-secret-key", + ConditionalAccessEnabled: true, }, } @@ -161,8 +163,9 @@ func TestCreateAccessIdentityProvider(t *testing.T) { Name: "Widget Corps OTP", Type: "github", Config: AccessIdentityProviderConfiguration{ - ClientID: "example_id", - ClientSecret: "a-secret-key", + ClientID: "example_id", + ClientSecret: "a-secret-key", + ConditionalAccessEnabled: true, }, } @@ -293,3 +296,121 @@ func TestDeleteAccessIdentityProvider(t *testing.T) { assert.Equal(t, want, actual) } } + +func TestListAccessIdentityProviderAuthContexts(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": [ + { + "id": "04709095-568a-40c4-bf23-5d9edbefe21e", + "uid": "04709095-568a-40c4-bf23-5d9edbefe21e", + "ac_id": "c1", + "display_name": "test_c1", + "description": "" + }, + { + "id": "a6c9b024-8fd1-48b7-9a05-8bca3a43f758", + "uid": "a6c9b024-8fd1-48b7-9a05-8bca3a43f758", + "ac_id": "c25", + "display_name": "test_c25", + "description": "" + } + ] + } + `) + } + + want := []AccessAuthContext{ + { + ID: "04709095-568a-40c4-bf23-5d9edbefe21e", + UID: "04709095-568a-40c4-bf23-5d9edbefe21e", + ACID: "c1", + DisplayName: "test_c1", + Description: "", + }, + { + ID: "a6c9b024-8fd1-48b7-9a05-8bca3a43f758", + UID: "a6c9b024-8fd1-48b7-9a05-8bca3a43f758", + ACID: "c25", + DisplayName: "test_c25", + Description: "", + }, + } + + mux.HandleFunc("/accounts/"+testAccountID+"/access/identity_providers/f174e90a-fafe-4643-bbbc-4a0ed4fc8415/auth_context", handler) + + actual, err := client.ListAccessIdentityProviderAuthContexts(context.Background(), testAccountRC, "f174e90a-fafe-4643-bbbc-4a0ed4fc8415") + + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } + + mux.HandleFunc("/zones/"+testZoneID+"/access/identity_providers/f174e90a-fafe-4643-bbbc-4a0ed4fc8415/auth_context", handler) + + actual, err = client.ListAccessIdentityProviderAuthContexts(context.Background(), testAccountRC, "f174e90a-fafe-4643-bbbc-4a0ed4fc8415") + + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestUpdateAccessIdentityProviderAuthContext(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPut, r.Method, "Expected method 'PUT', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id": "f174e90a-fafe-4643-bbbc-4a0ed4fc8415", + "name": "Widget Corps", + "type": "AzureAD", + "config": { + "client_id": "example_id", + "client_secret": "a-secret-key", + "conditional_access_enabled": true + } + } + } + `) + } + + want := AccessIdentityProvider{ + ID: "f174e90a-fafe-4643-bbbc-4a0ed4fc8415", + Name: "Widget Corps", + Type: "AzureAD", + Config: AccessIdentityProviderConfiguration{ + ClientID: "example_id", + ClientSecret: "a-secret-key", + ConditionalAccessEnabled: true, + }, + } + + mux.HandleFunc("/accounts/"+testAccountID+"/access/identity_providers/f174e90a-fafe-4643-bbbc-4a0ed4fc8415/auth_context", handler) + + actual, err := client.UpdateAccessIdentityProviderAuthContexts(context.Background(), testAccountRC, "f174e90a-fafe-4643-bbbc-4a0ed4fc8415") + + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } + + mux.HandleFunc("/zones/"+testZoneID+"/access/identity_providers/f174e90a-fafe-4643-bbbc-4a0ed4fc8415/auth_context", handler) + + actual, err = client.UpdateAccessIdentityProviderAuthContexts(context.Background(), testAccountRC, "f174e90a-fafe-4643-bbbc-4a0ed4fc8415") + + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} diff --git a/access_policy_test.go b/access_policy_test.go index e0c16372236..045c4533787 100644 --- a/access_policy_test.go +++ b/access_policy_test.go @@ -325,6 +325,141 @@ func TestCreateAccessPolicy(t *testing.T) { } } +func TestCreateAccessPolicyAuthContextRule(t *testing.T) { + setup() + defer teardown() + expectedAccessPolicyAuthContext := AccessPolicy{ + ID: "699d98642c564d2e855e9661899b7252", + Precedence: 1, + Decision: "allow", + CreatedAt: &createdAt, + UpdatedAt: &updatedAt, + Name: "Allow devs", + Include: []interface{}{ + map[string]interface{}{"email": map[string]interface{}{"email": "test@example.com"}}, + }, + Exclude: []interface{}{}, + Require: []interface{}{ + map[string]interface{}{"auth_context": map[string]interface{}{"id": "authContextID123", "identity_provider_id": "IDPIDtest123", "ac_id": "c1"}}, + }, + IsolationRequired: &isolationRequired, + PurposeJustificationRequired: &purposeJustificationRequired, + ApprovalRequired: &approvalRequired, + PurposeJustificationPrompt: &purposeJustificationPrompt, + ApprovalGroups: []AccessApprovalGroup{ + { + EmailListUuid: "2413b6d7-bbe5-48bd-8fbb-e52069c85561", + ApprovalsNeeded: 3, + }, + { + EmailAddresses: []string{"email1@example.com", "email2@example.com"}, + ApprovalsNeeded: 1, + }, + }, + } + + 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": "699d98642c564d2e855e9661899b7252", + "precedence": 1, + "decision": "allow", + "created_at": "2014-01-01T05:20:00.12345Z", + "updated_at": "2014-01-01T05:20:00.12345Z", + "name": "Allow devs", + "include": [ + { + "email": { + "email": "test@example.com" + } + } + ], + "exclude": [], + "require": [ + { + "auth_context": { + "id": "authContextID123", + "identity_provider_id": "IDPIDtest123", + "ac_id": "c1" + } + } + ], + "isolation_required": true, + "purpose_justification_required": true, + "purpose_justification_prompt": "Please provide a business reason for your need to access before continuing.", + "approval_required": true, + "approval_groups": [ + { + "email_list_uuid": "2413b6d7-bbe5-48bd-8fbb-e52069c85561", + "approvals_needed": 3 + }, + { + "email_addresses": ["email1@example.com", "email2@example.com"], + "approvals_needed": 1 + } + ] + } + } + `) + } + + accessPolicy := CreateAccessPolicyParams{ + ApplicationID: accessApplicationID, + Name: "Allow devs", + Include: []interface{}{ + AccessGroupEmail{struct { + Email string `json:"email"` + }{Email: "test@example.com"}}, + }, + Exclude: []interface{}{}, + Require: []interface{}{ + AccessGroupAzureAuthContext{struct { + ID string `json:"id"` + IdentityProviderID string `json:"identity_provider_id"` + ACID string `json:"ac_id"` + }{ + ID: "authContextID123", + IdentityProviderID: "IDPIDtest123", + ACID: "c1", + }}, + }, + Decision: "allow", + PurposeJustificationRequired: &purposeJustificationRequired, + PurposeJustificationPrompt: &purposeJustificationPrompt, + ApprovalGroups: []AccessApprovalGroup{ + { + EmailListUuid: "2413b6d7-bbe5-48bd-8fbb-e52069c85561", + ApprovalsNeeded: 3, + }, + { + EmailAddresses: []string{"email1@example.com", "email2@example.com"}, + ApprovalsNeeded: 1, + }, + }, + } + + mux.HandleFunc("/accounts/"+testAccountID+"/access/apps/"+accessApplicationID+"/policies", handler) + + actual, err := client.CreateAccessPolicy(context.Background(), testAccountRC, accessPolicy) + + if assert.NoError(t, err) { + assert.Equal(t, expectedAccessPolicyAuthContext, actual) + } + + mux.HandleFunc("/zones/"+testZoneID+"/access/apps/"+accessApplicationID+"/policies", handler) + + actual, err = client.CreateAccessPolicy(context.Background(), testZoneRC, accessPolicy) + + if assert.NoError(t, err) { + assert.Equal(t, expectedAccessPolicyAuthContext, actual) + } +} + func TestUpdateAccessPolicy(t *testing.T) { setup() defer teardown() From 6ae3c54ae07609ea51cecc2063a4ac55d1395530 Mon Sep 17 00:00:00 2001 From: Akemi Davisson Date: Tue, 1 Aug 2023 09:45:58 -0500 Subject: [PATCH 2/3] changelog --- .changelog/1344.txt | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .changelog/1344.txt diff --git a/.changelog/1344.txt b/.changelog/1344.txt new file mode 100644 index 00000000000..f800df64e06 --- /dev/null +++ b/.changelog/1344.txt @@ -0,0 +1,11 @@ +```release-note:enhancement +access_identity_provider: add attr conditional_access_enabled +``` + +```release-note:enhancement +access_group: add auth_context group ruletype +``` + +```release-note:enhancement +access_identity_provider: add auth context list/put endpoint +``` \ No newline at end of file From 3e14140d85e9a17d92ff77abcd9bd8aaf517750a Mon Sep 17 00:00:00 2001 From: Akemi Davisson Date: Wed, 2 Aug 2023 14:14:54 -0500 Subject: [PATCH 3/3] lint --- access_identity_provider.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/access_identity_provider.go b/access_identity_provider.go index daf7caa85c1..743ae137cf2 100644 --- a/access_identity_provider.go +++ b/access_identity_provider.go @@ -90,7 +90,7 @@ type UpdateAccessIdentityProviderParams struct { ScimConfig AccessIdentityProviderScimConfiguration `json:"scim_config"` } -// AccessAuthContext represents an Access Azure Identity Provider Auth Context +// AccessAuthContext represents an Access Azure Identity Provider Auth Context. type AccessAuthContext struct { ID string `json:"id"` UID string `json:"uid"` @@ -274,10 +274,10 @@ func (api *API) ListAccessIdentityProviderAuthContexts(ctx context.Context, rc * return accessAuthContextListResponse.Result, nil } -// UpdateAccessIdentityProvider updates an existing Access Identity +// UpdateAccessIdentityProviderAuthContexts updates an existing Access Identity // Provider. // AzureAD only -// Account API Reference: https://developers.cloudflare.com/api/operations/access-identity-providers-update-an-access-identity-provider +// Account API Reference: https://developers.cloudflare.com/api/operations/access-identity-providers-refresh-an-access-identity-provider-auth-contexts // Zone API Reference: https://developers.cloudflare.com/api/operations/zone-level-access-identity-providers-update-an-access-identity-provider func (api *API) UpdateAccessIdentityProviderAuthContexts(ctx context.Context, rc *ResourceContainer, identityProviderID string) (AccessIdentityProvider, error) { uri := fmt.Sprintf(