Repository: Nerzal/gocloak
Pull request #360 (2022-08-03 12:44:24)

feat: add functionality to register, get, list and delete required ac…
### What this MR does / why we need it:

The current API only allows to update existing required-actions, but the Keycloak REST API also support:
* register
* get
* delete

so this pull request adds these abilities to the api to. 

Nerzal: Hi, can you please run

> go fmt -w -s .

That should fix the pipeline
svzieg: Hey, 

Hopefully, this now should be fixed, but I had to use go 1.19. go 1.18 didn't format anything.
codecov[bot]: # [Codecov](https://codecov.io/gh/Nerzal/gocloak/pull/360?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nerzal) Report
> Merging [#360](https://codecov.io/gh/Nerzal/gocloak/pull/360?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nerzal) (6c4485b) into [main](https://codecov.io/gh/Nerzal/gocloak/commit/1c89abf61f250a344b37bd27fb79c64363892a3e?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nerzal) (1c89abf) will **decrease** coverage by `0.16%`.
> The diff coverage is `63.63%`.

```diff
@@            Coverage Diff             @@
##             main     #360      +/-   ##
==========================================
- Coverage   77.13%   76.96%   -0.17%     
==========================================
  Files           4        4              
  Lines        2038     2071      +33     
==========================================
+ Hits         1572     1594      +22     
- Misses        321      326       +5     
- Partials      145      151       +6     
```


| [Impacted Files](https://codecov.io/gh/Nerzal/gocloak/pull/360?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nerzal) | Coverage Δ | |
|---|---|---|
| [client.go](https://codecov.io/gh/Nerzal/gocloak/pull/360/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nerzal#diff-Y2xpZW50Lmdv) | `75.17% <63.63%> (-0.21%)` | :arrow_down: |
| [models.go](https://codecov.io/gh/Nerzal/gocloak/pull/360/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nerzal#diff-bW9kZWxzLmdv) | `93.28% <0.00%> (+0.67%)` | :arrow_up: |

:mega: Codecov can now indicate which changes are the most critical in Pull Requests. [Learn more](https://about.codecov.io/product/feature/runtime-insights/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nerzal)

Nerzal: looks good to me, thank you :) 

Might tight a day or two until i create a new tag, as i want to create a new release, when my PullRequest with the changes for keycloak 17+ is done

Patch:
diff --git a/client.go b/client.go
index 38498f4..d4c88c6 100644
--- a/client.go
+++ b/client.go
@@ -2196,7 +2196,7 @@ func (client *gocloak) ClearKeysCache(ctx context.Context, token, realm string)
 	return checkForError(resp, err, errMessage)
 }
 
-//GetAuthenticationFlows get all authentication flows from a realm
+// GetAuthenticationFlows get all authentication flows from a realm
 func (client *gocloak) GetAuthenticationFlows(ctx context.Context, token, realm string) ([]*AuthenticationFlowRepresentation, error) {
 	const errMessage = "could not retrieve authentication flows"
 	var result []*AuthenticationFlowRepresentation
@@ -2210,7 +2210,7 @@ func (client *gocloak) GetAuthenticationFlows(ctx context.Context, token, realm
 	return result, nil
 }
 
-//Create a new Authentication flow in a realm
+// Create a new Authentication flow in a realm
 func (client *gocloak) CreateAuthenticationFlow(ctx context.Context, token, realm string, flow AuthenticationFlowRepresentation) error {
 	const errMessage = "could not create authentication flows"
 	var result []*AuthenticationFlowRepresentation
@@ -2221,7 +2221,7 @@ func (client *gocloak) CreateAuthenticationFlow(ctx context.Context, token, real
 	return checkForError(resp, err, errMessage)
 }
 
-//DeleteAuthenticationFlow deletes a flow in a realm with the given ID
+// DeleteAuthenticationFlow deletes a flow in a realm with the given ID
 func (client *gocloak) DeleteAuthenticationFlow(ctx context.Context, token, realm, flowID string) error {
 	const errMessage = "could not delete authentication flows"
 	resp, err := client.getRequestWithBearerAuth(ctx, token).
@@ -2230,7 +2230,7 @@ func (client *gocloak) DeleteAuthenticationFlow(ctx context.Context, token, real
 	return checkForError(resp, err, errMessage)
 }
 
-//GetAuthenticationExecutions retrieves all executions of a given flow
+// GetAuthenticationExecutions retrieves all executions of a given flow
 func (client *gocloak) GetAuthenticationExecutions(ctx context.Context, token, realm, flow string) ([]*ModifyAuthenticationExecutionRepresentation, error) {
 	const errMessage = "could not retrieve authentication flows"
 	var result []*ModifyAuthenticationExecutionRepresentation
@@ -2244,7 +2244,7 @@ func (client *gocloak) GetAuthenticationExecutions(ctx context.Context, token, r
 	return result, nil
 }
 
-//CreateAuthenticationExecution creates a new execution for the given flow name in the given realm
+// CreateAuthenticationExecution creates a new execution for the given flow name in the given realm
 func (client *gocloak) CreateAuthenticationExecution(ctx context.Context, token, realm, flow string, execution CreateAuthenticationExecutionRepresentation) error {
 	const errMessage = "could not create authentication execution"
 	resp, err := client.getRequestWithBearerAuth(ctx, token).SetBody(execution).
@@ -2253,7 +2253,7 @@ func (client *gocloak) CreateAuthenticationExecution(ctx context.Context, token,
 	return checkForError(resp, err, errMessage)
 }
 
-//UpdateAuthenticationExecution updates an authentication execution for the given flow in the given realm
+// UpdateAuthenticationExecution updates an authentication execution for the given flow in the given realm
 func (client *gocloak) UpdateAuthenticationExecution(ctx context.Context, token, realm, flow string, execution ModifyAuthenticationExecutionRepresentation) error {
 	const errMessage = "could not update authentication execution"
 	resp, err := client.getRequestWithBearerAuth(ctx, token).SetBody(execution).
@@ -2271,7 +2271,7 @@ func (client *gocloak) DeleteAuthenticationExecution(ctx context.Context, token,
 	return checkForError(resp, err, errMessage)
 }
 
-//CreateAuthenticationExecutionFlow creates a new execution for the given flow name in the given realm
+// CreateAuthenticationExecutionFlow creates a new execution for the given flow name in the given realm
 func (client *gocloak) CreateAuthenticationExecutionFlow(ctx context.Context, token, realm, flow string, executionFlow CreateAuthenticationExecutionFlowRepresentation) error {
 	const errMessage = "could not create authentication execution flow"
 	resp, err := client.getRequestWithBearerAuth(ctx, token).SetBody(executionFlow).
@@ -3758,6 +3758,57 @@ func (client *gocloak) CreateClientScopesScopeMappingsRealmRoles(ctx context.Con
 	return checkForError(resp, err, errMessage)
 }
 
+// CreateRequiredAction creates a required action for a given realm
+func (client *gocloak) RegisterRequiredAction(ctx context.Context, token string, realm string, requiredAction RequiredActionProviderRepresentation) error {
+	const errMessage = "could not create required action"
+
+	resp, err := client.getRequestWithBearerAuth(ctx, token).
+		SetBody(requiredAction).
+		Post(client.getAdminRealmURL(realm, "authentication", "register-required-action"))
+
+	if err := checkForError(resp, err, errMessage); err != nil {
+		return err
+	}
+
+	return err
+}
+
+// GetRequiredActions gets a list of required actions for a given realm
+func (client *gocloak) GetRequiredActions(ctx context.Context, token string, realm string) ([]*RequiredActionProviderRepresentation, error) {
+	const errMessage = "could not get required actions"
+	var result []*RequiredActionProviderRepresentation
+
+	resp, err := client.getRequestWithBearerAuth(ctx, token).
+		SetResult(&result).
+		Get(client.getAdminRealmURL(realm, "authentication", "required-actions"))
+
+	if err := checkForError(resp, err, errMessage); err != nil {
+		return nil, err
+	}
+
+	return result, err
+}
+
+// GetRequiredAction gets a required action for a given realm
+func (client *gocloak) GetRequiredAction(ctx context.Context, token string, realm string, alias string) (*RequiredActionProviderRepresentation, error) {
+	const errMessage = "could not get required action"
+	var result RequiredActionProviderRepresentation
+
+	if alias == "" {
+		return nil, errors.New("alias is required for getting a required action")
+	}
+
+	resp, err := client.getRequestWithBearerAuth(ctx, token).
+		SetResult(&result).
+		Get(client.getAdminRealmURL(realm, "authentication", "required-actions", alias))
+
+	if err := checkForError(resp, err, errMessage); err != nil {
+		return nil, err
+	}
+
+	return &result, err
+}
+
 // UpdateRequiredAction updates a required action for a given realm
 func (client *gocloak) UpdateRequiredAction(ctx context.Context, token string, realm string, requiredAction RequiredActionProviderRepresentation) error {
 	const errMessage = "could not update required action"
@@ -3772,6 +3823,23 @@ func (client *gocloak) UpdateRequiredAction(ctx context.Context, token string, r
 	return err
 }
 
+// DeleteRequiredAction updates a required action for a given realm
+func (client *gocloak) DeleteRequiredAction(ctx context.Context, token string, realm string, alias string) error {
+	const errMessage = "could not delete required action"
+
+	if alias == "" {
+		return errors.New("alias is required for deleting a required action")
+	}
+	resp, err := client.getRequestWithBearerAuth(ctx, token).
+		Delete(client.getAdminRealmURL(realm, "authentication", "required-actions", alias))
+
+	if err := checkForError(resp, err, errMessage); err != nil {
+		return err
+	}
+
+	return err
+}
+
 // CreateClientScopesScopeMappingsClientRoles attaches a client role to a client scope (not client's scope)
 func (client *gocloak) CreateClientScopesScopeMappingsClientRoles(
 	ctx context.Context, token, realm, idOfClientScope, idOfClient string, roles []Role,
diff --git a/gocloak.go b/gocloak.go
index 91c4af3..ac80346 100644
--- a/gocloak.go
+++ b/gocloak.go
@@ -522,6 +522,14 @@ type GoCloak interface {
 	// RequiredActions API
 	// -------------------
 
+	// UpdateRequiredAction updates a required action for a given realm
+	RegisterRequiredAction(ctx context.Context, token string, realm string, requiredAction RequiredActionProviderRepresentation) error
 	// UpdateRequiredAction updates a required action for a given realm
 	UpdateRequiredAction(ctx context.Context, token string, realm string, requiredAction RequiredActionProviderRepresentation) error
+	// UpdateRequiredAction updates a required action for a given realm
+	GetRequiredAction(ctx context.Context, token string, realm string, alias string) (*RequiredActionProviderRepresentation, error)
+	// UpdateRequiredAction updates a required action for a given realm
+	GetRequiredActions(ctx context.Context, token string, realm string) ([]*RequiredActionProviderRepresentation, error)
+	// UpdateRequiredAction updates a required action for a given realm
+	DeleteRequiredAction(ctx context.Context, token string, realm string, alias string) error
 }


Test patch:
diff --git a/client_test.go b/client_test.go
index 6a4209d..55524ba 100644
--- a/client_test.go
+++ b/client_test.go
@@ -528,9 +528,10 @@ func NewClientWithDebug(t testing.TB) gocloak.GoCloak {
 }
 
 // FailRequest fails requests and returns an error
-//   err - returned error or nil to return the default error
-//   failN - number of requests to be failed
-//   skipN = number of requests to be executed and not failed by this function
+//
+//	err - returned error or nil to return the default error
+//	failN - number of requests to be failed
+//	skipN = number of requests to be executed and not failed by this function
 func FailRequest(client gocloak.GoCloak, err error, failN, skipN int) gocloak.GoCloak {
 	client.RestyClient().OnBeforeRequest(
 		func(c *resty.Client, r *resty.Request) error {
@@ -6490,6 +6491,68 @@ func TestGocloak_CreateAuthenticationFlowsAndCreateAuthenticationExecutionAndFlo
 	require.True(t, deleted, "Failed to delete authentication flow, no flow was deleted")
 }
 
+func TestGocloak_CreateAndGetRequiredAction(t *testing.T) {
+	t.Parallel()
+	cfg := GetConfig(t)
+	client := NewClientWithDebug(t)
+	token := GetAdminToken(t, client)
+	requiredAction := gocloak.RequiredActionProviderRepresentation{
+		Alias:         gocloak.StringP("VERIFY_EMAIL_NEW"),
+		Config:        nil,
+		DefaultAction: gocloak.BoolP(false),
+		Enabled:       gocloak.BoolP(true),
+		Name:          gocloak.StringP("Verify Email new"),
+		Priority:      gocloak.Int32P(50),
+		ProviderID:    gocloak.StringP("VERIFY_EMAIL_NEW"),
+	}
+	err := client.RegisterRequiredAction(context.Background(), token.AccessToken, cfg.GoCloak.Realm, requiredAction)
+	require.NoError(t, err, "Failed to register required action")
+
+	ra, err := client.GetRequiredAction(context.Background(), token.AccessToken, cfg.GoCloak.Realm, *requiredAction.Alias)
+	require.NoError(t, err, "Failed to get required action")
+	require.NotNil(t, ra, "required action created must not be nil")
+	require.Equal(t, *ra.Alias, *requiredAction.Alias, "required action alias must be equal with template")
+	t.Logf("got required action: %+v", ra)
+
+	ras, err := client.GetRequiredActions(context.Background(), token.AccessToken, cfg.GoCloak.Realm)
+	require.NoError(t, err, "Failed to get required actions")
+
+	for _, r := range ras {
+		t.Logf("got required action: %+v", r)
+		if r.Alias != nil && *r.Alias == *ra.Alias {
+			goto FOUND_RA
+		}
+	}
+	require.Fail(t, "required action not found in list of required actions")
+
+FOUND_RA:
+
+	err = client.DeleteRequiredAction(context.Background(), token.AccessToken, cfg.GoCloak.Realm, *requiredAction.Alias)
+	require.NoError(t, err, "Failed to Delete required action")
+}
+
+func TestGocloak_GetUnknownRequiredAction(t *testing.T) {
+	t.Parallel()
+	cfg := GetConfig(t)
+	client := NewClientWithDebug(t)
+	token := GetAdminToken(t, client)
+
+	ra, err := client.GetRequiredAction(context.Background(), token.AccessToken, cfg.GoCloak.Realm, "unknown_required_action")
+	require.Error(t, err, "Request should fail if no required action with the given name is there")
+	require.Nil(t, ra, "required action created must be nil if it could not be found")
+}
+
+func TestGocloak_GetEmptyAliasRequiredAction(t *testing.T) {
+	t.Parallel()
+	cfg := GetConfig(t)
+	client := NewClientWithDebug(t)
+	token := GetAdminToken(t, client)
+
+	ra, err := client.GetRequiredAction(context.Background(), token.AccessToken, cfg.GoCloak.Realm, "")
+	require.Error(t, err, "Request should fail if no alias is given")
+	require.Nil(t, ra, "required action created must be nil if it could not be found")
+}
+
 func TestGocloak_UpdateRequiredAction(t *testing.T) {
 	t.Parallel()
 	cfg := GetConfig(t)