feat: actions v2 for functions (#9420)

# Which Problems Are Solved

Actions v2 are not executed in different functions, as provided by the
actions v1.

# How the Problems Are Solved

Add functionality to call actions v2 through OIDC and SAML logic to
complement tokens and SAMLResponses.

# Additional Changes

- Corrected testing for retrieved intent information
- Added testing for IDP types
- Corrected handling of context for issuer in SAML logic

# Additional Context

- Closes #7247 
- Dependent on https://github.com/zitadel/saml/pull/97
- docs for migration are done in separate issue:
https://github.com/zitadel/zitadel/issues/9456

---------

Co-authored-by: Silvan <27845747+adlerhurst@users.noreply.github.com>
This commit is contained in:
Stefan Benz
2025-03-04 12:09:30 +01:00
committed by GitHub
parent d9d8339813
commit 0c87a96e2c
29 changed files with 1964 additions and 380 deletions

View File

@@ -3,6 +3,7 @@ package saml
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
@@ -26,7 +27,9 @@ import (
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/handler/crdb"
"github.com/zitadel/zitadel/internal/execution"
"github.com/zitadel/zitadel/internal/query"
exec_repo "github.com/zitadel/zitadel/internal/repository/execution"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
@@ -37,7 +40,8 @@ var _ provider.AuthStorage = &Storage{}
var _ provider.UserStorage = &Storage{}
const (
LoginClientHeader = "x-zitadel-login-client"
LoginClientHeader = "x-zitadel-login-client"
AttributeActionLogFormat = "urn:zitadel:iam:action:%s:log"
)
type Storage struct {
@@ -380,9 +384,86 @@ func (p *Storage) getCustomAttributes(ctx context.Context, user *query.User, use
return nil, err
}
}
function := exec_repo.ID(domain.ExecutionTypeFunction, domain.ActionFunctionPreSAMLResponse.LocalizationKey())
executionTargets, err := execution.QueryExecutionTargetsForFunction(ctx, p.query, function)
if err != nil {
return nil, err
}
// correct time for utc
user.CreationDate = user.CreationDate.UTC()
user.ChangeDate = user.ChangeDate.UTC()
info := &ContextInfo{
Function: function,
User: user,
UserGrants: userGrants.UserGrants,
}
resp, err := execution.CallTargets(ctx, executionTargets, info)
if err != nil {
return nil, err
}
contextInfoResponse, ok := resp.(*ContextInfoResponse)
if !ok || contextInfoResponse == nil {
return customAttributes, nil
}
attributeLogs := make([]string, 0)
for _, metadata := range contextInfoResponse.SetUserMetadata {
if _, err = p.command.SetUserMetadata(ctx, metadata, user.ID, user.ResourceOwner); err != nil {
attributeLogs = append(attributeLogs, fmt.Sprintf("failed to set user metadata key %q", metadata.Key))
}
}
for _, attribute := range contextInfoResponse.AppendAttribute {
customAttributes = appendCustomAttribute(customAttributes, attribute.Name, attribute.NameFormat, attribute.Value)
}
if len(attributeLogs) > 0 {
customAttributes = appendCustomAttribute(customAttributes, fmt.Sprintf(AttributeActionLogFormat, function), "", attributeLogs)
}
return customAttributes, nil
}
type ContextInfo struct {
Function string `json:"function,omitempty"`
User *query.User `json:"user,omitempty"`
UserGrants []*query.UserGrant `json:"user_grants,omitempty"`
Response *ContextInfoResponse `json:"response,omitempty"`
}
type ContextInfoResponse struct {
SetUserMetadata []*domain.Metadata `json:"set_user_metadata,omitempty"`
AppendAttribute []*AppendAttribute `json:"append_attribute,omitempty"`
}
type AppendAttribute struct {
Name string `json:"name"`
NameFormat string `json:"name_format"`
Value []string `json:"value"`
}
func (c *ContextInfo) GetHTTPRequestBody() []byte {
data, err := json.Marshal(c)
if err != nil {
return nil
}
return data
}
func (c *ContextInfo) SetHTTPResponseBody(resp []byte) error {
if !json.Valid(resp) {
return zerrors.ThrowPreconditionFailed(nil, "ACTION-4m9s2", "Errors.Execution.ResponseIsNotValidJSON")
}
if c.Response == nil {
c.Response = &ContextInfoResponse{}
}
return json.Unmarshal(resp, c.Response)
}
func (c *ContextInfo) GetContent() interface{} {
return c.Response
}
func (p *Storage) getGrants(ctx context.Context, userID, applicationID string) (*query.UserGrants, error) {
projectID, err := p.query.ProjectIDFromClientID(ctx, applicationID)
if err != nil {