chore: move the go code into a subfolder

This commit is contained in:
Florian Forster
2025-08-05 15:20:32 -07:00
parent 4ad22ba456
commit cd2921de26
2978 changed files with 373 additions and 300 deletions

View File

@@ -0,0 +1,141 @@
package actions
import (
"context"
"errors"
"fmt"
"github.com/dop251/goja_nodejs/require"
"github.com/sirupsen/logrus"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
)
type Config struct {
HTTP HTTPConfig
}
var ErrHalt = errors.New("interrupt")
type jsAction func(fields, fields) error
const (
actionStartedMessage = "action run started"
actionSucceededMessage = "action run succeeded"
)
func actionFailedMessage(err error) string {
return fmt.Sprintf("action run failed: %s", err.Error())
}
func Run(ctx context.Context, ctxParam contextFields, apiParam apiFields, script, name string, opts ...Option) (err error) {
config := newRunConfig(ctx, append(opts, withLogger(ctx))...)
if config.functionTimeout == 0 {
return zerrors.ThrowInternal(nil, "ACTIO-uCpCx", "Errrors.Internal")
}
remaining := logstoreService.Limit(ctx, config.instanceID)
config.cutTimeouts(remaining)
config.logger.Log(actionStartedMessage)
if remaining != nil && *remaining == 0 {
return zerrors.ThrowResourceExhausted(nil, "ACTIO-f19Ii", "Errors.Quota.Execution.Exhausted")
}
defer func() {
if err != nil {
config.logger.log(actionFailedMessage(err), logrus.ErrorLevel, true)
} else {
config.logger.log(actionSucceededMessage, logrus.InfoLevel, true)
}
if config.allowedToFail {
err = nil
}
}()
if err := executeScript(config, ctxParam, apiParam, script); err != nil {
return err
}
var fn jsAction
jsFn := config.vm.Get(name)
if jsFn == nil {
return errors.New("function not found")
}
if err := config.vm.ExportTo(jsFn, &fn); err != nil {
return err
}
t := config.StartFunction()
defer func() {
t.Stop()
}()
return executeFn(config, fn)
}
func executeScript(config *runConfig, ctxParam contextFields, apiParam apiFields, script string) (err error) {
t := config.StartScript()
defer func() {
t.Stop()
}()
if ctxParam != nil {
ctxParam(config.ctxParam)
}
if apiParam != nil {
apiParam(config.apiParam)
}
registry := new(require.Registry)
registry.Enable(config.vm)
for name, loader := range config.modules {
registry.RegisterNativeModule(name, loader)
}
// overload error if function panics
defer func() {
r := recover()
if r != nil {
err = r.(error)
return
}
}()
_, err = config.vm.RunString(script)
return err
}
func executeFn(config *runConfig, fn jsAction) (err error) {
defer func() {
r := recover()
if r == nil {
return
}
var ok bool
if err, ok = r.(error); ok {
return
}
e, ok := r.(string)
if ok {
err = errors.New(e)
return
}
err = fmt.Errorf("unknown error occurred: %v", r)
}()
if err = fn(config.ctxParam.fields, config.apiParam.fields); err != nil {
return err
}
return nil
}
func ActionToOptions(a *query.Action) []Option {
opts := make([]Option, 0, 1)
if a.AllowedToFail {
opts = append(opts, WithAllowedToFail())
}
return opts
}

View File

@@ -0,0 +1,73 @@
package actions
import (
"context"
"errors"
"testing"
"time"
"github.com/dop251/goja"
"github.com/zitadel/zitadel/internal/logstore"
"github.com/zitadel/zitadel/internal/logstore/record"
)
func TestRun(t *testing.T) {
SetLogstoreService(logstore.New[*record.ExecutionLog](nil, nil))
type args struct {
timeout time.Duration
api apiFields
ctx contextFields
script string
name string
opts []Option
}
tests := []struct {
name string
args args
wantErr func(error) bool
}{
{
name: "simple script",
args: args{
api: nil,
script: `
function testFunc() {
for (i = 0; i < 10; i++) {}
}`,
name: "testFunc",
opts: []Option{},
},
wantErr: func(err error) bool { return err == nil },
},
{
name: "throw error",
args: args{
api: nil,
script: "function testFunc() {throw 'some error'}",
name: "testFunc",
opts: []Option{},
},
wantErr: func(err error) bool {
gojaErr := new(goja.Exception)
if errors.As(err, &gojaErr) {
return gojaErr.Value().String() == "some error"
}
return false
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.timeout == 0 {
tt.args.timeout = 10 * time.Second
}
ctx, cancel := context.WithTimeout(context.Background(), tt.args.timeout)
if err := Run(ctx, tt.args.ctx, tt.args.api, tt.args.script, tt.args.name, tt.args.opts...); !tt.wantErr(err) {
t.Errorf("Run() unexpected error = (%[1]T) %[1]v", err)
}
cancel()
})
}
}

View File

@@ -0,0 +1,19 @@
package actions
type apiConfig struct {
FieldConfig
}
type apiFields func(*apiConfig)
func WithAPIFields(opts ...FieldOption) apiFields {
return func(p *apiConfig) {
if p.fields == nil {
p.fields = fields{}
}
for _, opt := range opts {
opt(&p.FieldConfig)
}
}
}

View File

@@ -0,0 +1,101 @@
package actions
import (
"context"
"time"
"github.com/dop251/goja"
"github.com/dop251/goja_nodejs/require"
"github.com/zitadel/logging"
)
const (
maxPrepareTimeout = 5 * time.Second
)
type Option func(*runConfig)
func WithAllowedToFail() Option {
return func(c *runConfig) {
c.allowedToFail = true
}
}
type runConfig struct {
allowedToFail bool
functionTimeout,
scriptTimeout time.Duration
modules map[string]require.ModuleLoader
logger *logger
instanceID string
vm *goja.Runtime
ctxParam *ctxConfig
apiParam *apiConfig
}
func newRunConfig(ctx context.Context, opts ...Option) *runConfig {
deadline, ok := ctx.Deadline()
if !ok {
logging.Warn("no timeout set on action run")
}
vm := goja.New()
vm.SetFieldNameMapper(goja.UncapFieldNameMapper())
config := &runConfig{
functionTimeout: time.Until(deadline),
scriptTimeout: maxPrepareTimeout,
modules: map[string]require.ModuleLoader{},
vm: vm,
ctxParam: &ctxConfig{
FieldConfig: FieldConfig{
Runtime: vm,
fields: fields{},
},
},
apiParam: &apiConfig{
FieldConfig: FieldConfig{
Runtime: vm,
fields: fields{},
},
},
}
for _, opt := range opts {
opt(config)
}
if config.scriptTimeout > config.functionTimeout {
config.scriptTimeout = config.functionTimeout
}
return config
}
func (c *runConfig) StartFunction() *time.Timer {
c.vm.ClearInterrupt()
return time.AfterFunc(c.functionTimeout, func() {
c.vm.Interrupt(ErrHalt)
})
}
func (c *runConfig) StartScript() *time.Timer {
c.vm.ClearInterrupt()
return time.AfterFunc(c.scriptTimeout, func() {
c.vm.Interrupt(ErrHalt)
})
}
func (c *runConfig) cutTimeouts(remainingSeconds *uint64) {
if remainingSeconds == nil {
return
}
remainingDur := time.Duration(*remainingSeconds) * time.Second
if c.functionTimeout > remainingDur {
c.functionTimeout = remainingDur
}
if c.scriptTimeout > remainingDur {
c.scriptTimeout = remainingDur
}
}

View File

@@ -0,0 +1,19 @@
package actions
type ctxConfig struct {
FieldConfig
}
type contextFields func(*ctxConfig)
func SetContextFields(opts ...FieldOption) contextFields {
return func(p *ctxConfig) {
if p.fields == nil {
p.fields = fields{}
}
for _, opt := range opts {
opt(&p.FieldConfig)
}
}
}

View File

@@ -0,0 +1,73 @@
package actions
import (
"github.com/dop251/goja"
"github.com/zitadel/logging"
)
type fields map[string]interface{}
type FieldOption func(*FieldConfig)
type FieldConfig struct {
fields
Runtime *goja.Runtime
}
func SetFields(name string, values ...interface{}) FieldOption {
return func(p *FieldConfig) {
if len(values) == 0 {
return
}
for _, value := range values {
val, ok := value.(FieldOption)
// is the lowest field and can be set without further checks
if !ok {
// {
// "value": "some value"
// }
p.set(name, value)
continue
}
var field fields
if f, ok := p.fields[name]; ok {
// check if the found field is an object
if field, ok = f.(fields); !ok {
// panic because overwriting fields is not allowed
logging.WithFields("sub", name).Warn("sub is not an object")
panic("unable to prepare parameter")
}
} else {
// field does not exist so far.
// sub object for field can be created
field = fields{}
p.fields[name] = field
}
fieldParam := FieldConfig{
Runtime: p.Runtime,
fields: field,
}
val(&fieldParam)
}
}
}
func (f *FieldConfig) set(name string, value interface{}) {
if _, ok := f.fields[name]; ok {
logging.WithFields("name", name).Error("tried to overwrite field")
panic("tried to overwrite field")
}
switch v := value.(type) {
case func(config *FieldConfig) interface{}:
f.fields[name] = v(f)
case func(config *FieldConfig) func(call goja.FunctionCall) goja.Value:
f.fields[name] = v(f)
default:
f.fields[name] = value
}
}

View File

@@ -0,0 +1,190 @@
package actions
import (
"fmt"
"testing"
"github.com/dop251/goja"
"github.com/zitadel/zitadel/internal/logstore"
"github.com/zitadel/zitadel/internal/logstore/record"
)
func TestSetFields(t *testing.T) {
SetLogstoreService(logstore.New[*record.ExecutionLog](nil, nil))
primitveFn := func(a string) { fmt.Println(a) }
complexFn := func(*FieldConfig) interface{} {
return primitveFn
}
tests := []struct {
name string
setFields FieldOption
want fields
shouldPanic bool
}{
{
name: "field is simple value",
setFields: SetFields("value", 5),
want: fields{
"value": 5,
},
},
{
name: "field is method",
setFields: SetFields("value", primitveFn),
want: fields{
"value": primitveFn,
},
},
{
name: "field is complex method",
setFields: SetFields("value", complexFn),
want: fields{
"value": primitveFn,
},
},
{
name: "field without value",
setFields: SetFields("value"),
want: fields{},
},
{
name: "field with empty value",
setFields: SetFields("value", ""),
want: fields{
"value": "",
},
},
{
name: "nested simple value",
setFields: SetFields(
"field",
SetFields("sub", 5),
),
want: fields{
"field": fields{
"sub": 5,
},
},
},
{
name: "nested multiple fields",
setFields: SetFields(
"field",
SetFields("sub1", 5),
SetFields("sub2", "asdf"),
SetFields("sub3", primitveFn),
),
want: fields{
"field": fields{
"sub1": 5,
"sub2": "asdf",
"sub3": primitveFn,
},
},
},
{
name: "try to overwrite field primitives",
setFields: SetFields(
"field",
SetFields("sub", 5),
SetFields("sub", primitveFn),
),
shouldPanic: true,
},
{
name: "try to overwrite primitives with fields",
setFields: SetFields(
"field",
SetFields("sub", 5),
SetFields("sub", SetFields("please", "panic")),
),
shouldPanic: true,
},
{
name: "try to overwrite fields with primitive",
setFields: SetFields(
"field",
SetFields("sub", SetFields("please", "panic")),
SetFields("sub", 5),
),
shouldPanic: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
panicked := false
if tt.shouldPanic {
defer func() {
if panicked != tt.shouldPanic {
t.Errorf("wanted panic: %v got %v", tt.shouldPanic, panicked)
}
}()
defer func() {
recover()
panicked = true
}()
}
config := &FieldConfig{
Runtime: goja.New(),
fields: fields{},
}
tt.setFields(config)
if !tt.shouldPanic && fmt.Sprint(config.fields) != fmt.Sprint(tt.want) {
t.Errorf("SetFields() = %v, want %v", fmt.Sprint(config.fields), fmt.Sprint(tt.want))
}
})
}
}
func TestSetFieldsExecuteMethods(t *testing.T) {
primitveFn := func(a string) { fmt.Println(a) }
complexFn := func(*FieldConfig) interface{} {
return primitveFn
}
tests := []struct {
name string
setFields FieldOption
want fields
shouldPanic bool
}{
{
name: "field is method",
setFields: SetFields("value", primitveFn),
want: fields{
"value": primitveFn,
},
},
{
name: "field is complex method",
setFields: SetFields("value", complexFn),
want: fields{
"value": primitveFn,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
panicked := false
if tt.shouldPanic {
defer func() {
if panicked != tt.shouldPanic {
t.Errorf("wanted panic: %v got %v", tt.shouldPanic, panicked)
}
}()
defer func() {
recover()
panicked = true
}()
}
config := &FieldConfig{
Runtime: goja.New(),
fields: fields{},
}
tt.setFields(config)
if !tt.shouldPanic && fmt.Sprint(config.fields) != fmt.Sprint(tt.want) {
t.Errorf("SetFields() = %v, want %v", fmt.Sprint(config.fields), fmt.Sprint(tt.want))
}
})
}
}

View File

@@ -0,0 +1,210 @@
package actions
import (
"bytes"
"context"
"encoding/json"
"io"
"net"
"net/http"
"net/url"
"strings"
"time"
"github.com/dop251/goja"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/zerrors"
)
func WithHTTP(ctx context.Context) Option {
return func(c *runConfig) {
c.modules["zitadel/http"] = func(runtime *goja.Runtime, module *goja.Object) {
requireHTTP(ctx, &http.Client{Transport: &transport{lookup: net.LookupIP}}, runtime, module)
}
}
}
type HTTP struct {
runtime *goja.Runtime
client *http.Client
}
func requireHTTP(ctx context.Context, client *http.Client, runtime *goja.Runtime, module *goja.Object) {
c := &HTTP{
client: client,
runtime: runtime,
}
o := module.Get("exports").(*goja.Object)
logging.OnError(o.Set("fetch", c.fetch(ctx))).Warn("unable to set module")
}
type fetchConfig struct {
Method string
Headers http.Header
Body io.Reader
}
var defaultFetchConfig = fetchConfig{
Method: http.MethodGet,
Headers: http.Header{
"Content-Type": []string{"application/json"},
"Accept": []string{"application/json"},
},
}
func (c *HTTP) fetchConfigFromArg(arg *goja.Object, config *fetchConfig) (err error) {
for _, key := range arg.Keys() {
switch key {
case "headers":
config.Headers = parseHeaders(arg.Get(key).ToObject(c.runtime))
case "method":
config.Method = arg.Get(key).String()
case "body":
body, err := arg.Get(key).ToObject(c.runtime).MarshalJSON()
if err != nil {
return err
}
config.Body = bytes.NewReader(body)
default:
return zerrors.ThrowInvalidArgument(nil, "ACTIO-OfUeA", "key is invalid")
}
}
return nil
}
type response struct {
Body string
Status int
Headers map[string][]string
runtime *goja.Runtime
}
func (r *response) Json() goja.Value {
var val interface{}
if err := json.Unmarshal([]byte(r.Body), &val); err != nil {
panic(err)
}
return r.runtime.ToValue(val)
}
func (r *response) Text() goja.Value {
return r.runtime.ToValue(r.Body)
}
func (c *HTTP) fetch(ctx context.Context) func(call goja.FunctionCall) goja.Value {
return func(call goja.FunctionCall) goja.Value {
req := c.buildHTTPRequest(ctx, call.Arguments)
if deadline, ok := ctx.Deadline(); ok {
c.client.Timeout = time.Until(deadline)
}
res, err := c.client.Do(req)
if err != nil {
logging.WithError(err).Debug("call failed")
panic(err)
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
logging.WithError(err).Warn("unable to parse body")
panic("unable to read response body")
}
return c.runtime.ToValue(&response{Status: res.StatusCode, Body: string(body), runtime: c.runtime})
}
}
// the first argument has to be a string and is required
// the second agrument is optional and an object with the following fields possible:
// - `Headers`: map with string key and value of type string or string array
// - `Body`: json body of the request
// - `Method`: http method type
func (c *HTTP) buildHTTPRequest(ctx context.Context, args []goja.Value) (req *http.Request) {
if len(args) > 2 {
logging.WithFields("count", len(args)).Debug("more than 2 args provided")
panic("too many args")
}
if len(args) == 0 {
panic("no url provided")
}
config := defaultFetchConfig
var err error
if len(args) == 2 {
if err = c.fetchConfigFromArg(args[1].ToObject(c.runtime), &config); err != nil {
panic(err)
}
}
req, err = http.NewRequestWithContext(ctx, config.Method, args[0].Export().(string), config.Body)
if err != nil {
panic(err)
}
req.Header = config.Headers
return req
}
func parseHeaders(headers *goja.Object) http.Header {
h := make(http.Header, len(headers.Keys()))
for _, k := range headers.Keys() {
header := headers.Get(k).Export()
var values []string
switch headerValue := header.(type) {
case string:
values = strings.Split(headerValue, ",")
case []any:
for _, v := range headerValue {
values = append(values, v.(string))
}
}
for _, v := range values {
h.Add(k, strings.TrimSpace(v))
}
}
return h
}
type transport struct {
lookup func(string) ([]net.IP, error)
}
func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) {
if httpConfig == nil || len(httpConfig.DenyList) == 0 {
return http.DefaultTransport.RoundTrip(req)
}
if err := t.isHostBlocked(httpConfig.DenyList, req.URL); err != nil {
return nil, zerrors.ThrowInvalidArgument(err, "ACTIO-N72d0", "host is denied")
}
return http.DefaultTransport.RoundTrip(req)
}
func (t *transport) isHostBlocked(denyList []AddressChecker, address *url.URL) error {
host := address.Hostname()
ip := net.ParseIP(host)
ips := []net.IP{ip}
// if the hostname is a domain, we need to check resolve the ip(s), since it might be denied
if ip == nil {
var err error
ips, err = t.lookup(host)
if err != nil {
return zerrors.ThrowInternal(err, "ACTIO-4m9s2", "lookup failed")
}
}
for _, denied := range denyList {
if err := denied.IsDenied(ips, host); err != nil {
return err
}
}
return nil
}
type AddressChecker interface {
IsDenied([]net.IP, string) error
}

View File

@@ -0,0 +1,119 @@
package actions
import (
"errors"
"fmt"
"net"
"reflect"
"strings"
"github.com/mitchellh/mapstructure"
)
func SetHTTPConfig(config *HTTPConfig) {
httpConfig = config
}
var httpConfig *HTTPConfig
type HTTPConfig struct {
DenyList []AddressChecker
}
func HTTPConfigDecodeHook(from, to reflect.Value) (interface{}, error) {
if to.Type() != reflect.TypeOf(HTTPConfig{}) {
return from.Interface(), nil
}
config := struct {
DenyList []string
}{}
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
DecodeHook: mapstructure.StringToTimeDurationHookFunc(),
WeaklyTypedInput: true,
Result: &config,
})
if err != nil {
return nil, err
}
if err = decoder.Decode(from.Interface()); err != nil {
return nil, err
}
c := HTTPConfig{
DenyList: make([]AddressChecker, 0),
}
for _, unsplit := range config.DenyList {
for _, split := range strings.Split(unsplit, ",") {
parsed, parseErr := NewHostChecker(split)
if parseErr != nil {
return nil, parseErr
}
if parsed != nil {
c.DenyList = append(c.DenyList, parsed)
}
}
}
return c, nil
}
func NewHostChecker(entry string) (AddressChecker, error) {
if entry == "" {
return nil, nil
}
_, network, err := net.ParseCIDR(entry)
if err == nil {
return &HostChecker{Net: network}, nil
}
if ip := net.ParseIP(entry); ip != nil {
return &HostChecker{IP: ip}, nil
}
return &HostChecker{Domain: entry}, nil
}
type HostChecker struct {
Net *net.IPNet
IP net.IP
Domain string
}
type AddressDeniedError struct {
deniedBy string
}
func NewAddressDeniedError(deniedBy string) *AddressDeniedError {
return &AddressDeniedError{deniedBy: deniedBy}
}
func (e *AddressDeniedError) Error() string {
return fmt.Sprintf("address is denied by '%s'", e.deniedBy)
}
func (e *AddressDeniedError) Is(target error) bool {
var addressDeniedErr *AddressDeniedError
if !errors.As(target, &addressDeniedErr) {
return false
}
return e.deniedBy == addressDeniedErr.deniedBy
}
func (c *HostChecker) IsDenied(ips []net.IP, address string) error {
// if the address matches the domain, no additional checks as needed
if c.Domain == address {
return NewAddressDeniedError(c.Domain)
}
// otherwise we need to check on ips (incl. the resolved ips of the host)
for _, ip := range ips {
if c.Net != nil && c.Net.Contains(ip) {
return NewAddressDeniedError(c.Net.String())
}
if c.IP != nil && c.IP.Equal(ip) {
return NewAddressDeniedError(c.IP.String())
}
}
return nil
}

View File

@@ -0,0 +1,480 @@
package actions
import (
"bytes"
"context"
"errors"
"io"
"net"
"net/http"
"net/url"
"reflect"
"testing"
"github.com/dop251/goja"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/logstore"
"github.com/zitadel/zitadel/internal/logstore/record"
"github.com/zitadel/zitadel/internal/zerrors"
)
func Test_isHostBlocked(t *testing.T) {
SetLogstoreService(logstore.New[*record.ExecutionLog](nil, nil))
var denyList = []AddressChecker{
mustNewHostChecker(t, "192.168.5.0/24"),
mustNewHostChecker(t, "127.0.0.1"),
mustNewHostChecker(t, "test.com"),
}
type fields struct {
lookup func(host string) ([]net.IP, error)
}
type args struct {
address *url.URL
}
tests := []struct {
name string
fields fields
args args
want error
}{
{
name: "in range",
args: args{
address: mustNewURL(t, "https://192.168.5.4/hodor"),
},
want: NewAddressDeniedError("192.168.5.0/24"),
},
{
name: "exact ip",
args: args{
address: mustNewURL(t, "http://127.0.0.1:8080/hodor"),
},
want: NewAddressDeniedError("127.0.0.1"),
},
{
name: "address match",
fields: fields{
lookup: func(host string) ([]net.IP, error) {
return []net.IP{net.ParseIP("194.264.52.4")}, nil
},
},
args: args{
address: mustNewURL(t, "https://test.com:42/hodor"),
},
want: NewAddressDeniedError("test.com"),
},
{
name: "address not match",
fields: fields{
lookup: func(host string) ([]net.IP, error) {
return []net.IP{net.ParseIP("194.264.52.4")}, nil
},
},
args: args{
address: mustNewURL(t, "https://test2.com/hodor"),
},
want: nil,
},
{
name: "looked up ip matches",
fields: fields{
lookup: func(host string) ([]net.IP, error) {
return []net.IP{net.ParseIP("127.0.0.1")}, nil
},
},
args: args{
address: mustNewURL(t, "https://test2.com/hodor"),
},
want: NewAddressDeniedError("127.0.0.1"),
},
{
name: "looked up failure",
fields: fields{
lookup: func(host string) ([]net.IP, error) {
return nil, errors.New("some error")
},
},
args: args{
address: mustNewURL(t, "https://test2.com/hodor"),
},
want: zerrors.ThrowInternal(nil, "ACTIO-4m9s2", "lookup failed"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
trans := &transport{
lookup: tt.fields.lookup,
}
got := trans.isHostBlocked(denyList, tt.args.address)
assert.ErrorIs(t, got, tt.want)
})
}
}
func mustNewHostChecker(t *testing.T, ip string) AddressChecker {
t.Helper()
checker, err := NewHostChecker(ip)
if err != nil {
t.Errorf("unable to parse cidr of %q because: %v", ip, err)
t.FailNow()
}
return checker
}
func mustNewURL(t *testing.T, raw string) *url.URL {
u, err := url.Parse(raw)
if err != nil {
t.Errorf("unable to parse address of %q because: %v", raw, err)
t.FailNow()
}
return u
}
func TestHTTP_fetchConfigFromArg(t *testing.T) {
runtime := goja.New()
runtime.SetFieldNameMapper(goja.UncapFieldNameMapper())
type args struct {
arg *goja.Object
}
tests := []struct {
name string
args args
wantConfig fetchConfig
wantErr func(error) bool
}{
{
name: "no fetch option provided",
args: args{
arg: runtime.ToValue(
struct{}{},
).ToObject(runtime),
},
wantConfig: fetchConfig{},
wantErr: func(err error) bool {
return err == nil
},
},
{
name: "header set as string",
args: args{
arg: runtime.ToValue(
&struct {
Headers map[string]string
}{
Headers: map[string]string{
"Authorization": "Bearer token",
},
},
).ToObject(runtime),
},
wantConfig: fetchConfig{
Headers: http.Header{
"Authorization": {"Bearer token"},
},
},
wantErr: func(err error) bool {
return err == nil
},
},
{
name: "header set as list",
args: args{
arg: runtime.ToValue(
&struct {
Headers map[string][]any
}{
Headers: map[string][]any{
"Authorization": {"Bearer token"},
},
},
).ToObject(runtime),
},
wantConfig: fetchConfig{
Headers: http.Header{
"Authorization": {"Bearer token"},
},
},
wantErr: func(err error) bool {
return err == nil
},
},
{
name: "method set",
args: args{
arg: runtime.ToValue(
&struct {
Method string
}{
Method: http.MethodPost,
},
).ToObject(runtime),
},
wantConfig: fetchConfig{
Method: http.MethodPost,
},
wantErr: func(err error) bool {
return err == nil
},
},
{
name: "body set",
args: args{
arg: runtime.ToValue(
&struct {
Body struct{ Id string }
}{
Body: struct{ Id string }{
Id: "asdf123",
},
},
).ToObject(runtime),
},
wantConfig: fetchConfig{
Body: bytes.NewReader([]byte(`{"id":"asdf123"}`)),
},
wantErr: func(err error) bool {
return err == nil
},
},
{
name: "invalid header",
args: args{
arg: runtime.ToValue(
&struct {
NotExists struct{}
}{
NotExists: struct{}{},
},
).ToObject(runtime),
},
wantConfig: fetchConfig{},
wantErr: func(err error) bool {
return zerrors.IsErrorInvalidArgument(err)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &HTTP{
runtime: runtime,
client: http.DefaultClient,
}
gotConfig := new(fetchConfig)
err := c.fetchConfigFromArg(tt.args.arg, gotConfig)
if !tt.wantErr(err) {
t.Errorf("HTTP.fetchConfigFromArg() error = %v", err)
return
}
if !reflect.DeepEqual(gotConfig.Headers, tt.wantConfig.Headers) {
t.Errorf("config.Headers got = %#v, want %#v", gotConfig.Headers, tt.wantConfig.Headers)
}
if gotConfig.Method != tt.wantConfig.Method {
t.Errorf("config.Method got = %#v, want %#v", gotConfig.Method, tt.wantConfig.Method)
}
if tt.wantConfig.Body == nil {
if gotConfig.Body != nil {
t.Errorf("didn't expect a body")
}
return
}
gotBody, _ := io.ReadAll(gotConfig.Body)
wantBody, _ := io.ReadAll(tt.wantConfig.Body)
if !reflect.DeepEqual(gotBody, wantBody) {
t.Errorf("config.Body got = %s, want %s", gotBody, wantBody)
}
})
}
}
func TestHTTP_buildHTTPRequest(t *testing.T) {
runtime := goja.New()
runtime.SetFieldNameMapper(goja.UncapFieldNameMapper())
type args struct {
args []goja.Value
}
tests := []struct {
name string
args args
wantReq *http.Request
shouldPanic bool
}{
{
name: "only url",
args: args{
args: []goja.Value{
runtime.ToValue("http://my-url.ch"),
},
},
wantReq: &http.Request{
Method: http.MethodGet,
URL: mustNewURL(t, "http://my-url.ch"),
Header: defaultFetchConfig.Headers,
Body: nil,
},
},
{
name: "no params",
args: args{
args: []goja.Value{
runtime.ToValue("http://my-url.ch"),
runtime.ToValue(&struct{}{}),
},
},
wantReq: &http.Request{
Method: http.MethodGet,
URL: mustNewURL(t, "http://my-url.ch"),
Header: defaultFetchConfig.Headers,
Body: nil,
},
},
{
name: "overwrite headers",
args: args{
args: []goja.Value{
runtime.ToValue("http://my-url.ch"),
runtime.ToValue(struct {
Headers map[string][]interface{}
}{
Headers: map[string][]interface{}{"Authorization": {"some token"}},
}),
},
},
wantReq: &http.Request{
Method: http.MethodGet,
URL: mustNewURL(t, "http://my-url.ch"),
Header: http.Header{
"Authorization": []string{"some token"},
},
Body: nil,
},
},
{
name: "post with body",
args: args{
args: []goja.Value{
runtime.ToValue("http://my-url.ch"),
runtime.ToValue(struct {
Body struct{ MyData string }
}{
Body: struct{ MyData string }{MyData: "hello world"},
}),
},
},
wantReq: &http.Request{
Method: http.MethodGet,
URL: mustNewURL(t, "http://my-url.ch"),
Header: defaultFetchConfig.Headers,
Body: io.NopCloser(bytes.NewReader([]byte(`{"myData":"hello world"}`))),
},
},
{
name: "too many args",
args: args{
args: []goja.Value{
runtime.ToValue("http://my-url.ch"),
runtime.ToValue("http://my-url.ch"),
runtime.ToValue("http://my-url.ch"),
},
},
wantReq: nil,
shouldPanic: true,
},
{
name: "no args",
args: args{
args: []goja.Value{},
},
wantReq: nil,
shouldPanic: true,
},
{
name: "invalid config",
args: args{
args: []goja.Value{
runtime.ToValue("http://my-url.ch"),
runtime.ToValue(struct {
Invalid bool
}{
Invalid: true,
}),
},
},
wantReq: nil,
shouldPanic: true,
},
{
name: "invalid method",
args: args{
args: []goja.Value{
runtime.ToValue("http://my-url.ch"),
runtime.ToValue(struct {
Method string
}{
Method: " asdf asdf",
}),
},
},
wantReq: nil,
shouldPanic: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
panicked := false
if tt.shouldPanic {
defer func() {
if panicked != tt.shouldPanic {
t.Errorf("wanted panic: %v got %v", tt.shouldPanic, panicked)
}
}()
defer func() {
recover()
panicked = true
}()
}
c := &HTTP{
runtime: runtime,
}
gotReq := c.buildHTTPRequest(context.Background(), tt.args.args)
if tt.shouldPanic {
return
}
if gotReq.URL.String() != tt.wantReq.URL.String() {
t.Errorf("url = %s, want %s", gotReq.URL, tt.wantReq.URL)
}
if !reflect.DeepEqual(gotReq.Header, tt.wantReq.Header) {
t.Errorf("headers = %v, want %v", gotReq.Header, tt.wantReq.Header)
}
if gotReq.Method != tt.wantReq.Method {
t.Errorf("method = %s, want %s", gotReq.Method, tt.wantReq.Method)
}
if tt.wantReq.Body == nil {
if gotReq.Body != nil {
t.Errorf("didn't expect a body")
}
return
}
gotBody, _ := io.ReadAll(gotReq.Body)
wantBody, _ := io.ReadAll(tt.wantReq.Body)
if !reflect.DeepEqual(gotBody, wantBody) {
t.Errorf("config.Body got = %s, want %s", gotBody, wantBody)
}
})
}
}

View File

@@ -0,0 +1,80 @@
package actions
import (
"context"
"time"
"github.com/dop251/goja"
"github.com/dop251/goja_nodejs/console"
"github.com/sirupsen/logrus"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/logstore"
"github.com/zitadel/zitadel/internal/logstore/record"
)
var (
logstoreService *logstore.Service[*record.ExecutionLog]
_ console.Printer = (*logger)(nil)
)
func SetLogstoreService(svc *logstore.Service[*record.ExecutionLog]) {
logstoreService = svc
}
type logger struct {
ctx context.Context
started time.Time
instanceID string
}
// newLogger returns a *logger instance that should only be used for a single action run.
// The first log call sets the started field for subsequent log calls
func newLogger(ctx context.Context, instanceID string) *logger {
return &logger{
ctx: ctx,
started: time.Time{},
instanceID: instanceID,
}
}
func (l *logger) Log(msg string) {
l.log(msg, logrus.InfoLevel, false)
}
func (l *logger) Warn(msg string) {
l.log(msg, logrus.WarnLevel, false)
}
func (l *logger) Error(msg string) {
l.log(msg, logrus.ErrorLevel, false)
}
func (l *logger) log(msg string, level logrus.Level, last bool) {
ts := time.Now()
if l.started.IsZero() {
l.started = ts
}
r := &record.ExecutionLog{
LogDate: ts,
InstanceID: l.instanceID,
Message: msg,
LogLevel: level,
}
if last {
r.Took = ts.Sub(l.started)
}
logstoreService.Handle(l.ctx, r)
}
func withLogger(ctx context.Context) Option {
instance := authz.GetInstance(ctx)
instanceID := instance.InstanceID()
return func(c *runConfig) {
c.logger = newLogger(ctx, instanceID)
c.instanceID = instanceID
c.modules["zitadel/log"] = func(runtime *goja.Runtime, module *goja.Object) {
console.RequireWithPrinter(c.logger)(runtime, module)
}
}
}

View File

@@ -0,0 +1,138 @@
package object
import (
"net"
"time"
"github.com/dop251/goja"
"github.com/zitadel/zitadel/internal/actions"
"github.com/zitadel/zitadel/internal/domain"
)
// AuthRequestField accepts the domain.AuthRequest by value, so it's not mutated
func AuthRequestField(authRequest *domain.AuthRequest) func(c *actions.FieldConfig) interface{} {
return func(c *actions.FieldConfig) interface{} {
return AuthRequestFromDomain(c, authRequest)
}
}
func AuthRequestFromDomain(c *actions.FieldConfig, request *domain.AuthRequest) goja.Value {
if request == nil {
return c.Runtime.ToValue(nil)
}
var maxAuthAge *time.Duration
if request.MaxAuthAge != nil {
maxAuthAgeCopy := *request.MaxAuthAge
maxAuthAge = &maxAuthAgeCopy
}
return c.Runtime.ToValue(&authRequest{
Id: request.ID,
AgentId: request.AgentID,
CreationDate: request.CreationDate,
ChangeDate: request.ChangeDate,
BrowserInfo: browserInfoFromDomain(request.BrowserInfo),
ApplicationId: request.ApplicationID,
CallbackUri: request.CallbackURI,
TransferState: request.TransferState,
Prompt: request.Prompt,
UiLocales: request.UiLocales,
LoginHint: request.LoginHint,
MaxAuthAge: maxAuthAge,
InstanceId: request.InstanceID,
Request: requestFromDomain(request.Request),
UserId: request.UserID,
UserName: request.UserName,
LoginName: request.LoginName,
DisplayName: request.DisplayName,
ResourceOwner: request.UserOrgID,
RequestedOrgId: request.RequestedOrgID,
RequestedOrgName: request.RequestedOrgName,
RequestedPrimaryDomain: request.RequestedPrimaryDomain,
RequestedOrgDomain: request.RequestedOrgDomain,
ApplicationResourceOwner: request.ApplicationResourceOwner,
PrivateLabelingSetting: request.PrivateLabelingSetting,
SelectedIdpConfigId: request.SelectedIDPConfigID,
LinkingUsers: externalUsersFromDomain(request.LinkingUsers),
PasswordVerified: request.PasswordVerified,
MfasVerified: request.MFAsVerified,
Audience: request.Audience,
AuthTime: request.AuthTime,
})
}
type authRequest struct {
Id string
AgentId string
CreationDate time.Time
ChangeDate time.Time
BrowserInfo *browserInfo
ApplicationId string
CallbackUri string
TransferState string
Prompt []domain.Prompt
UiLocales []string
LoginHint string
MaxAuthAge *time.Duration
InstanceId string
Request *request
UserId string
UserName string
LoginName string
DisplayName string
// UserOrgID string
ResourceOwner string
// requested by scope
RequestedOrgId string
// requested by scope
RequestedOrgName string
// requested by scope
RequestedPrimaryDomain string
// requested by scope
RequestedOrgDomain bool
// client
ApplicationResourceOwner string
PrivateLabelingSetting domain.PrivateLabelingSetting
SelectedIdpConfigId string
LinkingUsers []*externalUser
PasswordVerified bool
MfasVerified []domain.MFAType
Audience []string
AuthTime time.Time
}
func browserInfoFromDomain(info *domain.BrowserInfo) *browserInfo {
if info == nil {
return nil
}
return &browserInfo{
UserAgent: info.UserAgent,
AcceptLanguage: info.AcceptLanguage,
RemoteIp: info.RemoteIP,
}
}
func requestFromDomain(req domain.Request) *request {
r := new(request)
if oidcRequest, ok := req.(*domain.AuthRequestOIDC); ok {
r.Oidc = OIDCRequest{Scopes: oidcRequest.Scopes}
}
return r
}
type request struct {
Oidc OIDCRequest
}
type OIDCRequest struct {
Scopes []string
}
type browserInfo struct {
UserAgent string
AcceptLanguage string
RemoteIp net.IP
}

View File

@@ -0,0 +1,44 @@
package object
import (
"net/http"
"github.com/zitadel/zitadel/internal/actions"
)
// HTTPRequestField accepts the http.Request by value, so it's not mutated
func HTTPRequestField(request *http.Request) func(c *actions.FieldConfig) interface{} {
return func(c *actions.FieldConfig) interface{} {
return c.Runtime.ToValue(&httpRequest{
Method: request.Method,
Url: request.URL.String(),
Proto: request.Proto,
ContentLength: request.ContentLength,
Host: request.Host,
Form: copyMap(request.Form),
PostForm: copyMap(request.PostForm),
RemoteAddr: request.RemoteAddr,
Headers: copyMap(request.Header),
})
}
}
type httpRequest struct {
Method string
Url string
Proto string
ContentLength int64
Host string
Form map[string][]string
PostForm map[string][]string
RemoteAddr string
Headers map[string][]string
}
func copyMap(src map[string][]string) map[string][]string {
dst := make(map[string][]string)
for k, v := range src {
dst[k] = v
}
return dst
}

View File

@@ -0,0 +1,234 @@
package object
import (
"context"
"encoding/json"
"time"
"github.com/dop251/goja"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/actions"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
)
func OrgMetadataListFromQuery(c *actions.FieldConfig, orgMetadata *query.OrgMetadataList) goja.Value {
result := &metadataList{
Count: orgMetadata.Count,
Sequence: orgMetadata.Sequence,
Timestamp: orgMetadata.LastRun,
Metadata: make([]*metadata, len(orgMetadata.Metadata)),
}
for i, md := range orgMetadata.Metadata {
result.Metadata[i] = &metadata{
CreationDate: md.CreationDate,
ChangeDate: md.ChangeDate,
ResourceOwner: md.ResourceOwner,
Sequence: md.Sequence,
Key: md.Key,
Value: metadataByteArrayToValue(md.Value, c.Runtime),
}
}
return c.Runtime.ToValue(result)
}
func UserMetadataListFromQuery(c *actions.FieldConfig, metadata *query.UserMetadataList) goja.Value {
result := &userMetadataList{
Count: metadata.Count,
Sequence: metadata.Sequence,
Timestamp: metadata.LastRun,
Metadata: make([]*userMetadata, len(metadata.Metadata)),
}
for i, md := range metadata.Metadata {
result.Metadata[i] = &userMetadata{
CreationDate: md.CreationDate,
ChangeDate: md.ChangeDate,
ResourceOwner: md.ResourceOwner,
Sequence: md.Sequence,
Key: md.Key,
Value: metadataByteArrayToValue(md.Value, c.Runtime),
}
}
return c.Runtime.ToValue(result)
}
func UserMetadataListFromSlice(c *actions.FieldConfig, metadata []query.UserMetadata) goja.Value {
result := &userMetadataList{
// Count was the only field ever queried from the DB in the old implementation,
// so Sequence and LastRun are omitted.
Count: uint64(len(metadata)),
Metadata: make([]*userMetadata, len(metadata)),
}
for i, md := range metadata {
result.Metadata[i] = &userMetadata{
CreationDate: md.CreationDate,
ChangeDate: md.ChangeDate,
ResourceOwner: md.ResourceOwner,
Sequence: md.Sequence,
Key: md.Key,
Value: metadataByteArrayToValue(md.Value, c.Runtime),
}
}
return c.Runtime.ToValue(result)
}
func GetOrganizationMetadata(ctx context.Context, queries *query.Queries, c *actions.FieldConfig, organizationID string) goja.Value {
metadata, err := queries.SearchOrgMetadata(
ctx,
true,
organizationID,
&query.OrgMetadataSearchQueries{},
false,
)
if err != nil {
logging.WithError(err).Info("unable to get org metadata in action")
panic(err)
}
return OrgMetadataListFromQuery(c, metadata)
}
func metadataByteArrayToValue(val []byte, runtime *goja.Runtime) goja.Value {
var value interface{}
if !json.Valid(val) {
var err error
val, err = json.Marshal(string(val))
if err != nil {
logging.WithError(err).Debug("unable to marshal unknown value")
panic(err)
}
}
err := json.Unmarshal(val, &value)
if err != nil {
logging.WithError(err).Debug("unable to unmarshal into map")
panic(err)
}
return runtime.ToValue(value)
}
type metadataList struct {
Count uint64
Sequence uint64
Timestamp time.Time
Metadata []*metadata
}
type metadata struct {
CreationDate time.Time
ChangeDate time.Time
ResourceOwner string
Sequence uint64
Key string
Value goja.Value
}
type userMetadataList struct {
Count uint64
Sequence uint64
Timestamp time.Time
Metadata []*userMetadata
}
type userMetadata struct {
CreationDate time.Time
ChangeDate time.Time
ResourceOwner string
Sequence uint64
Key string
Value goja.Value
}
type MetadataList struct {
metadata []*Metadata
}
type Metadata struct {
Key string
// Value is for exporting to javascript
Value goja.Value
// value is for mapping to [domain.Metadata]
value []byte
}
func (md *MetadataList) AppendMetadataFunc(call goja.FunctionCall) goja.Value {
if len(call.Arguments) != 2 {
panic("exactly 2 (key, value) arguments expected")
}
value, err := json.Marshal(call.Arguments[1].Export())
if err != nil {
logging.WithError(err).Debug("unable to marshal")
panic(err)
}
md.metadata = append(md.metadata,
&Metadata{
Key: call.Arguments[0].Export().(string),
Value: call.Arguments[1],
value: value,
})
return nil
}
func (md *MetadataList) MetadataListFromDomain(runtime *goja.Runtime) interface{} {
for i, metadata := range md.metadata {
md.metadata[i].Value = metadataByteArrayToValue(metadata.value, runtime)
}
return &md.metadata
}
func MetadataListFromDomain(metadata []*domain.Metadata) *MetadataList {
list := &MetadataList{metadata: make([]*Metadata, len(metadata))}
for i, md := range metadata {
list.metadata[i] = &Metadata{
Key: md.Key,
value: md.Value,
}
}
return list
}
func MetadataListToDomain(metadataList *MetadataList) []*domain.Metadata {
if metadataList == nil {
return nil
}
list := make([]*domain.Metadata, len(metadataList.metadata))
for i, metadata := range metadataList.metadata {
value := metadata.value
if len(value) == 0 {
value = mapBytesToByteArray(metadata.Value.Export())
}
list[i] = &domain.Metadata{
Key: metadata.Key,
Value: value,
}
}
return list
}
// mapBytesToByteArray is used for backwards compatibility of old metadata.push method
// converts the Javascript uint8 array which is exported as []interface{} to a []byte
func mapBytesToByteArray(i interface{}) []byte {
bytes, ok := i.([]interface{})
if !ok {
return nil
}
value := make([]byte, len(bytes))
for i, val := range bytes {
b, ok := val.(int64)
if !ok {
return nil
}
value[i] = byte(b)
}
return value
}

View File

@@ -0,0 +1,14 @@
package object
import "github.com/dop251/goja"
func objectFromFirstArgument(call goja.FunctionCall, runtime *goja.Runtime) *goja.Object {
if len(call.Arguments) != 1 {
panic("exactly one argument expected")
}
object := call.Arguments[0].ToObject(runtime)
if object == nil {
panic("unable to unmarshal arg")
}
return object
}

View File

@@ -0,0 +1,181 @@
package object
import (
"time"
"github.com/dop251/goja"
"github.com/zitadel/zitadel/internal/actions"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
)
func UserFromExternalUser(c *actions.FieldConfig, user *domain.ExternalUser) goja.Value {
return c.Runtime.ToValue(externalUserFromDomain(user))
}
func externalUsersFromDomain(users []*domain.ExternalUser) []*externalUser {
externalUsers := make([]*externalUser, len(users))
for i, user := range users {
externalUsers[i] = externalUserFromDomain(user)
}
return externalUsers
}
func externalUserFromDomain(user *domain.ExternalUser) *externalUser {
return &externalUser{
ExternalId: user.ExternalUserID,
ExternalIdpId: user.IDPConfigID,
Human: human{
FirstName: user.FirstName,
LastName: user.LastName,
NickName: user.NickName,
DisplayName: user.DisplayName,
PreferredLanguage: user.PreferredLanguage.String(),
Email: user.Email,
IsEmailVerified: user.IsEmailVerified,
Phone: user.Phone,
IsPhoneVerified: user.IsPhoneVerified,
},
}
}
func UserFromHuman(c *actions.FieldConfig, user *domain.Human) goja.Value {
u := &humanUser{
Id: user.AggregateID,
CreationDate: user.CreationDate,
ChangeDate: user.ChangeDate,
ResourceOwner: user.ResourceOwner,
Sequence: user.Sequence,
State: user.State,
Username: user.Username,
LoginNames: user.LoginNames,
PreferredLoginName: user.PreferredLoginName,
}
if user.Profile != nil {
u.Human.FirstName = user.Profile.FirstName
u.Human.LastName = user.Profile.LastName
u.Human.NickName = user.Profile.NickName
u.Human.DisplayName = user.Profile.DisplayName
u.Human.PreferredLanguage = user.Profile.PreferredLanguage.String()
}
if user.Email != nil {
u.Human.Email = user.Email.EmailAddress
u.Human.IsEmailVerified = user.Email.IsEmailVerified
}
if user.Phone != nil {
u.Human.Phone = user.Phone.PhoneNumber
u.Human.IsPhoneVerified = user.Phone.IsPhoneVerified
}
return c.Runtime.ToValue(u)
}
func UserFromQuery(c *actions.FieldConfig, user *query.User) goja.Value {
if user.Human != nil {
return humanFromQuery(c, user)
}
return machineFromQuery(c, user)
}
func humanFromQuery(c *actions.FieldConfig, user *query.User) goja.Value {
return c.Runtime.ToValue(&humanUser{
Id: user.ID,
CreationDate: user.CreationDate,
ChangeDate: user.ChangeDate,
ResourceOwner: user.ResourceOwner,
Sequence: user.Sequence,
State: user.State,
Username: user.Username,
LoginNames: user.LoginNames,
PreferredLoginName: user.PreferredLoginName,
Human: human{
FirstName: user.Human.FirstName,
LastName: user.Human.LastName,
NickName: user.Human.NickName,
DisplayName: user.Human.DisplayName,
AvatarKey: user.Human.AvatarKey,
PreferredLanguage: user.Human.PreferredLanguage.String(),
Gender: user.Human.Gender,
Email: user.Human.Email,
IsEmailVerified: user.Human.IsEmailVerified,
Phone: user.Human.Phone,
IsPhoneVerified: user.Human.IsPhoneVerified,
},
})
}
func machineFromQuery(c *actions.FieldConfig, user *query.User) goja.Value {
return c.Runtime.ToValue(&machineUser{
Id: user.ID,
CreationDate: user.CreationDate,
ChangeDate: user.ChangeDate,
ResourceOwner: user.ResourceOwner,
Sequence: user.Sequence,
State: user.State,
Username: user.Username,
LoginNames: user.LoginNames,
PreferredLoginName: user.PreferredLoginName,
Machine: machine{
Name: user.Machine.Name,
Description: user.Machine.Description,
},
})
}
type externalUser struct {
ExternalId string
ExternalIdpId string
Human human
}
type humanUser struct {
Id string
CreationDate time.Time
ChangeDate time.Time
ResourceOwner string
Sequence uint64
State domain.UserState
Username string
LoginNames database.TextArray[string]
PreferredLoginName string
Human human
}
type human struct {
FirstName string
LastName string
NickName string
DisplayName string
AvatarKey string
PreferredLanguage string
Gender domain.Gender
Email domain.EmailAddress
IsEmailVerified bool
Phone domain.PhoneNumber
IsPhoneVerified bool
}
type machineUser struct {
Id string
CreationDate time.Time
ChangeDate time.Time
ResourceOwner string
Sequence uint64
State domain.UserState
Username string
LoginNames database.TextArray[string]
PreferredLoginName string
Machine machine
}
type machine struct {
Name string
Description string
}

View File

@@ -0,0 +1,178 @@
package object
import (
"context"
"time"
"github.com/dop251/goja"
"github.com/zitadel/zitadel/internal/actions"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
)
type UserGrants struct {
UserGrants []UserGrant
}
type UserGrant struct {
ProjectID string
ProjectGrantID string
Roles []string
}
type userGrantList struct {
Count uint64
Sequence uint64
Timestamp time.Time
Grants []*userGrant
}
type userGrant struct {
Id string
ProjectGrantId string
State domain.UserGrantState
UserGrantResourceOwner string
UserGrantResourceOwnerName string
CreationDate time.Time
ChangeDate time.Time
Sequence uint64
UserId string
UserResourceOwner string
Roles []string
ProjectId string
ProjectName string
GetOrgMetadata func(goja.FunctionCall) goja.Value
}
func AppendGrantFunc(userGrants *UserGrants) func(c *actions.FieldConfig) func(call goja.FunctionCall) goja.Value {
return func(c *actions.FieldConfig) func(call goja.FunctionCall) goja.Value {
return func(call goja.FunctionCall) goja.Value {
firstArg := objectFromFirstArgument(call, c.Runtime)
grant := UserGrant{}
mapObjectToGrant(firstArg, &grant)
userGrants.UserGrants = append(userGrants.UserGrants, grant)
return nil
}
}
}
func UserGrantsFromQuery(ctx context.Context, queries *query.Queries, c *actions.FieldConfig, userGrants *query.UserGrants) goja.Value {
if userGrants == nil {
return c.Runtime.ToValue(nil)
}
orgMetadata := make(map[string]goja.Value)
grantList := &userGrantList{
Count: userGrants.Count,
Sequence: userGrants.Sequence,
Timestamp: userGrants.LastRun,
Grants: make([]*userGrant, len(userGrants.UserGrants)),
}
for i, grant := range userGrants.UserGrants {
grantList.Grants[i] = &userGrant{
Id: grant.ID,
ProjectGrantId: grant.GrantID,
State: grant.State,
CreationDate: grant.CreationDate,
ChangeDate: grant.ChangeDate,
Sequence: grant.Sequence,
UserId: grant.UserID,
Roles: grant.Roles,
UserResourceOwner: grant.UserResourceOwner,
UserGrantResourceOwner: grant.ResourceOwner,
UserGrantResourceOwnerName: grant.OrgName,
ProjectId: grant.ProjectID,
ProjectName: grant.ProjectName,
GetOrgMetadata: func(call goja.FunctionCall) goja.Value {
if md, ok := orgMetadata[grant.ResourceOwner]; ok {
return md
}
orgMetadata[grant.ResourceOwner] = GetOrganizationMetadata(ctx, queries, c, grant.ResourceOwner)
return orgMetadata[grant.ResourceOwner]
},
}
}
return c.Runtime.ToValue(grantList)
}
func UserGrantsFromSlice(ctx context.Context, queries *query.Queries, c *actions.FieldConfig, userGrants []query.UserGrant) goja.Value {
if userGrants == nil {
return c.Runtime.ToValue(nil)
}
orgMetadata := make(map[string]goja.Value)
grantList := &userGrantList{
Count: uint64(len(userGrants)),
Grants: make([]*userGrant, len(userGrants)),
}
for i, grant := range userGrants {
grantList.Grants[i] = &userGrant{
Id: grant.ID,
ProjectGrantId: grant.GrantID,
State: grant.State,
CreationDate: grant.CreationDate,
ChangeDate: grant.ChangeDate,
Sequence: grant.Sequence,
UserId: grant.UserID,
Roles: grant.Roles,
UserResourceOwner: grant.UserResourceOwner,
UserGrantResourceOwner: grant.ResourceOwner,
UserGrantResourceOwnerName: grant.OrgName,
ProjectId: grant.ProjectID,
ProjectName: grant.ProjectName,
GetOrgMetadata: func(goja.FunctionCall) goja.Value {
if md, ok := orgMetadata[grant.ResourceOwner]; ok {
return md
}
orgMetadata[grant.ResourceOwner] = GetOrganizationMetadata(ctx, queries, c, grant.ResourceOwner)
return orgMetadata[grant.ResourceOwner]
},
}
}
return c.Runtime.ToValue(grantList)
}
func UserGrantsToDomain(userID string, actionUserGrants []UserGrant) []*domain.UserGrant {
if actionUserGrants == nil {
return nil
}
userGrants := make([]*domain.UserGrant, len(actionUserGrants))
for i, grant := range actionUserGrants {
userGrants[i] = &domain.UserGrant{
UserID: userID,
ProjectID: grant.ProjectID,
ProjectGrantID: grant.ProjectGrantID,
RoleKeys: grant.Roles,
}
}
return userGrants
}
func mapObjectToGrant(object *goja.Object, grant *UserGrant) {
for _, key := range object.Keys() {
switch key {
case "projectId", "projectID":
grant.ProjectID = object.Get(key).String()
case "projectGrantId", "projectGrantID":
grant.ProjectGrantID = object.Get(key).String()
case "roles":
if roles, ok := object.Get(key).Export().([]interface{}); ok {
for _, role := range roles {
if r, ok := role.(string); ok {
grant.Roles = append(grant.Roles, r)
}
}
}
}
}
if grant.ProjectID == "" {
panic("projectId not set")
}
}

View File

@@ -0,0 +1,83 @@
package actions
import (
"context"
"github.com/dop251/goja"
"github.com/google/uuid"
"github.com/zitadel/logging"
)
func WithUUID(ctx context.Context) Option {
return func(c *runConfig) {
c.modules["zitadel/uuid"] = func(runtime *goja.Runtime, module *goja.Object) {
requireUUID(ctx, runtime, module)
}
}
}
func requireUUID(_ context.Context, runtime *goja.Runtime, module *goja.Object) {
o := module.Get("exports").(*goja.Object)
logging.OnError(o.Set("v1", inRuntime(uuid.NewUUID, runtime))).Warn("unable to set module")
logging.OnError(o.Set("v3", inRuntimeHash(uuid.NewMD5, runtime))).Warn("unable to set module")
logging.OnError(o.Set("v4", inRuntime(uuid.NewRandom, runtime))).Warn("unable to set module")
logging.OnError(o.Set("v5", inRuntimeHash(uuid.NewSHA1, runtime))).Warn("unable to set module")
logging.OnError(o.Set("namespaceDNS", uuid.NameSpaceDNS)).Warn("unable to set namespace")
logging.OnError(o.Set("namespaceURL", uuid.NameSpaceURL)).Warn("unable to set namespace")
logging.OnError(o.Set("namespaceOID", uuid.NameSpaceOID)).Warn("unable to set namespace")
logging.OnError(o.Set("namespaceX500", uuid.NameSpaceX500)).Warn("unable to set namespace")
}
func inRuntime(function func() (uuid.UUID, error), runtime *goja.Runtime) func(call goja.FunctionCall) goja.Value {
return func(call goja.FunctionCall) goja.Value {
if len(call.Arguments) != 0 {
panic("invalid arg count")
}
uuid, err := function()
if err != nil {
logging.WithError(err)
panic(err)
}
return runtime.ToValue(uuid.String())
}
}
func inRuntimeHash(function func(uuid.UUID, []byte) uuid.UUID, runtime *goja.Runtime) func(call goja.FunctionCall) goja.Value {
return func(call goja.FunctionCall) goja.Value {
if len(call.Arguments) != 2 {
logging.WithFields("count", len(call.Arguments)).Debug("other than 2 args provided")
panic("invalid arg count")
}
var err error
var namespace uuid.UUID
switch n := call.Arguments[0].Export().(type) {
case string:
namespace, err = uuid.Parse(n)
if err != nil {
logging.WithError(err).Debug("namespace failed parsing as UUID")
panic(err)
}
case uuid.UUID:
namespace = n
default:
logging.WithError(err).Debug("invalid type for namespace")
panic(err)
}
var data []byte
switch d := call.Arguments[1].Export().(type) {
case string:
data = []byte(d)
case []byte:
data = d
default:
logging.WithError(err).Debug("invalid type for data")
panic(err)
}
return runtime.ToValue(function(namespace, data).String())
}
}

View File

@@ -0,0 +1,141 @@
package activity
import (
"context"
"strconv"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/api/authz"
http_utils "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/api/info"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/user"
)
const (
Activity = "activity"
PathKey = "zitadel-activity-path"
RequestMethodKey = "zitadel-activity-request-method"
)
type TriggerMethod int
const (
Unspecified TriggerMethod = iota
ResourceAPI
OIDCAccessToken
OIDCRefreshToken
SessionAPI
SAMLResponse
)
func (t TriggerMethod) String() string {
switch t {
case Unspecified:
return "unspecified"
case ResourceAPI:
return "resourceAPI"
case OIDCRefreshToken:
return "refreshToken"
case OIDCAccessToken:
return "accessToken"
case SessionAPI:
return "sessionAPI"
case SAMLResponse:
return "samlResponse"
default:
return "unknown"
}
}
// Trigger is used to log a specific events for a user (e.g. session or oidc token creation)
func Trigger(ctx context.Context, orgID, userID string, trigger TriggerMethod, reducer func(ctx context.Context, r eventstore.QueryReducer) error) {
if orgID == "" && userID != "" {
orgID = getOrgOfUser(ctx, userID, reducer)
}
ai := info.ActivityInfoFromContext(ctx)
triggerLog(
authz.GetInstance(ctx).InstanceID(),
orgID,
userID,
http_utils.DomainContext(ctx).Origin(), // TODO: origin?
trigger,
ai.Method,
ai.Path,
ai.RequestMethod,
"",
"",
authz.GetCtxData(ctx).SystemMemberships != nil,
)
}
func TriggerGRPCWithContext(ctx context.Context, trigger TriggerMethod) {
ai := info.ActivityInfoFromContext(ctx)
triggerLog(
authz.GetInstance(ctx).InstanceID(),
authz.GetCtxData(ctx).OrgID,
authz.GetCtxData(ctx).UserID,
http_utils.DomainContext(ctx).Origin(), // TODO: origin?
trigger,
ai.Method,
ai.Path,
ai.RequestMethod,
strconv.Itoa(int(ai.GRPCStatus)),
strconv.Itoa(runtime.HTTPStatusFromCode(ai.GRPCStatus)),
authz.GetCtxData(ctx).SystemMemberships != nil,
)
}
func triggerLog(instanceID, orgID, userID, domain string, trigger TriggerMethod, method, path, requestMethod, grpcStatus, httpStatus string, isSystemUser bool) {
logging.WithFields(
"instance", instanceID,
"org", orgID,
"user", userID,
"domain", domain,
"trigger", trigger.String(),
"method", method,
"path", path,
"grpcStatus", grpcStatus,
"httpStatus", httpStatus,
"requestMethod", requestMethod,
"isSystemUser", isSystemUser,
).Info(Activity)
}
func getOrgOfUser(ctx context.Context, userID string, reducer func(ctx context.Context, r eventstore.QueryReducer) error) string {
org := &orgIDOfUser{userID: userID}
err := reducer(ctx, org)
if err != nil {
logging.WithError(err).Error("could not get org id of user for trigger log")
return ""
}
return org.orgID
}
type orgIDOfUser struct {
eventstore.WriteModel
userID string
orgID string
}
func (u *orgIDOfUser) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
OrderDesc().
Limit(1).
AddQuery().
AggregateTypes(user.AggregateType).
AggregateIDs(u.userID).
Builder()
}
func (u *orgIDOfUser) Reduce() error {
if len(u.Events) == 0 {
return nil
}
u.orgID = u.Events[0].Aggregate().ResourceOwner
return nil
}

View File

@@ -0,0 +1,103 @@
package handler
import (
"context"
"errors"
"fmt"
"time"
"github.com/jackc/pgx/v5/pgconn"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/admin/repository/eventsourcing/view"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
handler2 "github.com/zitadel/zitadel/internal/eventstore/handler/v2"
"github.com/zitadel/zitadel/internal/static"
)
type Config struct {
Client *database.DB
Eventstore *eventstore.Eventstore
BulkLimit uint64
FailureCountUntilSkip uint64
TransactionDuration time.Duration
Handlers map[string]*ConfigOverwrites
ActiveInstancer interface {
ActiveInstances() []string
}
}
type ConfigOverwrites struct {
MinimumCycleDuration time.Duration
}
var projections []*handler.Handler
func Register(ctx context.Context, config Config, view *view.View, static static.Storage) {
if static == nil {
return
}
// make sure the slice does not contain old values
projections = nil
projections = append(projections, newStyling(ctx,
config.overwrite("Styling"),
static,
view,
))
}
func Projections() []*handler2.Handler {
return projections
}
func Start(ctx context.Context) {
for _, projection := range projections {
projection.Start(ctx)
}
}
func ProjectInstance(ctx context.Context) error {
for i, projection := range projections {
logging.WithFields("name", projection.ProjectionName(), "instance", authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(projections))).Info("starting admin projection")
for {
_, err := projection.Trigger(ctx)
if err == nil {
break
}
var pgErr *pgconn.PgError
errors.As(err, &pgErr)
if pgErr.Code != database.PgUniqueConstraintErrorCode {
return err
}
logging.WithFields("name", projection.ProjectionName(), "instance", authz.GetInstance(ctx).InstanceID()).WithError(err).Debug("admin projection failed because of unique constraint, retrying")
}
logging.WithFields("name", projection.ProjectionName(), "instance", authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(projections))).Info("admin projection done")
}
return nil
}
func (config Config) overwrite(viewModel string) handler2.Config {
c := handler2.Config{
Client: config.Client,
Eventstore: config.Eventstore,
BulkLimit: uint16(config.BulkLimit),
RequeueEvery: 3 * time.Minute,
MaxFailureCount: uint8(config.FailureCountUntilSkip),
TransactionDuration: config.TransactionDuration,
ActiveInstancer: config.ActiveInstancer,
}
overwrite, ok := config.Handlers[viewModel]
if !ok {
return c
}
if overwrite.MinimumCycleDuration > 0 {
c.RequeueEvery = overwrite.MinimumCycleDuration
}
return c
}

View File

@@ -0,0 +1,419 @@
package handler
import (
"bytes"
"context"
"fmt"
"io"
"strings"
"github.com/lucasb-eyer/go-colorful"
"github.com/muesli/gamut"
admin_view "github.com/zitadel/zitadel/internal/admin/repository/eventsourcing/view"
"github.com/zitadel/zitadel/internal/api/ui/login"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
iam_model "github.com/zitadel/zitadel/internal/iam/repository/view/model"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/static"
)
const (
stylingTable = "adminapi.styling2"
)
var _ handler.Projection = (*Styling)(nil)
type Styling struct {
static static.Storage
view *admin_view.View
}
func newStyling(ctx context.Context, config handler.Config, static static.Storage, view *admin_view.View) *handler.Handler {
return handler.NewHandler(
ctx,
&config,
&Styling{
static: static,
view: view,
},
)
}
// Name implements [handler.Projection]
func (*Styling) Name() string {
return stylingTable
}
// Reducers implements [handler.Projection]
func (s *Styling) Reducers() []handler.AggregateReducer {
return []handler.AggregateReducer{
{
Aggregate: org.AggregateType,
EventReducers: []handler.EventReducer{
{
Event: org.LabelPolicyAddedEventType,
Reduce: s.processLabelPolicy,
},
{
Event: org.LabelPolicyChangedEventType,
Reduce: s.processLabelPolicy,
},
{
Event: org.LabelPolicyLogoAddedEventType,
Reduce: s.processLabelPolicy,
},
{
Event: org.LabelPolicyLogoRemovedEventType,
Reduce: s.processLabelPolicy,
},
{
Event: org.LabelPolicyIconAddedEventType,
Reduce: s.processLabelPolicy,
},
{
Event: org.LabelPolicyIconRemovedEventType,
Reduce: s.processLabelPolicy,
},
{
Event: org.LabelPolicyLogoDarkAddedEventType,
Reduce: s.processLabelPolicy,
},
{
Event: org.LabelPolicyLogoDarkRemovedEventType,
Reduce: s.processLabelPolicy,
},
{
Event: org.LabelPolicyIconDarkAddedEventType,
Reduce: s.processLabelPolicy,
},
{
Event: org.LabelPolicyIconDarkRemovedEventType,
Reduce: s.processLabelPolicy,
},
{
Event: org.LabelPolicyFontAddedEventType,
Reduce: s.processLabelPolicy,
},
{
Event: org.LabelPolicyFontRemovedEventType,
Reduce: s.processLabelPolicy,
},
{
Event: org.LabelPolicyAssetsRemovedEventType,
Reduce: s.processLabelPolicy,
},
{
Event: org.LabelPolicyActivatedEventType,
Reduce: s.processLabelPolicy,
},
{
Event: org.OrgRemovedEventType,
Reduce: s.processLabelPolicy,
},
},
},
{
Aggregate: instance.AggregateType,
EventReducers: []handler.EventReducer{
{
Event: instance.LabelPolicyAddedEventType,
Reduce: s.processLabelPolicy,
},
{
Event: instance.LabelPolicyChangedEventType,
Reduce: s.processLabelPolicy,
},
{
Event: instance.LabelPolicyLogoAddedEventType,
Reduce: s.processLabelPolicy,
},
{
Event: instance.LabelPolicyLogoRemovedEventType,
Reduce: s.processLabelPolicy,
},
{
Event: instance.LabelPolicyIconAddedEventType,
Reduce: s.processLabelPolicy,
},
{
Event: instance.LabelPolicyIconRemovedEventType,
Reduce: s.processLabelPolicy,
},
{
Event: instance.LabelPolicyLogoDarkAddedEventType,
Reduce: s.processLabelPolicy,
},
{
Event: instance.LabelPolicyLogoDarkRemovedEventType,
Reduce: s.processLabelPolicy,
},
{
Event: instance.LabelPolicyIconDarkAddedEventType,
Reduce: s.processLabelPolicy,
},
{
Event: instance.LabelPolicyIconDarkRemovedEventType,
Reduce: s.processLabelPolicy,
},
{
Event: instance.LabelPolicyFontAddedEventType,
Reduce: s.processLabelPolicy,
},
{
Event: instance.LabelPolicyFontRemovedEventType,
Reduce: s.processLabelPolicy,
},
{
Event: instance.LabelPolicyAssetsRemovedEventType,
Reduce: s.processLabelPolicy,
},
{
Event: instance.LabelPolicyActivatedEventType,
Reduce: s.processLabelPolicy,
},
{
Event: instance.InstanceRemovedEventType,
Reduce: s.processLabelPolicy,
},
},
},
}
}
func (m *Styling) processLabelPolicy(event eventstore.Event) (_ *handler.Statement, err error) {
return handler.NewStatement(event, func(ctx context.Context, ex handler.Executer, projectionName string) error {
policy := new(iam_model.LabelPolicyView)
switch event.Type() {
case instance.LabelPolicyAddedEventType,
org.LabelPolicyAddedEventType:
err = policy.AppendEvent(event)
case instance.LabelPolicyChangedEventType,
org.LabelPolicyChangedEventType,
instance.LabelPolicyLogoAddedEventType,
org.LabelPolicyLogoAddedEventType,
instance.LabelPolicyLogoRemovedEventType,
org.LabelPolicyLogoRemovedEventType,
instance.LabelPolicyIconAddedEventType,
org.LabelPolicyIconAddedEventType,
instance.LabelPolicyIconRemovedEventType,
org.LabelPolicyIconRemovedEventType,
instance.LabelPolicyLogoDarkAddedEventType,
org.LabelPolicyLogoDarkAddedEventType,
instance.LabelPolicyLogoDarkRemovedEventType,
org.LabelPolicyLogoDarkRemovedEventType,
instance.LabelPolicyIconDarkAddedEventType,
org.LabelPolicyIconDarkAddedEventType,
instance.LabelPolicyIconDarkRemovedEventType,
org.LabelPolicyIconDarkRemovedEventType,
instance.LabelPolicyFontAddedEventType,
org.LabelPolicyFontAddedEventType,
instance.LabelPolicyFontRemovedEventType,
org.LabelPolicyFontRemovedEventType,
instance.LabelPolicyAssetsRemovedEventType,
org.LabelPolicyAssetsRemovedEventType:
policy, err = m.view.StylingByAggregateIDAndState(event.Aggregate().ID, event.Aggregate().InstanceID, int32(domain.LabelPolicyStatePreview))
if err != nil {
return err
}
err = policy.AppendEvent(event)
case instance.LabelPolicyActivatedEventType,
org.LabelPolicyActivatedEventType:
policy, err = m.view.StylingByAggregateIDAndState(event.Aggregate().ID, event.Aggregate().InstanceID, int32(domain.LabelPolicyStatePreview))
if err != nil {
return err
}
err = policy.AppendEvent(event)
if err != nil {
return err
}
err = m.generateStylingFile(policy)
case instance.InstanceRemovedEventType:
err = m.deleteInstanceFilesFromStorage(event.Aggregate().InstanceID)
if err != nil {
return err
}
return m.view.DeleteInstanceStyling(event)
case org.OrgRemovedEventType:
return m.view.UpdateOrgOwnerRemovedStyling(event)
default:
return nil
}
if err != nil {
return err
}
return m.view.PutStyling(policy, event)
}), nil
}
func (m *Styling) generateStylingFile(policy *iam_model.LabelPolicyView) error {
reader, size, err := m.writeFile(policy)
if err != nil {
return err
}
return m.uploadFilesToStorage(policy.InstanceID, policy.AggregateID, "text/css", reader, size)
}
func (m *Styling) writeFile(policy *iam_model.LabelPolicyView) (io.Reader, int64, error) {
cssContent := ""
cssContent += ":root {"
if policy.PrimaryColor != "" {
palette := m.generateColorPaletteRGBA255(policy.PrimaryColor)
for i, color := range palette {
cssContent += fmt.Sprintf("--zitadel-color-primary-%v: %s;", i, color)
}
}
if policy.BackgroundColor != "" {
palette := m.generateColorPaletteRGBA255(policy.BackgroundColor)
for i, color := range palette {
cssContent += fmt.Sprintf("--zitadel-color-background-%v: %s;", i, color)
}
}
if policy.WarnColor != "" {
palette := m.generateColorPaletteRGBA255(policy.WarnColor)
for i, color := range palette {
cssContent += fmt.Sprintf("--zitadel-color-warn-%v: %s;", i, color)
}
}
if policy.FontColor != "" {
cssContent += fmt.Sprintf("--zitadel-color-label: %s;", policy.FontColor)
palette := m.generateColorPaletteRGBA255(policy.FontColor)
for i, color := range palette {
cssContent += fmt.Sprintf("--zitadel-color-text-%v: %s;", i, color)
}
}
var fontname string
if policy.FontURL != "" {
split := strings.Split(policy.FontURL, "/")
fontname = split[len(split)-1]
cssContent += fmt.Sprintf("--zitadel-font-family: %s;", fontname)
}
cssContent += "}"
if policy.FontURL != "" {
cssContent += fmt.Sprintf(fontFaceTemplate, fontname, login.HandlerPrefix+login.EndpointDynamicResources, policy.AggregateID, policy.FontURL)
}
cssContent += ".lgn-dark-theme {"
if policy.PrimaryColorDark != "" {
palette := m.generateColorPaletteRGBA255(policy.PrimaryColorDark)
for i, color := range palette {
cssContent += fmt.Sprintf("--zitadel-color-primary-%v: %s;", i, color)
}
}
if policy.BackgroundColorDark != "" {
palette := m.generateColorPaletteRGBA255(policy.BackgroundColorDark)
for i, color := range palette {
cssContent += fmt.Sprintf("--zitadel-color-background-%v: %s;", i, color)
}
}
if policy.WarnColorDark != "" {
palette := m.generateColorPaletteRGBA255(policy.WarnColorDark)
for i, color := range palette {
cssContent += fmt.Sprintf("--zitadel-color-warn-%v: %s;", i, color)
}
}
if policy.FontColorDark != "" {
cssContent += fmt.Sprintf("--zitadel-color-label: %s;", policy.FontColorDark)
palette := m.generateColorPaletteRGBA255(policy.FontColorDark)
for i, color := range palette {
cssContent += fmt.Sprintf("--zitadel-color-text-%v: %s;", i, color)
}
}
cssContent += "}"
data := []byte(cssContent)
buffer := bytes.NewBuffer(data)
return buffer, int64(buffer.Len()), nil
}
const fontFaceTemplate = `
@font-face {
font-family: '%s';
font-style: normal;
font-display: swap;
src: url(%s?orgId=%s&filename=%s);
}
`
func (m *Styling) uploadFilesToStorage(instanceID, aggregateID, contentType string, reader io.Reader, size int64) error {
fileName := domain.CssPath + "/" + domain.CssVariablesFileName
//TODO: handle location as soon as possible
_, err := m.static.PutObject(context.Background(), instanceID, "", aggregateID, fileName, contentType, static.ObjectTypeStyling, reader, size)
return err
}
func (m *Styling) deleteInstanceFilesFromStorage(instanceID string) error {
return m.static.RemoveInstanceObjects(context.Background(), instanceID)
}
func (m *Styling) generateColorPaletteRGBA255(hex string) map[string]string {
palette := make(map[string]string)
defaultColor := gamut.Hex(hex)
color50, ok := colorful.MakeColor(gamut.Lighter(defaultColor, 0.52))
if ok {
palette["50"] = cssRGB(color50.RGB255())
}
color100, ok := colorful.MakeColor(gamut.Lighter(defaultColor, 0.37))
if ok {
palette["100"] = cssRGB(color100.RGB255())
}
color200, ok := colorful.MakeColor(gamut.Lighter(defaultColor, 0.26))
if ok {
palette["200"] = cssRGB(color200.RGB255())
}
color300, ok := colorful.MakeColor(gamut.Lighter(defaultColor, 0.12))
if ok {
palette["300"] = cssRGB(color300.RGB255())
}
color400, ok := colorful.MakeColor(gamut.Lighter(defaultColor, 0.06))
if ok {
palette["400"] = cssRGB(color400.RGB255())
}
color500, ok := colorful.MakeColor(defaultColor)
if ok {
palette["500"] = cssRGB(color500.RGB255())
}
color600, ok := colorful.MakeColor(gamut.Darker(defaultColor, 0.06))
if ok {
palette["600"] = cssRGB(color600.RGB255())
}
color700, ok := colorful.MakeColor(gamut.Darker(defaultColor, 0.12))
if ok {
palette["700"] = cssRGB(color700.RGB255())
}
color800, ok := colorful.MakeColor(gamut.Darker(defaultColor, 0.18))
if ok {
palette["800"] = cssRGB(color800.RGB255())
}
color900, ok := colorful.MakeColor(gamut.Darker(defaultColor, 0.24))
if ok {
palette["900"] = cssRGB(color900.RGB255())
}
colorContrast, ok := colorful.MakeColor(gamut.Contrast(defaultColor))
if ok {
palette["contrast"] = cssRGB(colorContrast.RGB255())
}
return palette
}
func cssRGB(r, g, b uint8) string {
return fmt.Sprintf("rgb(%v, %v, %v)", r, g, b)
}

View File

@@ -0,0 +1,27 @@
package eventsourcing
import (
"context"
admin_handler "github.com/zitadel/zitadel/internal/admin/repository/eventsourcing/handler"
admin_view "github.com/zitadel/zitadel/internal/admin/repository/eventsourcing/view"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/static"
)
type Config struct {
Spooler admin_handler.Config
}
func Start(ctx context.Context, conf Config, static static.Storage, dbClient *database.DB, queries *query.Queries) error {
view, err := admin_view.StartView(dbClient)
if err != nil {
return err
}
admin_handler.Register(ctx, conf.Spooler, view, static)
admin_handler.Start(ctx)
return nil
}

View File

@@ -0,0 +1,27 @@
package view
import (
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/iam/repository/view"
"github.com/zitadel/zitadel/internal/iam/repository/view/model"
)
const (
stylingTyble = "adminapi.styling2"
)
func (v *View) StylingByAggregateIDAndState(aggregateID, instanceID string, state int32) (*model.LabelPolicyView, error) {
return view.GetStylingByAggregateIDAndState(v.Db, stylingTyble, aggregateID, instanceID, state)
}
func (v *View) PutStyling(policy *model.LabelPolicyView, event eventstore.Event) error {
return view.PutStyling(v.Db, stylingTyble, policy)
}
func (v *View) DeleteInstanceStyling(event eventstore.Event) error {
return view.DeleteInstanceStyling(v.Db, stylingTyble, event.Aggregate().InstanceID)
}
func (v *View) UpdateOrgOwnerRemovedStyling(event eventstore.Event) error {
return view.UpdateOrgOwnerRemovedStyling(v.Db, stylingTyble, event.Aggregate().InstanceID, event.Aggregate().ID)
}

View File

@@ -0,0 +1,25 @@
package view
import (
"github.com/jinzhu/gorm"
"github.com/zitadel/zitadel/internal/database"
)
type View struct {
Db *gorm.DB
}
func StartView(sqlClient *database.DB) (*View, error) {
gorm, err := gorm.Open("postgres", sqlClient.DB)
if err != nil {
return nil, err
}
return &View{
Db: gorm,
}, nil
}
func (v *View) Health() (err error) {
return v.Db.DB().Ping()
}

View File

@@ -0,0 +1,351 @@
package api
import (
"context"
"crypto/tls"
"net/http"
"sort"
"strings"
"connectrpc.com/grpcreflect"
"github.com/gorilla/mux"
"github.com/improbable-eng/grpc-web/go/grpcweb"
"github.com/zitadel/logging"
"google.golang.org/grpc"
"google.golang.org/grpc/health"
healthpb "google.golang.org/grpc/health/grpc_health_v1"
"github.com/zitadel/zitadel/internal/api/authz"
grpc_api "github.com/zitadel/zitadel/internal/api/grpc"
"github.com/zitadel/zitadel/internal/api/grpc/server"
"github.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware"
http_util "github.com/zitadel/zitadel/internal/api/http"
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
"github.com/zitadel/zitadel/internal/api/ui/login"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/telemetry/metrics"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
system_pb "github.com/zitadel/zitadel/pkg/grpc/system"
)
var (
metricTypes = []metrics.MetricType{metrics.MetricTypeTotalCount, metrics.MetricTypeRequestCount, metrics.MetricTypeStatusCode}
)
type API struct {
port uint16
externalDomain string
grpcServer *grpc.Server
verifier authz.APITokenVerifier
health healthCheck
router *mux.Router
hostHeaders []string
grpcGateway *server.Gateway
healthServer *health.Server
accessInterceptor *http_mw.AccessInterceptor
queries *query.Queries
authConfig authz.Config
systemAuthZ authz.Config
connectServices map[string][]string
}
func (a *API) ListGrpcServices() []string {
serviceInfo := a.grpcServer.GetServiceInfo()
services := make([]string, len(serviceInfo)+len(a.connectServices))
i := 0
for servicename := range serviceInfo {
services[i] = servicename
i++
}
for prefix := range a.connectServices {
services[i] = strings.Trim(prefix, "/")
i++
}
sort.Strings(services)
return services
}
func (a *API) ListGrpcMethods() []string {
serviceInfo := a.grpcServer.GetServiceInfo()
methods := make([]string, 0)
for servicename, service := range serviceInfo {
for _, method := range service.Methods {
methods = append(methods, "/"+servicename+"/"+method.Name)
}
}
for service, methodList := range a.connectServices {
for _, method := range methodList {
methods = append(methods, service+method)
}
}
sort.Strings(methods)
return methods
}
type healthCheck interface {
Health(ctx context.Context) error
}
func New(
ctx context.Context,
port uint16,
router *mux.Router,
queries *query.Queries,
verifier authz.APITokenVerifier,
systemAuthz authz.Config,
authZ authz.Config,
tlsConfig *tls.Config,
externalDomain string,
hostHeaders []string,
accessInterceptor *http_mw.AccessInterceptor,
) (_ *API, err error) {
api := &API{
port: port,
externalDomain: externalDomain,
verifier: verifier,
health: queries,
router: router,
queries: queries,
accessInterceptor: accessInterceptor,
hostHeaders: hostHeaders,
authConfig: authZ,
systemAuthZ: systemAuthz,
connectServices: make(map[string][]string),
}
api.grpcServer = server.CreateServer(api.verifier, systemAuthz, authZ, queries, externalDomain, tlsConfig, accessInterceptor.AccessService())
api.grpcGateway, err = server.CreateGateway(ctx, port, hostHeaders, accessInterceptor, tlsConfig)
if err != nil {
return nil, err
}
api.registerHealthServer()
api.RegisterHandlerOnPrefix("/debug", api.healthHandler())
api.router.Handle("/", http.RedirectHandler(login.HandlerPrefix, http.StatusFound))
return api, nil
}
func (a *API) serverReflection() {
reflector := grpcreflect.NewStaticReflector(a.ListGrpcServices()...)
a.RegisterHandlerOnPrefix(grpcreflect.NewHandlerV1(reflector))
a.RegisterHandlerOnPrefix(grpcreflect.NewHandlerV1Alpha(reflector))
}
// RegisterServer registers a grpc service on the grpc server,
// creates a new grpc gateway and registers it as a separate http handler
//
// used for v1 api (system, admin, mgmt, auth)
func (a *API) RegisterServer(ctx context.Context, grpcServer server.WithGatewayPrefix, tlsConfig *tls.Config) error {
grpcServer.RegisterServer(a.grpcServer)
handler, prefix, err := server.CreateGatewayWithPrefix(
ctx,
grpcServer,
a.port,
a.hostHeaders,
a.accessInterceptor,
tlsConfig,
)
if err != nil {
return err
}
a.RegisterHandlerOnPrefix(prefix, handler)
a.verifier.RegisterServer(grpcServer.AppName(), grpcServer.MethodPrefix(), grpcServer.AuthMethods())
a.healthServer.SetServingStatus(grpcServer.MethodPrefix(), healthpb.HealthCheckResponse_SERVING)
return nil
}
// RegisterService registers a grpc service on the grpc server,
// and its gateway on the gateway handler
//
// used for >= v2 api (e.g. user, session, ...)
func (a *API) RegisterService(ctx context.Context, srv server.Server) error {
switch service := srv.(type) {
case server.GrpcServer:
service.RegisterServer(a.grpcServer)
case server.ConnectServer:
a.registerConnectServer(service)
}
if withGateway, ok := srv.(server.WithGateway); ok {
err := server.RegisterGateway(ctx, a.grpcGateway, withGateway)
if err != nil {
return err
}
}
a.verifier.RegisterServer(srv.AppName(), srv.MethodPrefix(), srv.AuthMethods())
a.healthServer.SetServingStatus(srv.MethodPrefix(), healthpb.HealthCheckResponse_SERVING)
return nil
}
func (a *API) registerConnectServer(service server.ConnectServer) {
prefix, handler := service.RegisterConnectServer(
connect_middleware.CallDurationHandler(),
connect_middleware.MetricsHandler(metricTypes, grpc_api.Probes...),
connect_middleware.NoCacheInterceptor(),
connect_middleware.InstanceInterceptor(a.queries, a.externalDomain, system_pb.SystemService_ServiceDesc.ServiceName, healthpb.Health_ServiceDesc.ServiceName),
connect_middleware.AccessStorageInterceptor(a.accessInterceptor.AccessService()),
connect_middleware.ErrorHandler(),
connect_middleware.LimitsInterceptor(system_pb.SystemService_ServiceDesc.ServiceName),
connect_middleware.AuthorizationInterceptor(a.verifier, a.systemAuthZ, a.authConfig),
connect_middleware.TranslationHandler(),
connect_middleware.QuotaExhaustedInterceptor(a.accessInterceptor.AccessService(), system_pb.SystemService_ServiceDesc.ServiceName),
connect_middleware.ExecutionHandler(a.queries),
connect_middleware.ValidationHandler(),
connect_middleware.ServiceHandler(),
connect_middleware.ActivityInterceptor(),
)
methods := service.FileDescriptor().Services().Get(0).Methods()
methodNames := make([]string, methods.Len())
for i := 0; i < methods.Len(); i++ {
methodNames[i] = string(methods.Get(i).Name())
}
a.connectServices[prefix] = methodNames
a.RegisterHandlerPrefixes(http_mw.CORSInterceptor(handler), prefix)
}
// HandleFunc allows registering a [http.HandlerFunc] on an exact
// path, instead of prefix like RegisterHandlerOnPrefix.
func (a *API) HandleFunc(path string, f http.HandlerFunc) {
a.router.HandleFunc(path, f)
}
// RegisterHandlerOnPrefix registers a http handler on a path prefix
// the prefix will not be passed to the actual handler
func (a *API) RegisterHandlerOnPrefix(prefix string, handler http.Handler) {
prefix = strings.TrimSuffix(prefix, "/")
subRouter := a.router.PathPrefix(prefix).Name(prefix).Subrouter()
subRouter.PathPrefix("").Handler(http.StripPrefix(prefix, handler))
}
// RegisterHandlerPrefixes registers a http handler on a multiple path prefixes
// the prefix will remain when calling the actual handler
func (a *API) RegisterHandlerPrefixes(handler http.Handler, prefixes ...string) {
for _, prefix := range prefixes {
prefix = strings.TrimSuffix(prefix, "/")
subRouter := a.router.PathPrefix(prefix).Name(prefix).Subrouter()
subRouter.PathPrefix("").Handler(handler)
}
}
func (a *API) registerHealthServer() {
healthServer := health.NewServer()
healthpb.RegisterHealthServer(a.grpcServer, healthServer)
a.healthServer = healthServer
}
func (a *API) RouteGRPC() {
// since all services are now registered, we can build the grpc server reflection and register the handler
a.serverReflection()
http2Route := a.router.
MatcherFunc(func(r *http.Request, _ *mux.RouteMatch) bool {
return r.ProtoMajor == 2
}).
Subrouter().
Name("grpc")
http2Route.
Methods(http.MethodPost).
HeadersRegexp(http_util.ContentType, `^application\/grpc(\+proto|\+json)?$`).
Handler(a.grpcServer)
a.routeGRPCWeb()
a.router.NewRoute().
Handler(a.grpcGateway.Handler()).
Name("grpc-gateway")
}
func (a *API) routeGRPCWeb() {
grpcWebServer := grpcweb.WrapServer(a.grpcServer,
grpcweb.WithAllowedRequestHeaders(
[]string{
http_util.Origin,
http_util.ContentType,
http_util.Accept,
http_util.AcceptLanguage,
http_util.Authorization,
http_util.ZitadelOrgID,
http_util.XUserAgent,
http_util.XGrpcWeb,
},
),
grpcweb.WithOriginFunc(func(_ string) bool {
return true
}),
)
a.router.Use(http_mw.RobotsTagHandler)
a.router.NewRoute().
Methods(http.MethodPost, http.MethodOptions).
MatcherFunc(
func(r *http.Request, _ *mux.RouteMatch) bool {
return grpcWebServer.IsGrpcWebRequest(r) || grpcWebServer.IsAcceptableGrpcCorsRequest(r)
}).
Handler(grpcWebServer).
Name("grpc-web")
}
func (a *API) healthHandler() http.Handler {
checks := []ValidationFunction{
func(ctx context.Context) error {
if err := a.health.Health(ctx); err != nil {
return zerrors.ThrowInternal(err, "API-F24h2", "DB CONNECTION ERROR")
}
return nil
},
}
handler := http.NewServeMux()
handler.HandleFunc("/healthz", handleHealth)
handler.HandleFunc("/ready", handleReadiness(checks))
handler.HandleFunc("/validate", handleValidate(checks))
handler.Handle("/metrics", metricsExporter())
return handler
}
func handleHealth(w http.ResponseWriter, r *http.Request) {
_, err := w.Write([]byte("ok"))
logging.WithFields("traceID", tracing.TraceIDFromCtx(r.Context())).OnError(err).Error("error writing ok for health")
}
func handleReadiness(checks []ValidationFunction) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
errs := validate(r.Context(), checks)
if len(errs) == 0 {
http_util.MarshalJSON(w, "ok", nil, http.StatusOK)
return
}
http_util.MarshalJSON(w, nil, errs[0], http.StatusPreconditionFailed)
}
}
func handleValidate(checks []ValidationFunction) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
errs := validate(r.Context(), checks)
if len(errs) == 0 {
http_util.MarshalJSON(w, "ok", nil, http.StatusOK)
return
}
http_util.MarshalJSON(w, errs, nil, http.StatusOK)
}
}
type ValidationFunction func(ctx context.Context) error
func validate(ctx context.Context, validations []ValidationFunction) []error {
errs := make([]error, 0)
for _, validation := range validations {
if err := validation(ctx); err != nil {
logging.WithFields("traceID", tracing.TraceIDFromCtx(ctx)).WithError(err).Error("validation failed")
errs = append(errs, err)
}
}
return errs
}
func metricsExporter() http.Handler {
exporter := metrics.GetExporter()
if exporter == nil {
return http.NotFoundHandler()
}
return exporter
}

View File

@@ -0,0 +1,252 @@
package assets
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
"github.com/gabriel-vasile/mimetype"
"github.com/gorilla/mux"
"github.com/zitadel/logging"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/api/authz"
http_util "github.com/zitadel/zitadel/internal/api/http"
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/i18n"
"github.com/zitadel/zitadel/internal/id"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/static"
"github.com/zitadel/zitadel/internal/zerrors"
)
const (
HandlerPrefix = "/assets/v1"
)
type Handler struct {
errorHandler ErrorHandler
storage static.Storage
commands *command.Commands
authInterceptor *http_mw.AuthInterceptor
idGenerator id.Generator
query *query.Queries
}
func (h *Handler) AuthInterceptor() *http_mw.AuthInterceptor {
return h.authInterceptor
}
func (h *Handler) Commands() *command.Commands {
return h.commands
}
func (h *Handler) ErrorHandler() ErrorHandler {
return h.errorHandler
}
func (h *Handler) Storage() static.Storage {
return h.storage
}
func AssetAPI() func(context.Context) string {
return func(ctx context.Context) string {
return http_util.DomainContext(ctx).Origin() + HandlerPrefix
}
}
type Uploader interface {
UploadAsset(ctx context.Context, info string, asset *command.AssetUpload, commands *command.Commands) error
ObjectName(data authz.CtxData) (string, error)
ResourceOwner(instance authz.Instance, data authz.CtxData) string
ContentTypeAllowed(contentType string) bool
MaxFileSize() int64
ObjectType() static.ObjectType
}
type Downloader interface {
ObjectName(ctx context.Context, path string) (string, error)
ResourceOwner(ctx context.Context, ownerPath string) string
}
type ErrorHandler func(w http.ResponseWriter, r *http.Request, err error, defaultCode int)
func DefaultErrorHandler(translator *i18n.Translator) func(w http.ResponseWriter, r *http.Request, err error, defaultCode int) {
return func(w http.ResponseWriter, r *http.Request, err error, defaultCode int) {
logging.WithFields("uri", r.RequestURI).WithError(err).Warn("error occurred on asset api")
code, ok := http_util.ZitadelErrorToHTTPStatusCode(err)
if !ok {
code = defaultCode
}
zErr := new(zerrors.ZitadelError)
if errors.As(err, &zErr) {
zErr.SetMessage(translator.LocalizeFromCtx(r.Context(), zErr.GetMessage(), nil))
zErr.Parent = nil // ensuring we don't leak any unwanted information
err = zErr
}
http.Error(w, err.Error(), code)
}
}
func NewHandler(commands *command.Commands, verifier authz.APITokenVerifier, systemAuthCOnfig authz.Config, authConfig authz.Config, idGenerator id.Generator, storage static.Storage, queries *query.Queries, callDurationInterceptor, instanceInterceptor, assetCacheInterceptor, accessInterceptor func(handler http.Handler) http.Handler) http.Handler {
translator, err := i18n.NewZitadelTranslator(language.English)
logging.OnError(err).Panic("unable to get translator")
h := &Handler{
commands: commands,
errorHandler: DefaultErrorHandler(translator),
authInterceptor: http_mw.AuthorizationInterceptor(verifier, systemAuthCOnfig, authConfig),
idGenerator: idGenerator,
storage: storage,
query: queries,
}
verifier.RegisterServer("Assets-API", "assets", AssetsService_AuthMethods)
router := mux.NewRouter()
csp := http_mw.SecurityHeaders(&http_mw.DefaultSCP, nil)
router.Use(callDurationInterceptor, instanceInterceptor, assetCacheInterceptor, accessInterceptor, csp)
RegisterRoutes(router, h)
router.PathPrefix("/{owner}").Methods("GET").HandlerFunc(DownloadHandleFunc(h, h.GetFile()))
return http_util.CopyHeadersToContext(http_mw.CORSInterceptor(router))
}
func (h *Handler) GetFile() Downloader {
return &publicFileDownloader{}
}
type publicFileDownloader struct{}
func (l *publicFileDownloader) ObjectName(_ context.Context, path string) (string, error) {
return path, nil
}
func (l *publicFileDownloader) ResourceOwner(_ context.Context, ownerPath string) string {
return ownerPath
}
const (
maxMemory = 2 << 20
paramFile = "file"
)
func UploadHandleFunc(s AssetsService, uploader Uploader) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ctxData := authz.GetCtxData(ctx)
err := r.ParseMultipartForm(maxMemory)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
file, handler, err := r.FormFile(paramFile)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
defer func() {
err = file.Close()
logging.OnError(err).Warn("could not close file")
}()
mimeType, err := mimetype.DetectReader(file)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_, err = file.Seek(0, io.SeekStart)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
size := handler.Size
if !uploader.ContentTypeAllowed(mimeType.String()) {
s.ErrorHandler()(w, r, fmt.Errorf("invalid content-type: %s", mimeType), http.StatusBadRequest)
return
}
if size > uploader.MaxFileSize() {
s.ErrorHandler()(w, r, fmt.Errorf("file too big, max file size is %vKB", uploader.MaxFileSize()/1024), http.StatusBadRequest)
return
}
resourceOwner := uploader.ResourceOwner(authz.GetInstance(ctx), ctxData)
objectName, err := uploader.ObjectName(ctxData)
if err != nil {
s.ErrorHandler()(w, r, fmt.Errorf("upload failed: %v", err), http.StatusInternalServerError)
return
}
uploadInfo := &command.AssetUpload{
ResourceOwner: resourceOwner,
ObjectName: objectName,
ContentType: mimeType.String(),
ObjectType: uploader.ObjectType(),
File: file,
Size: size,
}
err = uploader.UploadAsset(ctx, ctxData.OrgID, uploadInfo, s.Commands())
if err != nil {
s.ErrorHandler()(w, r, fmt.Errorf("upload failed: %w", err), http.StatusInternalServerError)
return
}
}
}
func DownloadHandleFunc(s AssetsService, downloader Downloader) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
if s.Storage() == nil {
return
}
ctx := r.Context()
ownerPath := mux.Vars(r)["owner"]
resourceOwner := downloader.ResourceOwner(ctx, ownerPath)
path := ""
if ownerPath != "" {
path = strings.Split(r.RequestURI, ownerPath+"/")[1]
}
objectName, err := downloader.ObjectName(ctx, path)
if err != nil {
s.ErrorHandler()(w, r, fmt.Errorf("download failed: %v", err), http.StatusInternalServerError)
return
}
if objectName == "" {
s.ErrorHandler()(w, r, fmt.Errorf("file not found: %v", path), http.StatusNotFound)
return
}
if err = GetAsset(w, r, resourceOwner, objectName, s.Storage()); err != nil {
s.ErrorHandler()(w, r, err, http.StatusInternalServerError)
}
}
}
func GetAsset(w http.ResponseWriter, r *http.Request, resourceOwner, objectName string, storage static.Storage) error {
split := strings.Split(objectName, "?v=")
if len(split) == 2 {
objectName = split[0]
}
data, getInfo, err := storage.GetObject(r.Context(), authz.GetInstance(r.Context()).InstanceID(), resourceOwner, objectName)
if err != nil {
return fmt.Errorf("download failed: %w", err)
}
info, err := getInfo()
if err != nil {
return fmt.Errorf("download failed: %w", err)
}
if info.Hash == strings.Trim(r.Header.Get(http_util.IfNoneMatch), "\"") {
w.Header().Set(http_util.LastModified, info.LastModified.Format(time.RFC1123))
w.Header().Set(http_util.Etag, "\""+info.Hash+"\"")
w.WriteHeader(304)
return nil
}
w.Header().Set(http_util.ContentLength, strconv.FormatInt(info.Size, 10))
w.Header().Set(http_util.ContentType, info.ContentType)
w.Header().Set(http_util.LastModified, info.LastModified.Format(time.RFC1123))
w.Header().Set(http_util.Etag, "\""+info.Hash+"\"")
_, err = w.Write(data)
logging.New().OnError(err).Error("error writing response for asset")
return nil
}

View File

@@ -0,0 +1,115 @@
Services:
IAM:
Prefix: "/instance"
Methods:
DefaultLabelPolicyLogo:
Path: "/policy/label/logo"
HasDarkMode: true
Handlers:
- Name: Upload
Comment:
Type: upload
Permission: iam.policy.write
- Name: Get
Comment:
Type: download
Permission: iam.policy.read
- Name: GetPreview
Comment:
Type: preview
Permission: iam.policy.read
DefaultLabelPolicyIcon:
Path: "/policy/label/icon"
HasDarkMode: true
Handlers:
- Name: Upload
Comment:
Type: upload
Permission: iam.policy.write
- Name: Get
Comment:
Type: download
Permission: iam.policy.read
- Name: GetPreview
Comment:
Type: preview
Permission: iam.policy.read
DefaultLabelPolicyFont:
Path: "/policy/label/font"
Handlers:
- Name: Upload
Comment:
Type: upload
Permission: iam.policy.write
- Name: Get
Comment:
Type: download
Permission: iam.policy.read
- Name: GetPreview
Comment:
Type: preview
Permission: iam.policy.read
Org:
Prefix: "/org"
Methods:
OrgLabelPolicyLogo:
Path: "/policy/label/logo"
HasDarkMode: true
Handlers:
- Name: Upload
Comment:
Type: upload
Permission: policy.write
- Name: Get
Comment:
Type: download
Permission: policy.read
- Name: GetPreview
Comment:
Type: preview
Permission: policy.read
OrgLabelPolicyIcon:
Path: "/policy/label/icon"
HasDarkMode: true
Handlers:
- Name: Upload
Comment:
Type: upload
Permission: policy.write
- Name: Get
Comment:
Type: download
Permission: policy.read
- Name: GetPreview
Comment:
Type: preview
Permission: policy.read
OrgLabelPolicyFont:
Path: "/policy/label/font"
Handlers:
- Name: Upload
Comment:
Type: upload
Permission: policy.write
- Name: Get
Comment:
Type: download
Permission: policy.read
- Name: GetPreview
Comment:
Type: preview
Permission: policy.read
Users:
Prefix: "/users"
Methods:
MyUserAvatar:
Path: "/me/avatar"
Handlers:
- Name: Upload
Comment:
Type: upload
Permission: authenticated
- Name: Get
Comment:
Type: download
Permission: authenticated

View File

@@ -0,0 +1,236 @@
package main
import (
"flag"
"io"
"os"
"text/template"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/config"
)
var (
directory = flag.String("directory", "./", "working directory: asset.yaml must be in this directory, files will be generated into parent directory")
assetsDocs = flag.String("assets", "../../../../docs/docs/apis/assets/assets.md", "path where the assets.md will be generated")
assetPrefix = flag.String("handler-prefix", "/assets/v1", "prefix of the handler paths")
)
func main() {
flag.Parse()
configFile := *directory + "asset.yaml"
authz, err := os.OpenFile(*directory+"../authz.go", os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0755)
logging.OnError(err).Fatal("cannot open authz file")
router, err := os.OpenFile(*directory+"../router.go", os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0755)
logging.OnError(err).Fatal("cannot open router file")
docs, err := os.OpenFile(*assetsDocs, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0755)
logging.OnError(err).Fatal("cannot open docs file")
GenerateAssetHandler(configFile, *assetPrefix, authz, router, docs)
}
type Method struct {
Path string
HasDarkMode bool
Handlers []Handler
}
type Handler struct {
Name string
Comment string
Type HandlerType
Permission string
}
func (a Handler) Method() string {
if a.Type == MethodTypeUpload {
return "POST"
}
return "GET"
}
func (a Handler) PathSuffix() string {
if a.Type == MethodTypePreview {
return "/_preview"
}
return ""
}
func (a Handler) MethodReturn() string {
if a.Type == MethodTypeUpload {
return "Uploader"
}
if a.Type == MethodTypeDownload {
return "Downloader"
}
if a.Type == MethodTypePreview {
return "Downloader"
}
return ""
}
func (a Handler) HandlerType() string {
if a.Type == MethodTypeUpload {
return "UploadHandleFunc"
}
if a.Type == MethodTypeDownload {
return "DownloadHandleFunc"
}
if a.Type == MethodTypePreview {
return "DownloadHandleFunc"
}
return ""
}
type HandlerType string
const (
MethodTypeUpload = "upload"
MethodTypeDownload = "download"
MethodTypePreview = "preview"
)
type Services map[string]Service
type Service struct {
Prefix string
Methods map[string]Method
}
func GenerateAssetHandler(configFilePath, handlerPrefix string, authz, router, docs io.Writer) {
conf := new(struct {
Services Services
})
err := config.Read(conf, configFilePath)
logging.Log("ASSETS-Dgbn4").OnError(err).Fatal("cannot read config")
tmplAuthz, err := template.New("").Parse(authzTmpl)
logging.Log("ASSETS-BGbbg").OnError(err).Fatal("cannot parse authz template")
tmplRouter, err := template.New("").Parse(routerTmpl)
logging.Log("ASSETS-gh4rq").OnError(err).Fatal("cannot parse router template")
tmplDocs, err := template.New("").Parse(docsTmpl)
logging.Log("ASSETS-FGdgs").OnError(err).Fatal("cannot parse docs template")
data := &struct {
GoPkgName string
Name string
Prefix string
Services Services
}{
GoPkgName: "assets",
Name: "AssetsService",
Prefix: handlerPrefix,
Services: conf.Services,
}
err = tmplAuthz.Execute(authz, data)
logging.Log("ASSETS-BHngj").OnError(err).Fatal("cannot generate authz")
err = tmplRouter.Execute(router, data)
logging.Log("ASSETS-Bfd41").OnError(err).Fatal("cannot generate router")
err = tmplDocs.Execute(docs, data)
logging.Log("ASSETS-Bfd41").OnError(err).Fatal("cannot generate docs")
}
const authzTmpl = `// Code generated by assets generator. DO NOT EDIT.
package {{.GoPkgName}}
import (
"github.com/zitadel/zitadel/internal/api/authz"
)
/**
* {{.Name}}
*/
{{ $prefix := .Prefix }}
var {{.Name}}_AuthMethods = authz.MethodMapping {
{{ range $service := .Services}}
{{ range $method := .Methods}}
{{ range $handler := .Handlers}}
{{ if $handler.Permission }}
"{{$handler.Method}}:{{$prefix}}{{$service.Prefix}}{{$method.Path}}{{$handler.PathSuffix}}": authz.Option{
Permission: "{{$handler.Permission}}",
},
{{ if $method.HasDarkMode }}
"{{$handler.Method}}:{{$prefix}}{{$service.Prefix}}{{$method.Path}}/dark{{$handler.PathSuffix}}": authz.Option{
Permission: "{{$handler.Permission}}",
},
{{end}}
{{end}}
{{end}}
{{end}}
{{end}}
}
`
const routerTmpl = `// Code generated by assets generator. DO NOT EDIT.
package {{.GoPkgName}}
import (
"github.com/gorilla/mux"
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/static"
)
type {{.Name}} interface {
AuthInterceptor() *http_mw.AuthInterceptor
Commands() *command.Commands
ErrorHandler() ErrorHandler
Storage() static.Storage
{{ range $service := .Services}}
{{ range $methodName, $method := .Methods}}
{{ range $handler := .Handlers}}
{{$handler.Name}}{{$methodName}}() {{if $handler.MethodReturn}}{{$handler.MethodReturn}}{{end}}
{{ if $method.HasDarkMode }}
{{$handler.Name}}{{$methodName}}Dark() {{if $handler.MethodReturn}}{{$handler.MethodReturn}}{{end}}
{{ end }}
{{ end }}
{{ end }}
{{ end }}
}
func RegisterRoutes(router *mux.Router, s {{.Name}}) {
router.Use(s.AuthInterceptor().Handler)
{{ range $service := .Services}}
{{ range $methodName, $method := .Methods}}
{{ range $handler := .Handlers}}
router.Path("{{$service.Prefix}}{{$method.Path}}{{$handler.PathSuffix}}").Methods("{{$handler.Method}}").HandlerFunc({{if $handler.HandlerType}}{{$handler.HandlerType}}(s, {{end}}s.{{$handler.Name}}{{$methodName}}(){{if $handler.HandlerType}}){{end}})
{{ if $method.HasDarkMode }}
router.Path("{{$service.Prefix}}{{$method.Path}}/dark{{$handler.PathSuffix}}").Methods("{{$handler.Method}}").HandlerFunc({{if $handler.HandlerType}}{{$handler.HandlerType}}(s, {{end}}s.{{$handler.Name}}{{$methodName}}Dark(){{if $handler.HandlerType}}){{end}})
{{ end }}
{{ end }}
{{ end }}
{{ end }}
}
`
const docsTmpl = `---
title: zitadel/assets
---
## {{.Name}}
{{ range $service := .Services}}
{{ range $methodName, $method := .Methods}}
{{ range $handler := .Handlers}}
### {{$handler.Name}}{{$methodName}}()
> {{$handler.Name}}{{$methodName}}()
{{$handler.Method}}: {{$service.Prefix}}{{$method.Path}}{{$handler.PathSuffix}}
{{ if $method.HasDarkMode }}
### {{$handler.Name}}{{$methodName}}()
> {{$handler.Name}}{{$methodName}}Dark()
{{$handler.Method}}: {{$service.Prefix}}{{$method.Path}}/dark{{$handler.PathSuffix}}
{{ end }}
{{ end }}
{{ end }}
{{ end }}
`

View File

@@ -0,0 +1,394 @@
package assets
import (
"context"
"strings"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/id"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/static"
)
func (h *Handler) UploadDefaultLabelPolicyLogo() Uploader {
return &labelPolicyLogoUploader{h.idGenerator, false, true, []string{"image/"}, 1 << 19}
}
func (h *Handler) UploadDefaultLabelPolicyLogoDark() Uploader {
return &labelPolicyLogoUploader{h.idGenerator, true, true, []string{"image/"}, 1 << 19}
}
func (h *Handler) UploadOrgLabelPolicyLogo() Uploader {
return &labelPolicyLogoUploader{h.idGenerator, false, false, []string{"image/"}, 1 << 19}
}
func (h *Handler) UploadOrgLabelPolicyLogoDark() Uploader {
return &labelPolicyLogoUploader{h.idGenerator, true, false, []string{"image/"}, 1 << 19}
}
type labelPolicyLogoUploader struct {
idGenerator id.Generator
darkMode bool
defaultPolicy bool
contentTypes []string
maxSize int64
}
func (l *labelPolicyLogoUploader) ContentTypeAllowed(contentType string) bool {
for _, ct := range l.contentTypes {
if strings.HasPrefix(contentType, ct) {
return true
}
}
return false
}
func (l *labelPolicyLogoUploader) ObjectType() static.ObjectType {
return static.ObjectTypeStyling
}
func (l *labelPolicyLogoUploader) MaxFileSize() int64 {
return l.maxSize
}
func (l *labelPolicyLogoUploader) ObjectName(_ authz.CtxData) (string, error) {
suffixID, err := l.idGenerator.Next()
if err != nil {
return "", err
}
prefix := domain.LabelPolicyLogoPath
if l.darkMode {
return prefix + "-" + domain.Dark + "-" + suffixID, nil
}
return prefix + "-" + suffixID, nil
}
func (l *labelPolicyLogoUploader) ResourceOwner(instance authz.Instance, ctxData authz.CtxData) string {
if l.defaultPolicy {
return instance.InstanceID()
}
return ctxData.OrgID
}
func (l *labelPolicyLogoUploader) UploadAsset(ctx context.Context, orgID string, upload *command.AssetUpload, commands *command.Commands) error {
if l.defaultPolicy {
if l.darkMode {
_, err := commands.AddLogoDarkDefaultLabelPolicy(ctx, upload)
return err
}
_, err := commands.AddLogoDefaultLabelPolicy(ctx, upload)
return err
}
if l.darkMode {
_, err := commands.AddLogoDarkLabelPolicy(ctx, orgID, upload)
return err
}
_, err := commands.AddLogoLabelPolicy(ctx, orgID, upload)
return err
}
func (h *Handler) GetDefaultLabelPolicyLogo() Downloader {
return &labelPolicyLogoDownloader{query: h.query, darkMode: false, defaultPolicy: true, preview: false}
}
func (h *Handler) GetDefaultLabelPolicyLogoDark() Downloader {
return &labelPolicyLogoDownloader{query: h.query, darkMode: true, defaultPolicy: true, preview: false}
}
func (h *Handler) GetPreviewDefaultLabelPolicyLogo() Downloader {
return &labelPolicyLogoDownloader{query: h.query, darkMode: false, defaultPolicy: true, preview: true}
}
func (h *Handler) GetPreviewDefaultLabelPolicyLogoDark() Downloader {
return &labelPolicyLogoDownloader{query: h.query, darkMode: true, defaultPolicy: true, preview: true}
}
func (h *Handler) GetOrgLabelPolicyLogo() Downloader {
return &labelPolicyLogoDownloader{query: h.query, darkMode: false, defaultPolicy: false, preview: false}
}
func (h *Handler) GetOrgLabelPolicyLogoDark() Downloader {
return &labelPolicyLogoDownloader{query: h.query, darkMode: true, defaultPolicy: false, preview: false}
}
func (h *Handler) GetPreviewOrgLabelPolicyLogo() Downloader {
return &labelPolicyLogoDownloader{query: h.query, darkMode: false, defaultPolicy: false, preview: true}
}
func (h *Handler) GetPreviewOrgLabelPolicyLogoDark() Downloader {
return &labelPolicyLogoDownloader{query: h.query, darkMode: true, defaultPolicy: false, preview: true}
}
type labelPolicyLogoDownloader struct {
query *query.Queries
darkMode bool
defaultPolicy bool
preview bool
}
func (l *labelPolicyLogoDownloader) ObjectName(ctx context.Context, path string) (string, error) {
policy, err := getLabelPolicy(ctx, l.defaultPolicy, l.preview, l.query)
if err != nil {
return "", nil
}
if l.darkMode {
return policy.Dark.LogoURL, nil
}
return policy.Light.LogoURL, nil
}
func (l *labelPolicyLogoDownloader) ResourceOwner(ctx context.Context, _ string) string {
return getLabelPolicyResourceOwner(ctx, l.defaultPolicy, l.preview, l.query)
}
func (h *Handler) UploadDefaultLabelPolicyIcon() Uploader {
return &labelPolicyIconUploader{h.idGenerator, false, true, []string{"image/"}, 1 << 19}
}
func (h *Handler) UploadDefaultLabelPolicyIconDark() Uploader {
return &labelPolicyIconUploader{h.idGenerator, true, true, []string{"image/"}, 1 << 19}
}
func (h *Handler) UploadOrgLabelPolicyIcon() Uploader {
return &labelPolicyIconUploader{h.idGenerator, false, false, []string{"image/"}, 1 << 19}
}
func (h *Handler) UploadOrgLabelPolicyIconDark() Uploader {
return &labelPolicyIconUploader{h.idGenerator, true, false, []string{"image/"}, 1 << 19}
}
type labelPolicyIconUploader struct {
idGenerator id.Generator
darkMode bool
defaultPolicy bool
contentTypes []string
maxSize int64
}
func (l *labelPolicyIconUploader) ContentTypeAllowed(contentType string) bool {
for _, ct := range l.contentTypes {
if strings.HasPrefix(contentType, ct) {
return true
}
}
return false
}
func (l *labelPolicyIconUploader) ObjectType() static.ObjectType {
return static.ObjectTypeStyling
}
func (l *labelPolicyIconUploader) MaxFileSize() int64 {
return l.maxSize
}
func (l *labelPolicyIconUploader) ObjectName(_ authz.CtxData) (string, error) {
suffixID, err := l.idGenerator.Next()
if err != nil {
return "", err
}
prefix := domain.LabelPolicyIconPath
if l.darkMode {
return prefix + "-" + domain.Dark + "-" + suffixID, nil
}
return prefix + "-" + suffixID, nil
}
func (l *labelPolicyIconUploader) ResourceOwner(instance authz.Instance, ctxData authz.CtxData) string {
if l.defaultPolicy {
return instance.InstanceID()
}
return ctxData.OrgID
}
func (l *labelPolicyIconUploader) UploadAsset(ctx context.Context, orgID string, upload *command.AssetUpload, commands *command.Commands) error {
if l.defaultPolicy {
if l.darkMode {
_, err := commands.AddIconDarkDefaultLabelPolicy(ctx, upload)
return err
}
_, err := commands.AddIconDefaultLabelPolicy(ctx, upload)
return err
}
if l.darkMode {
_, err := commands.AddIconDarkLabelPolicy(ctx, orgID, upload)
return err
}
_, err := commands.AddIconLabelPolicy(ctx, orgID, upload)
return err
}
func (h *Handler) GetDefaultLabelPolicyIcon() Downloader {
return &labelPolicyIconDownloader{query: h.query, darkMode: false, defaultPolicy: true, preview: false}
}
func (h *Handler) GetDefaultLabelPolicyIconDark() Downloader {
return &labelPolicyIconDownloader{query: h.query, darkMode: true, defaultPolicy: true, preview: false}
}
func (h *Handler) GetPreviewDefaultLabelPolicyIcon() Downloader {
return &labelPolicyIconDownloader{query: h.query, darkMode: false, defaultPolicy: true, preview: true}
}
func (h *Handler) GetPreviewDefaultLabelPolicyIconDark() Downloader {
return &labelPolicyIconDownloader{query: h.query, darkMode: true, defaultPolicy: true, preview: true}
}
func (h *Handler) GetOrgLabelPolicyIcon() Downloader {
return &labelPolicyIconDownloader{query: h.query, darkMode: false, defaultPolicy: false, preview: false}
}
func (h *Handler) GetOrgLabelPolicyIconDark() Downloader {
return &labelPolicyIconDownloader{query: h.query, darkMode: true, defaultPolicy: false, preview: false}
}
func (h *Handler) GetPreviewOrgLabelPolicyIcon() Downloader {
return &labelPolicyIconDownloader{query: h.query, darkMode: false, defaultPolicy: false, preview: true}
}
func (h *Handler) GetPreviewOrgLabelPolicyIconDark() Downloader {
return &labelPolicyIconDownloader{query: h.query, darkMode: true, defaultPolicy: false, preview: true}
}
type labelPolicyIconDownloader struct {
query *query.Queries
darkMode bool
defaultPolicy bool
preview bool
}
func (l *labelPolicyIconDownloader) ObjectName(ctx context.Context, path string) (string, error) {
policy, err := getLabelPolicy(ctx, l.defaultPolicy, l.preview, l.query)
if err != nil {
return "", nil
}
if l.darkMode {
return policy.Dark.IconURL, nil
}
return policy.Light.IconURL, nil
}
func (l *labelPolicyIconDownloader) ResourceOwner(ctx context.Context, _ string) string {
return getLabelPolicyResourceOwner(ctx, l.defaultPolicy, l.preview, l.query)
}
func (h *Handler) UploadDefaultLabelPolicyFont() Uploader {
return &labelPolicyFontUploader{h.idGenerator, true, []string{"font/", "application/octet-stream"}, 1 << 19}
}
func (h *Handler) UploadOrgLabelPolicyFont() Uploader {
return &labelPolicyFontUploader{h.idGenerator, false, []string{"font/", "application/octet-stream"}, 1 << 19}
}
type labelPolicyFontUploader struct {
idGenerator id.Generator
defaultPolicy bool
contentTypes []string
maxSize int64
}
func (l *labelPolicyFontUploader) ContentTypeAllowed(contentType string) bool {
for _, ct := range l.contentTypes {
if strings.HasPrefix(contentType, ct) {
return true
}
}
return false
}
func (l *labelPolicyFontUploader) ObjectType() static.ObjectType {
return static.ObjectTypeStyling
}
func (l *labelPolicyFontUploader) MaxFileSize() int64 {
return l.maxSize
}
func (l *labelPolicyFontUploader) ObjectName(_ authz.CtxData) (string, error) {
suffixID, err := l.idGenerator.Next()
if err != nil {
return "", err
}
prefix := domain.LabelPolicyFontPath
return prefix + "-" + suffixID, nil
}
func (l *labelPolicyFontUploader) ResourceOwner(instance authz.Instance, ctxData authz.CtxData) string {
if l.defaultPolicy {
return instance.InstanceID()
}
return ctxData.OrgID
}
func (l *labelPolicyFontUploader) UploadAsset(ctx context.Context, orgID string, upload *command.AssetUpload, commands *command.Commands) error {
if l.defaultPolicy {
_, err := commands.AddFontDefaultLabelPolicy(ctx, upload)
return err
}
_, err := commands.AddFontLabelPolicy(ctx, orgID, upload)
return err
}
func (h *Handler) GetDefaultLabelPolicyFont() Downloader {
return &labelPolicyFontDownloader{query: h.query, defaultPolicy: true, preview: false}
}
func (h *Handler) GetPreviewDefaultLabelPolicyFont() Downloader {
return &labelPolicyFontDownloader{query: h.query, defaultPolicy: true, preview: true}
}
func (h *Handler) GetOrgLabelPolicyFont() Downloader {
return &labelPolicyFontDownloader{query: h.query, defaultPolicy: false, preview: false}
}
func (h *Handler) GetPreviewOrgLabelPolicyFont() Downloader {
return &labelPolicyFontDownloader{query: h.query, defaultPolicy: true, preview: true}
}
type labelPolicyFontDownloader struct {
query *query.Queries
defaultPolicy bool
preview bool
}
func (l *labelPolicyFontDownloader) ObjectName(ctx context.Context, path string) (string, error) {
policy, err := getLabelPolicy(ctx, l.defaultPolicy, l.preview, l.query)
if err != nil {
return "", nil
}
return policy.FontURL, nil
}
func (l *labelPolicyFontDownloader) ResourceOwner(ctx context.Context, _ string) string {
return getLabelPolicyResourceOwner(ctx, l.defaultPolicy, l.preview, l.query)
}
func getLabelPolicy(ctx context.Context, defaultPolicy, preview bool, queries *query.Queries) (*query.LabelPolicy, error) {
if defaultPolicy {
if preview {
return queries.DefaultPreviewLabelPolicy(ctx)
}
return queries.DefaultActiveLabelPolicy(ctx)
}
if preview {
return queries.PreviewLabelPolicyByOrg(ctx, authz.GetCtxData(ctx).OrgID)
}
return queries.ActiveLabelPolicyByOrg(ctx, authz.GetCtxData(ctx).OrgID, false)
}
func getLabelPolicyResourceOwner(ctx context.Context, defaultPolicy, preview bool, queries *query.Queries) string {
if defaultPolicy {
return authz.GetInstance(ctx).InstanceID()
}
policy, err := getLabelPolicy(ctx, defaultPolicy, preview, queries)
if err != nil {
return ""
}
if policy.IsDefault {
return authz.GetInstance(ctx).InstanceID()
}
return authz.GetCtxData(ctx).OrgID
}

View File

@@ -0,0 +1,64 @@
package assets
import (
"context"
"strings"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/static"
)
func (h *Handler) UploadMyUserAvatar() Uploader {
return &myHumanAvatarUploader{[]string{"image/"}, 1 << 19}
}
type myHumanAvatarUploader struct {
contentTypes []string
maxSize int64
}
func (l *myHumanAvatarUploader) ContentTypeAllowed(contentType string) bool {
for _, ct := range l.contentTypes {
if strings.HasPrefix(contentType, ct) {
return true
}
}
return false
}
func (l *myHumanAvatarUploader) ObjectType() static.ObjectType {
return static.ObjectTypeUserAvatar
}
func (l *myHumanAvatarUploader) MaxFileSize() int64 {
return l.maxSize
}
func (l *myHumanAvatarUploader) ObjectName(ctxData authz.CtxData) (string, error) {
return domain.GetHumanAvatarAssetPath(ctxData.UserID), nil
}
func (l *myHumanAvatarUploader) ResourceOwner(_ authz.Instance, ctxData authz.CtxData) string {
return ctxData.ResourceOwner
}
func (l *myHumanAvatarUploader) UploadAsset(ctx context.Context, orgID string, upload *command.AssetUpload, commands *command.Commands) error {
_, err := commands.AddHumanAvatar(ctx, orgID, authz.GetCtxData(ctx).UserID, upload)
return err
}
func (h *Handler) GetMyUserAvatar() Downloader {
return &myHumanAvatarDownloader{}
}
type myHumanAvatarDownloader struct{}
func (l *myHumanAvatarDownloader) ObjectName(ctx context.Context, path string) (string, error) {
return domain.GetHumanAvatarAssetPath(authz.GetCtxData(ctx).UserID), nil
}
func (l *myHumanAvatarDownloader) ResourceOwner(ctx context.Context, _ string) string {
return authz.GetCtxData(ctx).ResourceOwner
}

View File

@@ -0,0 +1,44 @@
package authz
import (
"context"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
)
const (
BearerPrefix = "Bearer "
)
type MembershipsResolver interface {
SearchMyMemberships(ctx context.Context, orgID string, shouldTriggerBulk bool) ([]*Membership, error)
}
type authZRepo interface {
MembershipsResolver
VerifyAccessToken(ctx context.Context, token, verifierClientID, projectID string) (userID, agentID, clientID, prefLang, resourceOwner string, err error)
VerifierClientID(ctx context.Context, name string) (clientID, projectID string, err error)
ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (projectID string, origins []string, err error)
ExistsOrg(ctx context.Context, id, domain string) (string, error)
}
var _ AccessTokenVerifier = (*AccessTokenVerifierFromRepo)(nil)
type AccessTokenVerifierFromRepo struct {
authZRepo authZRepo
}
func StartAccessTokenVerifierFromRepo(authZRepo authZRepo) *AccessTokenVerifierFromRepo {
return &AccessTokenVerifierFromRepo{authZRepo: authZRepo}
}
func (a *AccessTokenVerifierFromRepo) VerifyAccessToken(ctx context.Context, token string) (userID, clientID, agentID, prefLang, resourceOwner string, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
userID, agentID, clientID, prefLang, resourceOwner, err = a.authZRepo.VerifyAccessToken(ctx, token, "", GetInstance(ctx).ProjectID())
return userID, clientID, agentID, prefLang, resourceOwner, err
}
type client struct {
name string
}

View File

@@ -0,0 +1,66 @@
package authz
import (
"context"
"testing"
"github.com/zitadel/zitadel/internal/zerrors"
)
func Test_extractBearerToken(t *testing.T) {
type args struct {
ctx context.Context
token string
verifier AccessTokenVerifier
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "no auth header set",
args: args{
ctx: context.Background(),
token: "",
},
wantErr: true,
},
{
name: "wrong auth header set",
args: args{
ctx: context.Background(),
token: "Basic sds",
},
wantErr: true,
},
{
name: "auth header set",
args: args{
ctx: context.Background(),
token: "Bearer AUTH",
verifier: AccessTokenVerifierFunc(func(context.Context, string) (string, string, string, string, string, error) {
return "", "", "", "", "", nil
}),
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := extractBearerToken(tt.args.token)
if tt.wantErr && err == nil {
t.Errorf("got wrong result, should get err: actual: %v ", err)
}
if !tt.wantErr && err != nil {
t.Errorf("got wrong result, should not get err: actual: %v ", err)
}
if tt.wantErr && !zerrors.IsUnauthenticated(err) {
t.Errorf("got wrong err: %v ", err)
}
})
}
}

View File

@@ -0,0 +1,69 @@
package authz
import (
"context"
"sync"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
)
// TODO: Define interfaces where they are accepted
type APITokenVerifier interface {
AccessTokenVerifier
SystemTokenVerifier
RegisterServer(appName, methodPrefix string, mappings MethodMapping)
CheckAuthMethod(method string) (Option, bool)
ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (_ string, _ []string, err error)
ExistsOrg(ctx context.Context, id, domain string) (orgID string, err error)
SearchMyMemberships(ctx context.Context, orgID string, shouldTriggerBulk bool) (_ []*Membership, err error)
}
type ApiTokenVerifier struct {
AccessTokenVerifier
SystemTokenVerifier
authZRepo authZRepo
clients sync.Map
authMethods MethodMapping
}
func StartAPITokenVerifier(authZRepo authZRepo, accessTokenVerifier AccessTokenVerifier, systemTokenVerifier SystemTokenVerifier) *ApiTokenVerifier {
return &ApiTokenVerifier{
authZRepo: authZRepo,
SystemTokenVerifier: systemTokenVerifier,
AccessTokenVerifier: accessTokenVerifier,
}
}
func (v *ApiTokenVerifier) RegisterServer(appName, methodPrefix string, mappings MethodMapping) {
v.clients.Store(methodPrefix, &client{name: appName})
if v.authMethods == nil {
v.authMethods = make(map[string]Option)
}
for method, option := range mappings {
v.authMethods[method] = option
}
}
func (v *ApiTokenVerifier) SearchMyMemberships(ctx context.Context, orgID string, shouldTriggerBulk bool) (_ []*Membership, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
return v.authZRepo.SearchMyMemberships(ctx, orgID, shouldTriggerBulk)
}
func (v *ApiTokenVerifier) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (_ string, _ []string, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
return v.authZRepo.ProjectIDAndOriginsByClientID(ctx, clientID)
}
func (v *ApiTokenVerifier) ExistsOrg(ctx context.Context, id, domain string) (orgID string, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
return v.authZRepo.ExistsOrg(ctx, id, domain)
}
func (v *ApiTokenVerifier) CheckAuthMethod(method string) (Option, bool) {
authOpt, ok := v.authMethods[method]
return authOpt, ok
}

View File

@@ -0,0 +1,158 @@
package authz
import (
"context"
"fmt"
"reflect"
"slices"
"strings"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
const (
authenticated = "authenticated"
)
// CheckUserAuthorization verifies that:
// - the token is active,
// - the organization (**either** provided by ID or verified domain) exists
// - the user is permitted to call the requested endpoint (permission option in proto)
// it will pass the [CtxData] and permission of the user into the ctx [context.Context]
func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID, orgDomain string, verifier APITokenVerifier, systemRolePermissionMapping []RoleMapping, rolePermissionMapping []RoleMapping, requiredAuthOption Option, method string) (ctxSetter func(context.Context) context.Context, err error) {
ctx, span := tracing.NewServerInterceptorSpan(ctx)
defer func() { span.EndWithError(err) }()
ctxData, err := VerifyTokenAndCreateCtxData(ctx, token, orgID, orgDomain, verifier, systemRolePermissionMapping)
if err != nil {
return nil, err
}
if requiredAuthOption.Permission == authenticated {
return func(parent context.Context) context.Context {
return context.WithValue(parent, dataKey, ctxData)
}, nil
}
requestedPermissions, allPermissions, err := getUserPermissions(ctx, verifier, requiredAuthOption.Permission, systemRolePermissionMapping, rolePermissionMapping, ctxData, ctxData.OrgID)
if err != nil {
return nil, err
}
ctx, userPermissionSpan := tracing.NewNamedSpan(ctx, "checkUserPermissions")
err = checkUserPermissions(req, requestedPermissions, requiredAuthOption)
userPermissionSpan.EndWithError(err)
if err != nil {
return nil, err
}
return func(parent context.Context) context.Context {
parent = context.WithValue(parent, dataKey, ctxData)
parent = context.WithValue(parent, allPermissionsKey, allPermissions)
parent = context.WithValue(parent, requestPermissionsKey, requestedPermissions)
return parent
}, nil
}
func checkUserPermissions(req interface{}, userPerms []string, authOpt Option) error {
if len(userPerms) == 0 {
return zerrors.ThrowPermissionDenied(nil, "AUTH-5mWD2", "No matching permissions found")
}
if authOpt.CheckParam == "" {
return nil
}
if HasGlobalPermission(userPerms) {
return nil
}
if hasContextPermission(req, authOpt.CheckParam, userPerms) {
return nil
}
return zerrors.ThrowPermissionDenied(nil, "AUTH-3jknH", "No matching permissions found")
}
func SplitPermission(perm string) (string, string) {
splittedPerm := strings.Split(perm, ":")
if len(splittedPerm) == 1 {
return splittedPerm[0], ""
}
return splittedPerm[0], splittedPerm[1]
}
func hasContextPermission(req interface{}, fieldName string, permissions []string) bool {
for _, perm := range permissions {
_, ctxID := SplitPermission(perm)
if checkPermissionContext(req, fieldName, ctxID) {
return true
}
}
return false
}
func checkPermissionContext(req interface{}, fieldName, roleContextID string) bool {
field := getFieldFromReq(req, fieldName)
return field != "" && field == roleContextID
}
func getFieldFromReq(req interface{}, field string) string {
v := reflect.Indirect(reflect.ValueOf(req)).FieldByName(field)
if reflect.ValueOf(v).IsZero() {
return ""
}
return fmt.Sprintf("%v", v.Interface())
}
func HasGlobalPermission(perms []string) bool {
for _, perm := range perms {
_, ctxID := SplitPermission(perm)
if ctxID == "" {
return true
}
}
return false
}
func GetAllPermissionCtxIDs(perms []string) []string {
ctxIDs := make([]string, 0)
for _, perm := range perms {
_, ctxID := SplitPermission(perm)
if ctxID != "" {
ctxIDs = append(ctxIDs, ctxID)
}
}
return ctxIDs
}
type SystemUserPermissions struct {
MemberType MemberType `json:"member_type"`
AggregateID string `json:"aggregate_id"`
ObjectID string `json:"object_id"`
Permissions []string `json:"permissions"`
}
// systemMembershipsToUserPermissions converts system memberships based on roles,
// to SystemUserPermissions, using the passed role mapping.
func systemMembershipsToUserPermissions(memberships Memberships, roleMap []RoleMapping) []SystemUserPermissions {
if memberships == nil {
return nil
}
systemUserPermissions := make([]SystemUserPermissions, len(memberships))
for i, systemPerm := range memberships {
permissions := make([]string, 0, len(systemPerm.Roles))
for _, role := range systemPerm.Roles {
permissions = append(permissions, getPermissionsFromRole(roleMap, role)...)
}
slices.Sort(permissions)
permissions = slices.Compact(permissions) // remove duplicates
systemUserPermissions[i].MemberType = systemPerm.MemberType
systemUserPermissions[i].AggregateID = systemPerm.AggregateID
systemUserPermissions[i].ObjectID = systemPerm.ObjectID
systemUserPermissions[i].Permissions = permissions
}
return systemUserPermissions
}

View File

@@ -0,0 +1,404 @@
package authz
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/zerrors"
)
type TestRequest struct {
Test string
}
func Test_CheckUserPermissions(t *testing.T) {
type args struct {
req *TestRequest
perms []string
authOpt Option
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "no permissions",
args: args{
req: &TestRequest{},
perms: []string{},
},
wantErr: true,
},
{
name: "has permission and no context requested",
args: args{
req: &TestRequest{},
perms: []string{"project.read"},
authOpt: Option{CheckParam: ""},
},
wantErr: false,
},
{
name: "context requested and has global permission",
args: args{
req: &TestRequest{Test: "Test"},
perms: []string{"project.read", "project.read:1"},
authOpt: Option{CheckParam: "Test"},
},
wantErr: false,
},
{
name: "context requested and has specific permission",
args: args{
req: &TestRequest{Test: "Test"},
perms: []string{"project.read:Test"},
authOpt: Option{CheckParam: "Test"},
},
wantErr: false,
},
{
name: "context requested and has no permission",
args: args{
req: &TestRequest{Test: "Hodor"},
perms: []string{"project.read:Test"},
authOpt: Option{CheckParam: "Test"},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := checkUserPermissions(tt.args.req, tt.args.perms, tt.args.authOpt)
if tt.wantErr && err == nil {
t.Errorf("got wrong result, should get err: actual: %v ", err)
}
if !tt.wantErr && err != nil {
t.Errorf("shouldn't get err: %v ", err)
}
if tt.wantErr && !zerrors.IsPermissionDenied(err) {
t.Errorf("got wrong err: %v ", err)
}
})
}
}
func Test_SplitPermission(t *testing.T) {
type args struct {
perm string
}
tests := []struct {
name string
args args
permName string
permCtxID string
}{
{
name: "permission with context id",
args: args{
perm: "project.read:ctxID",
},
permName: "project.read",
permCtxID: "ctxID",
},
{
name: "permission without context id",
args: args{
perm: "project.read",
},
permName: "project.read",
permCtxID: "",
},
{
name: "permission to many parts",
args: args{
perm: "project.read:1:0",
},
permName: "project.read",
permCtxID: "1",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
name, id := SplitPermission(tt.args.perm)
if name != tt.permName {
t.Errorf("got wrong result on name, expecting: %v, actual: %v ", tt.permName, name)
}
if id != tt.permCtxID {
t.Errorf("got wrong result on id, expecting: %v, actual: %v ", tt.permCtxID, id)
}
})
}
}
func Test_HasContextPermission(t *testing.T) {
type args struct {
req *TestRequest
fieldname string
perms []string
}
tests := []struct {
name string
args args
result bool
}{
{
name: "existing context permission",
args: args{
req: &TestRequest{Test: "right"},
fieldname: "Test",
perms: []string{"test:wrong", "test:right"},
},
result: true,
},
{
name: "not existing context permission",
args: args{
req: &TestRequest{Test: "test"},
fieldname: "Test",
perms: []string{"test:wrong", "test:wrong2"},
},
result: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := hasContextPermission(tt.args.req, tt.args.fieldname, tt.args.perms)
if result != tt.result {
t.Errorf("got wrong result, expecting: %v, actual: %v ", tt.result, result)
}
})
}
}
func Test_GetFieldFromReq(t *testing.T) {
type args struct {
req *TestRequest
fieldname string
}
tests := []struct {
name string
args args
result string
}{
{
name: "existing field",
args: args{
req: &TestRequest{Test: "TestValue"},
fieldname: "Test",
},
result: "TestValue",
},
{
name: "not existing field",
args: args{
req: &TestRequest{Test: "TestValue"},
fieldname: "Test2",
},
result: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := getFieldFromReq(tt.args.req, tt.args.fieldname)
if result != tt.result {
t.Errorf("got wrong result, expecting: %v, actual: %v ", tt.result, result)
}
})
}
}
func Test_HasGlobalPermission(t *testing.T) {
type args struct {
perms []string
}
tests := []struct {
name string
args args
result bool
}{
{
name: "global perm existing",
args: args{
perms: []string{"perm:1", "perm:2", "perm"},
},
result: true,
},
{
name: "global perm not existing",
args: args{
perms: []string{"perm:1", "perm:2", "perm:3"},
},
result: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := HasGlobalPermission(tt.args.perms)
if result != tt.result {
t.Errorf("got wrong result, expecting: %v, actual: %v ", tt.result, result)
}
})
}
}
func Test_GetPermissionCtxIDs(t *testing.T) {
type args struct {
perms []string
}
tests := []struct {
name string
args args
result []string
}{
{
name: "no specific permission",
args: args{
perms: []string{"perm"},
},
result: []string{},
},
{
name: "ctx id",
args: args{
perms: []string{"perm:1", "perm", "perm:3"},
},
result: []string{"1", "3"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := GetAllPermissionCtxIDs(tt.args.perms)
if !equalStringArray(result, tt.result) {
t.Errorf("got wrong result, expecting: %v, actual: %v ", tt.result, result)
}
})
}
}
func Test_systemMembershipsToUserPermissions(t *testing.T) {
roleMap := []RoleMapping{
{
Role: "FOO_BAR",
Permissions: []string{"foo.bar.read", "foo.bar.write"},
},
{
Role: "BAR_FOO",
Permissions: []string{"bar.foo.read", "bar.foo.write", "foo.bar.read"},
},
}
type args struct {
memberships Memberships
roleMap []RoleMapping
}
tests := []struct {
name string
args args
want []SystemUserPermissions
}{
{
name: "nil memberships",
args: args{
memberships: nil,
roleMap: roleMap,
},
want: nil,
},
{
name: "empty memberships",
args: args{
memberships: Memberships{},
roleMap: roleMap,
},
want: []SystemUserPermissions{},
},
{
name: "single membership",
args: args{
memberships: Memberships{
{
MemberType: MemberTypeSystem,
AggregateID: "1",
ObjectID: "2",
Roles: []string{"FOO_BAR"},
},
},
roleMap: roleMap,
},
want: []SystemUserPermissions{
{
MemberType: MemberTypeSystem,
AggregateID: "1",
ObjectID: "2",
Permissions: []string{"foo.bar.read", "foo.bar.write"},
},
},
},
{
name: "multiple memberships",
args: args{
memberships: Memberships{
{
MemberType: MemberTypeSystem,
AggregateID: "1",
ObjectID: "2",
Roles: []string{"FOO_BAR"},
},
{
MemberType: MemberTypeIAM,
AggregateID: "1",
ObjectID: "2",
Roles: []string{"BAR_FOO"},
},
},
roleMap: roleMap,
},
want: []SystemUserPermissions{
{
MemberType: MemberTypeSystem,
AggregateID: "1",
ObjectID: "2",
Permissions: []string{"foo.bar.read", "foo.bar.write"},
},
{
MemberType: MemberTypeIAM,
AggregateID: "1",
ObjectID: "2",
Permissions: []string{"bar.foo.read", "bar.foo.write", "foo.bar.read"},
},
},
},
{
name: "multiple roles",
args: args{
memberships: Memberships{
{
MemberType: MemberTypeSystem,
AggregateID: "1",
ObjectID: "2",
Roles: []string{"FOO_BAR", "BAR_FOO"},
},
},
roleMap: roleMap,
},
want: []SystemUserPermissions{
{
MemberType: MemberTypeSystem,
AggregateID: "1",
ObjectID: "2",
Permissions: []string{"bar.foo.read", "bar.foo.write", "foo.bar.read", "foo.bar.write"},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := systemMembershipsToUserPermissions(tt.args.memberships, tt.args.roleMap)
assert.Equal(t, tt.want, got)
})
}
}

View File

@@ -0,0 +1,27 @@
package authz
type Config struct {
RolePermissionMappings []RoleMapping
}
type RoleMapping struct {
Role string
Permissions []string
}
type MethodMapping map[string]Option
type Option struct {
Permission string
CheckParam string
AllowSelf bool
}
func getPermissionsFromRole(rolePermissionMappings []RoleMapping, role string) []string {
for _, roleMap := range rolePermissionMappings {
if roleMap.Role == role {
return roleMap.Permissions
}
}
return nil
}

View File

@@ -0,0 +1,197 @@
//go:generate enumer -type MemberType -trimprefix MemberType -json -sql
package authz
import (
"context"
"errors"
"strings"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/api/grpc"
http_util "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
type key int
const (
requestPermissionsKey key = 1
dataKey key = 2
allPermissionsKey key = 3
instanceKey key = 4
)
type CtxData struct {
UserID string
OrgID string
ProjectID string
AgentID string
PreferredLanguage string
ResourceOwner string
SystemMemberships Memberships
SystemUserPermissions []SystemUserPermissions
}
func (ctxData CtxData) IsZero() bool {
return ctxData.UserID == "" || ctxData.OrgID == "" && ctxData.SystemMemberships == nil
}
type Grants []*Grant
type Grant struct {
OrgID string
Roles []string
}
type Memberships []*Membership
type Membership struct {
MemberType MemberType
AggregateID string
InstanceID string
// ObjectID differs from aggregate id if object is sub of an aggregate
ObjectID string
Roles []string
}
type MemberType int32
const (
MemberTypeUnspecified MemberType = iota
MemberTypeOrganization
MemberTypeProject
MemberTypeProjectGrant
MemberTypeIAM
MemberTypeSystem
)
type TokenVerifier interface {
ExistsOrg(ctx context.Context, id, domain string) (string, error)
ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (projectID string, origins []string, err error)
AccessTokenVerifier
SystemTokenVerifier
}
type AccessTokenVerifier interface {
VerifyAccessToken(ctx context.Context, token string) (userID, clientID, agentID, prefLan, resourceOwner string, err error)
}
// AccessTokenVerifierFunc implements the SystemTokenVerifier interface so that a function can be used as a AccessTokenVerifier.
type AccessTokenVerifierFunc func(context.Context, string) (string, string, string, string, string, error)
func (a AccessTokenVerifierFunc) VerifyAccessToken(ctx context.Context, token string) (string, string, string, string, string, error) {
return a(ctx, token)
}
type SystemTokenVerifier interface {
VerifySystemToken(ctx context.Context, token string, orgID string) (matchingMemberships Memberships, userID string, err error)
}
// SystemTokenVerifierFunc implements the SystemTokenVerifier interface so that a function can be used as a SystemTokenVerifier.
type SystemTokenVerifierFunc func(context.Context, string, string) (Memberships, string, error)
func (s SystemTokenVerifierFunc) VerifySystemToken(ctx context.Context, token string, orgID string) (Memberships, string, error) {
return s(ctx, token, orgID)
}
func VerifyTokenAndCreateCtxData(ctx context.Context, token, orgID, orgDomain string, t APITokenVerifier, systemRoleMap []RoleMapping) (_ CtxData, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
tokenWOBearer, err := extractBearerToken(token)
if err != nil {
return CtxData{}, err
}
userID, clientID, agentID, prefLang, resourceOwner, err := t.VerifyAccessToken(ctx, tokenWOBearer)
var sysMemberships Memberships
if err != nil && !zerrors.IsUnauthenticated(err) {
return CtxData{}, err
}
if err != nil {
logging.WithFields("org_id", orgID, "org_domain", orgDomain).WithError(err).Warn("authz: verify access token")
var sysTokenErr error
sysMemberships, userID, sysTokenErr = t.VerifySystemToken(ctx, tokenWOBearer, orgID)
if sysTokenErr != nil || sysMemberships == nil {
return CtxData{}, zerrors.ThrowUnauthenticated(errors.Join(err, sysTokenErr), "AUTH-7fs1e", "Errors.Token.Invalid")
}
}
projectID, err := projectIDAndCheckOriginForClientID(ctx, clientID, t)
if err != nil {
return CtxData{}, err
}
if orgID == "" && orgDomain == "" {
orgID = resourceOwner
}
// System API calls don't have a resource owner
if orgID != "" || orgDomain != "" {
orgID, err = t.ExistsOrg(ctx, orgID, orgDomain)
if err != nil {
return CtxData{}, zerrors.ThrowPermissionDenied(nil, "AUTH-Bs7Ds", "Organisation doesn't exist")
}
}
return CtxData{
UserID: userID,
OrgID: orgID,
ProjectID: projectID,
AgentID: agentID,
PreferredLanguage: prefLang,
ResourceOwner: resourceOwner,
SystemMemberships: sysMemberships,
SystemUserPermissions: systemMembershipsToUserPermissions(sysMemberships, systemRoleMap),
}, nil
}
func projectIDAndCheckOriginForClientID(ctx context.Context, clientID string, t APITokenVerifier) (string, error) {
if clientID == "" {
return "", nil
}
projectID, origins, err := t.ProjectIDAndOriginsByClientID(ctx, clientID)
logging.WithFields("clientID", clientID).OnError(err).Debug("could not check projectID and origin of clientID (might be service account)")
// We used to check origins for every token, but service users shouldn't be used publicly (native app / SPA).
// Therefore, mostly won't send an origin and aren't able to configure them anyway.
// For the current time we will only check origins for tokens issued to users through apps (code / implicit flow).
if projectID == "" {
return "", nil
}
return projectID, checkOrigin(ctx, origins)
}
func SetCtxData(ctx context.Context, ctxData CtxData) context.Context {
return context.WithValue(ctx, dataKey, ctxData)
}
func GetCtxData(ctx context.Context) CtxData {
ctxData, _ := ctx.Value(dataKey).(CtxData)
return ctxData
}
func GetRequestPermissionsFromCtx(ctx context.Context) []string {
ctxPermission, _ := ctx.Value(requestPermissionsKey).([]string)
return ctxPermission
}
func checkOrigin(ctx context.Context, origins []string) error {
origin := grpc.GetGatewayHeader(ctx, http_util.Origin)
if origin == "" {
origin = http_util.OriginHeader(ctx)
if origin == "" {
return nil
}
}
if http_util.IsOriginAllowed(origins, origin) {
return nil
}
return zerrors.ThrowPermissionDenied(nil, "AUTH-DZG21", "Errors.OriginNotAllowed")
}
func extractBearerToken(token string) (part string, err error) {
parts := strings.Split(token, BearerPrefix)
if len(parts) != 2 {
return "", zerrors.ThrowUnauthenticated(nil, "AUTH-toLo1", "invalid auth header")
}
return parts[1], nil
}

View File

@@ -0,0 +1,37 @@
package authz
import (
"context"
"golang.org/x/text/language"
)
type MockContextInstanceOpts func(i *instance)
func WithMockDefaultLanguage(lang language.Tag) MockContextInstanceOpts {
return func(i *instance) {
i.defaultLanguage = lang
}
}
func NewMockContext(instanceID, orgID, userID string, opts ...MockContextInstanceOpts) context.Context {
ctx := context.WithValue(context.Background(), dataKey, CtxData{UserID: userID, OrgID: orgID})
i := &instance{id: instanceID}
for _, o := range opts {
o(i)
}
return context.WithValue(ctx, instanceKey, i)
}
func NewMockContextWithAgent(instanceID, orgID, userID, agentID string) context.Context {
ctx := context.WithValue(context.Background(), dataKey, CtxData{UserID: userID, OrgID: orgID, AgentID: agentID})
return context.WithValue(ctx, instanceKey, &instance{id: instanceID})
}
func NewMockContextWithPermissions(instanceID, orgID, userID string, permissions []string) context.Context {
ctx := context.WithValue(context.Background(), dataKey, CtxData{UserID: userID, OrgID: orgID})
ctx = context.WithValue(ctx, instanceKey, &instance{id: instanceID})
return context.WithValue(ctx, requestPermissionsKey, permissions)
}

View File

@@ -0,0 +1,17 @@
package authz
import (
"context"
"time"
)
func Detach(ctx context.Context) context.Context { return detachedContext{ctx} }
type detachedContext struct {
parent context.Context
}
func (v detachedContext) Deadline() (time.Time, bool) { return time.Time{}, false }
func (v detachedContext) Done() <-chan struct{} { return nil }
func (v detachedContext) Err() error { return nil }
func (v detachedContext) Value(key interface{}) interface{} { return v.parent.Value(key) }

View File

@@ -0,0 +1,144 @@
package authz
import (
"context"
"time"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/feature"
)
var emptyInstance = &instance{}
type Instance interface {
InstanceID() string
ProjectID() string
ConsoleClientID() string
ConsoleApplicationID() string
DefaultLanguage() language.Tag
DefaultOrganisationID() string
SecurityPolicyAllowedOrigins() []string
EnableImpersonation() bool
Block() *bool
AuditLogRetention() *time.Duration
Features() feature.Features
}
type InstanceVerifier interface {
InstanceByHost(ctx context.Context, host, publicDomain string) (Instance, error)
InstanceByID(ctx context.Context, id string) (Instance, error)
}
type instance struct {
id string
projectID string
appID string
clientID string
orgID string
defaultLanguage language.Tag
features feature.Features
}
func (i *instance) Block() *bool {
return nil
}
func (i *instance) AuditLogRetention() *time.Duration {
return nil
}
func (i *instance) InstanceID() string {
return i.id
}
func (i *instance) ProjectID() string {
return i.projectID
}
func (i *instance) ConsoleClientID() string {
return i.clientID
}
func (i *instance) ConsoleApplicationID() string {
return i.appID
}
func (i *instance) DefaultLanguage() language.Tag {
return i.defaultLanguage
}
func (i *instance) DefaultOrganisationID() string {
return i.orgID
}
func (i *instance) SecurityPolicyAllowedOrigins() []string {
return nil
}
func (i *instance) EnableImpersonation() bool {
return false
}
func (i *instance) Features() feature.Features {
return i.features
}
func GetInstance(ctx context.Context) Instance {
instance, ok := ctx.Value(instanceKey).(Instance)
if !ok {
return emptyInstance
}
return instance
}
func GetFeatures(ctx context.Context) feature.Features {
return GetInstance(ctx).Features()
}
func WithInstance(ctx context.Context, instance Instance) context.Context {
return context.WithValue(ctx, instanceKey, instance)
}
func WithInstanceID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, instanceKey, &instance{id: id})
}
func WithDefaultLanguage(ctx context.Context, defaultLanguage language.Tag) context.Context {
i, ok := ctx.Value(instanceKey).(*instance)
if !ok {
i = new(instance)
}
i.defaultLanguage = defaultLanguage
return context.WithValue(ctx, instanceKey, i)
}
func WithConsole(ctx context.Context, projectID, appID string) context.Context {
i, ok := ctx.Value(instanceKey).(*instance)
if !ok {
i = new(instance)
}
i.projectID = projectID
i.appID = appID
return context.WithValue(ctx, instanceKey, i)
}
func WithConsoleClientID(ctx context.Context, clientID string) context.Context {
i, ok := ctx.Value(instanceKey).(*instance)
if !ok {
i = new(instance)
}
i.clientID = clientID
return context.WithValue(ctx, instanceKey, i)
}
func WithFeatures(ctx context.Context, f feature.Features) context.Context {
i, ok := ctx.Value(instanceKey).(*instance)
if !ok {
i = new(instance)
}
i.features = f
return context.WithValue(ctx, instanceKey, i)
}

View File

@@ -0,0 +1,131 @@
package authz
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/feature"
)
func Test_Instance(t *testing.T) {
type args struct {
ctx context.Context
}
type res struct {
instanceID string
projectID string
consoleID string
features feature.Features
}
tests := []struct {
name string
args args
res res
}{
{
"empty context",
args{
context.Background(),
},
res{
instanceID: "",
projectID: "",
consoleID: "",
},
},
{
"WithInstanceID",
args{
WithInstanceID(context.Background(), "id"),
},
res{
instanceID: "id",
projectID: "",
consoleID: "",
},
},
{
"WithInstance",
args{
WithInstance(context.Background(), &mockInstance{}),
},
res{
instanceID: "instanceID",
projectID: "projectID",
consoleID: "consoleID",
},
},
{
"WithFeatures",
args{
WithFeatures(context.Background(), feature.Features{
LoginDefaultOrg: true,
}),
},
res{
features: feature.Features{
LoginDefaultOrg: true,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := GetInstance(tt.args.ctx)
assert.Equal(t, tt.res.instanceID, got.InstanceID())
assert.Equal(t, tt.res.projectID, got.ProjectID())
assert.Equal(t, tt.res.consoleID, got.ConsoleClientID())
assert.Equal(t, tt.res.features, got.Features())
})
}
}
type mockInstance struct{}
func (m *mockInstance) Block() *bool {
panic("shouldn't be called here")
}
func (m *mockInstance) AuditLogRetention() *time.Duration {
panic("shouldn't be called here")
}
func (m *mockInstance) InstanceID() string {
return "instanceID"
}
func (m *mockInstance) ProjectID() string {
return "projectID"
}
func (m *mockInstance) ConsoleClientID() string {
return "consoleID"
}
func (m *mockInstance) ConsoleApplicationID() string {
return "appID"
}
func (m *mockInstance) DefaultLanguage() language.Tag {
return language.English
}
func (m *mockInstance) DefaultOrganisationID() string {
return "orgID"
}
func (m *mockInstance) SecurityPolicyAllowedOrigins() []string {
return nil
}
func (m *mockInstance) EnableImpersonation() bool {
return false
}
func (m *mockInstance) Features() feature.Features {
return feature.Features{}
}

View File

@@ -0,0 +1,143 @@
// Code generated by "enumer -type MemberType -trimprefix MemberType -json -sql"; DO NOT EDIT.
package authz
import (
"database/sql/driver"
"encoding/json"
"fmt"
"strings"
)
const _MemberTypeName = "UnspecifiedOrganizationProjectProjectGrantIAMSystem"
var _MemberTypeIndex = [...]uint8{0, 11, 23, 30, 42, 45, 51}
const _MemberTypeLowerName = "unspecifiedorganizationprojectprojectgrantiamsystem"
func (i MemberType) String() string {
if i < 0 || i >= MemberType(len(_MemberTypeIndex)-1) {
return fmt.Sprintf("MemberType(%d)", i)
}
return _MemberTypeName[_MemberTypeIndex[i]:_MemberTypeIndex[i+1]]
}
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
func _MemberTypeNoOp() {
var x [1]struct{}
_ = x[MemberTypeUnspecified-(0)]
_ = x[MemberTypeOrganization-(1)]
_ = x[MemberTypeProject-(2)]
_ = x[MemberTypeProjectGrant-(3)]
_ = x[MemberTypeIAM-(4)]
_ = x[MemberTypeSystem-(5)]
}
var _MemberTypeValues = []MemberType{MemberTypeUnspecified, MemberTypeOrganization, MemberTypeProject, MemberTypeProjectGrant, MemberTypeIAM, MemberTypeSystem}
var _MemberTypeNameToValueMap = map[string]MemberType{
_MemberTypeName[0:11]: MemberTypeUnspecified,
_MemberTypeLowerName[0:11]: MemberTypeUnspecified,
_MemberTypeName[11:23]: MemberTypeOrganization,
_MemberTypeLowerName[11:23]: MemberTypeOrganization,
_MemberTypeName[23:30]: MemberTypeProject,
_MemberTypeLowerName[23:30]: MemberTypeProject,
_MemberTypeName[30:42]: MemberTypeProjectGrant,
_MemberTypeLowerName[30:42]: MemberTypeProjectGrant,
_MemberTypeName[42:45]: MemberTypeIAM,
_MemberTypeLowerName[42:45]: MemberTypeIAM,
_MemberTypeName[45:51]: MemberTypeSystem,
_MemberTypeLowerName[45:51]: MemberTypeSystem,
}
var _MemberTypeNames = []string{
_MemberTypeName[0:11],
_MemberTypeName[11:23],
_MemberTypeName[23:30],
_MemberTypeName[30:42],
_MemberTypeName[42:45],
_MemberTypeName[45:51],
}
// MemberTypeString retrieves an enum value from the enum constants string name.
// Throws an error if the param is not part of the enum.
func MemberTypeString(s string) (MemberType, error) {
if val, ok := _MemberTypeNameToValueMap[s]; ok {
return val, nil
}
if val, ok := _MemberTypeNameToValueMap[strings.ToLower(s)]; ok {
return val, nil
}
return 0, fmt.Errorf("%s does not belong to MemberType values", s)
}
// MemberTypeValues returns all values of the enum
func MemberTypeValues() []MemberType {
return _MemberTypeValues
}
// MemberTypeStrings returns a slice of all String values of the enum
func MemberTypeStrings() []string {
strs := make([]string, len(_MemberTypeNames))
copy(strs, _MemberTypeNames)
return strs
}
// IsAMemberType returns "true" if the value is listed in the enum definition. "false" otherwise
func (i MemberType) IsAMemberType() bool {
for _, v := range _MemberTypeValues {
if i == v {
return true
}
}
return false
}
// MarshalJSON implements the json.Marshaler interface for MemberType
func (i MemberType) MarshalJSON() ([]byte, error) {
return json.Marshal(i.String())
}
// UnmarshalJSON implements the json.Unmarshaler interface for MemberType
func (i *MemberType) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return fmt.Errorf("MemberType should be a string, got %s", data)
}
var err error
*i, err = MemberTypeString(s)
return err
}
func (i MemberType) Value() (driver.Value, error) {
return i.String(), nil
}
func (i *MemberType) Scan(value interface{}) error {
if value == nil {
return nil
}
var str string
switch v := value.(type) {
case []byte:
str = string(v)
case string:
str = v
case fmt.Stringer:
str = v.String()
default:
return fmt.Errorf("invalid value of MemberType: %[1]T(%[1]v)", value)
}
val, err := MemberTypeString(str)
if err != nil {
return err
}
*i = val
return nil
}

View File

@@ -0,0 +1,141 @@
package authz
import (
"context"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
func CheckPermission(ctx context.Context, resolver MembershipsResolver, systemUserRoleMapping []RoleMapping, roleMappings []RoleMapping, permission, orgID, resourceID string) (err error) {
requestedPermissions, _, err := getUserPermissions(ctx, resolver, permission, systemUserRoleMapping, roleMappings, GetCtxData(ctx), orgID)
if err != nil {
return err
}
_, userPermissionSpan := tracing.NewNamedSpan(ctx, "checkUserPermissions")
err = checkUserResourcePermissions(requestedPermissions, resourceID)
userPermissionSpan.EndWithError(err)
return err
}
// getUserPermissions retrieves the memberships of the authenticated user (on instance and provided organisation level),
// and maps them to permissions. It will return the requested permission(s) and all other granted permissions separately.
func getUserPermissions(ctx context.Context, resolver MembershipsResolver, requiredPerm string, systemUserRoleMappings []RoleMapping, roleMappings []RoleMapping, ctxData CtxData, orgID string) (requestedPermissions, allPermissions []string, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if ctxData.IsZero() {
return nil, nil, zerrors.ThrowUnauthenticated(nil, "AUTH-rKLWEH", "context missing")
}
if ctxData.SystemMemberships != nil {
requestedPermissions, allPermissions = mapMembershipsToPermissions(requiredPerm, ctxData.SystemMemberships, systemUserRoleMappings)
return requestedPermissions, allPermissions, nil
}
ctx = context.WithValue(ctx, dataKey, ctxData)
memberships, err := resolver.SearchMyMemberships(ctx, orgID, false)
if err != nil {
return nil, nil, err
}
if len(memberships) == 0 {
memberships, err = resolver.SearchMyMemberships(ctx, orgID, true)
if len(memberships) == 0 {
return nil, nil, zerrors.ThrowNotFound(nil, "AUTHZ-cdgFk", "membership not found")
}
if err != nil {
return nil, nil, err
}
}
requestedPermissions, allPermissions = mapMembershipsToPermissions(requiredPerm, memberships, roleMappings)
return requestedPermissions, allPermissions, nil
}
// checkUserResourcePermissions checks that if a user i granted either the requested permission globally (project.write)
// or the specific resource (project.write:123)
func checkUserResourcePermissions(userPerms []string, resourceID string) error {
if len(userPerms) == 0 {
return zerrors.ThrowPermissionDenied(nil, "AUTH-AWfge", "No matching permissions found")
}
if resourceID == "" {
return nil
}
if HasGlobalPermission(userPerms) {
return nil
}
if hasContextResourcePermission(userPerms, resourceID) {
return nil
}
return zerrors.ThrowPermissionDenied(nil, "AUTH-Swrgg2", "No matching permissions found")
}
func hasContextResourcePermission(permissions []string, resourceID string) bool {
for _, perm := range permissions {
_, ctxID := SplitPermission(perm)
if resourceID == ctxID {
return true
}
}
return false
}
func mapMembershipsToPermissions(requiredPerm string, memberships []*Membership, roleMappings []RoleMapping) (requestPermissions, allPermissions []string) {
requestPermissions = make([]string, 0)
allPermissions = make([]string, 0)
for _, membership := range memberships {
requestPermissions, allPermissions = mapMembershipToPerm(requiredPerm, membership, roleMappings, requestPermissions, allPermissions)
}
return requestPermissions, allPermissions
}
func mapMembershipToPerm(requiredPerm string, membership *Membership, roleMappings []RoleMapping, requestPermissions, allPermissions []string) ([]string, []string) {
roleNames, roleContextID := roleWithContext(membership)
for _, roleName := range roleNames {
perms := getPermissionsFromRole(roleMappings, roleName)
for _, p := range perms {
permWithCtx := addRoleContextIDToPerm(p, roleContextID)
if !ExistsPerm(allPermissions, permWithCtx) {
allPermissions = append(allPermissions, permWithCtx)
}
p, _ = SplitPermission(p)
if p == requiredPerm {
if !ExistsPerm(requestPermissions, permWithCtx) {
requestPermissions = append(requestPermissions, permWithCtx)
}
}
}
}
return requestPermissions, allPermissions
}
func addRoleContextIDToPerm(perm, roleContextID string) string {
if roleContextID != "" {
perm = perm + ":" + roleContextID
}
return perm
}
func ExistsPerm(existingPermissions []string, perm string) bool {
for _, existingPermission := range existingPermissions {
if existingPermission == perm {
return true
}
}
return false
}
func roleWithContext(membership *Membership) (roles []string, ctxID string) {
if membership.MemberType == MemberTypeProject || membership.MemberType == MemberTypeProjectGrant {
return membership.Roles, membership.ObjectID
}
return membership.Roles, ""
}

View File

@@ -0,0 +1,608 @@
package authz
import (
"context"
"testing"
"github.com/zitadel/zitadel/internal/zerrors"
)
func equalStringArray(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i, v := range a {
if v != b[i] {
return false
}
}
return true
}
type membershipsResolverFunc func(ctx context.Context, orgID string, shouldTriggerBulk bool) ([]*Membership, error)
func (m membershipsResolverFunc) SearchMyMemberships(ctx context.Context, orgID string, shouldTriggerBulk bool) ([]*Membership, error) {
return m(ctx, orgID, shouldTriggerBulk)
}
func Test_GetUserPermissions(t *testing.T) {
type args struct {
ctxData CtxData
membershipsResolver MembershipsResolver
requiredPerm string
authConfig Config
}
tests := []struct {
name string
args args
wantErr bool
errFunc func(err error) bool
result []string
}{
{
name: "Empty Context",
args: args{
ctxData: CtxData{},
membershipsResolver: membershipsResolverFunc(func(ctx context.Context, orgID string, shouldTriggerBulk bool) ([]*Membership, error) {
return []*Membership{{Roles: []string{"ORG_OWNER"}}}, nil
}),
requiredPerm: "project.read",
authConfig: Config{
RolePermissionMappings: []RoleMapping{
{
Role: "IAM_OWNER",
Permissions: []string{"project.read"},
},
{
Role: "ORG_OWNER",
Permissions: []string{"org.read", "project.read"},
},
},
},
},
wantErr: true,
errFunc: zerrors.IsUnauthenticated,
result: []string{"project.read"},
},
{
name: "No Grants",
args: args{
ctxData: CtxData{},
membershipsResolver: membershipsResolverFunc(func(ctx context.Context, orgID string, shouldTriggerBulk bool) ([]*Membership, error) {
return []*Membership{}, nil
}),
requiredPerm: "project.read",
authConfig: Config{
RolePermissionMappings: []RoleMapping{
{
Role: "IAM_OWNER",
Permissions: []string{"project.read"},
},
{
Role: "ORG_OWNER",
Permissions: []string{"org.read", "project.read"},
},
},
},
},
result: make([]string, 0),
},
{
name: "Get Permissions",
args: args{
ctxData: CtxData{UserID: "userID", OrgID: "orgID"},
membershipsResolver: membershipsResolverFunc(func(ctx context.Context, orgID string, shouldTriggerBulk bool) ([]*Membership, error) {
return []*Membership{
{
AggregateID: "IAM",
ObjectID: "IAM",
MemberType: MemberTypeIAM,
Roles: []string{"IAM_OWNER"},
},
}, nil
}),
requiredPerm: "project.read",
authConfig: Config{
RolePermissionMappings: []RoleMapping{
{
Role: "IAM_OWNER",
Permissions: []string{"project.read"},
},
{
Role: "ORG_OWNER",
Permissions: []string{"org.read", "project.read"},
},
},
},
},
result: []string{"project.read"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, perms, err := getUserPermissions(context.Background(), tt.args.membershipsResolver, tt.args.requiredPerm, nil, tt.args.authConfig.RolePermissionMappings, tt.args.ctxData, tt.args.ctxData.OrgID)
if tt.wantErr && err == nil {
t.Errorf("got wrong result, should get err: actual: %v ", err)
}
if tt.wantErr && !tt.errFunc(err) {
t.Errorf("got wrong err: %v ", err)
}
if !tt.wantErr && !equalStringArray(perms, tt.result) {
t.Errorf("got wrong result, expecting: %v, actual: %v ", tt.result, perms)
}
})
}
}
func Test_MapMembershipToPermissions(t *testing.T) {
type args struct {
requiredPerm string
membership []*Membership
authConfig Config
}
tests := []struct {
name string
args args
requestPerms []string
allPerms []string
}{
{
name: "One Role existing perm",
args: args{
requiredPerm: "project.read",
membership: []*Membership{
{
AggregateID: "1",
ObjectID: "1",
MemberType: MemberTypeOrganization,
Roles: []string{"ORG_OWNER"},
},
},
authConfig: Config{
RolePermissionMappings: []RoleMapping{
{
Role: "IAM_OWNER",
Permissions: []string{"project.read"},
},
{
Role: "ORG_OWNER",
Permissions: []string{"org.read", "project.read"},
},
},
},
},
requestPerms: []string{"project.read"},
allPerms: []string{"org.read", "project.read"},
},
{
name: "One Role not existing perm",
args: args{
requiredPerm: "project.write",
membership: []*Membership{
{
AggregateID: "1",
ObjectID: "1",
MemberType: MemberTypeOrganization,
Roles: []string{"ORG_OWNER"},
},
},
authConfig: Config{
RolePermissionMappings: []RoleMapping{
{
Role: "IAM_OWNER",
Permissions: []string{"project.read"},
},
{
Role: "ORG_OWNER",
Permissions: []string{"org.read", "project.read"},
},
},
},
},
requestPerms: []string{},
allPerms: []string{"org.read", "project.read"},
},
{
name: "Multiple Roles one existing",
args: args{
requiredPerm: "project.read",
membership: []*Membership{
{
AggregateID: "1",
ObjectID: "1",
MemberType: MemberTypeOrganization,
Roles: []string{"ORG_OWNER"},
},
{
AggregateID: "IAM",
ObjectID: "IAM",
MemberType: MemberTypeIAM,
Roles: []string{"IAM_OWNER"},
},
},
authConfig: Config{
RolePermissionMappings: []RoleMapping{
{
Role: "IAM_OWNER",
Permissions: []string{"project.read"},
},
{
Role: "ORG_OWNER",
Permissions: []string{"org.read", "project.read"},
},
},
},
},
requestPerms: []string{"project.read"},
allPerms: []string{"org.read", "project.read"},
},
{
name: "Multiple Roles, global and specific",
args: args{
requiredPerm: "project.read",
membership: []*Membership{
{
AggregateID: "2",
ObjectID: "2",
MemberType: MemberTypeOrganization,
Roles: []string{"ORG_OWNER"},
},
{
AggregateID: "1",
ObjectID: "1",
MemberType: MemberTypeProject,
Roles: []string{"PROJECT_OWNER"},
},
},
authConfig: Config{
RolePermissionMappings: []RoleMapping{
{
Role: "PROJECT_OWNER",
Permissions: []string{"project.read"},
},
{
Role: "ORG_OWNER",
Permissions: []string{"org.read", "project.read"},
},
},
},
},
requestPerms: []string{"project.read", "project.read:1"},
allPerms: []string{"org.read", "project.read", "project.read:1"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
requestPerms, allPerms := mapMembershipsToPermissions(tt.args.requiredPerm, tt.args.membership, tt.args.authConfig.RolePermissionMappings)
if !equalStringArray(requestPerms, tt.requestPerms) {
t.Errorf("got wrong requestPerms, expecting: %v, actual: %v ", tt.requestPerms, requestPerms)
}
if !equalStringArray(allPerms, tt.allPerms) {
t.Errorf("got wrong allPerms, expecting: %v, actual: %v ", tt.allPerms, allPerms)
}
})
}
}
func Test_MapMembershipToPerm(t *testing.T) {
type args struct {
requiredPerm string
membership *Membership
authConfig Config
requestPerms []string
allPerms []string
}
tests := []struct {
name string
args args
requestPerms []string
allPerms []string
}{
{
name: "first perm without context id",
args: args{
requiredPerm: "project.read",
membership: &Membership{
AggregateID: "Org",
ObjectID: "Org",
MemberType: MemberTypeOrganization,
Roles: []string{"ORG_OWNER"},
},
authConfig: Config{
RolePermissionMappings: []RoleMapping{
{
Role: "IAM_OWNER",
Permissions: []string{"project.read"},
},
{
Role: "ORG_OWNER",
Permissions: []string{"org.read", "project.read"},
},
},
},
requestPerms: []string{},
allPerms: []string{},
},
requestPerms: []string{"project.read"},
allPerms: []string{"org.read", "project.read"},
},
{
name: "existing perm without context id",
args: args{
requiredPerm: "project.read",
membership: &Membership{
AggregateID: "Org",
ObjectID: "Org",
MemberType: MemberTypeOrganization,
Roles: []string{"ORG_OWNER"},
},
authConfig: Config{
RolePermissionMappings: []RoleMapping{
{
Role: "IAM_OWNER",
Permissions: []string{"project.read"},
},
{
Role: "ORG_OWNER",
Permissions: []string{"org.read", "project.read"},
},
},
},
requestPerms: []string{"project.read"},
allPerms: []string{"org.read", "project.read"},
},
requestPerms: []string{"project.read"},
allPerms: []string{"org.read", "project.read"},
},
{
name: "first perm with context id",
args: args{
requiredPerm: "project.read",
membership: &Membership{
AggregateID: "1",
ObjectID: "1",
MemberType: MemberTypeProject,
Roles: []string{"PROJECT_OWNER"},
},
authConfig: Config{
RolePermissionMappings: []RoleMapping{
{
Role: "PROJECT_OWNER",
Permissions: []string{"project.read"},
},
{
Role: "ORG_OWNER",
Permissions: []string{"org.read", "project.read"},
},
},
},
requestPerms: []string{},
allPerms: []string{},
},
requestPerms: []string{"project.read:1"},
allPerms: []string{"project.read:1"},
},
{
name: "perm with context id, existing global",
args: args{
requiredPerm: "project.read",
membership: &Membership{
AggregateID: "1",
ObjectID: "1",
MemberType: MemberTypeProject,
Roles: []string{"PROJECT_OWNER"},
},
authConfig: Config{
RolePermissionMappings: []RoleMapping{
{
Role: "PROJECT_OWNER",
Permissions: []string{"project.read"},
},
{
Role: "ORG_OWNER",
Permissions: []string{"org.read", "project.read"},
},
},
},
requestPerms: []string{"project.read"},
allPerms: []string{"org.read", "project.read"},
},
requestPerms: []string{"project.read", "project.read:1"},
allPerms: []string{"org.read", "project.read", "project.read:1"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
requestPerms, allPerms := mapMembershipToPerm(tt.args.requiredPerm, tt.args.membership, tt.args.authConfig.RolePermissionMappings, tt.args.requestPerms, tt.args.allPerms)
if !equalStringArray(requestPerms, tt.requestPerms) {
t.Errorf("got wrong requestPerms, expecting: %v, actual: %v ", tt.requestPerms, requestPerms)
}
if !equalStringArray(allPerms, tt.allPerms) {
t.Errorf("got wrong allPerms, expecting: %v, actual: %v ", tt.allPerms, allPerms)
}
})
}
}
func Test_AddRoleContextIDToPerm(t *testing.T) {
type args struct {
perm string
ctxID string
}
tests := []struct {
name string
args args
result string
}{
{
name: "with ctx id",
args: args{
perm: "perm1",
ctxID: "2",
},
result: "perm1:2",
},
{
name: "with ctx id",
args: args{
perm: "perm1",
ctxID: "",
},
result: "perm1",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := addRoleContextIDToPerm(tt.args.perm, tt.args.ctxID)
if result != tt.result {
t.Errorf("got wrong result, expecting: %v, actual: %v ", tt.result, result)
}
})
}
}
func Test_ExistisPerm(t *testing.T) {
type args struct {
existingPermissions []string
perm string
}
tests := []struct {
name string
args args
result bool
}{
{
name: "not existing perm",
args: args{
existingPermissions: []string{"perm1", "perm2", "perm3"},
perm: "perm4",
},
result: false,
},
{
name: "existing perm",
args: args{
existingPermissions: []string{"perm1", "perm2", "perm3"},
perm: "perm2",
},
result: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ExistsPerm(tt.args.existingPermissions, tt.args.perm)
if result != tt.result {
t.Errorf("got wrong result, expecting: %v, actual: %v ", tt.result, result)
}
})
}
}
func Test_CheckUserResourcePermissions(t *testing.T) {
type args struct {
perms []string
resourceID string
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "no permissions",
args: args{
perms: []string{},
resourceID: "",
},
wantErr: true,
},
{
name: "has permission and no context requested",
args: args{
perms: []string{"project.read"},
resourceID: "",
},
wantErr: false,
},
{
name: "context requested and has global permission",
args: args{
perms: []string{"project.read", "project.read:1"},
resourceID: "Test",
},
wantErr: false,
},
{
name: "context requested and has specific permission",
args: args{
perms: []string{"project.read:Test"},
resourceID: "Test",
},
wantErr: false,
},
{
name: "context requested and has no permission",
args: args{
perms: []string{"project.read:Test"},
resourceID: "Hodor",
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := checkUserResourcePermissions(tt.args.perms, tt.args.resourceID)
if tt.wantErr && err == nil {
t.Errorf("got wrong result, should get err: actual: %v ", err)
}
if !tt.wantErr && err != nil {
t.Errorf("shouldn't get err: %v ", err)
}
if tt.wantErr && !zerrors.IsPermissionDenied(err) {
t.Errorf("got wrong err: %v ", err)
}
})
}
}
func Test_HasContextResourcePermission(t *testing.T) {
type args struct {
perms []string
resourceID string
}
tests := []struct {
name string
args args
result bool
}{
{
name: "existing context permission",
args: args{
perms: []string{"test:wrong", "test:right"},
resourceID: "right",
},
result: true,
},
{
name: "not existing context permission",
args: args{
perms: []string{"test:wrong", "test:wrong2"},
resourceID: "test",
},
result: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := hasContextResourcePermission(tt.args.perms, tt.args.resourceID)
if result != tt.result {
t.Errorf("got wrong result, expecting: %v, actual: %v ", tt.result, result)
}
})
}
}

View File

@@ -0,0 +1,32 @@
package authz
import (
"context"
"encoding/base64"
"fmt"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
const (
SessionTokenPrefix = "sess_"
SessionTokenFormat = SessionTokenPrefix + "%s:%s"
)
func SessionTokenVerifier(algorithm crypto.EncryptionAlgorithm) func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) {
return func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) {
decodedToken, err := base64.RawURLEncoding.DecodeString(sessionToken)
if err != nil {
return zerrors.ThrowInvalidArgument(err, "COMMAND-hi6Ph", "Errors.Session.Token.Invalid")
}
_, spanPasswordComparison := tracing.NewNamedSpan(ctx, "crypto.CompareHash")
token, err := algorithm.DecryptString(decodedToken, algorithm.EncryptionKeyID())
spanPasswordComparison.EndWithError(err)
if err != nil || token != fmt.Sprintf(SessionTokenFormat, sessionID, tokenID) {
return zerrors.ThrowPermissionDenied(err, "COMMAND-sGr42", "Errors.Session.Token.Invalid")
}
return nil
}
}

View File

@@ -0,0 +1,119 @@
package authz
import (
"context"
"crypto/rsa"
"errors"
"os"
"sync"
"time"
"github.com/go-jose/go-jose/v4"
"github.com/zitadel/oidc/v3/pkg/op"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/zerrors"
)
var _ SystemTokenVerifier = (*SystemTokenVerifierFromConfig)(nil)
type SystemTokenVerifierFromConfig struct {
systemJWTProfile *op.JWTProfileVerifier
systemUsers map[string]Memberships
}
func StartSystemTokenVerifierFromConfig(issuer string, keys map[string]*SystemAPIUser) (*SystemTokenVerifierFromConfig, error) {
systemUsers := make(map[string]Memberships, len(keys))
for userID, key := range keys {
if len(key.Memberships) == 0 {
systemUsers[userID] = Memberships{{MemberType: MemberTypeSystem, Roles: []string{"SYSTEM_OWNER"}}}
continue
}
for _, membership := range key.Memberships {
switch membership.MemberType {
case MemberTypeSystem, MemberTypeIAM, MemberTypeOrganization:
systemUsers[userID] = key.Memberships
case MemberTypeUnspecified, MemberTypeProject, MemberTypeProjectGrant:
return nil, errors.New("for system users, only the membership types System, IAM and Organization are supported")
default:
return nil, errors.New("unknown membership type")
}
}
}
return &SystemTokenVerifierFromConfig{
systemJWTProfile: op.NewJWTProfileVerifier(
&systemJWTStorage{
keys: keys,
cachedKeys: make(map[string]*rsa.PublicKey),
},
issuer,
1*time.Hour,
time.Second,
),
systemUsers: systemUsers,
}, nil
}
func (s *SystemTokenVerifierFromConfig) VerifySystemToken(ctx context.Context, token string, orgID string) (matchingMemberships Memberships, userID string, err error) {
jwtReq, err := op.VerifyJWTAssertion(ctx, token, s.systemJWTProfile)
if err != nil {
return nil, "", err
}
systemUserMemberships, ok := s.systemUsers[jwtReq.Subject]
if !ok {
return nil, "", zerrors.ThrowPermissionDenied(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong")
}
matchingMemberships = make(Memberships, 0, len(systemUserMemberships))
for _, membership := range systemUserMemberships {
if membership.MemberType == MemberTypeSystem ||
membership.MemberType == MemberTypeIAM && GetInstance(ctx).InstanceID() == membership.AggregateID ||
membership.MemberType == MemberTypeOrganization && orgID == membership.AggregateID {
matchingMemberships = append(matchingMemberships, membership)
}
}
return matchingMemberships, jwtReq.Subject, nil
}
type systemJWTStorage struct {
keys map[string]*SystemAPIUser
mutex sync.RWMutex
cachedKeys map[string]*rsa.PublicKey
}
type SystemAPIUser struct {
Path string // if a path is specified, the key will be read from that path
KeyData []byte // else you can also specify the data directly in the KeyData
Memberships Memberships
}
func (s *SystemAPIUser) readKey() (*rsa.PublicKey, error) {
if s.Path != "" {
var err error
s.KeyData, err = os.ReadFile(s.Path)
if err != nil {
return nil, zerrors.ThrowInternal(err, "AUTHZ-JK31F", "Errors.NotFound")
}
}
return crypto.BytesToPublicKey(s.KeyData)
}
func (s *systemJWTStorage) GetKeyByIDAndClientID(_ context.Context, _, userID string) (*jose.JSONWebKey, error) {
s.mutex.RLock()
cachedKey, ok := s.cachedKeys[userID]
s.mutex.RUnlock()
if ok {
return &jose.JSONWebKey{KeyID: userID, Key: cachedKey}, nil
}
key, ok := s.keys[userID]
if !ok {
return nil, zerrors.ThrowNotFound(nil, "AUTHZ-asfd3", "Errors.User.NotFound")
}
s.mutex.Lock()
defer s.mutex.Unlock()
publicKey, err := key.readKey()
if err != nil {
return nil, err
}
s.cachedKeys[userID] = publicKey
return &jose.JSONWebKey{KeyID: userID, Key: publicKey}, nil
}

View File

@@ -0,0 +1,16 @@
package authz
import (
"context"
"github.com/zitadel/zitadel/internal/zerrors"
)
// UserIDInCTX checks if the userID
// equals the authenticated user in the context.
func UserIDInCTX(ctx context.Context, userID string) error {
if GetCtxData(ctx).UserID != userID {
return zerrors.ThrowPermissionDenied(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong")
}
return nil
}

View File

@@ -0,0 +1,44 @@
package call
import (
"context"
"time"
)
type durationKey struct{}
var key *durationKey = (*durationKey)(nil)
// WithTimestamp sets [time.Now()] to the call field in the context
// if it's not already set.
func WithTimestamp(parent context.Context) context.Context {
if parent.Value(key) != nil {
return parent
}
return ResetTimestamp(parent)
}
// ResetTimestamp sets [time.Now()] to the call field in the context,
// overwriting any previously set call timestamp.
func ResetTimestamp(parent context.Context) context.Context {
return context.WithValue(parent, key, time.Now())
}
// FromContext returns the [time.Time] the call hit the api
func FromContext(ctx context.Context) (t time.Time) {
value := ctx.Value(key)
if t, ok := value.(time.Time); ok {
return t
}
return t
}
// Took returns the time the call took so far
func Took(ctx context.Context) time.Duration {
start := FromContext(ctx)
if start.IsZero() {
return 0
}
return time.Since(start)
}

View File

@@ -0,0 +1,119 @@
package call
import (
"context"
"testing"
"time"
)
func TestTook(t *testing.T) {
type args struct {
ctx context.Context
}
tests := []struct {
name string
args args
startIsZero bool
}{
{
name: "no start",
args: args{
ctx: context.Background(),
},
startIsZero: true,
},
{
name: "with start",
args: args{
ctx: WithTimestamp(context.Background()),
},
startIsZero: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Took(tt.args.ctx)
if tt.startIsZero && got != 0 {
t.Errorf("Duration should be 0 but was %v", got)
}
if !tt.startIsZero && got <= 0 {
t.Errorf("Duration should be greater 0 but was %d", got)
}
})
}
}
func TestFromContext(t *testing.T) {
type args struct {
ctx context.Context
}
tests := []struct {
name string
args args
isZero bool
}{
{
name: "no start",
args: args{
ctx: context.Background(),
},
isZero: true,
},
{
name: "with start",
args: args{
ctx: WithTimestamp(context.Background()),
},
isZero: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := FromContext(tt.args.ctx)
if tt.isZero != got.IsZero() {
t.Errorf("Time is zero should be %v but was %v", tt.isZero, got.IsZero())
}
})
}
}
func TestWithTimestamp(t *testing.T) {
start := time.Date(2019, 4, 29, 0, 0, 0, 0, time.UTC)
type args struct {
ctx context.Context
}
tests := []struct {
name string
args args
noPrevious bool
}{
{
name: "fresh context",
args: args{
ctx: context.WithValue(context.Background(), key, start),
},
noPrevious: true,
},
{
name: "with start",
args: args{
ctx: WithTimestamp(context.Background()),
},
noPrevious: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := WithTimestamp(tt.args.ctx)
val := got.Value(key).(time.Time)
if !tt.noPrevious && val.Before(start) {
t.Errorf("time should be now not %v", val)
}
if tt.noPrevious && val.After(start) {
t.Errorf("time should be start not %v", val)
}
})
}
}

View File

@@ -0,0 +1,151 @@
package action
import (
"google.golang.org/protobuf/types/known/durationpb"
object_grpc "github.com/zitadel/zitadel/internal/api/grpc/object"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
action_pb "github.com/zitadel/zitadel/pkg/grpc/action"
message_pb "github.com/zitadel/zitadel/pkg/grpc/message"
)
// for backward compatability: old enum identifiers are mapped as well
func FlowTypeToDomain(flowType string) domain.FlowType {
switch flowType {
case "FLOW_TYPE_EXTERNAL_AUTHENTICATION", domain.FlowTypeExternalAuthentication.ID():
return domain.FlowTypeExternalAuthentication
case domain.FlowTypeCustomiseToken.ID():
return domain.FlowTypeCustomiseToken
case domain.FlowTypeInternalAuthentication.ID():
return domain.FlowTypeInternalAuthentication
case domain.FlowTypeCustomizeSAMLResponse.ID():
return domain.FlowTypeCustomizeSAMLResponse
default:
return domain.FlowTypeUnspecified
}
}
func FlowTypeToPb(typ domain.FlowType) *action_pb.FlowType {
return &action_pb.FlowType{
Id: typ.ID(),
Name: &message_pb.LocalizedMessage{
Key: typ.LocalizationKey(),
},
}
}
// TriggerTypeToDomain maps the pb type to domain
// for backward compatability: old enum identifiers are mapped as well
func TriggerTypeToDomain(triggerType string) domain.TriggerType {
switch triggerType {
case "TRIGGER_TYPE_POST_AUTHENTICATION", domain.TriggerTypePostAuthentication.ID():
return domain.TriggerTypePostAuthentication
case "TRIGGER_TYPE_PRE_CREATION", domain.TriggerTypePreCreation.ID():
return domain.TriggerTypePreCreation
case "TRIGGER_TYPE_POST_CREATION", domain.TriggerTypePostCreation.ID():
return domain.TriggerTypePostCreation
case domain.TriggerTypePreAccessTokenCreation.ID():
return domain.TriggerTypePreAccessTokenCreation
case domain.TriggerTypePreUserinfoCreation.ID():
return domain.TriggerTypePreUserinfoCreation
case domain.TriggerTypePreSAMLResponseCreation.ID():
return domain.TriggerTypePreSAMLResponseCreation
default:
return domain.TriggerTypeUnspecified
}
}
func TriggerTypesToPb(types []domain.TriggerType) []*action_pb.TriggerType {
list := make([]*action_pb.TriggerType, len(types))
for i, typ := range types {
list[i] = TriggerTypeToPb(typ)
}
return list
}
func TriggerTypeToPb(typ domain.TriggerType) *action_pb.TriggerType {
return &action_pb.TriggerType{
Id: typ.ID(),
Name: &message_pb.LocalizedMessage{
Key: typ.LocalizationKey(),
},
}
}
func FlowToPb(flow *query.Flow) *action_pb.Flow {
return &action_pb.Flow{
Type: FlowTypeToPb(flow.Type),
Details: object_grpc.ChangeToDetailsPb(flow.Sequence, flow.ChangeDate, flow.ResourceOwner),
State: action_pb.FlowState_FLOW_STATE_ACTIVE, //TODO: state in next release
TriggerActions: TriggerActionsToPb(flow.TriggerActions),
}
}
func TriggerActionToPb(trigger domain.TriggerType, actions []*query.Action) *action_pb.TriggerAction {
return &action_pb.TriggerAction{
TriggerType: TriggerTypeToPb(trigger),
Actions: ActionsToPb(actions),
}
}
func TriggerActionsToPb(triggers map[domain.TriggerType][]*query.Action) []*action_pb.TriggerAction {
list := make([]*action_pb.TriggerAction, 0)
for trigger, actions := range triggers {
list = append(list, TriggerActionToPb(trigger, actions))
}
return list
}
func ActionsToPb(actions []*query.Action) []*action_pb.Action {
list := make([]*action_pb.Action, len(actions))
for i, action := range actions {
list[i] = ActionToPb(action)
}
return list
}
func ActionToPb(action *query.Action) *action_pb.Action {
return &action_pb.Action{
Id: action.ID,
Details: object_grpc.ChangeToDetailsPb(action.Sequence, action.ChangeDate, action.ResourceOwner),
State: ActionStateToPb(action.State),
Name: action.Name,
Script: action.Script,
Timeout: durationpb.New(action.Timeout()),
AllowedToFail: action.AllowedToFail,
}
}
func ActionStateToPb(state domain.ActionState) action_pb.ActionState {
switch state {
case domain.ActionStateActive:
return action_pb.ActionState_ACTION_STATE_ACTIVE
case domain.ActionStateInactive:
return action_pb.ActionState_ACTION_STATE_INACTIVE
default:
return action_pb.ActionState_ACTION_STATE_UNSPECIFIED
}
}
func ActionNameQuery(q *action_pb.ActionNameQuery) (query.SearchQuery, error) {
return query.NewActionNameSearchQuery(object_grpc.TextMethodToQuery(q.Method), q.Name)
}
func ActionStateQuery(q *action_pb.ActionStateQuery) (query.SearchQuery, error) {
return query.NewActionStateSearchQuery(ActionStateToDomain(q.State))
}
func ActionIDQuery(q *action_pb.ActionIDQuery) (query.SearchQuery, error) {
return query.NewActionIDSearchQuery(q.Id)
}
func ActionStateToDomain(state action_pb.ActionState) domain.ActionState {
switch state {
case action_pb.ActionState_ACTION_STATE_ACTIVE:
return domain.ActionStateActive
case action_pb.ActionState_ACTION_STATE_INACTIVE:
return domain.ActionStateInactive
default:
return domain.ActionStateUnspecified
}
}

View File

@@ -0,0 +1,92 @@
package action
import (
"context"
"connectrpc.com/connect"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/repository/execution"
"github.com/zitadel/zitadel/internal/zerrors"
action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta"
)
func (s *Server) SetExecution(ctx context.Context, req *connect.Request[action.SetExecutionRequest]) (*connect.Response[action.SetExecutionResponse], error) {
reqTargets := req.Msg.GetTargets()
targets := make([]*execution.Target, len(reqTargets))
for i, target := range reqTargets {
targets[i] = &execution.Target{Type: domain.ExecutionTargetTypeTarget, Target: target}
}
set := &command.SetExecution{
Targets: targets,
}
var err error
var details *domain.ObjectDetails
instanceID := authz.GetInstance(ctx).InstanceID()
switch t := req.Msg.GetCondition().GetConditionType().(type) {
case *action.Condition_Request:
cond := executionConditionFromRequest(t.Request)
details, err = s.command.SetExecutionRequest(ctx, cond, set, instanceID)
case *action.Condition_Response:
cond := executionConditionFromResponse(t.Response)
details, err = s.command.SetExecutionResponse(ctx, cond, set, instanceID)
case *action.Condition_Event:
cond := executionConditionFromEvent(t.Event)
details, err = s.command.SetExecutionEvent(ctx, cond, set, instanceID)
case *action.Condition_Function:
details, err = s.command.SetExecutionFunction(ctx, command.ExecutionFunctionCondition(t.Function.GetName()), set, instanceID)
default:
err = zerrors.ThrowInvalidArgument(nil, "ACTION-5r5Ju", "Errors.Execution.ConditionInvalid")
}
if err != nil {
return nil, err
}
return connect.NewResponse(&action.SetExecutionResponse{
SetDate: timestamppb.New(details.EventDate),
}), nil
}
func (s *Server) ListExecutionFunctions(ctx context.Context, _ *connect.Request[action.ListExecutionFunctionsRequest]) (*connect.Response[action.ListExecutionFunctionsResponse], error) {
return connect.NewResponse(&action.ListExecutionFunctionsResponse{
Functions: s.ListActionFunctions(),
}), nil
}
func (s *Server) ListExecutionMethods(ctx context.Context, _ *connect.Request[action.ListExecutionMethodsRequest]) (*connect.Response[action.ListExecutionMethodsResponse], error) {
return connect.NewResponse(&action.ListExecutionMethodsResponse{
Methods: s.ListGRPCMethods(),
}), nil
}
func (s *Server) ListExecutionServices(ctx context.Context, _ *connect.Request[action.ListExecutionServicesRequest]) (*connect.Response[action.ListExecutionServicesResponse], error) {
return connect.NewResponse(&action.ListExecutionServicesResponse{
Services: s.ListGRPCServices(),
}), nil
}
func executionConditionFromRequest(request *action.RequestExecution) *command.ExecutionAPICondition {
return &command.ExecutionAPICondition{
Method: request.GetMethod(),
Service: request.GetService(),
All: request.GetAll(),
}
}
func executionConditionFromResponse(response *action.ResponseExecution) *command.ExecutionAPICondition {
return &command.ExecutionAPICondition{
Method: response.GetMethod(),
Service: response.GetService(),
All: response.GetAll(),
}
}
func executionConditionFromEvent(event *action.EventExecution) *command.ExecutionEventCondition {
return &command.ExecutionEventCondition{
Event: event.GetEvent(),
Group: event.GetGroup(),
All: event.GetAll(),
}
}

View File

@@ -0,0 +1,565 @@
//go:build integration
package action_test
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/integration"
action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta"
)
func TestServer_SetExecution_Request(t *testing.T) {
instance := integration.NewInstance(CTX)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
ctx context.Context
req *action.SetExecutionRequest
wantSetDate bool
wantErr bool
}{
{
name: "missing permission",
ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner),
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_All{All: true},
},
},
},
},
wantErr: true,
},
{
name: "no condition, error",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{},
},
},
Targets: []string{targetResp.GetId()},
},
wantErr: true,
},
{
name: "method, not existing",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2beta.NotExistingService/List",
},
},
},
},
Targets: []string{targetResp.GetId()},
},
wantErr: true,
},
{
name: "method, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2beta.SessionService/ListSessions",
},
},
},
},
Targets: []string{targetResp.GetId()},
},
wantSetDate: true,
},
{
name: "service, not existing",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Service{
Service: "NotExistingService",
},
},
},
},
Targets: []string{targetResp.GetId()},
},
wantErr: true,
},
{
name: "service, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Service{
Service: "zitadel.session.v2beta.SessionService",
},
},
},
},
Targets: []string{targetResp.GetId()},
},
wantSetDate: true,
},
{
name: "all, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_All{
All: true,
},
},
},
},
Targets: []string{targetResp.GetId()},
},
wantSetDate: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// We want to have the same response no matter how often we call the function
creationDate := time.Now().UTC()
got, err := instance.Client.ActionV2beta.SetExecution(tt.ctx, tt.req)
setDate := time.Now().UTC()
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assertSetExecutionResponse(t, creationDate, setDate, tt.wantSetDate, got)
// cleanup to not impact other requests
instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition())
})
}
}
func assertSetExecutionResponse(t *testing.T, creationDate, setDate time.Time, expectedSetDate bool, actualResp *action.SetExecutionResponse) {
if expectedSetDate {
if !setDate.IsZero() {
assert.WithinRange(t, actualResp.GetSetDate().AsTime(), creationDate, setDate)
} else {
assert.WithinRange(t, actualResp.GetSetDate().AsTime(), creationDate, time.Now().UTC())
}
} else {
assert.Nil(t, actualResp.SetDate)
}
}
func TestServer_SetExecution_Response(t *testing.T) {
instance := integration.NewInstance(CTX)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
ctx context.Context
req *action.SetExecutionRequest
wantSetDate bool
wantErr bool
}{
{
name: "missing permission",
ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner),
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_All{All: true},
},
},
},
},
wantErr: true,
},
{
name: "no condition, error",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{},
},
},
Targets: []string{targetResp.GetId()},
},
wantErr: true,
},
{
name: "method, not existing",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_Method{
Method: "/zitadel.session.v2beta.NotExistingService/List",
},
},
},
},
Targets: []string{targetResp.GetId()},
},
wantErr: true,
},
{
name: "method, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_Method{
Method: "/zitadel.session.v2beta.SessionService/ListSessions",
},
},
},
},
Targets: []string{targetResp.GetId()},
},
wantSetDate: true,
},
{
name: "service, not existing",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_Service{
Service: "NotExistingService",
},
},
},
},
Targets: []string{targetResp.GetId()},
},
wantErr: true,
},
{
name: "service, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_Service{
Service: "zitadel.session.v2beta.SessionService",
},
},
},
},
Targets: []string{targetResp.GetId()},
},
wantSetDate: true,
},
{
name: "all, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_All{
All: true,
},
},
},
},
Targets: []string{targetResp.GetId()},
},
wantSetDate: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
creationDate := time.Now().UTC()
got, err := instance.Client.ActionV2beta.SetExecution(tt.ctx, tt.req)
setDate := time.Now().UTC()
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assertSetExecutionResponse(t, creationDate, setDate, tt.wantSetDate, got)
// cleanup to not impact other requests
instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition())
})
}
}
func TestServer_SetExecution_Event(t *testing.T) {
instance := integration.NewInstance(CTX)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
ctx context.Context
req *action.SetExecutionRequest
wantSetDate bool
wantErr bool
}{
{
name: "missing permission",
ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner),
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{
Condition: &action.EventExecution_All{
All: true,
},
},
},
},
},
wantErr: true,
},
{
name: "no condition, error",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{},
},
},
Targets: []string{targetResp.GetId()},
},
wantErr: true,
},
{
name: "event, not existing",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{
Condition: &action.EventExecution_Event{
Event: "user.human.notexisting",
},
},
},
},
Targets: []string{targetResp.GetId()},
},
wantErr: true,
},
{
name: "event, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{
Condition: &action.EventExecution_Event{
Event: "user.human.added",
},
},
},
},
Targets: []string{targetResp.GetId()},
},
wantSetDate: true,
},
{
name: "group, not existing",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{
Condition: &action.EventExecution_Group{
Group: "user.notexisting",
},
},
},
},
Targets: []string{targetResp.GetId()},
},
wantErr: true,
},
{
name: "group, level 1, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{
Condition: &action.EventExecution_Group{
Group: "user",
},
},
},
},
Targets: []string{targetResp.GetId()},
},
wantSetDate: true,
},
{
name: "group, level 2, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{
Condition: &action.EventExecution_Group{
Group: "user.human",
},
},
},
},
Targets: []string{targetResp.GetId()},
},
wantSetDate: true,
},
{
name: "all, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{
Condition: &action.EventExecution_All{
All: true,
},
},
},
},
Targets: []string{targetResp.GetId()},
},
wantSetDate: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
creationDate := time.Now().UTC()
got, err := instance.Client.ActionV2beta.SetExecution(tt.ctx, tt.req)
setDate := time.Now().UTC()
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assertSetExecutionResponse(t, creationDate, setDate, tt.wantSetDate, got)
// cleanup to not impact other requests
instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition())
})
}
}
func TestServer_SetExecution_Function(t *testing.T) {
instance := integration.NewInstance(CTX)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
ctx context.Context
req *action.SetExecutionRequest
wantSetDate bool
wantErr bool
}{
{
name: "missing permission",
ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner),
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_All{All: true},
},
},
},
},
wantErr: true,
},
{
name: "no condition, error",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{},
},
},
Targets: []string{targetResp.GetId()},
},
wantErr: true,
},
{
name: "function, not existing",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Function{
Function: &action.FunctionExecution{Name: "xxx"},
},
},
Targets: []string{targetResp.GetId()},
},
wantErr: true,
},
{
name: "function, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Function{
Function: &action.FunctionExecution{Name: "presamlresponse"},
},
},
Targets: []string{targetResp.GetId()},
},
wantSetDate: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
creationDate := time.Now().UTC()
got, err := instance.Client.ActionV2beta.SetExecution(tt.ctx, tt.req)
setDate := time.Now().UTC()
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assertSetExecutionResponse(t, creationDate, setDate, tt.wantSetDate, got)
// cleanup to not impact other requests
instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition())
})
}
}

View File

@@ -0,0 +1,784 @@
//go:build integration
package action_test
import (
"context"
"testing"
"time"
"github.com/brianvoe/gofakeit/v6"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/durationpb"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/integration"
action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta"
filter "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta"
)
func TestServer_GetTarget(t *testing.T) {
instance := integration.NewInstance(CTX)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
type args struct {
ctx context.Context
dep func(context.Context, *action.GetTargetRequest, *action.GetTargetResponse) error
req *action.GetTargetRequest
}
tests := []struct {
name string
args args
want *action.GetTargetResponse
wantErr bool
}{
{
name: "missing permission",
args: args{
ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner),
req: &action.GetTargetRequest{},
},
wantErr: true,
},
{
name: "not found",
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.GetTargetRequest{Id: "notexisting"},
},
wantErr: true,
},
{
name: "get, ok",
args: args{
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error {
name := gofakeit.Name()
resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false)
request.Id = resp.GetId()
response.Target.Id = resp.GetId()
response.Target.Name = name
response.Target.CreationDate = resp.GetCreationDate()
response.Target.ChangeDate = resp.GetCreationDate()
response.Target.SigningKey = resp.GetSigningKey()
return nil
},
req: &action.GetTargetRequest{},
},
want: &action.GetTargetResponse{
Target: &action.Target{
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.RESTWebhook{},
},
Timeout: durationpb.New(5 * time.Second),
},
},
},
{
name: "get, async, ok",
args: args{
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error {
name := gofakeit.Name()
resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeAsync, false)
request.Id = resp.GetId()
response.Target.Id = resp.GetId()
response.Target.Name = name
response.Target.CreationDate = resp.GetCreationDate()
response.Target.ChangeDate = resp.GetCreationDate()
response.Target.SigningKey = resp.GetSigningKey()
return nil
},
req: &action.GetTargetRequest{},
},
want: &action.GetTargetResponse{
Target: &action.Target{
Endpoint: "https://example.com",
TargetType: &action.Target_RestAsync{
RestAsync: &action.RESTAsync{},
},
Timeout: durationpb.New(5 * time.Second),
},
},
},
{
name: "get, webhook interruptOnError, ok",
args: args{
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error {
name := gofakeit.Name()
resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, true)
request.Id = resp.GetId()
response.Target.Id = resp.GetId()
response.Target.Name = name
response.Target.CreationDate = resp.GetCreationDate()
response.Target.ChangeDate = resp.GetCreationDate()
response.Target.SigningKey = resp.GetSigningKey()
return nil
},
req: &action.GetTargetRequest{},
},
want: &action.GetTargetResponse{
Target: &action.Target{
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.RESTWebhook{
InterruptOnError: true,
},
},
Timeout: durationpb.New(5 * time.Second),
},
},
},
{
name: "get, call, ok",
args: args{
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error {
name := gofakeit.Name()
resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeCall, false)
request.Id = resp.GetId()
response.Target.Id = resp.GetId()
response.Target.Name = name
response.Target.CreationDate = resp.GetCreationDate()
response.Target.ChangeDate = resp.GetCreationDate()
response.Target.SigningKey = resp.GetSigningKey()
return nil
},
req: &action.GetTargetRequest{},
},
want: &action.GetTargetResponse{
Target: &action.Target{
Endpoint: "https://example.com",
TargetType: &action.Target_RestCall{
RestCall: &action.RESTCall{
InterruptOnError: false,
},
},
Timeout: durationpb.New(5 * time.Second),
},
},
},
{
name: "get, call interruptOnError, ok",
args: args{
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error {
name := gofakeit.Name()
resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeCall, true)
request.Id = resp.GetId()
response.Target.Id = resp.GetId()
response.Target.Name = name
response.Target.CreationDate = resp.GetCreationDate()
response.Target.ChangeDate = resp.GetCreationDate()
response.Target.SigningKey = resp.GetSigningKey()
return nil
},
req: &action.GetTargetRequest{},
},
want: &action.GetTargetResponse{
Target: &action.Target{
Endpoint: "https://example.com",
TargetType: &action.Target_RestCall{
RestCall: &action.RESTCall{
InterruptOnError: true,
},
},
Timeout: durationpb.New(5 * time.Second),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.dep != nil {
err := tt.args.dep(tt.args.ctx, tt.args.req, tt.want)
require.NoError(t, err)
}
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(isolatedIAMOwnerCTX, 2*time.Minute)
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
got, err := instance.Client.ActionV2beta.GetTarget(tt.args.ctx, tt.args.req)
if tt.wantErr {
assert.Error(ttt, err, "Error: "+err.Error())
return
}
assert.NoError(ttt, err)
assert.EqualExportedValues(ttt, tt.want, got)
}, retryDuration, tick, "timeout waiting for expected target Executions")
})
}
}
func TestServer_ListTargets(t *testing.T) {
instance := integration.NewInstance(CTX)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
type args struct {
ctx context.Context
dep func(context.Context, *action.ListTargetsRequest, *action.ListTargetsResponse)
req *action.ListTargetsRequest
}
tests := []struct {
name string
args args
want *action.ListTargetsResponse
wantErr bool
}{
{
name: "missing permission",
args: args{
ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner),
req: &action.ListTargetsRequest{},
},
wantErr: true,
},
{
name: "list, not found",
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.ListTargetsRequest{
Filters: []*action.TargetSearchFilter{
{Filter: &action.TargetSearchFilter_InTargetIdsFilter{
InTargetIdsFilter: &action.InTargetIDsFilter{
TargetIds: []string{"notfound"},
},
},
},
},
},
},
want: &action.ListTargetsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 0,
AppliedLimit: 100,
},
Targets: []*action.Target{},
},
},
{
name: "list single id",
args: args{
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.ListTargetsRequest, response *action.ListTargetsResponse) {
name := gofakeit.Name()
resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false)
request.Filters[0].Filter = &action.TargetSearchFilter_InTargetIdsFilter{
InTargetIdsFilter: &action.InTargetIDsFilter{
TargetIds: []string{resp.GetId()},
},
}
response.Targets[0].Id = resp.GetId()
response.Targets[0].Name = name
response.Targets[0].CreationDate = resp.GetCreationDate()
response.Targets[0].ChangeDate = resp.GetCreationDate()
response.Targets[0].SigningKey = resp.GetSigningKey()
},
req: &action.ListTargetsRequest{
Filters: []*action.TargetSearchFilter{{}},
},
},
want: &action.ListTargetsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 1,
AppliedLimit: 100,
},
Targets: []*action.Target{
{
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.RESTWebhook{
InterruptOnError: false,
},
},
Timeout: durationpb.New(5 * time.Second),
},
},
},
}, {
name: "list single name",
args: args{
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.ListTargetsRequest, response *action.ListTargetsResponse) {
name := gofakeit.Name()
resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false)
request.Filters[0].Filter = &action.TargetSearchFilter_TargetNameFilter{
TargetNameFilter: &action.TargetNameFilter{
TargetName: name,
},
}
response.Targets[0].Id = resp.GetId()
response.Targets[0].Name = name
response.Targets[0].CreationDate = resp.GetCreationDate()
response.Targets[0].ChangeDate = resp.GetCreationDate()
response.Targets[0].SigningKey = resp.GetSigningKey()
},
req: &action.ListTargetsRequest{
Filters: []*action.TargetSearchFilter{{}},
},
},
want: &action.ListTargetsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 1,
AppliedLimit: 100,
},
Targets: []*action.Target{
{
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.RESTWebhook{
InterruptOnError: false,
},
},
Timeout: durationpb.New(5 * time.Second),
},
},
},
},
{
name: "list multiple id",
args: args{
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.ListTargetsRequest, response *action.ListTargetsResponse) {
name1 := gofakeit.Name()
name2 := gofakeit.Name()
name3 := gofakeit.Name()
resp1 := instance.CreateTarget(ctx, t, name1, "https://example.com", domain.TargetTypeWebhook, false)
resp2 := instance.CreateTarget(ctx, t, name2, "https://example.com", domain.TargetTypeCall, true)
resp3 := instance.CreateTarget(ctx, t, name3, "https://example.com", domain.TargetTypeAsync, false)
request.Filters[0].Filter = &action.TargetSearchFilter_InTargetIdsFilter{
InTargetIdsFilter: &action.InTargetIDsFilter{
TargetIds: []string{resp1.GetId(), resp2.GetId(), resp3.GetId()},
},
}
response.Targets[2].Id = resp1.GetId()
response.Targets[2].Name = name1
response.Targets[2].CreationDate = resp1.GetCreationDate()
response.Targets[2].ChangeDate = resp1.GetCreationDate()
response.Targets[2].SigningKey = resp1.GetSigningKey()
response.Targets[1].Id = resp2.GetId()
response.Targets[1].Name = name2
response.Targets[1].CreationDate = resp2.GetCreationDate()
response.Targets[1].ChangeDate = resp2.GetCreationDate()
response.Targets[1].SigningKey = resp2.GetSigningKey()
response.Targets[0].Id = resp3.GetId()
response.Targets[0].Name = name3
response.Targets[0].CreationDate = resp3.GetCreationDate()
response.Targets[0].ChangeDate = resp3.GetCreationDate()
response.Targets[0].SigningKey = resp3.GetSigningKey()
},
req: &action.ListTargetsRequest{
Filters: []*action.TargetSearchFilter{{}},
},
},
want: &action.ListTargetsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 3,
AppliedLimit: 100,
},
Targets: []*action.Target{
{
Endpoint: "https://example.com",
TargetType: &action.Target_RestAsync{
RestAsync: &action.RESTAsync{},
},
Timeout: durationpb.New(5 * time.Second),
},
{
Endpoint: "https://example.com",
TargetType: &action.Target_RestCall{
RestCall: &action.RESTCall{
InterruptOnError: true,
},
},
Timeout: durationpb.New(5 * time.Second),
},
{
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.RESTWebhook{
InterruptOnError: false,
},
},
Timeout: durationpb.New(5 * time.Second),
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.dep != nil {
tt.args.dep(tt.args.ctx, tt.args.req, tt.want)
}
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(isolatedIAMOwnerCTX, time.Minute)
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
got, listErr := instance.Client.ActionV2beta.ListTargets(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(ttt, listErr, "Error: "+listErr.Error())
return
}
require.NoError(ttt, listErr)
// always first check length, otherwise its failed anyway
if assert.Len(ttt, got.Targets, len(tt.want.Targets)) {
for i := range tt.want.Targets {
assert.EqualExportedValues(ttt, tt.want.Targets[i], got.Targets[i])
}
}
assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination)
}, retryDuration, tick, "timeout waiting for expected execution Executions")
})
}
}
func assertPaginationResponse(t *assert.CollectT, expected *filter.PaginationResponse, actual *filter.PaginationResponse) {
assert.Equal(t, expected.AppliedLimit, actual.AppliedLimit)
assert.Equal(t, expected.TotalResult, actual.TotalResult)
}
func TestServer_ListExecutions(t *testing.T) {
instance := integration.NewInstance(CTX)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false)
type args struct {
ctx context.Context
dep func(context.Context, *action.ListExecutionsRequest, *action.ListExecutionsResponse)
req *action.ListExecutionsRequest
}
tests := []struct {
name string
args args
want *action.ListExecutionsResponse
wantErr bool
}{
{
name: "missing permission",
args: args{
ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner),
req: &action.ListExecutionsRequest{},
},
wantErr: true,
},
{
name: "list request single condition",
args: args{
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) {
cond := request.Filters[0].GetInConditionsFilter().GetConditions()[0]
resp := instance.SetExecution(ctx, t, cond, []string{targetResp.GetId()})
// Set expected response with used values for SetExecution
response.Executions[0].CreationDate = resp.GetSetDate()
response.Executions[0].ChangeDate = resp.GetSetDate()
response.Executions[0].Condition = cond
},
req: &action.ListExecutionsRequest{
Filters: []*action.ExecutionSearchFilter{{
Filter: &action.ExecutionSearchFilter_InConditionsFilter{
InConditionsFilter: &action.InConditionsFilter{
Conditions: []*action.Condition{{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2.SessionService/GetSession",
},
},
},
}},
},
},
}},
},
},
want: &action.ListExecutionsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 1,
AppliedLimit: 100,
},
Executions: []*action.Execution{
{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2.SessionService/GetSession",
},
},
},
},
Targets: []string{targetResp.GetId()},
},
},
},
},
{
name: "list request single target",
args: args{
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) {
target := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false)
// add target as Filter to the request
request.Filters[0] = &action.ExecutionSearchFilter{
Filter: &action.ExecutionSearchFilter_TargetFilter{
TargetFilter: &action.TargetFilter{
TargetId: target.GetId(),
},
},
}
cond := &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.management.v1.ManagementService/UpdateAction",
},
},
},
}
resp := instance.SetExecution(ctx, t, cond, []string{target.GetId()})
response.Executions[0].CreationDate = resp.GetSetDate()
response.Executions[0].ChangeDate = resp.GetSetDate()
response.Executions[0].Condition = cond
response.Executions[0].Targets = []string{target.GetId()}
},
req: &action.ListExecutionsRequest{
Filters: []*action.ExecutionSearchFilter{{}},
},
},
want: &action.ListExecutionsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 1,
AppliedLimit: 100,
},
Executions: []*action.Execution{
{
Condition: &action.Condition{},
Targets: []string{""},
},
},
},
},
{
name: "list multiple conditions",
args: args{
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) {
request.Filters[0] = &action.ExecutionSearchFilter{
Filter: &action.ExecutionSearchFilter_InConditionsFilter{
InConditionsFilter: &action.InConditionsFilter{
Conditions: []*action.Condition{
{ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2.SessionService/GetSession",
},
},
}},
{ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2.SessionService/CreateSession",
},
},
}},
{ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2.SessionService/SetSession",
},
},
}},
},
},
},
}
cond1 := request.Filters[0].GetInConditionsFilter().GetConditions()[0]
resp1 := instance.SetExecution(ctx, t, cond1, []string{targetResp.GetId()})
response.Executions[2] = &action.Execution{
CreationDate: resp1.GetSetDate(),
ChangeDate: resp1.GetSetDate(),
Condition: cond1,
Targets: []string{targetResp.GetId()},
}
cond2 := request.Filters[0].GetInConditionsFilter().GetConditions()[1]
resp2 := instance.SetExecution(ctx, t, cond2, []string{targetResp.GetId()})
response.Executions[1] = &action.Execution{
CreationDate: resp2.GetSetDate(),
ChangeDate: resp2.GetSetDate(),
Condition: cond2,
Targets: []string{targetResp.GetId()},
}
cond3 := request.Filters[0].GetInConditionsFilter().GetConditions()[2]
resp3 := instance.SetExecution(ctx, t, cond3, []string{targetResp.GetId()})
response.Executions[0] = &action.Execution{
CreationDate: resp3.GetSetDate(),
ChangeDate: resp3.GetSetDate(),
Condition: cond3,
Targets: []string{targetResp.GetId()},
}
},
req: &action.ListExecutionsRequest{
Filters: []*action.ExecutionSearchFilter{
{},
},
},
},
want: &action.ListExecutionsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 3,
AppliedLimit: 100,
},
Executions: []*action.Execution{
{}, {}, {},
},
},
},
{
name: "list multiple conditions all types",
args: args{
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) {
conditions := request.Filters[0].GetInConditionsFilter().GetConditions()
for i, cond := range conditions {
resp := instance.SetExecution(ctx, t, cond, []string{targetResp.GetId()})
response.Executions[(len(conditions)-1)-i] = &action.Execution{
CreationDate: resp.GetSetDate(),
ChangeDate: resp.GetSetDate(),
Condition: cond,
Targets: []string{targetResp.GetId()},
}
}
},
req: &action.ListExecutionsRequest{
Filters: []*action.ExecutionSearchFilter{{
Filter: &action.ExecutionSearchFilter_InConditionsFilter{
InConditionsFilter: &action.InConditionsFilter{
Conditions: []*action.Condition{
{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Method{Method: "/zitadel.session.v2.SessionService/GetSession"}}}},
{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Service{Service: "zitadel.session.v2.SessionService"}}}},
{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_All{All: true}}}},
{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Method{Method: "/zitadel.session.v2.SessionService/GetSession"}}}},
{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Service{Service: "zitadel.session.v2.SessionService"}}}},
{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_All{All: true}}}},
{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Event{Event: "user.added"}}}},
{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Group{Group: "user"}}}},
{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_All{All: true}}}},
{ConditionType: &action.Condition_Function{Function: &action.FunctionExecution{Name: "presamlresponse"}}},
},
},
},
}},
},
},
want: &action.ListExecutionsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 10,
AppliedLimit: 100,
},
Executions: []*action.Execution{
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
},
},
},
{
name: "list multiple conditions all types, sort id",
args: args{
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) {
conditions := request.Filters[0].GetInConditionsFilter().GetConditions()
for i, cond := range conditions {
resp := instance.SetExecution(ctx, t, cond, []string{targetResp.GetId()})
response.Executions[i] = &action.Execution{
CreationDate: resp.GetSetDate(),
ChangeDate: resp.GetSetDate(),
Condition: cond,
Targets: []string{targetResp.GetId()},
}
}
},
req: &action.ListExecutionsRequest{
SortingColumn: gu.Ptr(action.ExecutionFieldName_EXECUTION_FIELD_NAME_ID),
Filters: []*action.ExecutionSearchFilter{{
Filter: &action.ExecutionSearchFilter_InConditionsFilter{
InConditionsFilter: &action.InConditionsFilter{
Conditions: []*action.Condition{
{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Method{Method: "/zitadel.session.v2.SessionService/GetSession"}}}},
{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Service{Service: "zitadel.session.v2.SessionService"}}}},
{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_All{All: true}}}},
{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Method{Method: "/zitadel.session.v2.SessionService/GetSession"}}}},
{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Service{Service: "zitadel.session.v2.SessionService"}}}},
{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_All{All: true}}}},
{ConditionType: &action.Condition_Function{Function: &action.FunctionExecution{Name: "presamlresponse"}}},
{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Event{Event: "user.added"}}}},
{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Group{Group: "user"}}}},
{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_All{All: true}}}},
},
},
},
}},
},
},
want: &action.ListExecutionsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 10,
AppliedLimit: 100,
},
Executions: []*action.Execution{
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.dep != nil {
tt.args.dep(tt.args.ctx, tt.args.req, tt.want)
}
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(isolatedIAMOwnerCTX, time.Minute)
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
got, listErr := instance.Client.ActionV2beta.ListExecutions(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(ttt, listErr, "Error: "+listErr.Error())
return
}
require.NoError(ttt, listErr)
// always first check length, otherwise its failed anyway
if assert.Len(ttt, got.Executions, len(tt.want.Executions)) {
assert.EqualExportedValues(ttt, got.Executions, tt.want.Executions)
}
assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination)
}, retryDuration, tick, "timeout waiting for expected execution Executions")
})
}
}

View File

@@ -0,0 +1,23 @@
//go:build integration
package action_test
import (
"context"
"os"
"testing"
"time"
)
var (
CTX context.Context
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
defer cancel()
CTX = ctx
return m.Run()
}())
}

View File

@@ -0,0 +1,550 @@
//go:build integration
package action_test
import (
"context"
"testing"
"time"
"github.com/brianvoe/gofakeit/v6"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/types/known/durationpb"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/integration"
action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta"
)
func TestServer_CreateTarget(t *testing.T) {
instance := integration.NewInstance(CTX)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
type want struct {
id bool
creationDate bool
signingKey bool
}
alreadyExistingTargetName := gofakeit.AppName()
instance.CreateTarget(isolatedIAMOwnerCTX, t, alreadyExistingTargetName, "https://example.com", domain.TargetTypeAsync, false)
tests := []struct {
name string
ctx context.Context
req *action.CreateTargetRequest
want
wantErr bool
}{
{
name: "missing permission",
ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner),
req: &action.CreateTargetRequest{
Name: gofakeit.Name(),
},
wantErr: true,
},
{
name: "empty name",
ctx: isolatedIAMOwnerCTX,
req: &action.CreateTargetRequest{
Name: "",
},
wantErr: true,
},
{
name: "empty type",
ctx: isolatedIAMOwnerCTX,
req: &action.CreateTargetRequest{
Name: gofakeit.Name(),
TargetType: nil,
},
wantErr: true,
},
{
name: "empty webhook url",
ctx: isolatedIAMOwnerCTX,
req: &action.CreateTargetRequest{
Name: gofakeit.Name(),
TargetType: &action.CreateTargetRequest_RestWebhook{
RestWebhook: &action.RESTWebhook{},
},
},
wantErr: true,
},
{
name: "empty request response url",
ctx: isolatedIAMOwnerCTX,
req: &action.CreateTargetRequest{
Name: gofakeit.Name(),
TargetType: &action.CreateTargetRequest_RestCall{
RestCall: &action.RESTCall{},
},
},
wantErr: true,
},
{
name: "empty timeout",
ctx: isolatedIAMOwnerCTX,
req: &action.CreateTargetRequest{
Name: gofakeit.Name(),
Endpoint: "https://example.com",
TargetType: &action.CreateTargetRequest_RestWebhook{
RestWebhook: &action.RESTWebhook{},
},
Timeout: nil,
},
wantErr: true,
},
{
name: "async, already existing, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.CreateTargetRequest{
Name: alreadyExistingTargetName,
Endpoint: "https://example.com",
TargetType: &action.CreateTargetRequest_RestAsync{
RestAsync: &action.RESTAsync{},
},
Timeout: durationpb.New(10 * time.Second),
},
wantErr: true,
},
{
name: "async, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.CreateTargetRequest{
Name: gofakeit.Name(),
Endpoint: "https://example.com",
TargetType: &action.CreateTargetRequest_RestAsync{
RestAsync: &action.RESTAsync{},
},
Timeout: durationpb.New(10 * time.Second),
},
want: want{
id: true,
creationDate: true,
signingKey: true,
},
},
{
name: "webhook, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.CreateTargetRequest{
Name: gofakeit.Name(),
Endpoint: "https://example.com",
TargetType: &action.CreateTargetRequest_RestWebhook{
RestWebhook: &action.RESTWebhook{
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
},
want: want{
id: true,
creationDate: true,
signingKey: true,
},
},
{
name: "webhook, interrupt on error, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.CreateTargetRequest{
Name: gofakeit.Name(),
Endpoint: "https://example.com",
TargetType: &action.CreateTargetRequest_RestWebhook{
RestWebhook: &action.RESTWebhook{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
},
want: want{
id: true,
creationDate: true,
signingKey: true,
},
},
{
name: "call, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.CreateTargetRequest{
Name: gofakeit.Name(),
Endpoint: "https://example.com",
TargetType: &action.CreateTargetRequest_RestCall{
RestCall: &action.RESTCall{
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
},
want: want{
id: true,
creationDate: true,
signingKey: true,
},
},
{
name: "call, interruptOnError, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.CreateTargetRequest{
Name: gofakeit.Name(),
Endpoint: "https://example.com",
TargetType: &action.CreateTargetRequest_RestCall{
RestCall: &action.RESTCall{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
},
want: want{
id: true,
creationDate: true,
signingKey: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
creationDate := time.Now().UTC()
got, err := instance.Client.ActionV2beta.CreateTarget(tt.ctx, tt.req)
changeDate := time.Now().UTC()
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assertCreateTargetResponse(t, creationDate, changeDate, tt.want.creationDate, tt.want.id, tt.want.signingKey, got)
})
}
}
func assertCreateTargetResponse(t *testing.T, creationDate, changeDate time.Time, expectedCreationDate, expectedID, expectedSigningKey bool, actualResp *action.CreateTargetResponse) {
if expectedCreationDate {
if !changeDate.IsZero() {
assert.WithinRange(t, actualResp.GetCreationDate().AsTime(), creationDate, changeDate)
} else {
assert.WithinRange(t, actualResp.GetCreationDate().AsTime(), creationDate, time.Now().UTC())
}
} else {
assert.Nil(t, actualResp.CreationDate)
}
if expectedID {
assert.NotEmpty(t, actualResp.GetId())
} else {
assert.Nil(t, actualResp.Id)
}
if expectedSigningKey {
assert.NotEmpty(t, actualResp.GetSigningKey())
} else {
assert.Nil(t, actualResp.SigningKey)
}
}
func TestServer_UpdateTarget(t *testing.T) {
instance := integration.NewInstance(CTX)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
type args struct {
ctx context.Context
req *action.UpdateTargetRequest
}
type want struct {
change bool
changeDate bool
signingKey bool
}
tests := []struct {
name string
prepare func(request *action.UpdateTargetRequest)
args args
want want
wantErr bool
}{
{
name: "missing permission",
prepare: func(request *action.UpdateTargetRequest) {
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId()
request.Id = targetID
},
args: args{
ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner),
req: &action.UpdateTargetRequest{
Name: gu.Ptr(gofakeit.Name()),
},
},
wantErr: true,
},
{
name: "not existing",
prepare: func(request *action.UpdateTargetRequest) {
request.Id = "notexisting"
return
},
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.UpdateTargetRequest{
Name: gu.Ptr(gofakeit.Name()),
},
},
wantErr: true,
},
{
name: "no change, ok",
prepare: func(request *action.UpdateTargetRequest) {
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId()
request.Id = targetID
},
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.UpdateTargetRequest{
Endpoint: gu.Ptr("https://example.com"),
},
},
want: want{
change: false,
changeDate: true,
signingKey: false,
},
},
{
name: "change name, ok",
prepare: func(request *action.UpdateTargetRequest) {
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId()
request.Id = targetID
},
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.UpdateTargetRequest{
Name: gu.Ptr(gofakeit.Name()),
},
},
want: want{
change: true,
changeDate: true,
signingKey: false,
},
},
{
name: "regenerate signingkey, ok",
prepare: func(request *action.UpdateTargetRequest) {
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId()
request.Id = targetID
},
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.UpdateTargetRequest{
ExpirationSigningKey: durationpb.New(0 * time.Second),
},
},
want: want{
change: true,
changeDate: true,
signingKey: true,
},
},
{
name: "change type, ok",
prepare: func(request *action.UpdateTargetRequest) {
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId()
request.Id = targetID
},
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.UpdateTargetRequest{
TargetType: &action.UpdateTargetRequest_RestCall{
RestCall: &action.RESTCall{
InterruptOnError: true,
},
},
},
},
want: want{
change: true,
changeDate: true,
signingKey: false,
},
},
{
name: "change url, ok",
prepare: func(request *action.UpdateTargetRequest) {
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId()
request.Id = targetID
},
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.UpdateTargetRequest{
Endpoint: gu.Ptr("https://example.com/hooks/new"),
},
},
want: want{
change: true,
changeDate: true,
signingKey: false,
},
},
{
name: "change timeout, ok",
prepare: func(request *action.UpdateTargetRequest) {
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId()
request.Id = targetID
},
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.UpdateTargetRequest{
Timeout: durationpb.New(20 * time.Second),
},
},
want: want{
change: true,
changeDate: true,
signingKey: false,
},
},
{
name: "change type async, ok",
prepare: func(request *action.UpdateTargetRequest) {
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeAsync, false).GetId()
request.Id = targetID
},
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.UpdateTargetRequest{
TargetType: &action.UpdateTargetRequest_RestAsync{
RestAsync: &action.RESTAsync{},
},
},
},
want: want{
change: true,
changeDate: true,
signingKey: false,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
creationDate := time.Now().UTC()
tt.prepare(tt.args.req)
got, err := instance.Client.ActionV2beta.UpdateTarget(tt.args.ctx, tt.args.req)
if tt.wantErr {
assert.Error(t, err)
return
}
changeDate := time.Time{}
if tt.want.change {
changeDate = time.Now().UTC()
}
assert.NoError(t, err)
assertUpdateTargetResponse(t, creationDate, changeDate, tt.want.changeDate, tt.want.signingKey, got)
})
}
}
func assertUpdateTargetResponse(t *testing.T, creationDate, changeDate time.Time, expectedChangeDate, expectedSigningKey bool, actualResp *action.UpdateTargetResponse) {
if expectedChangeDate {
if !changeDate.IsZero() {
assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, changeDate)
} else {
assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, time.Now().UTC())
}
} else {
assert.Nil(t, actualResp.ChangeDate)
}
if expectedSigningKey {
assert.NotEmpty(t, actualResp.GetSigningKey())
} else {
assert.Nil(t, actualResp.SigningKey)
}
}
func TestServer_DeleteTarget(t *testing.T) {
instance := integration.NewInstance(CTX)
iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
tests := []struct {
name string
ctx context.Context
prepare func(request *action.DeleteTargetRequest) (time.Time, time.Time)
req *action.DeleteTargetRequest
wantDeletionDate bool
wantErr bool
}{
{
name: "missing permission",
ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner),
req: &action.DeleteTargetRequest{
Id: "notexisting",
},
wantErr: true,
},
{
name: "empty id",
ctx: iamOwnerCtx,
req: &action.DeleteTargetRequest{
Id: "",
},
wantErr: true,
},
{
name: "delete target, not existing",
ctx: iamOwnerCtx,
req: &action.DeleteTargetRequest{
Id: "notexisting",
},
wantDeletionDate: false,
},
{
name: "delete target",
ctx: iamOwnerCtx,
prepare: func(request *action.DeleteTargetRequest) (time.Time, time.Time) {
creationDate := time.Now().UTC()
targetID := instance.CreateTarget(iamOwnerCtx, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId()
request.Id = targetID
return creationDate, time.Time{}
},
req: &action.DeleteTargetRequest{},
wantDeletionDate: true,
},
{
name: "delete target, already removed",
ctx: iamOwnerCtx,
prepare: func(request *action.DeleteTargetRequest) (time.Time, time.Time) {
creationDate := time.Now().UTC()
targetID := instance.CreateTarget(iamOwnerCtx, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId()
request.Id = targetID
instance.DeleteTarget(iamOwnerCtx, t, targetID)
return creationDate, time.Now().UTC()
},
req: &action.DeleteTargetRequest{},
wantDeletionDate: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var creationDate, deletionDate time.Time
if tt.prepare != nil {
creationDate, deletionDate = tt.prepare(tt.req)
}
got, err := instance.Client.ActionV2beta.DeleteTarget(tt.ctx, tt.req)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assertDeleteTargetResponse(t, creationDate, deletionDate, tt.wantDeletionDate, got)
})
}
}
func assertDeleteTargetResponse(t *testing.T, creationDate, deletionDate time.Time, expectedDeletionDate bool, actualResp *action.DeleteTargetResponse) {
if expectedDeletionDate {
if !deletionDate.IsZero() {
assert.WithinRange(t, actualResp.GetDeletionDate().AsTime(), creationDate, deletionDate)
} else {
assert.WithinRange(t, actualResp.GetDeletionDate().AsTime(), creationDate, time.Now().UTC())
}
} else {
assert.Nil(t, actualResp.DeletionDate)
}
}

View File

@@ -0,0 +1,404 @@
package action
import (
"context"
"strings"
"connectrpc.com/connect"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/timestamppb"
filter "github.com/zitadel/zitadel/internal/api/grpc/filter/v2beta"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta"
)
const (
conditionIDAllSegmentCount = 0
conditionIDRequestResponseServiceSegmentCount = 1
conditionIDRequestResponseMethodSegmentCount = 2
conditionIDEventGroupSegmentCount = 1
)
func (s *Server) GetTarget(ctx context.Context, req *connect.Request[action.GetTargetRequest]) (*connect.Response[action.GetTargetResponse], error) {
resp, err := s.query.GetTargetByID(ctx, req.Msg.GetId())
if err != nil {
return nil, err
}
return connect.NewResponse(&action.GetTargetResponse{
Target: targetToPb(resp),
}), nil
}
type InstanceContext interface {
GetInstanceId() string
GetInstanceDomain() string
}
type Context interface {
GetOwner() InstanceContext
}
func (s *Server) ListTargets(ctx context.Context, req *connect.Request[action.ListTargetsRequest]) (*connect.Response[action.ListTargetsResponse], error) {
queries, err := s.ListTargetsRequestToModel(req.Msg)
if err != nil {
return nil, err
}
resp, err := s.query.SearchTargets(ctx, queries)
if err != nil {
return nil, err
}
return connect.NewResponse(&action.ListTargetsResponse{
Targets: targetsToPb(resp.Targets),
Pagination: filter.QueryToPaginationPb(queries.SearchRequest, resp.SearchResponse),
}), nil
}
func (s *Server) ListExecutions(ctx context.Context, req *connect.Request[action.ListExecutionsRequest]) (*connect.Response[action.ListExecutionsResponse], error) {
queries, err := s.ListExecutionsRequestToModel(req.Msg)
if err != nil {
return nil, err
}
resp, err := s.query.SearchExecutions(ctx, queries)
if err != nil {
return nil, err
}
return connect.NewResponse(&action.ListExecutionsResponse{
Executions: executionsToPb(resp.Executions),
Pagination: filter.QueryToPaginationPb(queries.SearchRequest, resp.SearchResponse),
}), nil
}
func targetsToPb(targets []*query.Target) []*action.Target {
t := make([]*action.Target, len(targets))
for i, target := range targets {
t[i] = targetToPb(target)
}
return t
}
func targetToPb(t *query.Target) *action.Target {
target := &action.Target{
Id: t.ObjectDetails.ID,
Name: t.Name,
Timeout: durationpb.New(t.Timeout),
Endpoint: t.Endpoint,
SigningKey: t.SigningKey,
}
switch t.TargetType {
case domain.TargetTypeWebhook:
target.TargetType = &action.Target_RestWebhook{RestWebhook: &action.RESTWebhook{InterruptOnError: t.InterruptOnError}}
case domain.TargetTypeCall:
target.TargetType = &action.Target_RestCall{RestCall: &action.RESTCall{InterruptOnError: t.InterruptOnError}}
case domain.TargetTypeAsync:
target.TargetType = &action.Target_RestAsync{RestAsync: &action.RESTAsync{}}
default:
target.TargetType = nil
}
if !t.ObjectDetails.EventDate.IsZero() {
target.ChangeDate = timestamppb.New(t.ObjectDetails.EventDate)
}
if !t.ObjectDetails.CreationDate.IsZero() {
target.CreationDate = timestamppb.New(t.ObjectDetails.CreationDate)
}
return target
}
func (s *Server) ListTargetsRequestToModel(req *action.ListTargetsRequest) (*query.TargetSearchQueries, error) {
offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination)
if err != nil {
return nil, err
}
queries, err := targetQueriesToQuery(req.Filters)
if err != nil {
return nil, err
}
return &query.TargetSearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
SortingColumn: targetFieldNameToSortingColumn(req.SortingColumn),
},
Queries: queries,
}, nil
}
func targetQueriesToQuery(queries []*action.TargetSearchFilter) (_ []query.SearchQuery, err error) {
q := make([]query.SearchQuery, len(queries))
for i, qry := range queries {
q[i], err = targetQueryToQuery(qry)
if err != nil {
return nil, err
}
}
return q, nil
}
func targetQueryToQuery(filter *action.TargetSearchFilter) (query.SearchQuery, error) {
switch q := filter.Filter.(type) {
case *action.TargetSearchFilter_TargetNameFilter:
return targetNameQueryToQuery(q.TargetNameFilter)
case *action.TargetSearchFilter_InTargetIdsFilter:
return targetInTargetIdsQueryToQuery(q.InTargetIdsFilter)
default:
return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid")
}
}
func targetNameQueryToQuery(q *action.TargetNameFilter) (query.SearchQuery, error) {
return query.NewTargetNameSearchQuery(filter.TextMethodPbToQuery(q.Method), q.GetTargetName())
}
func targetInTargetIdsQueryToQuery(q *action.InTargetIDsFilter) (query.SearchQuery, error) {
return query.NewTargetInIDsSearchQuery(q.GetTargetIds())
}
// targetFieldNameToSortingColumn defaults to the creation date because this ensures deterministic pagination
func targetFieldNameToSortingColumn(field *action.TargetFieldName) query.Column {
if field == nil {
return query.TargetColumnCreationDate
}
switch *field {
case action.TargetFieldName_TARGET_FIELD_NAME_UNSPECIFIED:
return query.TargetColumnCreationDate
case action.TargetFieldName_TARGET_FIELD_NAME_ID:
return query.TargetColumnID
case action.TargetFieldName_TARGET_FIELD_NAME_CREATED_DATE:
return query.TargetColumnCreationDate
case action.TargetFieldName_TARGET_FIELD_NAME_CHANGED_DATE:
return query.TargetColumnChangeDate
case action.TargetFieldName_TARGET_FIELD_NAME_NAME:
return query.TargetColumnName
case action.TargetFieldName_TARGET_FIELD_NAME_TARGET_TYPE:
return query.TargetColumnTargetType
case action.TargetFieldName_TARGET_FIELD_NAME_URL:
return query.TargetColumnURL
case action.TargetFieldName_TARGET_FIELD_NAME_TIMEOUT:
return query.TargetColumnTimeout
case action.TargetFieldName_TARGET_FIELD_NAME_INTERRUPT_ON_ERROR:
return query.TargetColumnInterruptOnError
default:
return query.TargetColumnCreationDate
}
}
// executionFieldNameToSortingColumn defaults to the creation date because this ensures deterministic pagination
func executionFieldNameToSortingColumn(field *action.ExecutionFieldName) query.Column {
if field == nil {
return query.ExecutionColumnCreationDate
}
switch *field {
case action.ExecutionFieldName_EXECUTION_FIELD_NAME_UNSPECIFIED:
return query.ExecutionColumnCreationDate
case action.ExecutionFieldName_EXECUTION_FIELD_NAME_ID:
return query.ExecutionColumnID
case action.ExecutionFieldName_EXECUTION_FIELD_NAME_CREATED_DATE:
return query.ExecutionColumnCreationDate
case action.ExecutionFieldName_EXECUTION_FIELD_NAME_CHANGED_DATE:
return query.ExecutionColumnChangeDate
default:
return query.ExecutionColumnCreationDate
}
}
func (s *Server) ListExecutionsRequestToModel(req *action.ListExecutionsRequest) (*query.ExecutionSearchQueries, error) {
offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination)
if err != nil {
return nil, err
}
queries, err := executionQueriesToQuery(req.Filters)
if err != nil {
return nil, err
}
return &query.ExecutionSearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
SortingColumn: executionFieldNameToSortingColumn(req.SortingColumn),
},
Queries: queries,
}, nil
}
func executionQueriesToQuery(queries []*action.ExecutionSearchFilter) (_ []query.SearchQuery, err error) {
q := make([]query.SearchQuery, len(queries))
for i, query := range queries {
q[i], err = executionQueryToQuery(query)
if err != nil {
return nil, err
}
}
return q, nil
}
func executionQueryToQuery(searchQuery *action.ExecutionSearchFilter) (query.SearchQuery, error) {
switch q := searchQuery.Filter.(type) {
case *action.ExecutionSearchFilter_InConditionsFilter:
return inConditionsQueryToQuery(q.InConditionsFilter)
case *action.ExecutionSearchFilter_ExecutionTypeFilter:
return executionTypeToQuery(q.ExecutionTypeFilter)
case *action.ExecutionSearchFilter_TargetFilter:
return query.NewTargetSearchQuery(q.TargetFilter.GetTargetId())
default:
return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid")
}
}
func executionTypeToQuery(q *action.ExecutionTypeFilter) (query.SearchQuery, error) {
switch q.ExecutionType {
case action.ExecutionType_EXECUTION_TYPE_UNSPECIFIED:
return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeUnspecified)
case action.ExecutionType_EXECUTION_TYPE_REQUEST:
return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeRequest)
case action.ExecutionType_EXECUTION_TYPE_RESPONSE:
return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeResponse)
case action.ExecutionType_EXECUTION_TYPE_EVENT:
return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeEvent)
case action.ExecutionType_EXECUTION_TYPE_FUNCTION:
return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeFunction)
default:
return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeUnspecified)
}
}
func inConditionsQueryToQuery(q *action.InConditionsFilter) (query.SearchQuery, error) {
values := make([]string, len(q.GetConditions()))
for i, condition := range q.GetConditions() {
id, err := conditionToID(condition)
if err != nil {
return nil, err
}
values[i] = id
}
return query.NewExecutionInIDsSearchQuery(values)
}
func conditionToID(q *action.Condition) (string, error) {
switch t := q.GetConditionType().(type) {
case *action.Condition_Request:
cond := &command.ExecutionAPICondition{
Method: t.Request.GetMethod(),
Service: t.Request.GetService(),
All: t.Request.GetAll(),
}
return cond.ID(domain.ExecutionTypeRequest), nil
case *action.Condition_Response:
cond := &command.ExecutionAPICondition{
Method: t.Response.GetMethod(),
Service: t.Response.GetService(),
All: t.Response.GetAll(),
}
return cond.ID(domain.ExecutionTypeResponse), nil
case *action.Condition_Event:
cond := &command.ExecutionEventCondition{
Event: t.Event.GetEvent(),
Group: t.Event.GetGroup(),
All: t.Event.GetAll(),
}
return cond.ID(), nil
case *action.Condition_Function:
return command.ExecutionFunctionCondition(t.Function.GetName()).ID(), nil
default:
return "", zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid")
}
}
func executionsToPb(executions []*query.Execution) []*action.Execution {
e := make([]*action.Execution, len(executions))
for i, execution := range executions {
e[i] = executionToPb(execution)
}
return e
}
func executionToPb(e *query.Execution) *action.Execution {
targets := make([]string, len(e.Targets))
for i := range e.Targets {
switch e.Targets[i].Type {
case domain.ExecutionTargetTypeTarget:
targets[i] = e.Targets[i].Target
case domain.ExecutionTargetTypeInclude, domain.ExecutionTargetTypeUnspecified:
continue
default:
continue
}
}
exec := &action.Execution{
Condition: executionIDToCondition(e.ID),
Targets: targets,
}
if !e.ObjectDetails.EventDate.IsZero() {
exec.ChangeDate = timestamppb.New(e.ObjectDetails.EventDate)
}
if !e.ObjectDetails.CreationDate.IsZero() {
exec.CreationDate = timestamppb.New(e.ObjectDetails.CreationDate)
}
return exec
}
func executionIDToCondition(include string) *action.Condition {
if strings.HasPrefix(include, domain.ExecutionTypeRequest.String()) {
return includeRequestToCondition(strings.TrimPrefix(include, domain.ExecutionTypeRequest.String()))
}
if strings.HasPrefix(include, domain.ExecutionTypeResponse.String()) {
return includeResponseToCondition(strings.TrimPrefix(include, domain.ExecutionTypeResponse.String()))
}
if strings.HasPrefix(include, domain.ExecutionTypeEvent.String()) {
return includeEventToCondition(strings.TrimPrefix(include, domain.ExecutionTypeEvent.String()))
}
if strings.HasPrefix(include, domain.ExecutionTypeFunction.String()) {
return includeFunctionToCondition(strings.TrimPrefix(include, domain.ExecutionTypeFunction.String()))
}
return nil
}
func includeRequestToCondition(id string) *action.Condition {
switch strings.Count(id, "/") {
case conditionIDRequestResponseMethodSegmentCount:
return &action.Condition{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Method{Method: id}}}}
case conditionIDRequestResponseServiceSegmentCount:
return &action.Condition{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Service{Service: strings.TrimPrefix(id, "/")}}}}
case conditionIDAllSegmentCount:
return &action.Condition{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_All{All: true}}}}
default:
return nil
}
}
func includeResponseToCondition(id string) *action.Condition {
switch strings.Count(id, "/") {
case conditionIDRequestResponseMethodSegmentCount:
return &action.Condition{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Method{Method: id}}}}
case conditionIDRequestResponseServiceSegmentCount:
return &action.Condition{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Service{Service: strings.TrimPrefix(id, "/")}}}}
case conditionIDAllSegmentCount:
return &action.Condition{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_All{All: true}}}}
default:
return nil
}
}
func includeEventToCondition(id string) *action.Condition {
switch strings.Count(id, "/") {
case conditionIDEventGroupSegmentCount:
if strings.HasSuffix(id, command.EventGroupSuffix) {
return &action.Condition{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Group{Group: strings.TrimSuffix(strings.TrimPrefix(id, "/"), command.EventGroupSuffix)}}}}
} else {
return &action.Condition{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Event{Event: strings.TrimPrefix(id, "/")}}}}
}
case conditionIDAllSegmentCount:
return &action.Condition{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_All{All: true}}}}
default:
return nil
}
}
func includeFunctionToCondition(id string) *action.Condition {
return &action.Condition{ConditionType: &action.Condition_Function{Function: &action.FunctionExecution{Name: strings.TrimPrefix(id, "/")}}}
}

View File

@@ -0,0 +1,71 @@
package action
import (
"net/http"
"connectrpc.com/connect"
"google.golang.org/protobuf/reflect/protoreflect"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/server"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/config/systemdefaults"
"github.com/zitadel/zitadel/internal/query"
action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/action/v2beta/actionconnect"
)
var _ actionconnect.ActionServiceHandler = (*Server)(nil)
type Server struct {
systemDefaults systemdefaults.SystemDefaults
command *command.Commands
query *query.Queries
ListActionFunctions func() []string
ListGRPCMethods func() []string
ListGRPCServices func() []string
}
type Config struct{}
func CreateServer(
systemDefaults systemdefaults.SystemDefaults,
command *command.Commands,
query *query.Queries,
listActionFunctions func() []string,
listGRPCMethods func() []string,
listGRPCServices func() []string,
) *Server {
return &Server{
systemDefaults: systemDefaults,
command: command,
query: query,
ListActionFunctions: listActionFunctions,
ListGRPCMethods: listGRPCMethods,
ListGRPCServices: listGRPCServices,
}
}
func (s *Server) RegisterConnectServer(interceptors ...connect.Interceptor) (string, http.Handler) {
return actionconnect.NewActionServiceHandler(s, connect.WithInterceptors(interceptors...))
}
func (s *Server) FileDescriptor() protoreflect.FileDescriptor {
return action.File_zitadel_action_v2beta_action_service_proto
}
func (s *Server) AppName() string {
return action.ActionService_ServiceDesc.ServiceName
}
func (s *Server) MethodPrefix() string {
return action.ActionService_ServiceDesc.ServiceName
}
func (s *Server) AuthMethods() authz.MethodMapping {
return action.ActionService_AuthMethods
}
func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
return action.RegisterActionServiceHandler
}

View File

@@ -0,0 +1,126 @@
package action
import (
"context"
"connectrpc.com/connect"
"github.com/muhlemmer/gu"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta"
)
func (s *Server) CreateTarget(ctx context.Context, req *connect.Request[action.CreateTargetRequest]) (*connect.Response[action.CreateTargetResponse], error) {
add := createTargetToCommand(req.Msg)
instanceID := authz.GetInstance(ctx).InstanceID()
createdAt, err := s.command.AddTarget(ctx, add, instanceID)
if err != nil {
return nil, err
}
var creationDate *timestamppb.Timestamp
if !createdAt.IsZero() {
creationDate = timestamppb.New(createdAt)
}
return connect.NewResponse(&action.CreateTargetResponse{
Id: add.AggregateID,
CreationDate: creationDate,
SigningKey: add.SigningKey,
}), nil
}
func (s *Server) UpdateTarget(ctx context.Context, req *connect.Request[action.UpdateTargetRequest]) (*connect.Response[action.UpdateTargetResponse], error) {
instanceID := authz.GetInstance(ctx).InstanceID()
update := updateTargetToCommand(req.Msg)
changedAt, err := s.command.ChangeTarget(ctx, update, instanceID)
if err != nil {
return nil, err
}
var changeDate *timestamppb.Timestamp
if !changedAt.IsZero() {
changeDate = timestamppb.New(changedAt)
}
return connect.NewResponse(&action.UpdateTargetResponse{
ChangeDate: changeDate,
SigningKey: update.SigningKey,
}), nil
}
func (s *Server) DeleteTarget(ctx context.Context, req *connect.Request[action.DeleteTargetRequest]) (*connect.Response[action.DeleteTargetResponse], error) {
instanceID := authz.GetInstance(ctx).InstanceID()
deletedAt, err := s.command.DeleteTarget(ctx, req.Msg.GetId(), instanceID)
if err != nil {
return nil, err
}
var deletionDate *timestamppb.Timestamp
if !deletedAt.IsZero() {
deletionDate = timestamppb.New(deletedAt)
}
return connect.NewResponse(&action.DeleteTargetResponse{
DeletionDate: deletionDate,
}), nil
}
func createTargetToCommand(req *action.CreateTargetRequest) *command.AddTarget {
var (
targetType domain.TargetType
interruptOnError bool
)
switch t := req.GetTargetType().(type) {
case *action.CreateTargetRequest_RestWebhook:
targetType = domain.TargetTypeWebhook
interruptOnError = t.RestWebhook.InterruptOnError
case *action.CreateTargetRequest_RestCall:
targetType = domain.TargetTypeCall
interruptOnError = t.RestCall.InterruptOnError
case *action.CreateTargetRequest_RestAsync:
targetType = domain.TargetTypeAsync
}
return &command.AddTarget{
Name: req.GetName(),
TargetType: targetType,
Endpoint: req.GetEndpoint(),
Timeout: req.GetTimeout().AsDuration(),
InterruptOnError: interruptOnError,
}
}
func updateTargetToCommand(req *action.UpdateTargetRequest) *command.ChangeTarget {
expirationSigningKey := false
// TODO handle expiration, currently only immediate expiration is supported
if req.GetExpirationSigningKey() != nil {
expirationSigningKey = true
}
if req == nil {
return nil
}
target := &command.ChangeTarget{
ObjectRoot: models.ObjectRoot{
AggregateID: req.GetId(),
},
Name: req.Name,
Endpoint: req.Endpoint,
ExpirationSigningKey: expirationSigningKey,
}
if req.TargetType != nil {
switch t := req.GetTargetType().(type) {
case *action.UpdateTargetRequest_RestWebhook:
target.TargetType = gu.Ptr(domain.TargetTypeWebhook)
target.InterruptOnError = gu.Ptr(t.RestWebhook.InterruptOnError)
case *action.UpdateTargetRequest_RestCall:
target.TargetType = gu.Ptr(domain.TargetTypeCall)
target.InterruptOnError = gu.Ptr(t.RestCall.InterruptOnError)
case *action.UpdateTargetRequest_RestAsync:
target.TargetType = gu.Ptr(domain.TargetTypeAsync)
target.InterruptOnError = gu.Ptr(false)
}
}
if req.Timeout != nil {
target.Timeout = gu.Ptr(req.GetTimeout().AsDuration())
}
return target
}

View File

@@ -0,0 +1,229 @@
package action
import (
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/types/known/durationpb"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta"
)
func Test_createTargetToCommand(t *testing.T) {
type args struct {
req *action.CreateTargetRequest
}
tests := []struct {
name string
args args
want *command.AddTarget
}{
{
name: "nil",
args: args{nil},
want: &command.AddTarget{
Name: "",
Endpoint: "",
Timeout: 0,
InterruptOnError: false,
},
},
{
name: "all fields (webhook)",
args: args{&action.CreateTargetRequest{
Name: "target 1",
Endpoint: "https://example.com/hooks/1",
TargetType: &action.CreateTargetRequest_RestWebhook{
RestWebhook: &action.RESTWebhook{},
},
Timeout: durationpb.New(10 * time.Second),
}},
want: &command.AddTarget{
Name: "target 1",
TargetType: domain.TargetTypeWebhook,
Endpoint: "https://example.com/hooks/1",
Timeout: 10 * time.Second,
InterruptOnError: false,
},
},
{
name: "all fields (async)",
args: args{&action.CreateTargetRequest{
Name: "target 1",
Endpoint: "https://example.com/hooks/1",
TargetType: &action.CreateTargetRequest_RestAsync{
RestAsync: &action.RESTAsync{},
},
Timeout: durationpb.New(10 * time.Second),
}},
want: &command.AddTarget{
Name: "target 1",
TargetType: domain.TargetTypeAsync,
Endpoint: "https://example.com/hooks/1",
Timeout: 10 * time.Second,
InterruptOnError: false,
},
},
{
name: "all fields (interrupting response)",
args: args{&action.CreateTargetRequest{
Name: "target 1",
Endpoint: "https://example.com/hooks/1",
TargetType: &action.CreateTargetRequest_RestCall{
RestCall: &action.RESTCall{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
}},
want: &command.AddTarget{
Name: "target 1",
TargetType: domain.TargetTypeCall,
Endpoint: "https://example.com/hooks/1",
Timeout: 10 * time.Second,
InterruptOnError: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := createTargetToCommand(tt.args.req)
assert.Equal(t, tt.want, got)
})
}
}
func Test_updateTargetToCommand(t *testing.T) {
type args struct {
req *action.UpdateTargetRequest
}
tests := []struct {
name string
args args
want *command.ChangeTarget
}{
{
name: "nil",
args: args{nil},
want: nil,
},
{
name: "all fields nil",
args: args{&action.UpdateTargetRequest{
Name: nil,
TargetType: nil,
Timeout: nil,
}},
want: &command.ChangeTarget{
Name: nil,
TargetType: nil,
Endpoint: nil,
Timeout: nil,
InterruptOnError: nil,
},
},
{
name: "all fields empty",
args: args{&action.UpdateTargetRequest{
Name: gu.Ptr(""),
TargetType: nil,
Timeout: durationpb.New(0),
}},
want: &command.ChangeTarget{
Name: gu.Ptr(""),
TargetType: nil,
Endpoint: nil,
Timeout: gu.Ptr(0 * time.Second),
InterruptOnError: nil,
},
},
{
name: "all fields (webhook)",
args: args{&action.UpdateTargetRequest{
Name: gu.Ptr("target 1"),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
TargetType: &action.UpdateTargetRequest_RestWebhook{
RestWebhook: &action.RESTWebhook{
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
}},
want: &command.ChangeTarget{
Name: gu.Ptr("target 1"),
TargetType: gu.Ptr(domain.TargetTypeWebhook),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
Timeout: gu.Ptr(10 * time.Second),
InterruptOnError: gu.Ptr(false),
},
},
{
name: "all fields (webhook interrupt)",
args: args{&action.UpdateTargetRequest{
Name: gu.Ptr("target 1"),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
TargetType: &action.UpdateTargetRequest_RestWebhook{
RestWebhook: &action.RESTWebhook{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
}},
want: &command.ChangeTarget{
Name: gu.Ptr("target 1"),
TargetType: gu.Ptr(domain.TargetTypeWebhook),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
Timeout: gu.Ptr(10 * time.Second),
InterruptOnError: gu.Ptr(true),
},
},
{
name: "all fields (async)",
args: args{&action.UpdateTargetRequest{
Name: gu.Ptr("target 1"),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
TargetType: &action.UpdateTargetRequest_RestAsync{
RestAsync: &action.RESTAsync{},
},
Timeout: durationpb.New(10 * time.Second),
}},
want: &command.ChangeTarget{
Name: gu.Ptr("target 1"),
TargetType: gu.Ptr(domain.TargetTypeAsync),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
Timeout: gu.Ptr(10 * time.Second),
InterruptOnError: gu.Ptr(false),
},
},
{
name: "all fields (interrupting response)",
args: args{&action.UpdateTargetRequest{
Name: gu.Ptr("target 1"),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
TargetType: &action.UpdateTargetRequest_RestCall{
RestCall: &action.RESTCall{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
}},
want: &command.ChangeTarget{
Name: gu.Ptr("target 1"),
TargetType: gu.Ptr(domain.TargetTypeCall),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
Timeout: gu.Ptr(10 * time.Second),
InterruptOnError: gu.Ptr(true),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := updateTargetToCommand(tt.args.req)
assert.Equal(t, tt.want, got)
})
}
}

View File

@@ -0,0 +1,540 @@
package admin
import (
"context"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/object"
text_grpc "github.com/zitadel/zitadel/internal/api/grpc/text"
"github.com/zitadel/zitadel/internal/domain"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
)
func (s *Server) GetDefaultInitMessageText(ctx context.Context, req *admin_pb.GetDefaultInitMessageTextRequest) (*admin_pb.GetDefaultInitMessageTextResponse, error) {
msg, err := s.query.DefaultMessageTextByTypeAndLanguageFromFileSystem(ctx, domain.InitCodeMessageType, req.Language)
if err != nil {
return nil, err
}
return &admin_pb.GetDefaultInitMessageTextResponse{
CustomText: text_grpc.ModelCustomMessageTextToPb(msg),
}, nil
}
func (s *Server) GetCustomInitMessageText(ctx context.Context, req *admin_pb.GetCustomInitMessageTextRequest) (*admin_pb.GetCustomInitMessageTextResponse, error) {
msg, err := s.query.CustomMessageTextByTypeAndLanguage(ctx, authz.GetInstance(ctx).InstanceID(), domain.InitCodeMessageType, req.Language, false)
if err != nil {
return nil, err
}
return &admin_pb.GetCustomInitMessageTextResponse{
CustomText: text_grpc.ModelCustomMessageTextToPb(msg),
}, nil
}
func (s *Server) SetDefaultInitMessageText(ctx context.Context, req *admin_pb.SetDefaultInitMessageTextRequest) (*admin_pb.SetDefaultInitMessageTextResponse, error) {
result, err := s.command.SetDefaultMessageText(ctx, authz.GetInstance(ctx).InstanceID(), SetInitCustomTextToDomain(req))
if err != nil {
return nil, err
}
return &admin_pb.SetDefaultInitMessageTextResponse{
Details: object.ChangeToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) ResetCustomInitMessageTextToDefault(ctx context.Context, req *admin_pb.ResetCustomInitMessageTextToDefaultRequest) (*admin_pb.ResetCustomInitMessageTextToDefaultResponse, error) {
result, err := s.command.RemoveInstanceMessageTexts(ctx, domain.InitCodeMessageType, language.Make(req.Language))
if err != nil {
return nil, err
}
return &admin_pb.ResetCustomInitMessageTextToDefaultResponse{
Details: object.ChangeToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) GetDefaultPasswordResetMessageText(ctx context.Context, req *admin_pb.GetDefaultPasswordResetMessageTextRequest) (*admin_pb.GetDefaultPasswordResetMessageTextResponse, error) {
msg, err := s.query.DefaultMessageTextByTypeAndLanguageFromFileSystem(ctx, domain.PasswordResetMessageType, req.Language)
if err != nil {
return nil, err
}
return &admin_pb.GetDefaultPasswordResetMessageTextResponse{
CustomText: text_grpc.ModelCustomMessageTextToPb(msg),
}, nil
}
func (s *Server) GetCustomPasswordResetMessageText(ctx context.Context, req *admin_pb.GetCustomPasswordResetMessageTextRequest) (*admin_pb.GetCustomPasswordResetMessageTextResponse, error) {
msg, err := s.query.CustomMessageTextByTypeAndLanguage(ctx, authz.GetInstance(ctx).InstanceID(), domain.PasswordResetMessageType, req.Language, false)
if err != nil {
return nil, err
}
return &admin_pb.GetCustomPasswordResetMessageTextResponse{
CustomText: text_grpc.ModelCustomMessageTextToPb(msg),
}, nil
}
func (s *Server) SetDefaultPasswordResetMessageText(ctx context.Context, req *admin_pb.SetDefaultPasswordResetMessageTextRequest) (*admin_pb.SetDefaultPasswordResetMessageTextResponse, error) {
result, err := s.command.SetDefaultMessageText(ctx, authz.GetInstance(ctx).InstanceID(), SetPasswordResetCustomTextToDomain(req))
if err != nil {
return nil, err
}
return &admin_pb.SetDefaultPasswordResetMessageTextResponse{
Details: object.ChangeToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) ResetCustomPasswordResetMessageTextToDefault(ctx context.Context, req *admin_pb.ResetCustomPasswordResetMessageTextToDefaultRequest) (*admin_pb.ResetCustomPasswordResetMessageTextToDefaultResponse, error) {
result, err := s.command.RemoveInstanceMessageTexts(ctx, domain.PasswordResetMessageType, language.Make(req.Language))
if err != nil {
return nil, err
}
return &admin_pb.ResetCustomPasswordResetMessageTextToDefaultResponse{
Details: object.ChangeToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) GetDefaultVerifyEmailMessageText(ctx context.Context, req *admin_pb.GetDefaultVerifyEmailMessageTextRequest) (*admin_pb.GetDefaultVerifyEmailMessageTextResponse, error) {
msg, err := s.query.DefaultMessageTextByTypeAndLanguageFromFileSystem(ctx, domain.VerifyEmailMessageType, req.Language)
if err != nil {
return nil, err
}
return &admin_pb.GetDefaultVerifyEmailMessageTextResponse{
CustomText: text_grpc.ModelCustomMessageTextToPb(msg),
}, nil
}
func (s *Server) GetCustomVerifyEmailMessageText(ctx context.Context, req *admin_pb.GetCustomVerifyEmailMessageTextRequest) (*admin_pb.GetCustomVerifyEmailMessageTextResponse, error) {
msg, err := s.query.CustomMessageTextByTypeAndLanguage(ctx, authz.GetInstance(ctx).InstanceID(), domain.VerifyEmailMessageType, req.Language, false)
if err != nil {
return nil, err
}
return &admin_pb.GetCustomVerifyEmailMessageTextResponse{
CustomText: text_grpc.ModelCustomMessageTextToPb(msg),
}, nil
}
func (s *Server) SetDefaultVerifyEmailMessageText(ctx context.Context, req *admin_pb.SetDefaultVerifyEmailMessageTextRequest) (*admin_pb.SetDefaultVerifyEmailMessageTextResponse, error) {
result, err := s.command.SetDefaultMessageText(ctx, authz.GetInstance(ctx).InstanceID(), SetVerifyEmailCustomTextToDomain(req))
if err != nil {
return nil, err
}
return &admin_pb.SetDefaultVerifyEmailMessageTextResponse{
Details: object.ChangeToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) ResetCustomVerifyEmailMessageTextToDefault(ctx context.Context, req *admin_pb.ResetCustomVerifyEmailMessageTextToDefaultRequest) (*admin_pb.ResetCustomVerifyEmailMessageTextToDefaultResponse, error) {
result, err := s.command.RemoveInstanceMessageTexts(ctx, domain.VerifyEmailMessageType, language.Make(req.Language))
if err != nil {
return nil, err
}
return &admin_pb.ResetCustomVerifyEmailMessageTextToDefaultResponse{
Details: object.ChangeToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) GetDefaultVerifyPhoneMessageText(ctx context.Context, req *admin_pb.GetDefaultVerifyPhoneMessageTextRequest) (*admin_pb.GetDefaultVerifyPhoneMessageTextResponse, error) {
msg, err := s.query.DefaultMessageTextByTypeAndLanguageFromFileSystem(ctx, domain.VerifyPhoneMessageType, req.Language)
if err != nil {
return nil, err
}
return &admin_pb.GetDefaultVerifyPhoneMessageTextResponse{
CustomText: text_grpc.ModelCustomMessageTextToPb(msg),
}, nil
}
func (s *Server) GetCustomVerifyPhoneMessageText(ctx context.Context, req *admin_pb.GetCustomVerifyPhoneMessageTextRequest) (*admin_pb.GetCustomVerifyPhoneMessageTextResponse, error) {
msg, err := s.query.CustomMessageTextByTypeAndLanguage(ctx, authz.GetInstance(ctx).InstanceID(), domain.VerifyPhoneMessageType, req.Language, false)
if err != nil {
return nil, err
}
return &admin_pb.GetCustomVerifyPhoneMessageTextResponse{
CustomText: text_grpc.ModelCustomMessageTextToPb(msg),
}, nil
}
func (s *Server) SetDefaultVerifyPhoneMessageText(ctx context.Context, req *admin_pb.SetDefaultVerifyPhoneMessageTextRequest) (*admin_pb.SetDefaultVerifyPhoneMessageTextResponse, error) {
result, err := s.command.SetDefaultMessageText(ctx, authz.GetInstance(ctx).InstanceID(), SetVerifyPhoneCustomTextToDomain(req))
if err != nil {
return nil, err
}
return &admin_pb.SetDefaultVerifyPhoneMessageTextResponse{
Details: object.ChangeToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) ResetCustomVerifyPhoneMessageTextToDefault(ctx context.Context, req *admin_pb.ResetCustomVerifyPhoneMessageTextToDefaultRequest) (*admin_pb.ResetCustomVerifyPhoneMessageTextToDefaultResponse, error) {
result, err := s.command.RemoveInstanceMessageTexts(ctx, domain.VerifyPhoneMessageType, language.Make(req.Language))
if err != nil {
return nil, err
}
return &admin_pb.ResetCustomVerifyPhoneMessageTextToDefaultResponse{
Details: object.ChangeToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) GetDefaultVerifySMSOTPMessageText(ctx context.Context, req *admin_pb.GetDefaultVerifySMSOTPMessageTextRequest) (*admin_pb.GetDefaultVerifySMSOTPMessageTextResponse, error) {
msg, err := s.query.DefaultMessageTextByTypeAndLanguageFromFileSystem(ctx, domain.VerifySMSOTPMessageType, req.Language)
if err != nil {
return nil, err
}
return &admin_pb.GetDefaultVerifySMSOTPMessageTextResponse{
CustomText: text_grpc.ModelCustomMessageTextToPb(msg),
}, nil
}
func (s *Server) GetCustomVerifySMSOTPMessageText(ctx context.Context, req *admin_pb.GetCustomVerifySMSOTPMessageTextRequest) (*admin_pb.GetCustomVerifySMSOTPMessageTextResponse, error) {
msg, err := s.query.CustomMessageTextByTypeAndLanguage(ctx, authz.GetInstance(ctx).InstanceID(), domain.VerifySMSOTPMessageType, req.Language, false)
if err != nil {
return nil, err
}
return &admin_pb.GetCustomVerifySMSOTPMessageTextResponse{
CustomText: text_grpc.ModelCustomMessageTextToPb(msg),
}, nil
}
func (s *Server) SetDefaultVerifySMSOTPMessageText(ctx context.Context, req *admin_pb.SetDefaultVerifySMSOTPMessageTextRequest) (*admin_pb.SetDefaultVerifySMSOTPMessageTextResponse, error) {
result, err := s.command.SetDefaultMessageText(ctx, authz.GetInstance(ctx).InstanceID(), SetVerifySMSOTPCustomTextToDomain(req))
if err != nil {
return nil, err
}
return &admin_pb.SetDefaultVerifySMSOTPMessageTextResponse{
Details: object.ChangeToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) ResetCustomVerifySMSOTPMessageTextToDefault(ctx context.Context, req *admin_pb.ResetCustomVerifySMSOTPMessageTextToDefaultRequest) (*admin_pb.ResetCustomVerifySMSOTPMessageTextToDefaultResponse, error) {
result, err := s.command.RemoveInstanceMessageTexts(ctx, domain.VerifySMSOTPMessageType, language.Make(req.Language))
if err != nil {
return nil, err
}
return &admin_pb.ResetCustomVerifySMSOTPMessageTextToDefaultResponse{
Details: object.ChangeToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) GetDefaultVerifyEmailOTPMessageText(ctx context.Context, req *admin_pb.GetDefaultVerifyEmailOTPMessageTextRequest) (*admin_pb.GetDefaultVerifyEmailOTPMessageTextResponse, error) {
msg, err := s.query.DefaultMessageTextByTypeAndLanguageFromFileSystem(ctx, domain.VerifyEmailOTPMessageType, req.Language)
if err != nil {
return nil, err
}
return &admin_pb.GetDefaultVerifyEmailOTPMessageTextResponse{
CustomText: text_grpc.ModelCustomMessageTextToPb(msg),
}, nil
}
func (s *Server) GetCustomVerifyEmailOTPMessageText(ctx context.Context, req *admin_pb.GetCustomVerifyEmailOTPMessageTextRequest) (*admin_pb.GetCustomVerifyEmailOTPMessageTextResponse, error) {
msg, err := s.query.CustomMessageTextByTypeAndLanguage(ctx, authz.GetInstance(ctx).InstanceID(), domain.VerifyEmailOTPMessageType, req.Language, false)
if err != nil {
return nil, err
}
return &admin_pb.GetCustomVerifyEmailOTPMessageTextResponse{
CustomText: text_grpc.ModelCustomMessageTextToPb(msg),
}, nil
}
func (s *Server) SetDefaultVerifyEmailOTPMessageText(ctx context.Context, req *admin_pb.SetDefaultVerifyEmailOTPMessageTextRequest) (*admin_pb.SetDefaultVerifyEmailOTPMessageTextResponse, error) {
result, err := s.command.SetDefaultMessageText(ctx, authz.GetInstance(ctx).InstanceID(), SetVerifyEmailOTPCustomTextToDomain(req))
if err != nil {
return nil, err
}
return &admin_pb.SetDefaultVerifyEmailOTPMessageTextResponse{
Details: object.ChangeToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) ResetCustomVerifyEmailOTPMessageTextToDefault(ctx context.Context, req *admin_pb.ResetCustomVerifyEmailOTPMessageTextToDefaultRequest) (*admin_pb.ResetCustomVerifyEmailOTPMessageTextToDefaultResponse, error) {
result, err := s.command.RemoveInstanceMessageTexts(ctx, domain.VerifyEmailOTPMessageType, language.Make(req.Language))
if err != nil {
return nil, err
}
return &admin_pb.ResetCustomVerifyEmailOTPMessageTextToDefaultResponse{
Details: object.ChangeToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) GetDefaultDomainClaimedMessageText(ctx context.Context, req *admin_pb.GetDefaultDomainClaimedMessageTextRequest) (*admin_pb.GetDefaultDomainClaimedMessageTextResponse, error) {
msg, err := s.query.DefaultMessageTextByTypeAndLanguageFromFileSystem(ctx, domain.DomainClaimedMessageType, req.Language)
if err != nil {
return nil, err
}
return &admin_pb.GetDefaultDomainClaimedMessageTextResponse{
CustomText: text_grpc.ModelCustomMessageTextToPb(msg),
}, nil
}
func (s *Server) GetCustomDomainClaimedMessageText(ctx context.Context, req *admin_pb.GetCustomDomainClaimedMessageTextRequest) (*admin_pb.GetCustomDomainClaimedMessageTextResponse, error) {
msg, err := s.query.CustomMessageTextByTypeAndLanguage(ctx, authz.GetInstance(ctx).InstanceID(), domain.DomainClaimedMessageType, req.Language, false)
if err != nil {
return nil, err
}
return &admin_pb.GetCustomDomainClaimedMessageTextResponse{
CustomText: text_grpc.ModelCustomMessageTextToPb(msg),
}, nil
}
func (s *Server) SetDefaultDomainClaimedMessageText(ctx context.Context, req *admin_pb.SetDefaultDomainClaimedMessageTextRequest) (*admin_pb.SetDefaultDomainClaimedMessageTextResponse, error) {
result, err := s.command.SetDefaultMessageText(ctx, authz.GetInstance(ctx).InstanceID(), SetDomainClaimedCustomTextToDomain(req))
if err != nil {
return nil, err
}
return &admin_pb.SetDefaultDomainClaimedMessageTextResponse{
Details: object.ChangeToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) ResetCustomDomainClaimedMessageTextToDefault(ctx context.Context, req *admin_pb.ResetCustomDomainClaimedMessageTextToDefaultRequest) (*admin_pb.ResetCustomDomainClaimedMessageTextToDefaultResponse, error) {
result, err := s.command.RemoveInstanceMessageTexts(ctx, domain.DomainClaimedMessageType, language.Make(req.Language))
if err != nil {
return nil, err
}
return &admin_pb.ResetCustomDomainClaimedMessageTextToDefaultResponse{
Details: object.ChangeToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) GetDefaultPasswordChangeMessageText(ctx context.Context, req *admin_pb.GetDefaultPasswordChangeMessageTextRequest) (*admin_pb.GetDefaultPasswordChangeMessageTextResponse, error) {
msg, err := s.query.DefaultMessageTextByTypeAndLanguageFromFileSystem(ctx, domain.PasswordChangeMessageType, req.Language)
if err != nil {
return nil, err
}
return &admin_pb.GetDefaultPasswordChangeMessageTextResponse{
CustomText: text_grpc.ModelCustomMessageTextToPb(msg),
}, nil
}
func (s *Server) GetCustomPasswordChangeMessageText(ctx context.Context, req *admin_pb.GetCustomPasswordChangeMessageTextRequest) (*admin_pb.GetCustomPasswordChangeMessageTextResponse, error) {
msg, err := s.query.CustomMessageTextByTypeAndLanguage(ctx, authz.GetInstance(ctx).InstanceID(), domain.PasswordChangeMessageType, req.Language, false)
if err != nil {
return nil, err
}
return &admin_pb.GetCustomPasswordChangeMessageTextResponse{
CustomText: text_grpc.ModelCustomMessageTextToPb(msg),
}, nil
}
func (s *Server) SetDefaultPasswordChangeMessageText(ctx context.Context, req *admin_pb.SetDefaultPasswordChangeMessageTextRequest) (*admin_pb.SetDefaultPasswordChangeMessageTextResponse, error) {
result, err := s.command.SetDefaultMessageText(ctx, authz.GetInstance(ctx).InstanceID(), SetPasswordChangeCustomTextToDomain(req))
if err != nil {
return nil, err
}
return &admin_pb.SetDefaultPasswordChangeMessageTextResponse{
Details: object.ChangeToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) ResetCustomPasswordChangeMessageTextToDefault(ctx context.Context, req *admin_pb.ResetCustomPasswordChangeMessageTextToDefaultRequest) (*admin_pb.ResetCustomPasswordChangeMessageTextToDefaultResponse, error) {
result, err := s.command.RemoveInstanceMessageTexts(ctx, domain.PasswordChangeMessageType, language.Make(req.Language))
if err != nil {
return nil, err
}
return &admin_pb.ResetCustomPasswordChangeMessageTextToDefaultResponse{
Details: object.ChangeToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) GetDefaultInviteUserMessageText(ctx context.Context, req *admin_pb.GetDefaultInviteUserMessageTextRequest) (*admin_pb.GetDefaultInviteUserMessageTextResponse, error) {
msg, err := s.query.DefaultMessageTextByTypeAndLanguageFromFileSystem(ctx, domain.InviteUserMessageType, req.Language)
if err != nil {
return nil, err
}
return &admin_pb.GetDefaultInviteUserMessageTextResponse{
CustomText: text_grpc.ModelCustomMessageTextToPb(msg),
}, nil
}
func (s *Server) GetCustomInviteUserMessageText(ctx context.Context, req *admin_pb.GetCustomInviteUserMessageTextRequest) (*admin_pb.GetCustomInviteUserMessageTextResponse, error) {
msg, err := s.query.CustomMessageTextByTypeAndLanguage(ctx, authz.GetInstance(ctx).InstanceID(), domain.InviteUserMessageType, req.Language, false)
if err != nil {
return nil, err
}
return &admin_pb.GetCustomInviteUserMessageTextResponse{
CustomText: text_grpc.ModelCustomMessageTextToPb(msg),
}, nil
}
func (s *Server) SetDefaultInviteUserMessageText(ctx context.Context, req *admin_pb.SetDefaultInviteUserMessageTextRequest) (*admin_pb.SetDefaultInviteUserMessageTextResponse, error) {
result, err := s.command.SetDefaultMessageText(ctx, authz.GetInstance(ctx).InstanceID(), SetInviteUserCustomTextToDomain(req))
if err != nil {
return nil, err
}
return &admin_pb.SetDefaultInviteUserMessageTextResponse{
Details: object.ChangeToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) ResetCustomInviteUserMessageTextToDefault(ctx context.Context, req *admin_pb.ResetCustomInviteUserMessageTextToDefaultRequest) (*admin_pb.ResetCustomInviteUserMessageTextToDefaultResponse, error) {
result, err := s.command.RemoveInstanceMessageTexts(ctx, domain.InviteUserMessageType, language.Make(req.Language))
if err != nil {
return nil, err
}
return &admin_pb.ResetCustomInviteUserMessageTextToDefaultResponse{
Details: object.ChangeToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) GetDefaultPasswordlessRegistrationMessageText(ctx context.Context, req *admin_pb.GetDefaultPasswordlessRegistrationMessageTextRequest) (*admin_pb.GetDefaultPasswordlessRegistrationMessageTextResponse, error) {
msg, err := s.query.DefaultMessageTextByTypeAndLanguageFromFileSystem(ctx, domain.PasswordlessRegistrationMessageType, req.Language)
if err != nil {
return nil, err
}
return &admin_pb.GetDefaultPasswordlessRegistrationMessageTextResponse{
CustomText: text_grpc.ModelCustomMessageTextToPb(msg),
}, nil
}
func (s *Server) GetCustomPasswordlessRegistrationMessageText(ctx context.Context, req *admin_pb.GetCustomPasswordlessRegistrationMessageTextRequest) (*admin_pb.GetCustomPasswordlessRegistrationMessageTextResponse, error) {
msg, err := s.query.CustomMessageTextByTypeAndLanguage(ctx, authz.GetInstance(ctx).InstanceID(), domain.PasswordlessRegistrationMessageType, req.Language, false)
if err != nil {
return nil, err
}
return &admin_pb.GetCustomPasswordlessRegistrationMessageTextResponse{
CustomText: text_grpc.ModelCustomMessageTextToPb(msg),
}, nil
}
func (s *Server) SetDefaultPasswordlessRegistrationMessageText(ctx context.Context, req *admin_pb.SetDefaultPasswordlessRegistrationMessageTextRequest) (*admin_pb.SetDefaultPasswordlessRegistrationMessageTextResponse, error) {
result, err := s.command.SetDefaultMessageText(ctx, authz.GetInstance(ctx).InstanceID(), SetPasswordlessRegistrationCustomTextToDomain(req))
if err != nil {
return nil, err
}
return &admin_pb.SetDefaultPasswordlessRegistrationMessageTextResponse{
Details: object.ChangeToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) ResetCustomPasswordlessRegistrationMessageTextToDefault(ctx context.Context, req *admin_pb.ResetCustomPasswordlessRegistrationMessageTextToDefaultRequest) (*admin_pb.ResetCustomPasswordlessRegistrationMessageTextToDefaultResponse, error) {
result, err := s.command.RemoveInstanceMessageTexts(ctx, domain.PasswordlessRegistrationMessageType, language.Make(req.Language))
if err != nil {
return nil, err
}
return &admin_pb.ResetCustomPasswordlessRegistrationMessageTextToDefaultResponse{
Details: object.ChangeToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) GetDefaultLoginTexts(ctx context.Context, req *admin_pb.GetDefaultLoginTextsRequest) (*admin_pb.GetDefaultLoginTextsResponse, error) {
msg, err := s.query.GetDefaultLoginTexts(ctx, req.Language)
if err != nil {
return nil, err
}
return &admin_pb.GetDefaultLoginTextsResponse{
CustomText: text_grpc.CustomLoginTextToPb(msg),
}, nil
}
func (s *Server) GetCustomLoginTexts(ctx context.Context, req *admin_pb.GetCustomLoginTextsRequest) (*admin_pb.GetCustomLoginTextsResponse, error) {
msg, err := s.query.GetCustomLoginTexts(ctx, authz.GetInstance(ctx).InstanceID(), req.Language)
if err != nil {
return nil, err
}
return &admin_pb.GetCustomLoginTextsResponse{
CustomText: text_grpc.CustomLoginTextToPb(msg),
}, nil
}
func (s *Server) SetCustomLoginText(ctx context.Context, req *admin_pb.SetCustomLoginTextsRequest) (*admin_pb.SetCustomLoginTextsResponse, error) {
result, err := s.command.SetCustomInstanceLoginText(ctx, SetLoginTextToDomain(req))
if err != nil {
return nil, err
}
return &admin_pb.SetCustomLoginTextsResponse{
Details: object.ChangeToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) ResetCustomLoginTextToDefault(ctx context.Context, req *admin_pb.ResetCustomLoginTextsToDefaultRequest) (*admin_pb.ResetCustomLoginTextsToDefaultResponse, error) {
result, err := s.command.RemoveCustomInstanceLoginTexts(ctx, language.Make(req.Language))
if err != nil {
return nil, err
}
return &admin_pb.ResetCustomLoginTextsToDefaultResponse{
Details: object.ChangeToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}

View File

@@ -0,0 +1,196 @@
package admin
import (
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/api/grpc/text"
"github.com/zitadel/zitadel/internal/domain"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
)
func SetInitCustomTextToDomain(msg *admin_pb.SetDefaultInitMessageTextRequest) *domain.CustomMessageText {
langTag := language.Make(msg.Language)
return &domain.CustomMessageText{
MessageTextType: domain.InitCodeMessageType,
Language: langTag,
Title: msg.Title,
PreHeader: msg.PreHeader,
Subject: msg.Subject,
Greeting: msg.Greeting,
Text: msg.Text,
ButtonText: msg.ButtonText,
FooterText: msg.FooterText,
}
}
func SetPasswordResetCustomTextToDomain(msg *admin_pb.SetDefaultPasswordResetMessageTextRequest) *domain.CustomMessageText {
langTag := language.Make(msg.Language)
return &domain.CustomMessageText{
MessageTextType: domain.PasswordResetMessageType,
Language: langTag,
Title: msg.Title,
PreHeader: msg.PreHeader,
Subject: msg.Subject,
Greeting: msg.Greeting,
Text: msg.Text,
ButtonText: msg.ButtonText,
FooterText: msg.FooterText,
}
}
func SetVerifyEmailCustomTextToDomain(msg *admin_pb.SetDefaultVerifyEmailMessageTextRequest) *domain.CustomMessageText {
langTag := language.Make(msg.Language)
return &domain.CustomMessageText{
MessageTextType: domain.VerifyEmailMessageType,
Language: langTag,
Title: msg.Title,
PreHeader: msg.PreHeader,
Subject: msg.Subject,
Greeting: msg.Greeting,
Text: msg.Text,
ButtonText: msg.ButtonText,
FooterText: msg.FooterText,
}
}
func SetVerifyPhoneCustomTextToDomain(msg *admin_pb.SetDefaultVerifyPhoneMessageTextRequest) *domain.CustomMessageText {
langTag := language.Make(msg.Language)
return &domain.CustomMessageText{
MessageTextType: domain.VerifyPhoneMessageType,
Language: langTag,
Title: msg.Title,
PreHeader: msg.PreHeader,
Subject: msg.Subject,
Greeting: msg.Greeting,
Text: msg.Text,
ButtonText: msg.ButtonText,
FooterText: msg.FooterText,
}
}
func SetVerifySMSOTPCustomTextToDomain(msg *admin_pb.SetDefaultVerifySMSOTPMessageTextRequest) *domain.CustomMessageText {
langTag := language.Make(msg.Language)
return &domain.CustomMessageText{
MessageTextType: domain.VerifySMSOTPMessageType,
Language: langTag,
Text: msg.Text,
}
}
func SetVerifyEmailOTPCustomTextToDomain(msg *admin_pb.SetDefaultVerifyEmailOTPMessageTextRequest) *domain.CustomMessageText {
langTag := language.Make(msg.Language)
return &domain.CustomMessageText{
MessageTextType: domain.VerifyEmailOTPMessageType,
Language: langTag,
Title: msg.Title,
PreHeader: msg.PreHeader,
Subject: msg.Subject,
Greeting: msg.Greeting,
Text: msg.Text,
ButtonText: msg.ButtonText,
FooterText: msg.FooterText,
}
}
func SetDomainClaimedCustomTextToDomain(msg *admin_pb.SetDefaultDomainClaimedMessageTextRequest) *domain.CustomMessageText {
langTag := language.Make(msg.Language)
return &domain.CustomMessageText{
MessageTextType: domain.DomainClaimedMessageType,
Language: langTag,
Title: msg.Title,
PreHeader: msg.PreHeader,
Subject: msg.Subject,
Greeting: msg.Greeting,
Text: msg.Text,
ButtonText: msg.ButtonText,
FooterText: msg.FooterText,
}
}
func SetPasswordChangeCustomTextToDomain(msg *admin_pb.SetDefaultPasswordChangeMessageTextRequest) *domain.CustomMessageText {
langTag := language.Make(msg.Language)
return &domain.CustomMessageText{
MessageTextType: domain.PasswordChangeMessageType,
Language: langTag,
Title: msg.Title,
PreHeader: msg.PreHeader,
Subject: msg.Subject,
Greeting: msg.Greeting,
Text: msg.Text,
ButtonText: msg.ButtonText,
FooterText: msg.FooterText,
}
}
func SetInviteUserCustomTextToDomain(msg *admin_pb.SetDefaultInviteUserMessageTextRequest) *domain.CustomMessageText {
langTag := language.Make(msg.Language)
return &domain.CustomMessageText{
MessageTextType: domain.InviteUserMessageType,
Language: langTag,
Title: msg.Title,
PreHeader: msg.PreHeader,
Subject: msg.Subject,
Greeting: msg.Greeting,
Text: msg.Text,
ButtonText: msg.ButtonText,
FooterText: msg.FooterText,
}
}
func SetPasswordlessRegistrationCustomTextToDomain(msg *admin_pb.SetDefaultPasswordlessRegistrationMessageTextRequest) *domain.CustomMessageText {
langTag := language.Make(msg.Language)
return &domain.CustomMessageText{
MessageTextType: domain.PasswordlessRegistrationMessageType,
Language: langTag,
Title: msg.Title,
PreHeader: msg.PreHeader,
Subject: msg.Subject,
Greeting: msg.Greeting,
Text: msg.Text,
ButtonText: msg.ButtonText,
FooterText: msg.FooterText,
}
}
func SetLoginTextToDomain(req *admin_pb.SetCustomLoginTextsRequest) *domain.CustomLoginText {
langTag := language.Make(req.Language)
result := &domain.CustomLoginText{
Language: langTag,
}
result.SelectAccount = text.SelectAccountScreenTextPbToDomain(req.SelectAccountText)
result.Login = text.LoginScreenTextPbToDomain(req.LoginText)
result.Password = text.PasswordScreenTextPbToDomain(req.PasswordText)
result.UsernameChange = text.UsernameChangeScreenTextPbToDomain(req.UsernameChangeText)
result.UsernameChangeDone = text.UsernameChangeDoneScreenTextPbToDomain(req.UsernameChangeDoneText)
result.Password = text.PasswordScreenTextPbToDomain(req.PasswordText)
result.InitPassword = text.InitPasswordScreenTextPbToDomain(req.InitPasswordText)
result.InitPasswordDone = text.InitPasswordDoneScreenTextPbToDomain(req.InitPasswordDoneText)
result.EmailVerification = text.EmailVerificationScreenTextPbToDomain(req.EmailVerificationText)
result.EmailVerificationDone = text.EmailVerificationDoneScreenTextPbToDomain(req.EmailVerificationDoneText)
result.InitUser = text.InitializeUserScreenTextPbToDomain(req.InitializeUserText)
result.InitUserDone = text.InitializeDoneScreenTextPbToDomain(req.InitializeDoneText)
result.InitMFAPrompt = text.InitMFAPromptScreenTextPbToDomain(req.InitMfaPromptText)
result.InitMFAOTP = text.InitMFAOTPScreenTextPbToDomain(req.InitMfaOtpText)
result.InitMFAU2F = text.InitMFAU2FScreenTextPbToDomain(req.InitMfaU2FText)
result.InitMFADone = text.InitMFADoneScreenTextPbToDomain(req.InitMfaDoneText)
result.MFAProvider = text.MFAProvidersTextPbToDomain(req.MfaProvidersText)
result.VerifyMFAOTP = text.VerifyMFAOTPScreenTextPbToDomain(req.VerifyMfaOtpText)
result.VerifyMFAU2F = text.VerifyMFAU2FScreenTextPbToDomain(req.VerifyMfaU2FText)
result.Passwordless = text.PasswordlessScreenTextPbToDomain(req.PasswordlessText)
result.PasswordlessPrompt = text.PasswordlessPromptScreenTextPbToDomain(req.PasswordlessPromptText)
result.PasswordlessRegistration = text.PasswordlessRegistrationScreenTextPbToDomain(req.PasswordlessRegistrationText)
result.PasswordlessRegistrationDone = text.PasswordlessRegistrationDoneScreenTextPbToDomain(req.PasswordlessRegistrationDoneText)
result.PasswordChange = text.PasswordChangeScreenTextPbToDomain(req.PasswordChangeText)
result.PasswordChangeDone = text.PasswordChangeDoneScreenTextPbToDomain(req.PasswordChangeDoneText)
result.PasswordResetDone = text.PasswordResetDoneScreenTextPbToDomain(req.PasswordResetDoneText)
result.RegisterOption = text.RegistrationOptionScreenTextPbToDomain(req.RegistrationOptionText)
result.RegistrationUser = text.RegistrationUserScreenTextPbToDomain(req.RegistrationUserText)
result.ExternalRegistrationUserOverview = text.ExternalRegistrationUserOverviewScreenTextPbToDomain(req.ExternalRegistrationUserOverviewText)
result.RegistrationOrg = text.RegistrationOrgScreenTextPbToDomain(req.RegistrationOrgText)
result.LinkingUsersDone = text.LinkingUserDoneScreenTextPbToDomain(req.LinkingUserDoneText)
result.ExternalNotFound = text.ExternalUserNotFoundScreenTextPbToDomain(req.ExternalUserNotFoundText)
result.LoginSuccess = text.SuccessLoginScreenTextPbToDomain(req.SuccessLoginText)
result.LogoutDone = text.LogoutDoneScreenTextPbToDomain(req.LogoutText)
result.Footer = text.FooterTextPbToDomain(req.FooterText)
return result
}

View File

@@ -0,0 +1,121 @@
package admin
import (
"context"
"github.com/zitadel/zitadel/internal/api/grpc/object"
policy_grpc "github.com/zitadel/zitadel/internal/api/grpc/policy"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
)
func (s *Server) GetDomainPolicy(ctx context.Context, _ *admin_pb.GetDomainPolicyRequest) (*admin_pb.GetDomainPolicyResponse, error) {
policy, err := s.query.DefaultDomainPolicy(ctx)
if err != nil {
return nil, err
}
return &admin_pb.GetDomainPolicyResponse{Policy: policy_grpc.DomainPolicyToPb(policy)}, nil
}
func (s *Server) GetCustomDomainPolicy(ctx context.Context, req *admin_pb.GetCustomDomainPolicyRequest) (*admin_pb.GetCustomDomainPolicyResponse, error) {
policy, err := s.query.DomainPolicyByOrg(ctx, true, req.OrgId, false)
if err != nil {
return nil, err
}
return &admin_pb.GetCustomDomainPolicyResponse{Policy: policy_grpc.DomainPolicyToPb(policy)}, nil
}
func (s *Server) AddCustomDomainPolicy(ctx context.Context, req *admin_pb.AddCustomDomainPolicyRequest) (*admin_pb.AddCustomDomainPolicyResponse, error) {
details, err := s.command.AddOrgDomainPolicy(ctx, req.OrgId, req.UserLoginMustBeDomain, req.ValidateOrgDomains, req.SmtpSenderAddressMatchesInstanceDomain)
if err != nil {
return nil, err
}
return &admin_pb.AddCustomDomainPolicyResponse{
Details: object.DomainToAddDetailsPb(details),
}, nil
}
func (s *Server) UpdateDomainPolicy(ctx context.Context, req *admin_pb.UpdateDomainPolicyRequest) (*admin_pb.UpdateDomainPolicyResponse, error) {
details, err := s.command.ChangeDefaultDomainPolicy(ctx, req.UserLoginMustBeDomain, req.ValidateOrgDomains, req.SmtpSenderAddressMatchesInstanceDomain)
if err != nil {
return nil, err
}
return &admin_pb.UpdateDomainPolicyResponse{
Details: object.DomainToChangeDetailsPb(details),
}, nil
}
func (s *Server) UpdateCustomDomainPolicy(ctx context.Context, req *admin_pb.UpdateCustomDomainPolicyRequest) (*admin_pb.UpdateCustomDomainPolicyResponse, error) {
details, err := s.command.ChangeOrgDomainPolicy(ctx, req.OrgId, req.UserLoginMustBeDomain, req.ValidateOrgDomains, req.SmtpSenderAddressMatchesInstanceDomain)
if err != nil {
return nil, err
}
return &admin_pb.UpdateCustomDomainPolicyResponse{
Details: object.DomainToChangeDetailsPb(details),
}, nil
}
func (s *Server) ResetCustomDomainPolicyToDefault(ctx context.Context, req *admin_pb.ResetCustomDomainPolicyToDefaultRequest) (*admin_pb.ResetCustomDomainPolicyToDefaultResponse, error) {
details, err := s.command.RemoveOrgDomainPolicy(ctx, req.OrgId)
if err != nil {
return nil, err
}
return &admin_pb.ResetCustomDomainPolicyToDefaultResponse{Details: object.DomainToChangeDetailsPb(details)}, nil
}
// the following requests only exist for backwards compatibility
// OrgIAMPolicy has been replaced by DomainPolicy, which also extends it with validateOrgDomains and smtpSenderAddressMatchesInstanceDomain
// Add and Update requests will therefore set the previous default (true)
func (s *Server) AddCustomOrgIAMPolicy(ctx context.Context, req *admin_pb.AddCustomOrgIAMPolicyRequest) (*admin_pb.AddCustomOrgIAMPolicyResponse, error) {
details, err := s.command.AddOrgDomainPolicy(ctx, req.OrgId, req.UserLoginMustBeDomain, true, true)
if err != nil {
return nil, err
}
return &admin_pb.AddCustomOrgIAMPolicyResponse{
Details: object.DomainToAddDetailsPb(details),
}, nil
}
func (s *Server) UpdateOrgIAMPolicy(ctx context.Context, req *admin_pb.UpdateOrgIAMPolicyRequest) (*admin_pb.UpdateOrgIAMPolicyResponse, error) {
details, err := s.command.ChangeDefaultDomainPolicy(ctx, req.UserLoginMustBeDomain, true, true)
if err != nil {
return nil, err
}
return &admin_pb.UpdateOrgIAMPolicyResponse{
Details: object.DomainToChangeDetailsPb(details),
}, nil
}
func (s *Server) UpdateCustomOrgIAMPolicy(ctx context.Context, req *admin_pb.UpdateCustomOrgIAMPolicyRequest) (*admin_pb.UpdateCustomOrgIAMPolicyResponse, error) {
details, err := s.command.ChangeOrgDomainPolicy(ctx, req.OrgId, req.UserLoginMustBeDomain, true, true)
if err != nil {
return nil, err
}
return &admin_pb.UpdateCustomOrgIAMPolicyResponse{
Details: object.DomainToChangeDetailsPb(details),
}, nil
}
func (s *Server) GetOrgIAMPolicy(ctx context.Context, _ *admin_pb.GetOrgIAMPolicyRequest) (*admin_pb.GetOrgIAMPolicyResponse, error) {
policy, err := s.query.DefaultDomainPolicy(ctx)
if err != nil {
return nil, err
}
return &admin_pb.GetOrgIAMPolicyResponse{Policy: policy_grpc.DomainPolicyToOrgIAMPb(policy)}, nil
}
func (s *Server) GetCustomOrgIAMPolicy(ctx context.Context, req *admin_pb.GetCustomOrgIAMPolicyRequest) (*admin_pb.GetCustomOrgIAMPolicyResponse, error) {
policy, err := s.query.DomainPolicyByOrg(ctx, true, req.OrgId, false)
if err != nil {
return nil, err
}
return &admin_pb.GetCustomOrgIAMPolicyResponse{Policy: policy_grpc.DomainPolicyToOrgIAMPb(policy)}, nil
}
func (s *Server) ResetCustomOrgIAMPolicyToDefault(ctx context.Context, req *admin_pb.ResetCustomOrgIAMPolicyToDefaultRequest) (*admin_pb.ResetCustomOrgIAMPolicyToDefaultResponse, error) {
details, err := s.command.RemoveOrgDomainPolicy(ctx, req.OrgId)
if err != nil {
return nil, err
}
return &admin_pb.ResetCustomOrgIAMPolicyToDefaultResponse{Details: object.DomainToChangeDetailsPb(details)}, nil
}

View File

@@ -0,0 +1,140 @@
package admin
import (
"context"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/object"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
)
func (s *Server) GetEmailProvider(ctx context.Context, req *admin_pb.GetEmailProviderRequest) (*admin_pb.GetEmailProviderResponse, error) {
smtp, err := s.query.SMTPConfigActive(ctx, authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
return &admin_pb.GetEmailProviderResponse{
Config: emailProviderToProviderPb(smtp),
}, nil
}
func (s *Server) GetEmailProviderById(ctx context.Context, req *admin_pb.GetEmailProviderByIdRequest) (*admin_pb.GetEmailProviderByIdResponse, error) {
smtp, err := s.query.SMTPConfigByID(ctx, authz.GetInstance(ctx).InstanceID(), req.Id)
if err != nil {
return nil, err
}
return &admin_pb.GetEmailProviderByIdResponse{
Config: emailProviderToProviderPb(smtp),
}, nil
}
func (s *Server) AddEmailProviderSMTP(ctx context.Context, req *admin_pb.AddEmailProviderSMTPRequest) (*admin_pb.AddEmailProviderSMTPResponse, error) {
config := addEmailProviderSMTPToConfig(ctx, req)
if err := s.command.AddSMTPConfig(ctx, config); err != nil {
return nil, err
}
return &admin_pb.AddEmailProviderSMTPResponse{
Details: object.DomainToChangeDetailsPb(config.Details),
Id: config.ID,
}, nil
}
func (s *Server) UpdateEmailProviderSMTP(ctx context.Context, req *admin_pb.UpdateEmailProviderSMTPRequest) (*admin_pb.UpdateEmailProviderSMTPResponse, error) {
config := updateEmailProviderSMTPToConfig(ctx, req)
if err := s.command.ChangeSMTPConfig(ctx, config); err != nil {
return nil, err
}
return &admin_pb.UpdateEmailProviderSMTPResponse{
Details: object.DomainToChangeDetailsPb(config.Details),
}, nil
}
func (s *Server) AddEmailProviderHTTP(ctx context.Context, req *admin_pb.AddEmailProviderHTTPRequest) (*admin_pb.AddEmailProviderHTTPResponse, error) {
config := addEmailProviderHTTPToConfig(ctx, req)
if err := s.command.AddSMTPConfigHTTP(ctx, config); err != nil {
return nil, err
}
return &admin_pb.AddEmailProviderHTTPResponse{
Details: object.DomainToChangeDetailsPb(config.Details),
Id: config.ID,
}, nil
}
func (s *Server) UpdateEmailProviderHTTP(ctx context.Context, req *admin_pb.UpdateEmailProviderHTTPRequest) (*admin_pb.UpdateEmailProviderHTTPResponse, error) {
config := updateEmailProviderHTTPToConfig(ctx, req)
if err := s.command.ChangeSMTPConfigHTTP(ctx, config); err != nil {
return nil, err
}
return &admin_pb.UpdateEmailProviderHTTPResponse{
Details: object.DomainToChangeDetailsPb(config.Details),
}, nil
}
func (s *Server) RemoveEmailProvider(ctx context.Context, req *admin_pb.RemoveEmailProviderRequest) (*admin_pb.RemoveEmailProviderResponse, error) {
details, err := s.command.RemoveSMTPConfig(ctx, authz.GetInstance(ctx).InstanceID(), req.Id)
if err != nil {
return nil, err
}
return &admin_pb.RemoveEmailProviderResponse{
Details: object.DomainToChangeDetailsPb(details),
}, nil
}
func (s *Server) UpdateEmailProviderSMTPPassword(ctx context.Context, req *admin_pb.UpdateEmailProviderSMTPPasswordRequest) (*admin_pb.UpdateEmailProviderSMTPPasswordResponse, error) {
details, err := s.command.ChangeSMTPConfigPassword(ctx, authz.GetInstance(ctx).InstanceID(), req.Id, req.Password)
if err != nil {
return nil, err
}
return &admin_pb.UpdateEmailProviderSMTPPasswordResponse{
Details: object.DomainToChangeDetailsPb(details),
}, nil
}
func (s *Server) ListEmailProviders(ctx context.Context, req *admin_pb.ListEmailProvidersRequest) (*admin_pb.ListEmailProvidersResponse, error) {
queries, err := listEmailProvidersToModel(req)
if err != nil {
return nil, err
}
result, err := s.query.SearchSMTPConfigs(ctx, queries)
if err != nil {
return nil, err
}
return &admin_pb.ListEmailProvidersResponse{
Details: object.ToListDetails(result.Count, result.Sequence, result.LastRun),
Result: emailProvidersToPb(result.Configs),
}, nil
}
func (s *Server) ActivateEmailProvider(ctx context.Context, req *admin_pb.ActivateEmailProviderRequest) (*admin_pb.ActivateEmailProviderResponse, error) {
result, err := s.command.ActivateSMTPConfig(ctx, authz.GetInstance(ctx).InstanceID(), req.Id)
if err != nil {
return nil, err
}
return &admin_pb.ActivateEmailProviderResponse{
Details: object.DomainToAddDetailsPb(result),
}, nil
}
func (s *Server) DeactivateEmailProvider(ctx context.Context, req *admin_pb.DeactivateEmailProviderRequest) (*admin_pb.DeactivateEmailProviderResponse, error) {
result, err := s.command.DeactivateSMTPConfig(ctx, authz.GetInstance(ctx).InstanceID(), req.Id)
if err != nil {
return nil, err
}
return &admin_pb.DeactivateEmailProviderResponse{
Details: object.DomainToAddDetailsPb(result),
}, nil
}
func (s *Server) TestEmailProviderById(ctx context.Context, req *admin_pb.TestEmailProviderSMTPByIdRequest) (*admin_pb.TestEmailProviderSMTPByIdResponse, error) {
if err := s.command.TestSMTPConfigById(ctx, authz.GetInstance(ctx).InstanceID(), req.Id, req.ReceiverAddress); err != nil {
return nil, err
}
return &admin_pb.TestEmailProviderSMTPByIdResponse{}, nil
}
func (s *Server) TestEmailProviderSMTP(ctx context.Context, req *admin_pb.TestEmailProviderSMTPRequest) (*admin_pb.TestEmailProviderSMTPResponse, error) {
if err := s.command.TestSMTPConfig(ctx, authz.GetInstance(ctx).InstanceID(), req.Id, req.ReceiverAddress, testEmailProviderSMTPToConfig(req)); err != nil {
return nil, err
}
return &admin_pb.TestEmailProviderSMTPResponse{}, nil
}

View File

@@ -0,0 +1,145 @@
package admin
import (
"context"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/object"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
"github.com/zitadel/zitadel/internal/query"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
settings_pb "github.com/zitadel/zitadel/pkg/grpc/settings"
)
func listEmailProvidersToModel(req *admin_pb.ListEmailProvidersRequest) (*query.SMTPConfigsSearchQueries, error) {
offset, limit, asc := object.ListQueryToModel(req.Query)
return &query.SMTPConfigsSearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
},
}, nil
}
func emailProvidersToPb(configs []*query.SMTPConfig) []*settings_pb.EmailProvider {
c := make([]*settings_pb.EmailProvider, len(configs))
for i, config := range configs {
c[i] = emailProviderToProviderPb(config)
}
return c
}
func emailProviderToProviderPb(config *query.SMTPConfig) *settings_pb.EmailProvider {
return &settings_pb.EmailProvider{
Details: object.ToViewDetailsPb(config.Sequence, config.CreationDate, config.ChangeDate, config.ResourceOwner),
Id: config.ID,
Description: config.Description,
State: emailProviderStateToPb(config.State),
Config: emailProviderToPb(config),
}
}
func emailProviderStateToPb(state domain.SMTPConfigState) settings_pb.EmailProviderState {
switch state {
case domain.SMTPConfigStateUnspecified, domain.SMTPConfigStateRemoved:
return settings_pb.EmailProviderState_EMAIL_PROVIDER_STATE_UNSPECIFIED
case domain.SMTPConfigStateActive:
return settings_pb.EmailProviderState_EMAIL_PROVIDER_ACTIVE
case domain.SMTPConfigStateInactive:
return settings_pb.EmailProviderState_EMAIL_PROVIDER_INACTIVE
default:
return settings_pb.EmailProviderState_EMAIL_PROVIDER_STATE_UNSPECIFIED
}
}
func emailProviderToPb(config *query.SMTPConfig) settings_pb.EmailConfig {
if config.SMTPConfig != nil {
return smtpToPb(config.SMTPConfig)
}
if config.HTTPConfig != nil {
return httpToPb(config.HTTPConfig)
}
return nil
}
func httpToPb(http *query.HTTP) *settings_pb.EmailProvider_Http {
return &settings_pb.EmailProvider_Http{
Http: &settings_pb.EmailProviderHTTP{
Endpoint: http.Endpoint,
},
}
}
func smtpToPb(config *query.SMTP) *settings_pb.EmailProvider_Smtp {
return &settings_pb.EmailProvider_Smtp{
Smtp: &settings_pb.EmailProviderSMTP{
Tls: config.TLS,
Host: config.Host,
User: config.User,
SenderAddress: config.SenderAddress,
SenderName: config.SenderName,
},
}
}
func addEmailProviderSMTPToConfig(ctx context.Context, req *admin_pb.AddEmailProviderSMTPRequest) *command.AddSMTPConfig {
return &command.AddSMTPConfig{
ResourceOwner: authz.GetInstance(ctx).InstanceID(),
Description: req.Description,
Tls: req.Tls,
From: req.SenderAddress,
FromName: req.SenderName,
ReplyToAddress: req.ReplyToAddress,
Host: req.Host,
User: req.User,
Password: req.Password,
}
}
func updateEmailProviderSMTPToConfig(ctx context.Context, req *admin_pb.UpdateEmailProviderSMTPRequest) *command.ChangeSMTPConfig {
return &command.ChangeSMTPConfig{
ResourceOwner: authz.GetInstance(ctx).InstanceID(),
ID: req.Id,
Description: req.Description,
Tls: req.Tls,
From: req.SenderAddress,
FromName: req.SenderName,
ReplyToAddress: req.ReplyToAddress,
Host: req.Host,
User: req.User,
Password: req.Password,
}
}
func addEmailProviderHTTPToConfig(ctx context.Context, req *admin_pb.AddEmailProviderHTTPRequest) *command.AddSMTPConfigHTTP {
return &command.AddSMTPConfigHTTP{
ResourceOwner: authz.GetInstance(ctx).InstanceID(),
Description: req.Description,
Endpoint: req.Endpoint,
}
}
func updateEmailProviderHTTPToConfig(ctx context.Context, req *admin_pb.UpdateEmailProviderHTTPRequest) *command.ChangeSMTPConfigHTTP {
return &command.ChangeSMTPConfigHTTP{
ResourceOwner: authz.GetInstance(ctx).InstanceID(),
ID: req.Id,
Description: req.Description,
Endpoint: req.Endpoint,
}
}
func testEmailProviderSMTPToConfig(req *admin_pb.TestEmailProviderSMTPRequest) *smtp.Config {
return &smtp.Config{
Tls: req.Tls,
From: req.SenderAddress,
FromName: req.SenderName,
SMTP: smtp.SMTP{
Host: req.Host,
User: req.User,
Password: req.Password,
},
}
}

View File

@@ -0,0 +1,120 @@
package admin
import (
"context"
"slices"
"time"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/eventstore"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
)
const (
maxLimit = 1000
)
func (s *Server) ListEvents(ctx context.Context, in *admin_pb.ListEventsRequest) (*admin_pb.ListEventsResponse, error) {
filter, err := eventRequestToFilter(ctx, in)
if err != nil {
return nil, err
}
events, err := s.query.SearchEvents(ctx, filter)
if err != nil {
return nil, err
}
return admin_pb.EventsToPb(ctx, events)
}
func (s *Server) ListEventTypes(ctx context.Context, in *admin_pb.ListEventTypesRequest) (*admin_pb.ListEventTypesResponse, error) {
eventTypes := s.query.SearchEventTypes(ctx)
return admin_pb.EventTypesToPb(eventTypes), nil
}
func (s *Server) ListAggregateTypes(ctx context.Context, in *admin_pb.ListAggregateTypesRequest) (*admin_pb.ListAggregateTypesResponse, error) {
aggregateTypes := s.query.SearchAggregateTypes(ctx)
return admin_pb.AggregateTypesToPb(aggregateTypes), nil
}
func eventRequestToFilter(ctx context.Context, req *admin_pb.ListEventsRequest) (*eventstore.SearchQueryBuilder, error) {
var fromTime, sinceTime, untilTime time.Time
// We ignore the deprecation warning here because we still need to support the deprecated field.
//nolint:staticcheck
if creationDatePb := req.GetCreationDate(); creationDatePb != nil {
fromTime = creationDatePb.AsTime()
}
if fromTimePb := req.GetFrom(); fromTimePb != nil {
fromTime = fromTimePb.AsTime()
}
if timeRange := req.GetRange(); timeRange != nil {
// If range is set, we ignore the from and the deprecated creation_date fields
fromTime = time.Time{}
if timeSincePb := timeRange.GetSince(); timeSincePb != nil {
sinceTime = timeSincePb.AsTime()
}
if timeUntilPb := timeRange.GetUntil(); timeUntilPb != nil {
untilTime = timeUntilPb.AsTime()
}
}
eventTypes := make([]eventstore.EventType, len(req.EventTypes))
for i, eventType := range req.EventTypes {
eventTypes[i] = eventstore.EventType(eventType)
}
aggregateIDs := make([]string, 0, 1)
if req.AggregateId != "" {
aggregateIDs = append(aggregateIDs, req.AggregateId)
}
aggregateTypes := make([]eventstore.AggregateType, len(req.AggregateTypes))
for i, aggregateType := range req.AggregateTypes {
aggregateTypes[i] = eventstore.AggregateType(aggregateType)
}
if len(aggregateTypes) == 0 {
aggregateTypes = aggregateTypesFromEventTypes(eventTypes)
}
aggregateTypes = slices.Compact(aggregateTypes)
limit := uint64(req.Limit)
if limit == 0 || limit > maxLimit {
limit = maxLimit
}
builder := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
OrderDesc().
InstanceID(authz.GetInstance(ctx).InstanceID()).
Limit(limit).
AwaitOpenTransactions().
ResourceOwner(req.ResourceOwner).
EditorUser(req.EditorUserId).
SequenceGreater(req.Sequence).
CreationDateAfter(sinceTime).
CreationDateBefore(untilTime)
if len(aggregateIDs) > 0 || len(aggregateTypes) > 0 || len(eventTypes) > 0 {
builder.AddQuery().
AggregateIDs(aggregateIDs...).
AggregateTypes(aggregateTypes...).
EventTypes(eventTypes...).
Builder()
}
if req.GetAsc() {
builder.OrderAsc()
builder.CreationDateAfter(fromTime)
} else {
builder.CreationDateBefore(fromTime)
}
return builder, nil
}
func aggregateTypesFromEventTypes(eventTypes []eventstore.EventType) []eventstore.AggregateType {
aggregateTypes := make([]eventstore.AggregateType, 0, len(eventTypes))
for _, eventType := range eventTypes {
aggregateTypes = append(aggregateTypes, eventstore.AggregateTypeFromEventType(eventType))
}
return aggregateTypes
}

View File

@@ -0,0 +1,58 @@
package admin
import (
"reflect"
"testing"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/deviceauth"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/repository/user"
)
func Test_aggregateTypesFromEventTypes(t *testing.T) {
type args struct {
eventTypes []eventstore.EventType
}
tests := []struct {
name string
args args
want []eventstore.AggregateType
}{
{
name: "no event types",
args: args{
eventTypes: []eventstore.EventType{},
},
want: []eventstore.AggregateType{},
},
{
name: "only by prefix",
args: args{
eventTypes: []eventstore.EventType{user.MachineAddedEventType, org.OrgAddedEventType},
},
want: []eventstore.AggregateType{user.AggregateType, org.AggregateType},
},
{
name: "with special",
args: args{
eventTypes: []eventstore.EventType{deviceauth.ApprovedEventType, org.OrgAddedEventType},
},
want: []eventstore.AggregateType{deviceauth.AggregateType, org.AggregateType},
},
{
name: "duplicates",
args: args{
eventTypes: []eventstore.EventType{org.OrgAddedEventType, org.OrgChangedEventType},
},
want: []eventstore.AggregateType{org.AggregateType, org.AggregateType},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := aggregateTypesFromEventTypes(tt.args.eventTypes); !reflect.DeepEqual(got, tt.want) {
t.Errorf("aggregateTypesFromEventTypes() = %v, want %v", got, tt.want)
}
})
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,32 @@
package admin
import (
"context"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/query"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
)
func (s *Server) ListFailedEvents(ctx context.Context, _ *admin_pb.ListFailedEventsRequest) (*admin_pb.ListFailedEventsResponse, error) {
instanceID := authz.GetInstance(ctx).InstanceID()
instanceIDQuery, err := query.NewFailedEventInstanceIDSearchQuery(instanceID)
if err != nil {
return nil, err
}
failedEvents, err := s.query.SearchFailedEvents(ctx, &query.FailedEventSearchQueries{
Queries: []query.SearchQuery{instanceIDQuery},
})
if err != nil {
return nil, err
}
return &admin_pb.ListFailedEventsResponse{Result: FailedEventsToPb(s.database, failedEvents)}, nil
}
func (s *Server) RemoveFailedEvent(ctx context.Context, req *admin_pb.RemoveFailedEventRequest) (*admin_pb.RemoveFailedEventResponse, error) {
err := s.query.RemoveFailedEvent(ctx, req.ViewName, authz.GetInstance(ctx).InstanceID(), req.FailedSequence)
if err != nil {
return nil, err
}
return &admin_pb.RemoveFailedEventResponse{}, nil
}

View File

@@ -0,0 +1,31 @@
package admin
import (
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/query"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
)
func FailedEventsToPb(database string, failedEvents *query.FailedEvents) []*admin_pb.FailedEvent {
events := make([]*admin_pb.FailedEvent, len(failedEvents.FailedEvents))
for i, failedEvent := range failedEvents.FailedEvents {
events[i] = FailedEventToPb(database, failedEvent)
}
return events
}
func FailedEventToPb(database string, failedEvent *query.FailedEvent) *admin_pb.FailedEvent {
var lastFailed *timestamppb.Timestamp
if !failedEvent.LastFailed.IsZero() {
lastFailed = timestamppb.New(failedEvent.LastFailed)
}
return &admin_pb.FailedEvent{
Database: database,
ViewName: failedEvent.ProjectionName,
FailedSequence: failedEvent.FailedSequence,
FailureCount: failedEvent.FailureCount,
ErrorMessage: failedEvent.Error,
LastFailed: lastFailed,
}
}

View File

@@ -0,0 +1,30 @@
package admin
import (
"context"
"github.com/muhlemmer/gu"
"github.com/zitadel/logging"
object_pb "github.com/zitadel/zitadel/internal/api/grpc/object"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
"github.com/zitadel/zitadel/internal/query/projection"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
)
func (s *Server) ActivateFeatureLoginDefaultOrg(ctx context.Context, _ *admin_pb.ActivateFeatureLoginDefaultOrgRequest) (*admin_pb.ActivateFeatureLoginDefaultOrgResponse, error) {
details, err := s.command.SetInstanceFeatures(ctx, &command.InstanceFeatures{
LoginDefaultOrg: gu.Ptr(true),
})
if err != nil {
return nil, err
}
_, err = projection.InstanceFeatureProjection.Trigger(ctx, handler.WithAwaitRunning())
logging.OnError(err).Warn("trigger instance feature projection")
return &admin_pb.ActivateFeatureLoginDefaultOrgResponse{
Details: object_pb.DomainToChangeDetailsPb(details),
}, nil
}

View File

@@ -0,0 +1,72 @@
package admin
import (
"context"
"time"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/member"
"github.com/zitadel/zitadel/internal/api/grpc/object"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
)
func (s *Server) ListIAMMemberRoles(ctx context.Context, req *admin_pb.ListIAMMemberRolesRequest) (*admin_pb.ListIAMMemberRolesResponse, error) {
roles := s.query.GetIAMMemberRoles()
return &admin_pb.ListIAMMemberRolesResponse{
Roles: roles,
Details: object.ToListDetails(uint64(len(roles)), 0, time.Now()),
}, nil
}
func (s *Server) ListIAMMembers(ctx context.Context, req *admin_pb.ListIAMMembersRequest) (*admin_pb.ListIAMMembersResponse, error) {
queries, err := ListIAMMembersRequestToQuery(req)
if err != nil {
return nil, err
}
res, err := s.query.IAMMembers(ctx, queries)
if err != nil {
return nil, err
}
return &admin_pb.ListIAMMembersResponse{
Details: object.ToListDetails(res.Count, res.Sequence, res.LastRun),
Result: member.MembersToPb("", res.Members),
}, nil
}
func (s *Server) AddIAMMember(ctx context.Context, req *admin_pb.AddIAMMemberRequest) (*admin_pb.AddIAMMemberResponse, error) {
member, err := s.command.AddInstanceMember(ctx, AddIAMMemberToCommand(req, authz.GetInstance(ctx).InstanceID()))
if err != nil {
return nil, err
}
return &admin_pb.AddIAMMemberResponse{
Details: object.AddToDetailsPb(
member.Sequence,
member.EventDate,
member.ResourceOwner,
),
}, nil
}
func (s *Server) UpdateIAMMember(ctx context.Context, req *admin_pb.UpdateIAMMemberRequest) (*admin_pb.UpdateIAMMemberResponse, error) {
member, err := s.command.ChangeInstanceMember(ctx, UpdateIAMMemberToCommand(req, authz.GetInstance(ctx).InstanceID()))
if err != nil {
return nil, err
}
return &admin_pb.UpdateIAMMemberResponse{
Details: object.ChangeToDetailsPb(
member.Sequence,
member.EventDate,
member.ResourceOwner,
),
}, nil
}
func (s *Server) RemoveIAMMember(ctx context.Context, req *admin_pb.RemoveIAMMemberRequest) (*admin_pb.RemoveIAMMemberResponse, error) {
objectDetails, err := s.command.RemoveInstanceMember(ctx, authz.GetInstance(ctx).InstanceID(), req.UserId)
if err != nil {
return nil, err
}
return &admin_pb.RemoveIAMMemberResponse{
Details: object.DomainToChangeDetailsPb(objectDetails),
}, nil
}

View File

@@ -0,0 +1,62 @@
package admin
import (
member_grpc "github.com/zitadel/zitadel/internal/api/grpc/member"
"github.com/zitadel/zitadel/internal/api/grpc/object"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/query"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
member_pb "github.com/zitadel/zitadel/pkg/grpc/member"
)
func AddIAMMemberToCommand(req *admin_pb.AddIAMMemberRequest, instanceID string) *command.AddInstanceMember {
return &command.AddInstanceMember{
InstanceID: instanceID,
UserID: req.UserId,
Roles: req.Roles,
}
}
func UpdateIAMMemberToCommand(req *admin_pb.UpdateIAMMemberRequest, instanceID string) *command.ChangeInstanceMember {
return &command.ChangeInstanceMember{
InstanceID: instanceID,
UserID: req.UserId,
Roles: req.Roles,
}
}
func ListIAMMembersRequestToQuery(req *admin_pb.ListIAMMembersRequest) (*query.IAMMembersQuery, error) {
offset, limit, asc := object.ListQueryToModel(req.Query)
queries, err := member_grpc.MemberQueriesToQuery(req.Queries)
if err != nil {
return nil, err
}
return &query.IAMMembersQuery{
MembersQuery: query.MembersQuery{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
SortingColumn: fieldNameToMemberColumn(req.SortingColumn),
},
Queries: queries,
},
}, nil
}
func fieldNameToMemberColumn(fieldName member_pb.MemberFieldColumnName) query.Column {
switch fieldName {
case member_pb.MemberFieldColumnName_MEMBER_FIELD_NAME_UNSPECIFIED:
return query.InstanceMemberInstanceID
case member_pb.MemberFieldColumnName_MEMBER_FIELD_NAME_USER_ID:
return query.InstanceMemberUserID
case member_pb.MemberFieldColumnName_MEMBER_FIELD_NAME_CREATION_DATE:
return query.InstanceMemberCreationDate
case member_pb.MemberFieldColumnName_MEMBER_FIELD_NAME_CHANGE_DATE:
return query.InstanceMemberChangeDate
case member_pb.MemberFieldColumnName_MEMBER_FIELD_NAME_USER_RESOURCE_OWNER:
return query.InstanceMemberResourceOwner
default:
return query.Column{}
}
}

View File

@@ -0,0 +1,60 @@
package admin
import (
"testing"
"github.com/zitadel/zitadel/internal/test"
"github.com/zitadel/zitadel/pkg/grpc/admin"
)
func TestAddIAMMemberToDomain(t *testing.T) {
type args struct {
req *admin.AddIAMMemberRequest
}
tests := []struct {
name string
args args
}{
{
name: "all fields filled",
args: args{
req: &admin.AddIAMMemberRequest{
UserId: "1232452",
Roles: []string{"admin"},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := AddIAMMemberToCommand(tt.args.req, "INSTANCE")
test.AssertFieldsMapped(t, got, "ObjectRoot")
})
}
}
func TestUpdateIAMMemberToDomain(t *testing.T) {
type args struct {
req *admin.UpdateIAMMemberRequest
}
tests := []struct {
name string
args args
}{
{
name: "all fields filled",
args: args{
req: &admin.UpdateIAMMemberRequest{
UserId: "1232452",
Roles: []string{"admin"},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := UpdateIAMMemberToCommand(tt.args.req, "INSTANCE")
test.AssertFieldsMapped(t, got, "ObjectRoot")
})
}
}

View File

@@ -0,0 +1,66 @@
package admin
import (
"context"
"github.com/zitadel/zitadel/internal/api/grpc/object"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
)
func (s *Server) ListSecretGenerators(ctx context.Context, req *admin_pb.ListSecretGeneratorsRequest) (*admin_pb.ListSecretGeneratorsResponse, error) {
queries, err := listSecretGeneratorToModel(req)
if err != nil {
return nil, err
}
result, err := s.query.SearchSecretGenerators(ctx, queries)
if err != nil {
return nil, err
}
return &admin_pb.ListSecretGeneratorsResponse{
Result: SecretGeneratorsToPb(result.SecretGenerators),
Details: object.ToListDetails(result.Count, result.Sequence, result.LastRun),
}, nil
}
func (s *Server) GetSecretGenerator(ctx context.Context, req *admin_pb.GetSecretGeneratorRequest) (*admin_pb.GetSecretGeneratorResponse, error) {
generator, err := s.query.SecretGeneratorByType(ctx, SecretGeneratorTypeToDomain(req.GeneratorType))
if err != nil {
return nil, err
}
return &admin_pb.GetSecretGeneratorResponse{
SecretGenerator: SecretGeneratorToPb(generator),
}, nil
}
func (s *Server) UpdateSecretGenerator(ctx context.Context, req *admin_pb.UpdateSecretGeneratorRequest) (*admin_pb.UpdateSecretGeneratorResponse, error) {
details, err := s.command.ChangeSecretGeneratorConfig(ctx, SecretGeneratorTypeToDomain(req.GeneratorType), UpdateSecretGeneratorToConfig(req))
if err != nil {
return nil, err
}
return &admin_pb.UpdateSecretGeneratorResponse{
Details: object.ChangeToDetailsPb(
details.Sequence,
details.EventDate,
details.ResourceOwner),
}, nil
}
func (s *Server) GetSecurityPolicy(ctx context.Context, req *admin_pb.GetSecurityPolicyRequest) (*admin_pb.GetSecurityPolicyResponse, error) {
policy, err := s.query.SecurityPolicy(ctx)
if err != nil {
return nil, err
}
return &admin_pb.GetSecurityPolicyResponse{
Policy: SecurityPolicyToPb(policy),
}, nil
}
func (s *Server) SetSecurityPolicy(ctx context.Context, req *admin_pb.SetSecurityPolicyRequest) (*admin_pb.SetSecurityPolicyResponse, error) {
details, err := s.command.SetSecurityPolicy(ctx, securityPolicyToCommand(req))
if err != nil {
return nil, err
}
return &admin_pb.SetSecurityPolicyResponse{
Details: object.DomainToChangeDetailsPb(details),
}, nil
}

View File

@@ -0,0 +1,198 @@
package admin
import (
"context"
"google.golang.org/protobuf/types/known/durationpb"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/object"
obj_grpc "github.com/zitadel/zitadel/internal/api/grpc/object"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
settings_pb "github.com/zitadel/zitadel/pkg/grpc/settings"
)
func listSecretGeneratorToModel(req *admin_pb.ListSecretGeneratorsRequest) (*query.SecretGeneratorSearchQueries, error) {
offset, limit, asc := object.ListQueryToModel(req.Query)
queries, err := SecretGeneratorQueriesToModel(req.Queries)
if err != nil {
return nil, err
}
return &query.SecretGeneratorSearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
},
Queries: queries,
}, nil
}
func SecretGeneratorQueriesToModel(queries []*settings_pb.SecretGeneratorQuery) (_ []query.SearchQuery, err error) {
q := make([]query.SearchQuery, len(queries))
for i, query := range queries {
q[i], err = SecretGeneratorQueryToModel(query)
if err != nil {
return nil, err
}
}
return q, nil
}
func SecretGeneratorQueryToModel(apiQuery *settings_pb.SecretGeneratorQuery) (query.SearchQuery, error) {
switch q := apiQuery.Query.(type) {
case *settings_pb.SecretGeneratorQuery_TypeQuery:
domainType := SecretGeneratorTypeToDomain(q.TypeQuery.GeneratorType)
return query.NewSecretGeneratorTypeSearchQuery(int32(domainType))
default:
return nil, zerrors.ThrowInvalidArgument(nil, "ORG-fm9es", "List.Query.Invalid")
}
}
func UpdateSecretGeneratorToConfig(req *admin_pb.UpdateSecretGeneratorRequest) *crypto.GeneratorConfig {
return &crypto.GeneratorConfig{
Length: uint(req.Length),
Expiry: req.Expiry.AsDuration(),
IncludeUpperLetters: req.IncludeUpperLetters,
IncludeLowerLetters: req.IncludeLowerLetters,
IncludeDigits: req.IncludeDigits,
IncludeSymbols: req.IncludeSymbols,
}
}
func SecretGeneratorsToPb(generators []*query.SecretGenerator) []*settings_pb.SecretGenerator {
list := make([]*settings_pb.SecretGenerator, len(generators))
for i, generator := range generators {
list[i] = SecretGeneratorToPb(generator)
}
return list
}
func SecretGeneratorToPb(generator *query.SecretGenerator) *settings_pb.SecretGenerator {
mapped := &settings_pb.SecretGenerator{
GeneratorType: SecretGeneratorTypeToPb(generator.GeneratorType),
Length: uint32(generator.Length),
Expiry: durationpb.New(generator.Expiry),
IncludeUpperLetters: generator.IncludeUpperLetters,
IncludeLowerLetters: generator.IncludeLowerLetters,
IncludeDigits: generator.IncludeDigits,
IncludeSymbols: generator.IncludeSymbols,
Details: obj_grpc.ToViewDetailsPb(generator.Sequence, generator.CreationDate, generator.ChangeDate, generator.AggregateID),
}
return mapped
}
func SecretGeneratorTypeToPb(generatorType domain.SecretGeneratorType) settings_pb.SecretGeneratorType {
switch generatorType {
case domain.SecretGeneratorTypeInitCode:
return settings_pb.SecretGeneratorType_SECRET_GENERATOR_TYPE_INIT_CODE
case domain.SecretGeneratorTypeVerifyEmailCode:
return settings_pb.SecretGeneratorType_SECRET_GENERATOR_TYPE_VERIFY_EMAIL_CODE
case domain.SecretGeneratorTypeVerifyPhoneCode:
return settings_pb.SecretGeneratorType_SECRET_GENERATOR_TYPE_VERIFY_PHONE_CODE
case domain.SecretGeneratorTypePasswordResetCode:
return settings_pb.SecretGeneratorType_SECRET_GENERATOR_TYPE_PASSWORD_RESET_CODE
case domain.SecretGeneratorTypePasswordlessInitCode:
return settings_pb.SecretGeneratorType_SECRET_GENERATOR_TYPE_PASSWORDLESS_INIT_CODE
case domain.SecretGeneratorTypeAppSecret:
return settings_pb.SecretGeneratorType_SECRET_GENERATOR_TYPE_APP_SECRET
case domain.SecretGeneratorTypeOTPSMS:
return settings_pb.SecretGeneratorType_SECRET_GENERATOR_TYPE_OTP_SMS
case domain.SecretGeneratorTypeOTPEmail:
return settings_pb.SecretGeneratorType_SECRET_GENERATOR_TYPE_OTP_EMAIL
default:
return settings_pb.SecretGeneratorType_SECRET_GENERATOR_TYPE_UNSPECIFIED
}
}
func SecretGeneratorTypeToDomain(generatorType settings_pb.SecretGeneratorType) domain.SecretGeneratorType {
switch generatorType {
case settings_pb.SecretGeneratorType_SECRET_GENERATOR_TYPE_INIT_CODE:
return domain.SecretGeneratorTypeInitCode
case settings_pb.SecretGeneratorType_SECRET_GENERATOR_TYPE_VERIFY_EMAIL_CODE:
return domain.SecretGeneratorTypeVerifyEmailCode
case settings_pb.SecretGeneratorType_SECRET_GENERATOR_TYPE_VERIFY_PHONE_CODE:
return domain.SecretGeneratorTypeVerifyPhoneCode
case settings_pb.SecretGeneratorType_SECRET_GENERATOR_TYPE_PASSWORD_RESET_CODE:
return domain.SecretGeneratorTypePasswordResetCode
case settings_pb.SecretGeneratorType_SECRET_GENERATOR_TYPE_PASSWORDLESS_INIT_CODE:
return domain.SecretGeneratorTypePasswordlessInitCode
case settings_pb.SecretGeneratorType_SECRET_GENERATOR_TYPE_APP_SECRET:
return domain.SecretGeneratorTypeAppSecret
case settings_pb.SecretGeneratorType_SECRET_GENERATOR_TYPE_OTP_SMS:
return domain.SecretGeneratorTypeOTPSMS
case settings_pb.SecretGeneratorType_SECRET_GENERATOR_TYPE_OTP_EMAIL:
return domain.SecretGeneratorTypeOTPEmail
default:
return domain.SecretGeneratorTypeUnspecified
}
}
func addSMTPToConfig(ctx context.Context, req *admin_pb.AddSMTPConfigRequest) *command.AddSMTPConfig {
return &command.AddSMTPConfig{
ResourceOwner: authz.GetInstance(ctx).InstanceID(),
Description: req.Description,
Tls: req.Tls,
From: req.SenderAddress,
FromName: req.SenderName,
ReplyToAddress: req.ReplyToAddress,
Host: req.Host,
User: req.User,
Password: req.Password,
}
}
func updateSMTPToConfig(ctx context.Context, req *admin_pb.UpdateSMTPConfigRequest) *command.ChangeSMTPConfig {
return &command.ChangeSMTPConfig{
ResourceOwner: authz.GetInstance(ctx).InstanceID(),
ID: req.Id,
Description: req.Description,
Tls: req.Tls,
From: req.SenderAddress,
FromName: req.SenderName,
ReplyToAddress: req.ReplyToAddress,
Host: req.Host,
User: req.User,
Password: req.Password,
}
}
func SMTPConfigToPb(smtp *query.SMTPConfig) *settings_pb.SMTPConfig {
if smtp.SMTPConfig != nil {
return &settings_pb.SMTPConfig{
Description: smtp.Description,
Tls: smtp.SMTPConfig.TLS,
SenderAddress: smtp.SMTPConfig.SenderAddress,
SenderName: smtp.SMTPConfig.SenderName,
ReplyToAddress: smtp.SMTPConfig.ReplyToAddress,
Host: smtp.SMTPConfig.Host,
User: smtp.SMTPConfig.User,
Details: obj_grpc.ToViewDetailsPb(smtp.Sequence, smtp.CreationDate, smtp.ChangeDate, smtp.ResourceOwner),
Id: smtp.ID,
State: settings_pb.SMTPConfigState(smtp.State),
}
}
return nil
}
func SecurityPolicyToPb(policy *query.SecurityPolicy) *settings_pb.SecurityPolicy {
return &settings_pb.SecurityPolicy{
Details: obj_grpc.ToViewDetailsPb(policy.Sequence, policy.CreationDate, policy.ChangeDate, policy.AggregateID),
EnableIframeEmbedding: policy.EnableIframeEmbedding,
AllowedOrigins: policy.AllowedOrigins,
EnableImpersonation: policy.EnableImpersonation,
}
}
func securityPolicyToCommand(req *admin_pb.SetSecurityPolicyRequest) *command.SecurityPolicy {
return &command.SecurityPolicy{
EnableIframeEmbedding: req.GetEnableIframeEmbedding(),
AllowedOrigins: req.GetAllowedOrigins(),
EnableImpersonation: req.GetEnableImpersonation(),
}
}

View File

@@ -0,0 +1,468 @@
package admin
import (
"context"
"github.com/zitadel/zitadel/internal/api/authz"
idp_grpc "github.com/zitadel/zitadel/internal/api/grpc/idp"
object_pb "github.com/zitadel/zitadel/internal/api/grpc/object"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
)
func (s *Server) GetIDPByID(ctx context.Context, req *admin_pb.GetIDPByIDRequest) (*admin_pb.GetIDPByIDResponse, error) {
idp, err := s.query.IDPByIDAndResourceOwner(ctx, true, req.Id, authz.GetInstance(ctx).InstanceID(), false)
if err != nil {
return nil, err
}
return &admin_pb.GetIDPByIDResponse{Idp: idp_grpc.IDPViewToPb(idp)}, nil
}
func (s *Server) ListIDPs(ctx context.Context, req *admin_pb.ListIDPsRequest) (*admin_pb.ListIDPsResponse, error) {
queries, err := listIDPsToModel(authz.GetInstance(ctx).InstanceID(), req)
if err != nil {
return nil, err
}
resp, err := s.query.IDPs(ctx, queries, false)
if err != nil {
return nil, err
}
return &admin_pb.ListIDPsResponse{
Result: idp_grpc.IDPViewsToPb(resp.IDPs),
Details: object_pb.ToListDetails(resp.Count, resp.Sequence, resp.LastRun),
}, nil
}
func (s *Server) AddOIDCIDP(ctx context.Context, req *admin_pb.AddOIDCIDPRequest) (*admin_pb.AddOIDCIDPResponse, error) {
config, err := s.command.AddDefaultIDPConfig(ctx, addOIDCIDPRequestToDomain(req))
if err != nil {
return nil, err
}
return &admin_pb.AddOIDCIDPResponse{
IdpId: config.IDPConfigID,
Details: object_pb.AddToDetailsPb(
config.Sequence,
config.ChangeDate,
config.ResourceOwner,
),
}, nil
}
func (s *Server) AddJWTIDP(ctx context.Context, req *admin_pb.AddJWTIDPRequest) (*admin_pb.AddJWTIDPResponse, error) {
config, err := s.command.AddDefaultIDPConfig(ctx, addJWTIDPRequestToDomain(req))
if err != nil {
return nil, err
}
return &admin_pb.AddJWTIDPResponse{
IdpId: config.IDPConfigID,
Details: object_pb.AddToDetailsPb(
config.Sequence,
config.ChangeDate,
config.ResourceOwner,
),
}, nil
}
func (s *Server) UpdateIDP(ctx context.Context, req *admin_pb.UpdateIDPRequest) (*admin_pb.UpdateIDPResponse, error) {
config, err := s.command.ChangeDefaultIDPConfig(ctx, updateIDPToDomain(req))
if err != nil {
return nil, err
}
return &admin_pb.UpdateIDPResponse{
Details: object_pb.ChangeToDetailsPb(
config.Sequence,
config.ChangeDate,
config.ResourceOwner,
),
}, nil
}
func (s *Server) DeactivateIDP(ctx context.Context, req *admin_pb.DeactivateIDPRequest) (*admin_pb.DeactivateIDPResponse, error) {
objectDetails, err := s.command.DeactivateDefaultIDPConfig(ctx, req.IdpId)
if err != nil {
return nil, err
}
return &admin_pb.DeactivateIDPResponse{Details: object_pb.DomainToChangeDetailsPb(objectDetails)}, nil
}
func (s *Server) ReactivateIDP(ctx context.Context, req *admin_pb.ReactivateIDPRequest) (*admin_pb.ReactivateIDPResponse, error) {
objectDetails, err := s.command.ReactivateDefaultIDPConfig(ctx, req.IdpId)
if err != nil {
return nil, err
}
return &admin_pb.ReactivateIDPResponse{Details: object_pb.DomainToChangeDetailsPb(objectDetails)}, nil
}
func (s *Server) RemoveIDP(ctx context.Context, req *admin_pb.RemoveIDPRequest) (*admin_pb.RemoveIDPResponse, error) {
providerQuery, err := query.NewIDPIDSearchQuery(req.IdpId)
if err != nil {
return nil, err
}
idps, err := s.query.IDPs(ctx, &query.IDPSearchQueries{
Queries: []query.SearchQuery{providerQuery},
}, true)
if err != nil {
return nil, err
}
idpQuery, err := query.NewIDPUserLinkIDPIDSearchQuery(req.IdpId)
if err != nil {
return nil, err
}
userLinks, err := s.query.IDPUserLinks(ctx, &query.IDPUserLinksSearchQuery{
Queries: []query.SearchQuery{idpQuery},
}, nil)
if err != nil {
return nil, err
}
objectDetails, err := s.command.RemoveDefaultIDPConfig(ctx, req.IdpId, idpsToDomain(idps.IDPs), idpUserLinksToDomain(userLinks.Links)...)
if err != nil {
return nil, err
}
return &admin_pb.RemoveIDPResponse{Details: object_pb.DomainToChangeDetailsPb(objectDetails)}, nil
}
func (s *Server) UpdateIDPOIDCConfig(ctx context.Context, req *admin_pb.UpdateIDPOIDCConfigRequest) (*admin_pb.UpdateIDPOIDCConfigResponse, error) {
config, err := s.command.ChangeDefaultIDPOIDCConfig(ctx, updateOIDCConfigToDomain(req))
if err != nil {
return nil, err
}
return &admin_pb.UpdateIDPOIDCConfigResponse{
Details: object_pb.ChangeToDetailsPb(
config.Sequence,
config.ChangeDate,
config.ResourceOwner,
),
}, nil
}
func (s *Server) UpdateIDPJWTConfig(ctx context.Context, req *admin_pb.UpdateIDPJWTConfigRequest) (*admin_pb.UpdateIDPJWTConfigResponse, error) {
config, err := s.command.ChangeDefaultIDPJWTConfig(ctx, updateJWTConfigToDomain(req))
if err != nil {
return nil, err
}
return &admin_pb.UpdateIDPJWTConfigResponse{
Details: object_pb.ChangeToDetailsPb(
config.Sequence,
config.ChangeDate,
config.ResourceOwner,
),
}, nil
}
func (s *Server) GetProviderByID(ctx context.Context, req *admin_pb.GetProviderByIDRequest) (*admin_pb.GetProviderByIDResponse, error) {
instanceIDQuery, err := query.NewIDPTemplateResourceOwnerSearchQuery(authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
idp, err := s.query.IDPTemplateByID(ctx, true, req.Id, false, nil, instanceIDQuery)
if err != nil {
return nil, err
}
return &admin_pb.GetProviderByIDResponse{Idp: idp_grpc.ProviderToPb(idp)}, nil
}
func (s *Server) ListProviders(ctx context.Context, req *admin_pb.ListProvidersRequest) (*admin_pb.ListProvidersResponse, error) {
queries, err := listProvidersToQuery(authz.GetInstance(ctx).InstanceID(), req)
if err != nil {
return nil, err
}
resp, err := s.query.IDPTemplates(ctx, queries, false)
if err != nil {
return nil, err
}
return &admin_pb.ListProvidersResponse{
Result: idp_grpc.ProvidersToPb(resp.Templates),
Details: object_pb.ToListDetails(resp.Count, resp.Sequence, resp.LastRun),
}, nil
}
func (s *Server) AddGenericOAuthProvider(ctx context.Context, req *admin_pb.AddGenericOAuthProviderRequest) (*admin_pb.AddGenericOAuthProviderResponse, error) {
id, details, err := s.command.AddInstanceGenericOAuthProvider(ctx, addGenericOAuthProviderToCommand(req))
if err != nil {
return nil, err
}
return &admin_pb.AddGenericOAuthProviderResponse{
Id: id,
Details: object_pb.DomainToAddDetailsPb(details),
}, nil
}
func (s *Server) UpdateGenericOAuthProvider(ctx context.Context, req *admin_pb.UpdateGenericOAuthProviderRequest) (*admin_pb.UpdateGenericOAuthProviderResponse, error) {
details, err := s.command.UpdateInstanceGenericOAuthProvider(ctx, req.Id, updateGenericOAuthProviderToCommand(req))
if err != nil {
return nil, err
}
return &admin_pb.UpdateGenericOAuthProviderResponse{
Details: object_pb.DomainToChangeDetailsPb(details),
}, nil
}
func (s *Server) AddGenericOIDCProvider(ctx context.Context, req *admin_pb.AddGenericOIDCProviderRequest) (*admin_pb.AddGenericOIDCProviderResponse, error) {
id, details, err := s.command.AddInstanceGenericOIDCProvider(ctx, addGenericOIDCProviderToCommand(req))
if err != nil {
return nil, err
}
return &admin_pb.AddGenericOIDCProviderResponse{
Id: id,
Details: object_pb.DomainToAddDetailsPb(details),
}, nil
}
func (s *Server) UpdateGenericOIDCProvider(ctx context.Context, req *admin_pb.UpdateGenericOIDCProviderRequest) (*admin_pb.UpdateGenericOIDCProviderResponse, error) {
details, err := s.command.UpdateInstanceGenericOIDCProvider(ctx, req.Id, updateGenericOIDCProviderToCommand(req))
if err != nil {
return nil, err
}
return &admin_pb.UpdateGenericOIDCProviderResponse{
Details: object_pb.DomainToChangeDetailsPb(details),
}, nil
}
func (s *Server) MigrateGenericOIDCProvider(ctx context.Context, req *admin_pb.MigrateGenericOIDCProviderRequest) (*admin_pb.MigrateGenericOIDCProviderResponse, error) {
var details *domain.ObjectDetails
var err error
if req.GetAzure() != nil {
details, err = s.command.MigrateInstanceGenericOIDCToAzureADProvider(ctx, req.GetId(), addAzureADProviderToCommand(req.GetAzure()))
} else if req.GetGoogle() != nil {
details, err = s.command.MigrateInstanceGenericOIDCToGoogleProvider(ctx, req.GetId(), addGoogleProviderToCommand(req.GetGoogle()))
}
if err != nil {
return nil, err
}
return &admin_pb.MigrateGenericOIDCProviderResponse{
Details: object_pb.DomainToAddDetailsPb(details),
}, nil
}
func (s *Server) AddJWTProvider(ctx context.Context, req *admin_pb.AddJWTProviderRequest) (*admin_pb.AddJWTProviderResponse, error) {
id, details, err := s.command.AddInstanceJWTProvider(ctx, addJWTProviderToCommand(req))
if err != nil {
return nil, err
}
return &admin_pb.AddJWTProviderResponse{
Id: id,
Details: object_pb.DomainToAddDetailsPb(details),
}, nil
}
func (s *Server) UpdateJWTProvider(ctx context.Context, req *admin_pb.UpdateJWTProviderRequest) (*admin_pb.UpdateJWTProviderResponse, error) {
details, err := s.command.UpdateInstanceJWTProvider(ctx, req.Id, updateJWTProviderToCommand(req))
if err != nil {
return nil, err
}
return &admin_pb.UpdateJWTProviderResponse{
Details: object_pb.DomainToChangeDetailsPb(details),
}, nil
}
func (s *Server) AddAzureADProvider(ctx context.Context, req *admin_pb.AddAzureADProviderRequest) (*admin_pb.AddAzureADProviderResponse, error) {
id, details, err := s.command.AddInstanceAzureADProvider(ctx, addAzureADProviderToCommand(req))
if err != nil {
return nil, err
}
return &admin_pb.AddAzureADProviderResponse{
Id: id,
Details: object_pb.DomainToAddDetailsPb(details),
}, nil
}
func (s *Server) UpdateAzureADProvider(ctx context.Context, req *admin_pb.UpdateAzureADProviderRequest) (*admin_pb.UpdateAzureADProviderResponse, error) {
details, err := s.command.UpdateInstanceAzureADProvider(ctx, req.Id, updateAzureADProviderToCommand(req))
if err != nil {
return nil, err
}
return &admin_pb.UpdateAzureADProviderResponse{
Details: object_pb.DomainToChangeDetailsPb(details),
}, nil
}
func (s *Server) AddGitHubProvider(ctx context.Context, req *admin_pb.AddGitHubProviderRequest) (*admin_pb.AddGitHubProviderResponse, error) {
id, details, err := s.command.AddInstanceGitHubProvider(ctx, addGitHubProviderToCommand(req))
if err != nil {
return nil, err
}
return &admin_pb.AddGitHubProviderResponse{
Id: id,
Details: object_pb.DomainToAddDetailsPb(details),
}, nil
}
func (s *Server) UpdateGitHubProvider(ctx context.Context, req *admin_pb.UpdateGitHubProviderRequest) (*admin_pb.UpdateGitHubProviderResponse, error) {
details, err := s.command.UpdateInstanceGitHubProvider(ctx, req.Id, updateGitHubProviderToCommand(req))
if err != nil {
return nil, err
}
return &admin_pb.UpdateGitHubProviderResponse{
Details: object_pb.DomainToChangeDetailsPb(details),
}, nil
}
func (s *Server) AddGitHubEnterpriseServerProvider(ctx context.Context, req *admin_pb.AddGitHubEnterpriseServerProviderRequest) (*admin_pb.AddGitHubEnterpriseServerProviderResponse, error) {
id, details, err := s.command.AddInstanceGitHubEnterpriseProvider(ctx, addGitHubEnterpriseProviderToCommand(req))
if err != nil {
return nil, err
}
return &admin_pb.AddGitHubEnterpriseServerProviderResponse{
Id: id,
Details: object_pb.DomainToAddDetailsPb(details),
}, nil
}
func (s *Server) UpdateGitHubEnterpriseServerProvider(ctx context.Context, req *admin_pb.UpdateGitHubEnterpriseServerProviderRequest) (*admin_pb.UpdateGitHubEnterpriseServerProviderResponse, error) {
details, err := s.command.UpdateInstanceGitHubEnterpriseProvider(ctx, req.Id, updateGitHubEnterpriseProviderToCommand(req))
if err != nil {
return nil, err
}
return &admin_pb.UpdateGitHubEnterpriseServerProviderResponse{
Details: object_pb.DomainToChangeDetailsPb(details),
}, nil
}
func (s *Server) AddGitLabProvider(ctx context.Context, req *admin_pb.AddGitLabProviderRequest) (*admin_pb.AddGitLabProviderResponse, error) {
id, details, err := s.command.AddInstanceGitLabProvider(ctx, addGitLabProviderToCommand(req))
if err != nil {
return nil, err
}
return &admin_pb.AddGitLabProviderResponse{
Id: id,
Details: object_pb.DomainToAddDetailsPb(details),
}, nil
}
func (s *Server) UpdateGitLabProvider(ctx context.Context, req *admin_pb.UpdateGitLabProviderRequest) (*admin_pb.UpdateGitLabProviderResponse, error) {
details, err := s.command.UpdateInstanceGitLabProvider(ctx, req.Id, updateGitLabProviderToCommand(req))
if err != nil {
return nil, err
}
return &admin_pb.UpdateGitLabProviderResponse{
Details: object_pb.DomainToChangeDetailsPb(details),
}, nil
}
func (s *Server) AddGitLabSelfHostedProvider(ctx context.Context, req *admin_pb.AddGitLabSelfHostedProviderRequest) (*admin_pb.AddGitLabSelfHostedProviderResponse, error) {
id, details, err := s.command.AddInstanceGitLabSelfHostedProvider(ctx, addGitLabSelfHostedProviderToCommand(req))
if err != nil {
return nil, err
}
return &admin_pb.AddGitLabSelfHostedProviderResponse{
Id: id,
Details: object_pb.DomainToAddDetailsPb(details),
}, nil
}
func (s *Server) UpdateGitLabSelfHostedProvider(ctx context.Context, req *admin_pb.UpdateGitLabSelfHostedProviderRequest) (*admin_pb.UpdateGitLabSelfHostedProviderResponse, error) {
details, err := s.command.UpdateInstanceGitLabSelfHostedProvider(ctx, req.Id, updateGitLabSelfHostedProviderToCommand(req))
if err != nil {
return nil, err
}
return &admin_pb.UpdateGitLabSelfHostedProviderResponse{
Details: object_pb.DomainToChangeDetailsPb(details),
}, nil
}
func (s *Server) AddGoogleProvider(ctx context.Context, req *admin_pb.AddGoogleProviderRequest) (*admin_pb.AddGoogleProviderResponse, error) {
id, details, err := s.command.AddInstanceGoogleProvider(ctx, addGoogleProviderToCommand(req))
if err != nil {
return nil, err
}
return &admin_pb.AddGoogleProviderResponse{
Id: id,
Details: object_pb.DomainToAddDetailsPb(details),
}, nil
}
func (s *Server) UpdateGoogleProvider(ctx context.Context, req *admin_pb.UpdateGoogleProviderRequest) (*admin_pb.UpdateGoogleProviderResponse, error) {
details, err := s.command.UpdateInstanceGoogleProvider(ctx, req.Id, updateGoogleProviderToCommand(req))
if err != nil {
return nil, err
}
return &admin_pb.UpdateGoogleProviderResponse{
Details: object_pb.DomainToChangeDetailsPb(details),
}, nil
}
func (s *Server) AddLDAPProvider(ctx context.Context, req *admin_pb.AddLDAPProviderRequest) (*admin_pb.AddLDAPProviderResponse, error) {
id, details, err := s.command.AddInstanceLDAPProvider(ctx, addLDAPProviderToCommand(req))
if err != nil {
return nil, err
}
return &admin_pb.AddLDAPProviderResponse{
Id: id,
Details: object_pb.DomainToAddDetailsPb(details),
}, nil
}
func (s *Server) UpdateLDAPProvider(ctx context.Context, req *admin_pb.UpdateLDAPProviderRequest) (*admin_pb.UpdateLDAPProviderResponse, error) {
details, err := s.command.UpdateInstanceLDAPProvider(ctx, req.Id, updateLDAPProviderToCommand(req))
if err != nil {
return nil, err
}
return &admin_pb.UpdateLDAPProviderResponse{
Details: object_pb.DomainToChangeDetailsPb(details),
}, nil
}
func (s *Server) AddAppleProvider(ctx context.Context, req *admin_pb.AddAppleProviderRequest) (*admin_pb.AddAppleProviderResponse, error) {
id, details, err := s.command.AddInstanceAppleProvider(ctx, addAppleProviderToCommand(req))
if err != nil {
return nil, err
}
return &admin_pb.AddAppleProviderResponse{
Id: id,
Details: object_pb.DomainToAddDetailsPb(details),
}, nil
}
func (s *Server) UpdateAppleProvider(ctx context.Context, req *admin_pb.UpdateAppleProviderRequest) (*admin_pb.UpdateAppleProviderResponse, error) {
details, err := s.command.UpdateInstanceAppleProvider(ctx, req.Id, updateAppleProviderToCommand(req))
if err != nil {
return nil, err
}
return &admin_pb.UpdateAppleProviderResponse{
Details: object_pb.DomainToChangeDetailsPb(details),
}, nil
}
func (s *Server) AddSAMLProvider(ctx context.Context, req *admin_pb.AddSAMLProviderRequest) (*admin_pb.AddSAMLProviderResponse, error) {
id, details, err := s.command.AddInstanceSAMLProvider(ctx, addSAMLProviderToCommand(req))
if err != nil {
return nil, err
}
return &admin_pb.AddSAMLProviderResponse{
Id: id,
Details: object_pb.DomainToAddDetailsPb(details),
}, nil
}
func (s *Server) UpdateSAMLProvider(ctx context.Context, req *admin_pb.UpdateSAMLProviderRequest) (*admin_pb.UpdateSAMLProviderResponse, error) {
details, err := s.command.UpdateInstanceSAMLProvider(ctx, req.Id, updateSAMLProviderToCommand(req))
if err != nil {
return nil, err
}
return &admin_pb.UpdateSAMLProviderResponse{
Details: object_pb.DomainToChangeDetailsPb(details),
}, nil
}
func (s *Server) RegenerateSAMLProviderCertificate(ctx context.Context, req *admin_pb.RegenerateSAMLProviderCertificateRequest) (*admin_pb.RegenerateSAMLProviderCertificateResponse, error) {
details, err := s.command.RegenerateInstanceSAMLProviderCertificate(ctx, req.Id)
if err != nil {
return nil, err
}
return &admin_pb.RegenerateSAMLProviderCertificateResponse{
Details: object_pb.DomainToChangeDetailsPb(details),
}, nil
}
func (s *Server) DeleteProvider(ctx context.Context, req *admin_pb.DeleteProviderRequest) (*admin_pb.DeleteProviderResponse, error) {
details, err := s.command.DeleteInstanceProvider(ctx, req.Id)
if err != nil {
return nil, err
}
return &admin_pb.DeleteProviderResponse{
Details: object_pb.DomainToChangeDetailsPb(details),
}, nil
}

View File

@@ -0,0 +1,527 @@
package admin
import (
"github.com/crewjam/saml"
"github.com/muhlemmer/gu"
idp_grpc "github.com/zitadel/zitadel/internal/api/grpc/idp"
"github.com/zitadel/zitadel/internal/api/grpc/object"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
idp_pb "github.com/zitadel/zitadel/pkg/grpc/idp"
)
func addOIDCIDPRequestToDomain(req *admin_pb.AddOIDCIDPRequest) *domain.IDPConfig {
return &domain.IDPConfig{
Name: req.Name,
OIDCConfig: addOIDCIDPRequestToDomainOIDCIDPConfig(req),
StylingType: idp_grpc.IDPStylingTypeToDomain(req.StylingType),
Type: domain.IDPConfigTypeOIDC,
AutoRegister: req.AutoRegister,
}
}
func addOIDCIDPRequestToDomainOIDCIDPConfig(req *admin_pb.AddOIDCIDPRequest) *domain.OIDCIDPConfig {
return &domain.OIDCIDPConfig{
ClientID: req.ClientId,
ClientSecretString: req.ClientSecret,
Issuer: req.Issuer,
Scopes: req.Scopes,
IDPDisplayNameMapping: idp_grpc.MappingFieldToDomain(req.DisplayNameMapping),
UsernameMapping: idp_grpc.MappingFieldToDomain(req.UsernameMapping),
}
}
func addJWTIDPRequestToDomain(req *admin_pb.AddJWTIDPRequest) *domain.IDPConfig {
return &domain.IDPConfig{
Name: req.Name,
JWTConfig: addJWTIDPRequestToDomainJWTIDPConfig(req),
StylingType: idp_grpc.IDPStylingTypeToDomain(req.StylingType),
Type: domain.IDPConfigTypeJWT,
AutoRegister: req.AutoRegister,
}
}
func addJWTIDPRequestToDomainJWTIDPConfig(req *admin_pb.AddJWTIDPRequest) *domain.JWTIDPConfig {
return &domain.JWTIDPConfig{
JWTEndpoint: req.JwtEndpoint,
Issuer: req.Issuer,
KeysEndpoint: req.KeysEndpoint,
HeaderName: req.HeaderName,
}
}
func updateIDPToDomain(req *admin_pb.UpdateIDPRequest) *domain.IDPConfig {
return &domain.IDPConfig{
IDPConfigID: req.IdpId,
Name: req.Name,
StylingType: idp_grpc.IDPStylingTypeToDomain(req.StylingType),
AutoRegister: req.AutoRegister,
}
}
func updateOIDCConfigToDomain(req *admin_pb.UpdateIDPOIDCConfigRequest) *domain.OIDCIDPConfig {
return &domain.OIDCIDPConfig{
IDPConfigID: req.IdpId,
ClientID: req.ClientId,
ClientSecretString: req.ClientSecret,
Issuer: req.Issuer,
Scopes: req.Scopes,
IDPDisplayNameMapping: idp_grpc.MappingFieldToDomain(req.DisplayNameMapping),
UsernameMapping: idp_grpc.MappingFieldToDomain(req.UsernameMapping),
}
}
func updateJWTConfigToDomain(req *admin_pb.UpdateIDPJWTConfigRequest) *domain.JWTIDPConfig {
return &domain.JWTIDPConfig{
IDPConfigID: req.IdpId,
JWTEndpoint: req.JwtEndpoint,
Issuer: req.Issuer,
KeysEndpoint: req.KeysEndpoint,
HeaderName: req.HeaderName,
}
}
func listIDPsToModel(instanceID string, req *admin_pb.ListIDPsRequest) (*query.IDPSearchQueries, error) {
offset, limit, asc := object.ListQueryToModel(req.Query)
queries, err := idpQueriesToModel(req.Queries)
if err != nil {
return nil, err
}
iamQuery, err := query.NewIDPResourceOwnerSearchQuery(instanceID)
if err != nil {
return nil, err
}
queries = append(queries, iamQuery)
return &query.IDPSearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
SortingColumn: idp_grpc.FieldNameToModel(req.SortingColumn),
},
Queries: queries,
}, nil
}
func idpQueriesToModel(queries []*admin_pb.IDPQuery) (q []query.SearchQuery, err error) {
q = make([]query.SearchQuery, len(queries))
for i, query := range queries {
q[i], err = idpQueryToModel(query)
if err != nil {
return nil, err
}
}
return q, nil
}
func idpQueryToModel(idpQuery *admin_pb.IDPQuery) (query.SearchQuery, error) {
switch q := idpQuery.Query.(type) {
case *admin_pb.IDPQuery_IdpNameQuery:
return query.NewIDPNameSearchQuery(object.TextMethodToQuery(q.IdpNameQuery.Method), q.IdpNameQuery.Name)
case *admin_pb.IDPQuery_IdpIdQuery:
return query.NewIDPIDSearchQuery(q.IdpIdQuery.Id)
default:
return nil, zerrors.ThrowInvalidArgument(nil, "ADMIN-VmqQu", "List.Query.Invalid")
}
}
func idpsToDomain(idps []*query.IDP) []*domain.IDPProvider {
idpProvider := make([]*domain.IDPProvider, len(idps))
for i, idp := range idps {
idpProvider[i] = &domain.IDPProvider{
ObjectRoot: models.ObjectRoot{
AggregateID: idp.ResourceOwner,
},
IDPConfigID: idp.ID,
Type: idp.OwnerType,
}
}
return idpProvider
}
func idpUserLinksToDomain(idps []*query.IDPUserLink) []*domain.UserIDPLink {
externalIDPs := make([]*domain.UserIDPLink, len(idps))
for i, idp := range idps {
externalIDPs[i] = &domain.UserIDPLink{
ObjectRoot: models.ObjectRoot{
AggregateID: idp.UserID,
ResourceOwner: idp.ResourceOwner,
},
IDPConfigID: idp.IDPID,
ExternalUserID: idp.ProvidedUserID,
DisplayName: idp.ProvidedUsername,
}
}
return externalIDPs
}
func listProvidersToQuery(instanceID string, req *admin_pb.ListProvidersRequest) (*query.IDPTemplateSearchQueries, error) {
offset, limit, asc := object.ListQueryToModel(req.Query)
queries, err := providerQueriesToQuery(req.Queries)
if err != nil {
return nil, err
}
iamQuery, err := query.NewIDPTemplateResourceOwnerSearchQuery(instanceID)
if err != nil {
return nil, err
}
queries = append(queries, iamQuery)
return &query.IDPTemplateSearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
},
Queries: queries,
}, nil
}
func providerQueriesToQuery(queries []*admin_pb.ProviderQuery) (q []query.SearchQuery, err error) {
q = make([]query.SearchQuery, len(queries))
for i, query := range queries {
q[i], err = providerQueryToQuery(query)
if err != nil {
return nil, err
}
}
return q, nil
}
func providerQueryToQuery(idpQuery *admin_pb.ProviderQuery) (query.SearchQuery, error) {
switch q := idpQuery.Query.(type) {
case *admin_pb.ProviderQuery_IdpNameQuery:
return query.NewIDPTemplateNameSearchQuery(object.TextMethodToQuery(q.IdpNameQuery.Method), q.IdpNameQuery.Name)
case *admin_pb.ProviderQuery_IdpIdQuery:
return query.NewIDPTemplateIDSearchQuery(q.IdpIdQuery.Id)
default:
return nil, zerrors.ThrowInvalidArgument(nil, "ADMIN-Dr2aa", "List.Query.Invalid")
}
}
func addGenericOAuthProviderToCommand(req *admin_pb.AddGenericOAuthProviderRequest) command.GenericOAuthProvider {
return command.GenericOAuthProvider{
Name: req.Name,
ClientID: req.ClientId,
ClientSecret: req.ClientSecret,
AuthorizationEndpoint: req.AuthorizationEndpoint,
TokenEndpoint: req.TokenEndpoint,
UserEndpoint: req.UserEndpoint,
Scopes: req.Scopes,
IDAttribute: req.IdAttribute,
UsePKCE: req.UsePkce,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func updateGenericOAuthProviderToCommand(req *admin_pb.UpdateGenericOAuthProviderRequest) command.GenericOAuthProvider {
return command.GenericOAuthProvider{
Name: req.Name,
ClientID: req.ClientId,
ClientSecret: req.ClientSecret,
AuthorizationEndpoint: req.AuthorizationEndpoint,
TokenEndpoint: req.TokenEndpoint,
UserEndpoint: req.UserEndpoint,
Scopes: req.Scopes,
IDAttribute: req.IdAttribute,
UsePKCE: req.UsePkce,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func addGenericOIDCProviderToCommand(req *admin_pb.AddGenericOIDCProviderRequest) command.GenericOIDCProvider {
return command.GenericOIDCProvider{
Name: req.Name,
Issuer: req.Issuer,
ClientID: req.ClientId,
ClientSecret: req.ClientSecret,
Scopes: req.Scopes,
IsIDTokenMapping: req.IsIdTokenMapping,
UsePKCE: req.UsePkce,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func updateGenericOIDCProviderToCommand(req *admin_pb.UpdateGenericOIDCProviderRequest) command.GenericOIDCProvider {
return command.GenericOIDCProvider{
Name: req.Name,
Issuer: req.Issuer,
ClientID: req.ClientId,
ClientSecret: req.ClientSecret,
Scopes: req.Scopes,
IsIDTokenMapping: req.IsIdTokenMapping,
UsePKCE: req.UsePkce,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func addJWTProviderToCommand(req *admin_pb.AddJWTProviderRequest) command.JWTProvider {
return command.JWTProvider{
Name: req.Name,
Issuer: req.Issuer,
JWTEndpoint: req.JwtEndpoint,
KeyEndpoint: req.KeysEndpoint,
HeaderName: req.HeaderName,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func updateJWTProviderToCommand(req *admin_pb.UpdateJWTProviderRequest) command.JWTProvider {
return command.JWTProvider{
Name: req.Name,
Issuer: req.Issuer,
JWTEndpoint: req.JwtEndpoint,
KeyEndpoint: req.KeysEndpoint,
HeaderName: req.HeaderName,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func addAzureADProviderToCommand(req *admin_pb.AddAzureADProviderRequest) command.AzureADProvider {
return command.AzureADProvider{
Name: req.Name,
ClientID: req.ClientId,
ClientSecret: req.ClientSecret,
Scopes: req.Scopes,
Tenant: idp_grpc.AzureADTenantToCommand(req.Tenant),
EmailVerified: req.EmailVerified,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func updateAzureADProviderToCommand(req *admin_pb.UpdateAzureADProviderRequest) command.AzureADProvider {
return command.AzureADProvider{
Name: req.Name,
ClientID: req.ClientId,
ClientSecret: req.ClientSecret,
Scopes: req.Scopes,
Tenant: idp_grpc.AzureADTenantToCommand(req.Tenant),
EmailVerified: req.EmailVerified,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func addGitHubProviderToCommand(req *admin_pb.AddGitHubProviderRequest) command.GitHubProvider {
return command.GitHubProvider{
Name: req.Name,
ClientID: req.ClientId,
ClientSecret: req.ClientSecret,
Scopes: req.Scopes,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func updateGitHubProviderToCommand(req *admin_pb.UpdateGitHubProviderRequest) command.GitHubProvider {
return command.GitHubProvider{
Name: req.Name,
ClientID: req.ClientId,
ClientSecret: req.ClientSecret,
Scopes: req.Scopes,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func addGitHubEnterpriseProviderToCommand(req *admin_pb.AddGitHubEnterpriseServerProviderRequest) command.GitHubEnterpriseProvider {
return command.GitHubEnterpriseProvider{
Name: req.Name,
ClientID: req.ClientId,
ClientSecret: req.ClientSecret,
AuthorizationEndpoint: req.AuthorizationEndpoint,
TokenEndpoint: req.TokenEndpoint,
UserEndpoint: req.UserEndpoint,
Scopes: req.Scopes,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func updateGitHubEnterpriseProviderToCommand(req *admin_pb.UpdateGitHubEnterpriseServerProviderRequest) command.GitHubEnterpriseProvider {
return command.GitHubEnterpriseProvider{
Name: req.Name,
ClientID: req.ClientId,
ClientSecret: req.ClientSecret,
AuthorizationEndpoint: req.AuthorizationEndpoint,
TokenEndpoint: req.TokenEndpoint,
UserEndpoint: req.UserEndpoint,
Scopes: req.Scopes,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func addGitLabProviderToCommand(req *admin_pb.AddGitLabProviderRequest) command.GitLabProvider {
return command.GitLabProvider{
Name: req.Name,
ClientID: req.ClientId,
ClientSecret: req.ClientSecret,
Scopes: req.Scopes,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func updateGitLabProviderToCommand(req *admin_pb.UpdateGitLabProviderRequest) command.GitLabProvider {
return command.GitLabProvider{
Name: req.Name,
ClientID: req.ClientId,
ClientSecret: req.ClientSecret,
Scopes: req.Scopes,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func addGitLabSelfHostedProviderToCommand(req *admin_pb.AddGitLabSelfHostedProviderRequest) command.GitLabSelfHostedProvider {
return command.GitLabSelfHostedProvider{
Name: req.Name,
Issuer: req.Issuer,
ClientID: req.ClientId,
ClientSecret: req.ClientSecret,
Scopes: req.Scopes,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func updateGitLabSelfHostedProviderToCommand(req *admin_pb.UpdateGitLabSelfHostedProviderRequest) command.GitLabSelfHostedProvider {
return command.GitLabSelfHostedProvider{
Name: req.Name,
Issuer: req.Issuer,
ClientID: req.ClientId,
ClientSecret: req.ClientSecret,
Scopes: req.Scopes,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func addGoogleProviderToCommand(req *admin_pb.AddGoogleProviderRequest) command.GoogleProvider {
return command.GoogleProvider{
Name: req.Name,
ClientID: req.ClientId,
ClientSecret: req.ClientSecret,
Scopes: req.Scopes,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func updateGoogleProviderToCommand(req *admin_pb.UpdateGoogleProviderRequest) command.GoogleProvider {
return command.GoogleProvider{
Name: req.Name,
ClientID: req.ClientId,
ClientSecret: req.ClientSecret,
Scopes: req.Scopes,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func addLDAPProviderToCommand(req *admin_pb.AddLDAPProviderRequest) command.LDAPProvider {
return command.LDAPProvider{
Name: req.Name,
Servers: req.Servers,
StartTLS: req.StartTls,
BaseDN: req.BaseDn,
BindDN: req.BindDn,
BindPassword: req.BindPassword,
UserBase: req.UserBase,
UserObjectClasses: req.UserObjectClasses,
UserFilters: req.UserFilters,
Timeout: req.Timeout.AsDuration(),
RootCA: req.RootCa,
LDAPAttributes: idp_grpc.LDAPAttributesToCommand(req.Attributes),
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func updateLDAPProviderToCommand(req *admin_pb.UpdateLDAPProviderRequest) command.LDAPProvider {
return command.LDAPProvider{
Name: req.Name,
Servers: req.Servers,
StartTLS: req.StartTls,
BaseDN: req.BaseDn,
BindDN: req.BindDn,
BindPassword: req.BindPassword,
UserBase: req.UserBase,
UserObjectClasses: req.UserObjectClasses,
UserFilters: req.UserFilters,
Timeout: req.Timeout.AsDuration(),
LDAPAttributes: idp_grpc.LDAPAttributesToCommand(req.Attributes),
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
RootCA: req.RootCa,
}
}
func addAppleProviderToCommand(req *admin_pb.AddAppleProviderRequest) command.AppleProvider {
return command.AppleProvider{
Name: req.Name,
ClientID: req.ClientId,
TeamID: req.TeamId,
KeyID: req.KeyId,
PrivateKey: req.PrivateKey,
Scopes: req.Scopes,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func updateAppleProviderToCommand(req *admin_pb.UpdateAppleProviderRequest) command.AppleProvider {
return command.AppleProvider{
Name: req.Name,
ClientID: req.ClientId,
TeamID: req.TeamId,
KeyID: req.KeyId,
PrivateKey: req.PrivateKey,
Scopes: req.Scopes,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func addSAMLProviderToCommand(req *admin_pb.AddSAMLProviderRequest) *command.SAMLProvider {
var nameIDFormat *domain.SAMLNameIDFormat
if req.NameIdFormat != nil {
nameIDFormat = gu.Ptr(idp_grpc.SAMLNameIDFormatToDomain(req.GetNameIdFormat()))
}
return &command.SAMLProvider{
Name: req.Name,
Metadata: req.GetMetadataXml(),
MetadataURL: req.GetMetadataUrl(),
Binding: bindingToCommand(req.Binding),
WithSignedRequest: req.WithSignedRequest,
NameIDFormat: nameIDFormat,
TransientMappingAttributeName: req.GetTransientMappingAttributeName(),
FederatedLogoutEnabled: req.GetFederatedLogoutEnabled(),
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func updateSAMLProviderToCommand(req *admin_pb.UpdateSAMLProviderRequest) *command.SAMLProvider {
var nameIDFormat *domain.SAMLNameIDFormat
if req.NameIdFormat != nil {
nameIDFormat = gu.Ptr(idp_grpc.SAMLNameIDFormatToDomain(req.GetNameIdFormat()))
}
return &command.SAMLProvider{
Name: req.Name,
Metadata: req.GetMetadataXml(),
MetadataURL: req.GetMetadataUrl(),
Binding: bindingToCommand(req.Binding),
WithSignedRequest: req.WithSignedRequest,
NameIDFormat: nameIDFormat,
TransientMappingAttributeName: req.GetTransientMappingAttributeName(),
FederatedLogoutEnabled: req.GetFederatedLogoutEnabled(),
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func bindingToCommand(binding idp_pb.SAMLBinding) string {
switch binding {
case idp_pb.SAMLBinding_SAML_BINDING_UNSPECIFIED:
return ""
case idp_pb.SAMLBinding_SAML_BINDING_POST:
return saml.HTTPPostBinding
case idp_pb.SAMLBinding_SAML_BINDING_REDIRECT:
return saml.HTTPRedirectBinding
case idp_pb.SAMLBinding_SAML_BINDING_ARTIFACT:
return saml.HTTPArtifactBinding
default:
return ""
}
}

View File

@@ -0,0 +1,159 @@
package admin
import (
"testing"
"github.com/zitadel/zitadel/internal/test"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
"github.com/zitadel/zitadel/pkg/grpc/idp"
)
func Test_addOIDCIDPRequestToDomain(t *testing.T) {
type args struct {
req *admin_pb.AddOIDCIDPRequest
}
tests := []struct {
name string
args args
}{
{
name: "all fields filled",
args: args{
req: &admin_pb.AddOIDCIDPRequest{
Name: "ZITADEL",
StylingType: idp.IDPStylingType_STYLING_TYPE_GOOGLE,
ClientId: "test1234",
ClientSecret: "test4321",
Issuer: "zitadel.ch",
Scopes: []string{"email", "profile"},
DisplayNameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_EMAIL,
UsernameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_PREFERRED_USERNAME,
AutoRegister: true,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := addOIDCIDPRequestToDomain(tt.args.req)
test.AssertFieldsMapped(t, got,
"ObjectRoot",
"OIDCConfig.ClientSecret",
"OIDCConfig.ObjectRoot",
"OIDCConfig.IDPConfigID",
"IDPConfigID",
"State",
"OIDCConfig.AuthorizationEndpoint",
"OIDCConfig.TokenEndpoint",
"Type",
"JWTConfig",
)
})
}
}
func Test_addOIDCIDPRequestToDomainOIDCIDPConfig(t *testing.T) {
type args struct {
req *admin_pb.AddOIDCIDPRequest
}
tests := []struct {
name string
args args
}{
{
name: "all fields filled",
args: args{
req: &admin_pb.AddOIDCIDPRequest{
ClientId: "test1234",
ClientSecret: "test4321",
Issuer: "zitadel.ch",
Scopes: []string{"email", "profile"},
DisplayNameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_EMAIL,
UsernameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_PREFERRED_USERNAME,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := addOIDCIDPRequestToDomainOIDCIDPConfig(tt.args.req)
test.AssertFieldsMapped(t, got,
"ObjectRoot",
"ClientSecret",
"IDPConfigID",
"AuthorizationEndpoint",
"TokenEndpoint",
)
})
}
}
func Test_updateIDPToDomain(t *testing.T) {
type args struct {
req *admin_pb.UpdateIDPRequest
}
tests := []struct {
name string
args args
}{
{
name: "all fields filled",
args: args{
req: &admin_pb.UpdateIDPRequest{
IdpId: "13523",
Name: "new name",
StylingType: idp.IDPStylingType_STYLING_TYPE_GOOGLE,
AutoRegister: true,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := updateIDPToDomain(tt.args.req)
test.AssertFieldsMapped(t, got,
"ObjectRoot",
"OIDCConfig",
"JWTConfig",
"State",
"Type",
)
})
}
}
func Test_updateOIDCConfigToDomain(t *testing.T) {
type args struct {
req *admin_pb.UpdateIDPOIDCConfigRequest
}
tests := []struct {
name string
args args
}{
{
name: "all fields filled",
args: args{
req: &admin_pb.UpdateIDPOIDCConfigRequest{
IdpId: "4208",
Issuer: "zitadel.ch",
ClientId: "ZITEADEL",
ClientSecret: "i'm so secret",
Scopes: []string{"profile"},
DisplayNameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_EMAIL,
UsernameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_PREFERRED_USERNAME,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := updateOIDCConfigToDomain(tt.args.req)
test.AssertFieldsMapped(t, got,
"ObjectRoot",
"ClientSecret",
"AuthorizationEndpoint",
"TokenEndpoint",
)
})
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
package admin
import (
"context"
"github.com/zitadel/zitadel/pkg/grpc/admin"
)
func (s *Server) Healthz(context.Context, *admin.HealthzRequest) (*admin.HealthzResponse, error) {
return &admin.HealthzResponse{}, nil
}

View File

@@ -0,0 +1,80 @@
package admin
import (
"context"
instance_grpc "github.com/zitadel/zitadel/internal/api/grpc/instance"
"github.com/zitadel/zitadel/internal/api/grpc/object"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
)
func (s *Server) GetMyInstance(ctx context.Context, _ *admin_pb.GetMyInstanceRequest) (*admin_pb.GetMyInstanceResponse, error) {
instance, err := s.query.Instance(ctx, true)
if err != nil {
return nil, err
}
return &admin_pb.GetMyInstanceResponse{
Instance: instance_grpc.InstanceDetailToPb(instance),
}, nil
}
func (s *Server) ListInstanceDomains(ctx context.Context, req *admin_pb.ListInstanceDomainsRequest) (*admin_pb.ListInstanceDomainsResponse, error) {
queries, err := ListInstanceDomainsRequestToModel(req)
if err != nil {
return nil, err
}
domains, err := s.query.SearchInstanceDomains(ctx, queries)
if err != nil {
return nil, err
}
return &admin_pb.ListInstanceDomainsResponse{
Result: instance_grpc.DomainsToPb(domains.Domains),
SortingColumn: req.SortingColumn,
Details: object.ToListDetails(
domains.Count,
domains.Sequence,
domains.LastRun,
),
}, nil
}
func (s *Server) ListInstanceTrustedDomains(ctx context.Context, req *admin_pb.ListInstanceTrustedDomainsRequest) (*admin_pb.ListInstanceTrustedDomainsResponse, error) {
queries, err := ListInstanceTrustedDomainsRequestToModel(req)
if err != nil {
return nil, err
}
domains, err := s.query.SearchInstanceTrustedDomains(ctx, queries)
if err != nil {
return nil, err
}
return &admin_pb.ListInstanceTrustedDomainsResponse{
Result: instance_grpc.TrustedDomainsToPb(domains.Domains),
SortingColumn: req.SortingColumn,
Details: object.ToListDetails(
domains.Count,
domains.Sequence,
domains.LastRun,
),
}, nil
}
func (s *Server) AddInstanceTrustedDomain(ctx context.Context, req *admin_pb.AddInstanceTrustedDomainRequest) (*admin_pb.AddInstanceTrustedDomainResponse, error) {
details, err := s.command.AddTrustedDomain(ctx, req.Domain)
if err != nil {
return nil, err
}
return &admin_pb.AddInstanceTrustedDomainResponse{
Details: object.DomainToAddDetailsPb(details),
}, nil
}
func (s *Server) RemoveInstanceTrustedDomain(ctx context.Context, req *admin_pb.RemoveInstanceTrustedDomainRequest) (*admin_pb.RemoveInstanceTrustedDomainResponse, error) {
details, err := s.command.RemoveTrustedDomain(ctx, req.Domain)
if err != nil {
return nil, err
}
return &admin_pb.RemoveInstanceTrustedDomainResponse{
Details: object.DomainToChangeDetailsPb(details),
}, nil
}

View File

@@ -0,0 +1,73 @@
package admin
import (
instance_grpc "github.com/zitadel/zitadel/internal/api/grpc/instance"
"github.com/zitadel/zitadel/internal/api/grpc/object"
"github.com/zitadel/zitadel/internal/query"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
"github.com/zitadel/zitadel/pkg/grpc/instance"
)
func ListInstanceDomainsRequestToModel(req *admin_pb.ListInstanceDomainsRequest) (*query.InstanceDomainSearchQueries, error) {
offset, limit, asc := object.ListQueryToModel(req.Query)
queries, err := instance_grpc.DomainQueriesToModel(req.Queries)
if err != nil {
return nil, err
}
return &query.InstanceDomainSearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
SortingColumn: fieldNameToInstanceDomainColumn(req.SortingColumn),
},
Queries: queries,
}, nil
}
func fieldNameToInstanceDomainColumn(fieldName instance.DomainFieldName) query.Column {
switch fieldName {
case instance.DomainFieldName_DOMAIN_FIELD_NAME_DOMAIN:
return query.InstanceDomainDomainCol
case instance.DomainFieldName_DOMAIN_FIELD_NAME_PRIMARY:
return query.InstanceDomainIsPrimaryCol
case instance.DomainFieldName_DOMAIN_FIELD_NAME_GENERATED:
return query.InstanceDomainIsGeneratedCol
case instance.DomainFieldName_DOMAIN_FIELD_NAME_CREATION_DATE:
return query.InstanceDomainCreationDateCol
default:
return query.Column{}
}
}
func ListInstanceTrustedDomainsRequestToModel(req *admin_pb.ListInstanceTrustedDomainsRequest) (*query.InstanceTrustedDomainSearchQueries, error) {
offset, limit, asc := object.ListQueryToModel(req.Query)
queries, err := instance_grpc.TrustedDomainQueriesToModel(req.Queries)
if err != nil {
return nil, err
}
return &query.InstanceTrustedDomainSearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
SortingColumn: fieldNameToInstanceTrustedDomainColumn(req.SortingColumn),
},
Queries: queries,
}, nil
}
func fieldNameToInstanceTrustedDomainColumn(fieldName instance.DomainFieldName) query.Column {
switch fieldName {
case instance.DomainFieldName_DOMAIN_FIELD_NAME_DOMAIN:
return query.InstanceTrustedDomainDomainCol
case instance.DomainFieldName_DOMAIN_FIELD_NAME_CREATION_DATE:
return query.InstanceTrustedDomainCreationDateCol
case instance.DomainFieldName_DOMAIN_FIELD_NAME_UNSPECIFIED,
instance.DomainFieldName_DOMAIN_FIELD_NAME_PRIMARY,
instance.DomainFieldName_DOMAIN_FIELD_NAME_GENERATED:
return query.InstanceTrustedDomainCreationDateCol
default:
return query.Column{}
}
}

View File

@@ -0,0 +1,328 @@
//go:build integration
package admin_test
import (
"context"
"testing"
"time"
"github.com/brianvoe/gofakeit/v6"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
"github.com/zitadel/zitadel/pkg/grpc/member"
"github.com/zitadel/zitadel/pkg/grpc/object"
)
var iamRoles = []string{
"IAM_OWNER",
"IAM_OWNER_VIEWER",
"IAM_ORG_MANAGER",
"IAM_USER_MANAGER",
"IAM_ADMIN_IMPERSONATOR",
"IAM_END_USER_IMPERSONATOR",
"IAM_LOGIN_CLIENT",
}
func TestServer_ListIAMMemberRoles(t *testing.T) {
got, err := Client.ListIAMMemberRoles(AdminCTX, &admin_pb.ListIAMMemberRolesRequest{})
assert.NoError(t, err)
assert.ElementsMatch(t, iamRoles, got.GetRoles())
}
func TestServer_ListIAMMembers(t *testing.T) {
user := Instance.CreateHumanUserVerified(AdminCTX, Instance.DefaultOrg.Id, gofakeit.Email(), gofakeit.Phone())
_, err := Client.AddIAMMember(AdminCTX, &admin_pb.AddIAMMemberRequest{
UserId: user.GetUserId(),
Roles: iamRoles,
})
require.NoError(t, err)
type args struct {
ctx context.Context
req *admin_pb.ListIAMMembersRequest
}
tests := []struct {
name string
args args
want *admin_pb.ListIAMMembersResponse
wantErr bool
}{
{
name: "permission error",
args: args{
ctx: Instance.WithAuthorization(CTX, integration.UserTypeOrgOwner),
req: &admin_pb.ListIAMMembersRequest{
Query: &object.ListQuery{},
Queries: []*member.SearchQuery{{
Query: &member.SearchQuery_UserIdQuery{
UserIdQuery: &member.UserIDQuery{
UserId: user.GetUserId(),
},
},
}},
},
},
wantErr: true,
},
{
name: "success",
args: args{
ctx: AdminCTX,
req: &admin_pb.ListIAMMembersRequest{
Query: &object.ListQuery{},
Queries: []*member.SearchQuery{{
Query: &member.SearchQuery_UserIdQuery{
UserIdQuery: &member.UserIDQuery{
UserId: user.GetUserId(),
},
},
}},
},
},
want: &admin_pb.ListIAMMembersResponse{
Result: []*member.Member{{
UserId: user.GetUserId(),
Roles: iamRoles,
}},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.args.ctx, time.Minute)
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
got, err := Client.ListIAMMembers(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(ct, err)
return
}
require.NoError(ct, err)
wantResult := tt.want.GetResult()
gotResult := got.GetResult()
if assert.Len(ct, gotResult, len(wantResult)) {
for i, want := range wantResult {
assert.Equal(ct, want.GetUserId(), gotResult[i].GetUserId())
assert.ElementsMatch(ct, want.GetRoles(), gotResult[i].GetRoles())
}
}
}, retryDuration, tick)
})
}
}
func TestServer_AddIAMMember(t *testing.T) {
user := Instance.CreateHumanUserVerified(AdminCTX, Instance.DefaultOrg.Id, gofakeit.Email(), gofakeit.Phone())
type args struct {
ctx context.Context
req *admin_pb.AddIAMMemberRequest
}
tests := []struct {
name string
args args
want *admin_pb.AddIAMMemberResponse
wantErr bool
}{
{
name: "permission error",
args: args{
ctx: Instance.WithAuthorization(CTX, integration.UserTypeOrgOwner),
req: &admin_pb.AddIAMMemberRequest{
UserId: user.GetUserId(),
Roles: iamRoles,
},
},
wantErr: true,
},
{
name: "success",
args: args{
ctx: AdminCTX,
req: &admin_pb.AddIAMMemberRequest{
UserId: user.GetUserId(),
Roles: iamRoles,
},
},
want: &admin_pb.AddIAMMemberResponse{
Details: &object.ObjectDetails{
ResourceOwner: Instance.ID(),
},
},
},
{
name: "unknown roles error",
args: args{
ctx: AdminCTX,
req: &admin_pb.AddIAMMemberRequest{
UserId: user.GetUserId(),
Roles: []string{"FOO", "BAR"},
},
},
wantErr: true,
},
{
name: "org role error",
args: args{
ctx: AdminCTX,
req: &admin_pb.AddIAMMemberRequest{
UserId: user.GetUserId(),
Roles: []string{"ORG_OWNER"},
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.AddIAMMember(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
})
}
}
func TestServer_UpdateIAMMember(t *testing.T) {
user := Instance.CreateHumanUserVerified(AdminCTX, Instance.DefaultOrg.Id, gofakeit.Email(), gofakeit.Phone())
_, err := Client.AddIAMMember(AdminCTX, &admin_pb.AddIAMMemberRequest{
UserId: user.GetUserId(),
Roles: []string{"IAM_OWNER"},
})
require.NoError(t, err)
type args struct {
ctx context.Context
req *admin_pb.UpdateIAMMemberRequest
}
tests := []struct {
name string
args args
want *admin_pb.UpdateIAMMemberResponse
wantErr bool
}{
{
name: "permission error",
args: args{
ctx: Instance.WithAuthorization(CTX, integration.UserTypeOrgOwner),
req: &admin_pb.UpdateIAMMemberRequest{
UserId: user.GetUserId(),
Roles: iamRoles,
},
},
wantErr: true,
},
{
name: "success",
args: args{
ctx: AdminCTX,
req: &admin_pb.UpdateIAMMemberRequest{
UserId: user.GetUserId(),
Roles: iamRoles,
},
},
want: &admin_pb.UpdateIAMMemberResponse{
Details: &object.ObjectDetails{
ResourceOwner: Instance.ID(),
ChangeDate: timestamppb.Now(),
},
},
},
{
name: "unknown roles error",
args: args{
ctx: AdminCTX,
req: &admin_pb.UpdateIAMMemberRequest{
UserId: user.GetUserId(),
Roles: []string{"FOO", "BAR"},
},
},
wantErr: true,
},
{
name: "org role error",
args: args{
ctx: AdminCTX,
req: &admin_pb.UpdateIAMMemberRequest{
UserId: user.GetUserId(),
Roles: []string{"ORG_OWNER"},
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.UpdateIAMMember(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
})
}
}
func TestServer_RemoveIAMMember(t *testing.T) {
user := Instance.CreateHumanUserVerified(AdminCTX, Instance.DefaultOrg.Id, gofakeit.Email(), gofakeit.Phone())
_, err := Client.AddIAMMember(AdminCTX, &admin_pb.AddIAMMemberRequest{
UserId: user.GetUserId(),
Roles: []string{"IAM_OWNER"},
})
require.NoError(t, err)
type args struct {
ctx context.Context
req *admin_pb.RemoveIAMMemberRequest
}
tests := []struct {
name string
args args
want *admin_pb.RemoveIAMMemberResponse
wantErr bool
}{
{
name: "permission error",
args: args{
ctx: Instance.WithAuthorization(CTX, integration.UserTypeOrgOwner),
req: &admin_pb.RemoveIAMMemberRequest{
UserId: user.GetUserId(),
},
},
wantErr: true,
},
{
name: "success",
args: args{
ctx: AdminCTX,
req: &admin_pb.RemoveIAMMemberRequest{
UserId: user.GetUserId(),
},
},
want: &admin_pb.RemoveIAMMemberResponse{
Details: &object.ObjectDetails{
ResourceOwner: Instance.ID(),
ChangeDate: timestamppb.Now(),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.RemoveIAMMember(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
})
}
}

View File

@@ -0,0 +1,172 @@
//go:build integration
package admin_test
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
"github.com/zitadel/zitadel/pkg/grpc/object"
"github.com/zitadel/zitadel/pkg/grpc/settings"
)
func TestServer_GetSecurityPolicy(t *testing.T) {
instance := integration.NewInstance(CTX)
adminCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
_, err := instance.Client.Admin.SetSecurityPolicy(adminCtx, &admin_pb.SetSecurityPolicyRequest{
EnableIframeEmbedding: true,
AllowedOrigins: []string{"foo.com", "bar.com"},
EnableImpersonation: true,
})
require.NoError(t, err)
tests := []struct {
name string
ctx context.Context
want *admin_pb.GetSecurityPolicyResponse
wantErr bool
}{
{
name: "permission error",
ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner),
wantErr: true,
},
{
name: "success",
ctx: adminCtx,
want: &admin_pb.GetSecurityPolicyResponse{
Policy: &settings.SecurityPolicy{
EnableIframeEmbedding: true,
AllowedOrigins: []string{"foo.com", "bar.com"},
EnableImpersonation: true,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.ctx, time.Minute)
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
resp, err := instance.Client.Admin.GetSecurityPolicy(tt.ctx, &admin_pb.GetSecurityPolicyRequest{})
if tt.wantErr {
require.Error(ttt, err)
return
}
require.NoError(ttt, err)
got, want := resp.GetPolicy(), tt.want.GetPolicy()
assert.Equal(ttt, want.GetEnableIframeEmbedding(), got.GetEnableIframeEmbedding(), "enable iframe embedding")
assert.Equal(ttt, want.GetAllowedOrigins(), got.GetAllowedOrigins(), "allowed origins")
assert.Equal(ttt, want.GetEnableImpersonation(), got.GetEnableImpersonation(), "enable impersonation")
}, retryDuration, tick, "timeout waiting for expected target result")
})
}
}
func TestServer_SetSecurityPolicy(t *testing.T) {
instance := integration.NewInstance(CTX)
adminCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
type args struct {
ctx context.Context
req *admin_pb.SetSecurityPolicyRequest
}
tests := []struct {
name string
args args
want *admin_pb.SetSecurityPolicyResponse
wantErr bool
}{
{
name: "permission error",
args: args{
ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner),
req: &admin_pb.SetSecurityPolicyRequest{
EnableIframeEmbedding: true,
AllowedOrigins: []string{"foo.com", "bar.com"},
EnableImpersonation: true,
},
},
wantErr: true,
},
{
name: "success allowed origins",
args: args{
ctx: adminCtx,
req: &admin_pb.SetSecurityPolicyRequest{
AllowedOrigins: []string{"foo.com", "bar.com"},
},
},
want: &admin_pb.SetSecurityPolicyResponse{
Details: &object.ObjectDetails{
ChangeDate: timestamppb.Now(),
ResourceOwner: instance.ID(),
},
},
},
{
name: "success iframe embedding",
args: args{
ctx: adminCtx,
req: &admin_pb.SetSecurityPolicyRequest{
EnableIframeEmbedding: true,
},
},
want: &admin_pb.SetSecurityPolicyResponse{
Details: &object.ObjectDetails{
ChangeDate: timestamppb.Now(),
ResourceOwner: instance.ID(),
},
},
},
{
name: "success impersonation",
args: args{
ctx: adminCtx,
req: &admin_pb.SetSecurityPolicyRequest{
EnableImpersonation: true,
},
},
want: &admin_pb.SetSecurityPolicyResponse{
Details: &object.ObjectDetails{
ChangeDate: timestamppb.Now(),
ResourceOwner: instance.ID(),
},
},
},
{
name: "success all",
args: args{
ctx: adminCtx,
req: &admin_pb.SetSecurityPolicyRequest{
EnableIframeEmbedding: true,
AllowedOrigins: []string{"foo.com", "bar.com"},
EnableImpersonation: true,
},
},
want: &admin_pb.SetSecurityPolicyResponse{
Details: &object.ObjectDetails{
ChangeDate: timestamppb.Now(),
ResourceOwner: instance.ID(),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := instance.Client.Admin.SetSecurityPolicy(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
})
}
}

View File

@@ -0,0 +1,515 @@
//go:build integration
package admin_test
import (
"testing"
"time"
"github.com/brianvoe/gofakeit/v6"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/admin"
"github.com/zitadel/zitadel/pkg/grpc/management"
v1 "github.com/zitadel/zitadel/pkg/grpc/v1"
)
func TestServer_ImportData(t *testing.T) {
orgIDs := generateIDs(10)
projectIDs := generateIDs(10)
userIDs := generateIDs(10)
grantIDs := generateIDs(10)
tests := []struct {
name string
req *admin.ImportDataRequest
want *admin.ImportDataResponse
wantErr bool
}{
{
name: "success",
req: &admin.ImportDataRequest{
Data: &admin.ImportDataRequest_DataOrgs{
DataOrgs: &admin.ImportDataOrg{
Orgs: []*admin.DataOrg{
{
OrgId: orgIDs[0],
Org: &management.AddOrgRequest{
Name: gofakeit.ProductName(),
},
Projects: []*v1.DataProject{
{
ProjectId: projectIDs[0],
Project: &management.AddProjectRequest{
Name: gofakeit.AppName(),
ProjectRoleAssertion: true,
},
},
{
ProjectId: projectIDs[1],
Project: &management.AddProjectRequest{
Name: gofakeit.AppName(),
ProjectRoleAssertion: false,
},
},
},
ProjectRoles: []*management.AddProjectRoleRequest{
{
ProjectId: projectIDs[0],
RoleKey: "role1",
DisplayName: "role1",
},
{
ProjectId: projectIDs[0],
RoleKey: "role2",
DisplayName: "role2",
},
{
ProjectId: projectIDs[1],
RoleKey: "role3",
DisplayName: "role3",
},
{
ProjectId: projectIDs[1],
RoleKey: "role4",
DisplayName: "role4",
},
},
HumanUsers: []*v1.DataHumanUser{
{
UserId: userIDs[0],
User: &management.ImportHumanUserRequest{
UserName: gofakeit.Username(),
Profile: &management.ImportHumanUserRequest_Profile{
FirstName: gofakeit.FirstName(),
LastName: gofakeit.LastName(),
DisplayName: gofakeit.Username(),
PreferredLanguage: gofakeit.LanguageBCP(),
},
Email: &management.ImportHumanUserRequest_Email{
Email: gofakeit.Email(),
IsEmailVerified: true,
},
},
},
{
UserId: userIDs[1],
User: &management.ImportHumanUserRequest{
UserName: gofakeit.Username(),
Profile: &management.ImportHumanUserRequest_Profile{
FirstName: gofakeit.FirstName(),
LastName: gofakeit.LastName(),
DisplayName: gofakeit.Username(),
PreferredLanguage: gofakeit.LanguageBCP(),
},
Email: &management.ImportHumanUserRequest_Email{
Email: gofakeit.Email(),
IsEmailVerified: true,
},
},
},
},
ProjectGrants: []*v1.DataProjectGrant{
{
GrantId: grantIDs[0],
ProjectGrant: &management.AddProjectGrantRequest{
ProjectId: projectIDs[0],
GrantedOrgId: orgIDs[1],
RoleKeys: []string{"role1", "role2"},
},
},
{
GrantId: grantIDs[1],
ProjectGrant: &management.AddProjectGrantRequest{
ProjectId: projectIDs[1],
GrantedOrgId: orgIDs[1],
RoleKeys: []string{"role3", "role4"},
},
},
{
GrantId: grantIDs[2],
ProjectGrant: &management.AddProjectGrantRequest{
ProjectId: projectIDs[0],
GrantedOrgId: orgIDs[2],
RoleKeys: []string{"role1", "role2"},
},
},
{
GrantId: grantIDs[3],
ProjectGrant: &management.AddProjectGrantRequest{
ProjectId: projectIDs[1],
GrantedOrgId: orgIDs[2],
RoleKeys: []string{"role3", "role4"},
},
},
},
},
{
OrgId: orgIDs[1],
Org: &management.AddOrgRequest{
Name: gofakeit.ProductName(),
},
UserGrants: []*management.AddUserGrantRequest{
{
UserId: userIDs[0],
ProjectId: projectIDs[0],
ProjectGrantId: grantIDs[0],
},
{
UserId: userIDs[0],
ProjectId: projectIDs[1],
ProjectGrantId: grantIDs[1],
},
},
},
{
OrgId: orgIDs[2],
Org: &management.AddOrgRequest{
Name: gofakeit.ProductName(),
},
UserGrants: []*management.AddUserGrantRequest{
{
UserId: userIDs[1],
ProjectId: projectIDs[0],
ProjectGrantId: grantIDs[2],
},
{
UserId: userIDs[1],
ProjectId: projectIDs[1],
ProjectGrantId: grantIDs[3],
},
},
},
},
},
},
Timeout: time.Minute.String(),
},
want: &admin.ImportDataResponse{
Success: &admin.ImportDataSuccess{
Orgs: []*admin.ImportDataSuccessOrg{
{
OrgId: orgIDs[0],
ProjectIds: projectIDs[0:2],
ProjectRoles: []string{
projectIDs[0] + "_role1",
projectIDs[0] + "_role2",
projectIDs[1] + "_role3",
projectIDs[1] + "_role4",
},
HumanUserIds: userIDs[0:2],
ProjectGrants: []*admin.ImportDataSuccessProjectGrant{
{
GrantId: grantIDs[0],
ProjectId: projectIDs[0],
OrgId: orgIDs[1],
},
{
GrantId: grantIDs[1],
ProjectId: projectIDs[1],
OrgId: orgIDs[1],
},
{
GrantId: grantIDs[2],
ProjectId: projectIDs[0],
OrgId: orgIDs[2],
},
{
GrantId: grantIDs[3],
ProjectId: projectIDs[1],
OrgId: orgIDs[2],
},
},
},
{
OrgId: orgIDs[1],
UserGrants: []*admin.ImportDataSuccessUserGrant{
{
ProjectId: projectIDs[0],
UserId: userIDs[0],
},
{
UserId: userIDs[0],
ProjectId: projectIDs[1],
},
},
},
{
OrgId: orgIDs[2],
UserGrants: []*admin.ImportDataSuccessUserGrant{
{
ProjectId: projectIDs[0],
UserId: userIDs[1],
},
{
UserId: userIDs[1],
ProjectId: projectIDs[1],
},
},
},
},
},
},
},
{
name: "duplicate project grant error",
req: &admin.ImportDataRequest{
Data: &admin.ImportDataRequest_DataOrgs{
DataOrgs: &admin.ImportDataOrg{
Orgs: []*admin.DataOrg{
{
OrgId: orgIDs[4],
Org: &management.AddOrgRequest{
Name: gofakeit.ProductName(),
},
},
{
OrgId: orgIDs[3],
Org: &management.AddOrgRequest{
Name: gofakeit.ProductName(),
},
Projects: []*v1.DataProject{
{
ProjectId: projectIDs[2],
Project: &management.AddProjectRequest{
Name: gofakeit.AppName(),
ProjectRoleAssertion: true,
},
},
{
ProjectId: projectIDs[3],
Project: &management.AddProjectRequest{
Name: gofakeit.AppName(),
ProjectRoleAssertion: false,
},
},
},
ProjectRoles: []*management.AddProjectRoleRequest{
{
ProjectId: projectIDs[2],
RoleKey: "role1",
DisplayName: "role1",
},
{
ProjectId: projectIDs[2],
RoleKey: "role2",
DisplayName: "role2",
},
{
ProjectId: projectIDs[3],
RoleKey: "role3",
DisplayName: "role3",
},
{
ProjectId: projectIDs[3],
RoleKey: "role4",
DisplayName: "role4",
},
},
ProjectGrants: []*v1.DataProjectGrant{
{
GrantId: grantIDs[4],
ProjectGrant: &management.AddProjectGrantRequest{
ProjectId: projectIDs[2],
GrantedOrgId: orgIDs[4],
RoleKeys: []string{"role1", "role2"},
},
},
{
GrantId: grantIDs[4],
ProjectGrant: &management.AddProjectGrantRequest{
ProjectId: projectIDs[2],
GrantedOrgId: orgIDs[4],
RoleKeys: []string{"role1", "role2"},
},
},
},
},
},
},
},
Timeout: time.Minute.String(),
},
want: &admin.ImportDataResponse{
Errors: []*admin.ImportDataError{
{
Type: "project_grant",
Id: orgIDs[3] + "_" + projectIDs[2] + "_" + orgIDs[4],
Message: "ID=V3-DKcYh Message=Errors.Project.Grant.AlreadyExists Parent=(ERROR: duplicate key value violates unique constraint \"unique_constraints_pkey\" (SQLSTATE 23505))",
},
},
Success: &admin.ImportDataSuccess{
Orgs: []*admin.ImportDataSuccessOrg{
{
OrgId: orgIDs[4],
},
{
OrgId: orgIDs[3],
ProjectIds: projectIDs[2:4],
ProjectRoles: []string{
projectIDs[2] + "_role1",
projectIDs[2] + "_role2",
projectIDs[3] + "_role3",
projectIDs[3] + "_role4",
},
ProjectGrants: []*admin.ImportDataSuccessProjectGrant{
{
GrantId: grantIDs[4],
ProjectId: projectIDs[2],
OrgId: orgIDs[4],
},
},
},
},
},
},
},
{
name: "duplicate project grant member error",
req: &admin.ImportDataRequest{
Data: &admin.ImportDataRequest_DataOrgs{
DataOrgs: &admin.ImportDataOrg{
Orgs: []*admin.DataOrg{
{
OrgId: orgIDs[6],
Org: &management.AddOrgRequest{
Name: gofakeit.ProductName(),
},
},
{
OrgId: orgIDs[5],
Org: &management.AddOrgRequest{
Name: gofakeit.ProductName(),
},
Projects: []*v1.DataProject{
{
ProjectId: projectIDs[4],
Project: &management.AddProjectRequest{
Name: gofakeit.AppName(),
ProjectRoleAssertion: true,
},
},
},
ProjectRoles: []*management.AddProjectRoleRequest{
{
ProjectId: projectIDs[4],
RoleKey: "role1",
DisplayName: "role1",
},
{
ProjectId: projectIDs[4],
RoleKey: "role2",
DisplayName: "role2",
},
},
HumanUsers: []*v1.DataHumanUser{
{
UserId: userIDs[2],
User: &management.ImportHumanUserRequest{
UserName: gofakeit.Username(),
Profile: &management.ImportHumanUserRequest_Profile{
FirstName: gofakeit.FirstName(),
LastName: gofakeit.LastName(),
DisplayName: gofakeit.Username(),
PreferredLanguage: gofakeit.LanguageBCP(),
},
Email: &management.ImportHumanUserRequest_Email{
Email: gofakeit.Email(),
IsEmailVerified: true,
},
},
},
},
ProjectGrants: []*v1.DataProjectGrant{
{
GrantId: grantIDs[5],
ProjectGrant: &management.AddProjectGrantRequest{
ProjectId: projectIDs[4],
GrantedOrgId: orgIDs[6],
RoleKeys: []string{"role1", "role2"},
},
},
},
ProjectGrantMembers: []*management.AddProjectGrantMemberRequest{
{
ProjectId: projectIDs[4],
GrantId: grantIDs[5],
UserId: userIDs[2],
Roles: []string{"PROJECT_GRANT_OWNER"},
},
{
ProjectId: projectIDs[4],
GrantId: grantIDs[5],
UserId: userIDs[2],
Roles: []string{"PROJECT_GRANT_OWNER"},
},
},
},
},
},
},
Timeout: time.Minute.String(),
},
want: &admin.ImportDataResponse{
Errors: []*admin.ImportDataError{
{
Type: "project_grant_member",
Id: orgIDs[5] + "_" + projectIDs[4] + "_" + grantIDs[5] + "_" + userIDs[2],
Message: "ID=PROJECT-37fug Message=Errors.AlreadyExists",
},
},
Success: &admin.ImportDataSuccess{
Orgs: []*admin.ImportDataSuccessOrg{
{
OrgId: orgIDs[6],
},
{
OrgId: orgIDs[5],
ProjectIds: projectIDs[4:5],
ProjectRoles: []string{
projectIDs[4] + "_role1",
projectIDs[4] + "_role2",
},
HumanUserIds: userIDs[2:3],
ProjectGrants: []*admin.ImportDataSuccessProjectGrant{
{
GrantId: grantIDs[5],
ProjectId: projectIDs[4],
OrgId: orgIDs[6],
},
},
ProjectGrantMembers: []*admin.ImportDataSuccessProjectGrantMember{
{
ProjectId: projectIDs[4],
GrantId: grantIDs[5],
UserId: userIDs[2],
},
},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.ImportData(AdminCTX, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.EqualProto(t, tt.want, got)
})
}
}
func generateIDs(n int) []string {
ids := make([]string, n)
for i := range ids {
ids[i] = uuid.NewString()
}
return ids
}

View File

@@ -0,0 +1,20 @@
//go:build integration
package admin_test
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/pkg/grpc/admin"
)
func TestServer_Healthz(t *testing.T) {
ctx, cancel := context.WithTimeout(AdminCTX, time.Minute)
defer cancel()
_, err := Instance.Client.Admin.Healthz(ctx, &admin.HealthzRequest{})
require.NoError(t, err)
}

View File

@@ -0,0 +1,102 @@
//go:build integration
package admin_test
import (
"bytes"
"context"
"io"
"net/http"
"net/http/cookiejar"
"net/url"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/admin"
)
func TestServer_Restrictions_DisallowPublicOrgRegistration(t *testing.T) {
instance := integration.NewInstance(CTX)
regOrgUrl, err := url.Parse("http://" + instance.Domain + ":8080/ui/login/register/org")
require.NoError(t, err)
// The CSRF cookie must be sent with every request.
// We can simulate a browser session using a cookie jar.
jar, err := cookiejar.New(nil)
require.NoError(t, err)
browserSession := &http.Client{Jar: jar}
var csrfToken string
iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
t.Run("public org registration is allowed by default", func(tt *testing.T) {
csrfToken = awaitPubOrgRegAllowed(tt, iamOwnerCtx, instance.Client, browserSession, regOrgUrl)
})
t.Run("disallowing public org registration disables the endpoints", func(tt *testing.T) {
_, err = instance.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{DisallowPublicOrgRegistration: gu.Ptr(true)})
require.NoError(tt, err)
awaitPubOrgRegDisallowed(tt, iamOwnerCtx, instance.Client, browserSession, regOrgUrl, csrfToken)
})
t.Run("allowing public org registration again re-enables the endpoints", func(tt *testing.T) {
_, err = instance.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{DisallowPublicOrgRegistration: gu.Ptr(false)})
require.NoError(tt, err)
awaitPubOrgRegAllowed(tt, iamOwnerCtx, instance.Client, browserSession, regOrgUrl)
})
}
// awaitPubOrgRegAllowed doesn't accept a CSRF token, as we expected it to always produce a new one
func awaitPubOrgRegAllowed(t *testing.T, ctx context.Context, cc *integration.Client, client *http.Client, parsedURL *url.URL) string {
csrfToken := awaitGetSSRGetResponse(t, ctx, client, parsedURL, http.StatusOK)
awaitPostFormResponse(t, ctx, client, parsedURL, http.StatusOK, csrfToken)
restrictions, err := cc.Admin.GetRestrictions(ctx, &admin.GetRestrictionsRequest{})
require.NoError(t, err)
require.False(t, restrictions.DisallowPublicOrgRegistration)
return csrfToken
}
// awaitPubOrgRegDisallowed accepts an old CSRF token, as we don't expect to get a CSRF token from the GET request anymore
func awaitPubOrgRegDisallowed(t *testing.T, ctx context.Context, cc *integration.Client, client *http.Client, parsedURL *url.URL, reuseOldCSRFToken string) {
awaitGetSSRGetResponse(t, ctx, client, parsedURL, http.StatusNotFound)
awaitPostFormResponse(t, ctx, client, parsedURL, http.StatusConflict, reuseOldCSRFToken)
restrictions, err := cc.Admin.GetRestrictions(ctx, &admin.GetRestrictionsRequest{})
require.NoError(t, err)
require.True(t, restrictions.DisallowPublicOrgRegistration)
}
// awaitGetSSRGetResponse cuts the CSRF token from the response body if it exists
func awaitGetSSRGetResponse(t *testing.T, ctx context.Context, client *http.Client, parsedURL *url.URL, expectCode int) string {
var csrfToken []byte
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute)
require.EventuallyWithT(t,
func(tt *assert.CollectT) {
resp, err := client.Get(parsedURL.String())
require.NoError(tt, err)
body, err := io.ReadAll(resp.Body)
require.NoError(tt, err)
searchField := `<input type="hidden" name="gorilla.csrf.Token" value="`
_, after, hasCsrfToken := bytes.Cut(body, []byte(searchField))
if hasCsrfToken {
csrfToken, _, _ = bytes.Cut(after, []byte(`">`))
}
assert.Equal(tt, resp.StatusCode, expectCode)
}, retryDuration, tick, "awaiting successful get SSR get response failed",
)
return string(csrfToken)
}
// awaitPostFormResponse needs a valid CSRF token to make it to the actual endpoint implementation and get the expected status code
func awaitPostFormResponse(t *testing.T, ctx context.Context, client *http.Client, parsedURL *url.URL, expectCode int, csrfToken string) {
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute)
require.EventuallyWithT(t,
func(tt *assert.CollectT) {
resp, err := client.PostForm(parsedURL.String(), url.Values{
"gorilla.csrf.Token": {csrfToken},
})
require.NoError(tt, err)
assert.Equal(tt, resp.StatusCode, expectCode)
}, retryDuration, tick, "awaiting successful Post Form failed",
)
}

View File

@@ -0,0 +1,263 @@
//go:build integration
package admin_test
import (
"context"
"encoding/json"
"io"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/text/language"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/admin"
"github.com/zitadel/zitadel/pkg/grpc/management"
"github.com/zitadel/zitadel/pkg/grpc/text"
"github.com/zitadel/zitadel/pkg/grpc/user"
)
func TestServer_Restrictions_AllowedLanguages(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
defer cancel()
var (
defaultAndAllowedLanguage = language.German
supportedLanguagesStr = []string{language.German.String(), language.English.String(), language.Japanese.String()}
disallowedLanguage = language.Spanish
unsupportedLanguage = language.Afrikaans
)
instance := integration.NewInstance(ctx)
iamOwnerCtx := instance.WithAuthorization(ctx, integration.UserTypeIAMOwner)
t.Run("assumed defaults are correct", func(tt *testing.T) {
tt.Run("languages are not restricted by default", func(ttt *testing.T) {
restrictions, err := instance.Client.Admin.GetRestrictions(iamOwnerCtx, &admin.GetRestrictionsRequest{})
require.NoError(ttt, err)
require.Len(ttt, restrictions.AllowedLanguages, 0)
})
tt.Run("default language is English by default", func(ttt *testing.T) {
defaultLang, err := instance.Client.Admin.GetDefaultLanguage(iamOwnerCtx, &admin.GetDefaultLanguageRequest{})
require.NoError(ttt, err)
require.Equal(ttt, language.Make(defaultLang.Language), language.English)
})
tt.Run("the discovery endpoint returns all supported languages", func(ttt *testing.T) {
awaitDiscoveryEndpoint(ttt, ctx, instance.Domain, supportedLanguagesStr, nil)
})
})
t.Run("restricting the default language fails", func(tt *testing.T) {
_, err := instance.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{AllowedLanguages: &admin.SelectLanguages{List: []string{defaultAndAllowedLanguage.String()}}})
expectStatus, ok := status.FromError(err)
require.True(tt, ok)
require.Equal(tt, codes.FailedPrecondition, expectStatus.Code())
})
t.Run("not defining any restrictions throws an error", func(tt *testing.T) {
_, err := instance.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{})
expectStatus, ok := status.FromError(err)
require.True(tt, ok)
require.Equal(tt, codes.InvalidArgument, expectStatus.Code())
})
t.Run("setting the default language works", func(tt *testing.T) {
setAndAwaitDefaultLanguage(iamOwnerCtx, instance.Client, tt, defaultAndAllowedLanguage)
})
t.Run("restricting allowed languages works", func(tt *testing.T) {
setAndAwaitAllowedLanguages(iamOwnerCtx, instance.Client, tt, []string{defaultAndAllowedLanguage.String()})
})
t.Run("GetAllowedLanguage returns only the allowed languages", func(tt *testing.T) {
expectContains, expectNotContains := []string{defaultAndAllowedLanguage.String()}, []string{disallowedLanguage.String()}
adminResp, err := instance.Client.Admin.GetAllowedLanguages(iamOwnerCtx, &admin.GetAllowedLanguagesRequest{})
require.NoError(t, err)
langs := adminResp.GetLanguages()
assert.Condition(t, contains(langs, expectContains))
assert.Condition(t, not(contains(langs, expectNotContains)))
})
t.Run("setting the default language to a disallowed language fails", func(tt *testing.T) {
_, err := instance.Client.Admin.SetDefaultLanguage(iamOwnerCtx, &admin.SetDefaultLanguageRequest{Language: disallowedLanguage.String()})
expectStatus, ok := status.FromError(err)
require.True(tt, ok)
require.Equal(tt, codes.FailedPrecondition, expectStatus.Code())
})
t.Run("the list of supported languages includes the disallowed languages", func(tt *testing.T) {
supported, err := instance.Client.Admin.GetSupportedLanguages(iamOwnerCtx, &admin.GetSupportedLanguagesRequest{})
require.NoError(tt, err)
require.Condition(tt, contains(supported.GetLanguages(), supportedLanguagesStr))
})
t.Run("the disallowed language is not listed in the discovery endpoint", func(tt *testing.T) {
awaitDiscoveryEndpoint(tt, ctx, instance.Domain, []string{defaultAndAllowedLanguage.String()}, []string{disallowedLanguage.String()})
})
t.Run("the login ui is rendered in the default language", func(tt *testing.T) {
awaitLoginUILanguage(tt, ctx, instance.Domain, disallowedLanguage, defaultAndAllowedLanguage, "Passwort")
})
t.Run("preferred languages are not restricted by the supported languages", func(tt *testing.T) {
tt.Run("change user profile", func(ttt *testing.T) {
resp, err := instance.Client.Mgmt.ListUsers(iamOwnerCtx, &management.ListUsersRequest{Queries: []*user.SearchQuery{{Query: &user.SearchQuery_UserNameQuery{UserNameQuery: &user.UserNameQuery{
UserName: "zitadel-admin@zitadel.localhost"}},
}}})
require.NoError(ttt, err)
require.Len(ttt, resp.GetResult(), 1)
humanAdmin := resp.GetResult()[0]
profile := humanAdmin.GetHuman().GetProfile()
require.NotEqual(ttt, unsupportedLanguage.String(), profile.GetPreferredLanguage())
_, updateErr := instance.Client.Mgmt.UpdateHumanProfile(iamOwnerCtx, &management.UpdateHumanProfileRequest{
PreferredLanguage: unsupportedLanguage.String(),
UserId: humanAdmin.GetId(),
FirstName: profile.GetFirstName(),
LastName: profile.GetLastName(),
NickName: profile.GetNickName(),
DisplayName: profile.GetDisplayName(),
Gender: profile.GetGender(),
})
require.NoError(ttt, updateErr)
})
})
t.Run("custom texts are only restricted by the supported languages", func(tt *testing.T) {
_, err := instance.Client.Admin.SetCustomLoginText(iamOwnerCtx, &admin.SetCustomLoginTextsRequest{
Language: disallowedLanguage.String(),
EmailVerificationText: &text.EmailVerificationScreenText{
Description: "hodor",
},
})
assert.NoError(tt, err)
_, err = instance.Client.Mgmt.SetCustomLoginText(iamOwnerCtx, &management.SetCustomLoginTextsRequest{
Language: disallowedLanguage.String(),
EmailVerificationText: &text.EmailVerificationScreenText{
Description: "hodor",
},
})
assert.NoError(tt, err)
_, err = instance.Client.Mgmt.SetCustomInitMessageText(iamOwnerCtx, &management.SetCustomInitMessageTextRequest{
Language: disallowedLanguage.String(),
Text: "hodor",
})
assert.NoError(tt, err)
_, err = instance.Client.Admin.SetDefaultInitMessageText(iamOwnerCtx, &admin.SetDefaultInitMessageTextRequest{
Language: disallowedLanguage.String(),
Text: "hodor",
})
assert.NoError(tt, err)
})
t.Run("allowing all languages works", func(tt *testing.T) {
tt.Run("restricting allowed languages works", func(ttt *testing.T) {
setAndAwaitAllowedLanguages(iamOwnerCtx, instance.Client, ttt, make([]string, 0))
})
})
t.Run("allowing the language makes it usable again", func(tt *testing.T) {
tt.Run("the previously disallowed language is listed in the discovery endpoint again", func(ttt *testing.T) {
awaitDiscoveryEndpoint(ttt, ctx, instance.Domain, []string{disallowedLanguage.String()}, nil)
})
tt.Run("the login ui is rendered in the previously disallowed language", func(ttt *testing.T) {
awaitLoginUILanguage(ttt, ctx, instance.Domain, disallowedLanguage, disallowedLanguage, "Contraseña")
})
})
}
func setAndAwaitAllowedLanguages(ctx context.Context, cc *integration.Client, t *testing.T, selectLanguages []string) {
_, err := cc.Admin.SetRestrictions(ctx, &admin.SetRestrictionsRequest{AllowedLanguages: &admin.SelectLanguages{List: selectLanguages}})
require.NoError(t, err)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute)
require.EventuallyWithT(t,
func(tt *assert.CollectT) {
restrictions, getErr := cc.Admin.GetRestrictions(ctx, &admin.GetRestrictionsRequest{})
expectLanguages := selectLanguages
if len(selectLanguages) == 0 {
expectLanguages = nil
}
assert.NoError(tt, getErr)
assert.Equal(tt, expectLanguages, restrictions.GetAllowedLanguages())
}, retryDuration, tick, "awaiting successful GetAllowedLanguages failed",
)
}
func setAndAwaitDefaultLanguage(ctx context.Context, cc *integration.Client, t *testing.T, lang language.Tag) {
_, err := cc.Admin.SetDefaultLanguage(ctx, &admin.SetDefaultLanguageRequest{Language: lang.String()})
require.NoError(t, err)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute)
require.EventuallyWithT(t, func(tt *assert.CollectT) {
defaultLang, getErr := cc.Admin.GetDefaultLanguage(ctx, &admin.GetDefaultLanguageRequest{})
assert.NoError(tt, getErr)
assert.Equal(tt, lang.String(), defaultLang.GetLanguage())
}, retryDuration, tick, "awaiting successful GetDefaultLanguage failed",
)
}
func awaitDiscoveryEndpoint(t *testing.T, ctx context.Context, domain string, containsUILocales, notContainsUILocales []string) {
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute)
require.EventuallyWithT(t, func(tt *assert.CollectT) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://"+domain+":8080/.well-known/openid-configuration", nil)
require.NoError(tt, err)
resp, err := http.DefaultClient.Do(req)
require.NoError(tt, err)
require.Equal(tt, http.StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
defer func() {
require.NoError(tt, resp.Body.Close())
}()
require.NoError(tt, err)
doc := struct {
UILocalesSupported []string `json:"ui_locales_supported"`
}{}
require.NoError(tt, json.Unmarshal(body, &doc))
if containsUILocales != nil {
assert.Condition(tt, contains(doc.UILocalesSupported, containsUILocales))
}
if notContainsUILocales != nil {
assert.Condition(tt, not(contains(doc.UILocalesSupported, notContainsUILocales)))
}
}, retryDuration, tick, "awaiting successful call to Discovery endpoint failed",
)
}
func awaitLoginUILanguage(t *testing.T, ctx context.Context, domain string, acceptLanguage language.Tag, expectLang language.Tag, containsText string) {
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute)
require.EventuallyWithT(t, func(tt *assert.CollectT) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://"+domain+":8080/ui/login/register", nil)
req.Header.Set("Accept-Language", acceptLanguage.String())
require.NoError(tt, err)
resp, err := http.DefaultClient.Do(req)
require.NoError(tt, err)
assert.Equal(tt, http.StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
defer func() {
require.NoError(tt, resp.Body.Close())
}()
require.NoError(tt, err)
assert.Containsf(tt, string(body), containsText, "login ui language is in "+expectLang.String())
}, retryDuration, tick, "awaiting successful LoginUI in specific language failed",
)
}
// We would love to use assert.Contains here, but it doesn't work with slices of strings
func contains(container []string, subset []string) assert.Comparison {
return func() bool {
if subset == nil {
return true
}
for _, str := range subset {
var found bool
for _, containerStr := range container {
if str == containerStr {
found = true
break
}
}
if !found {
return false
}
}
return true
}
}
func not(cmp assert.Comparison) assert.Comparison {
return func() bool {
return !cmp()
}
}

View File

@@ -0,0 +1,42 @@
//go:build integration
package admin_test
import (
"context"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/integration"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
)
var (
CTX, AdminCTX context.Context
Instance *integration.Instance
Client admin_pb.AdminServiceClient
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
defer cancel()
Instance = integration.NewInstance(ctx)
CTX = ctx
AdminCTX = Instance.WithAuthorization(ctx, integration.UserTypeIAMOwner)
Client = Instance.Client.Admin
return m.Run()
}())
}
var _ assert.TestingT = (*noopAssertionT)(nil)
type noopAssertionT struct{}
func (*noopAssertionT) FailNow() {}
func (*noopAssertionT) Errorf(string, ...interface{}) {}

View File

@@ -0,0 +1,123 @@
package admin
import (
"context"
"github.com/zitadel/zitadel/internal/api/grpc/object"
policy_grpc "github.com/zitadel/zitadel/internal/api/grpc/policy"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
)
func (s *Server) GetLabelPolicy(ctx context.Context, req *admin_pb.GetLabelPolicyRequest) (*admin_pb.GetLabelPolicyResponse, error) {
policy, err := s.query.DefaultActiveLabelPolicy(ctx)
if err != nil {
return nil, err
}
return &admin_pb.GetLabelPolicyResponse{Policy: policy_grpc.ModelLabelPolicyToPb(policy, s.assetsAPIDomain(ctx))}, nil
}
func (s *Server) GetPreviewLabelPolicy(ctx context.Context, req *admin_pb.GetPreviewLabelPolicyRequest) (*admin_pb.GetPreviewLabelPolicyResponse, error) {
policy, err := s.query.DefaultPreviewLabelPolicy(ctx)
if err != nil {
return nil, err
}
return &admin_pb.GetPreviewLabelPolicyResponse{Policy: policy_grpc.ModelLabelPolicyToPb(policy, s.assetsAPIDomain(ctx))}, nil
}
func (s *Server) UpdateLabelPolicy(ctx context.Context, req *admin_pb.UpdateLabelPolicyRequest) (*admin_pb.UpdateLabelPolicyResponse, error) {
policy, err := s.command.ChangeDefaultLabelPolicy(ctx, updateLabelPolicyToDomain(req))
if err != nil {
return nil, err
}
return &admin_pb.UpdateLabelPolicyResponse{
Details: object.ChangeToDetailsPb(
policy.Sequence,
policy.ChangeDate,
policy.ResourceOwner,
),
}, nil
}
func (s *Server) ActivateLabelPolicy(ctx context.Context, req *admin_pb.ActivateLabelPolicyRequest) (*admin_pb.ActivateLabelPolicyResponse, error) {
policy, err := s.command.ActivateDefaultLabelPolicy(ctx)
if err != nil {
return nil, err
}
return &admin_pb.ActivateLabelPolicyResponse{
Details: object.ChangeToDetailsPb(
policy.Sequence,
policy.EventDate,
policy.ResourceOwner,
),
}, nil
}
func (s *Server) RemoveLabelPolicyLogo(ctx context.Context, req *admin_pb.RemoveLabelPolicyLogoRequest) (*admin_pb.RemoveLabelPolicyLogoResponse, error) {
policy, err := s.command.RemoveLogoDefaultLabelPolicy(ctx)
if err != nil {
return nil, err
}
return &admin_pb.RemoveLabelPolicyLogoResponse{
Details: object.ChangeToDetailsPb(
policy.Sequence,
policy.EventDate,
policy.ResourceOwner,
),
}, nil
}
func (s *Server) RemoveLabelPolicyLogoDark(ctx context.Context, req *admin_pb.RemoveLabelPolicyLogoDarkRequest) (*admin_pb.RemoveLabelPolicyLogoDarkResponse, error) {
policy, err := s.command.RemoveLogoDarkDefaultLabelPolicy(ctx)
if err != nil {
return nil, err
}
return &admin_pb.RemoveLabelPolicyLogoDarkResponse{
Details: object.ChangeToDetailsPb(
policy.Sequence,
policy.EventDate,
policy.ResourceOwner,
),
}, nil
}
func (s *Server) RemoveLabelPolicyIcon(ctx context.Context, req *admin_pb.RemoveLabelPolicyIconRequest) (*admin_pb.RemoveLabelPolicyIconResponse, error) {
policy, err := s.command.RemoveIconDefaultLabelPolicy(ctx)
if err != nil {
return nil, err
}
return &admin_pb.RemoveLabelPolicyIconResponse{
Details: object.ChangeToDetailsPb(
policy.Sequence,
policy.EventDate,
policy.ResourceOwner,
),
}, nil
}
func (s *Server) RemoveLabelPolicyIconDark(ctx context.Context, req *admin_pb.RemoveLabelPolicyIconDarkRequest) (*admin_pb.RemoveLabelPolicyIconDarkResponse, error) {
policy, err := s.command.RemoveIconDarkDefaultLabelPolicy(ctx)
if err != nil {
return nil, err
}
return &admin_pb.RemoveLabelPolicyIconDarkResponse{
Details: object.ChangeToDetailsPb(
policy.Sequence,
policy.EventDate,
policy.ResourceOwner,
),
}, nil
}
func (s *Server) RemoveLabelPolicyFont(ctx context.Context, req *admin_pb.RemoveLabelPolicyFontRequest) (*admin_pb.RemoveLabelPolicyFontResponse, error) {
policy, err := s.command.RemoveFontDefaultLabelPolicy(ctx)
if err != nil {
return nil, err
}
return &admin_pb.RemoveLabelPolicyFontResponse{
Details: object.ChangeToDetailsPb(
policy.Sequence,
policy.EventDate,
policy.ResourceOwner,
),
}, nil
}

View File

@@ -0,0 +1,38 @@
package admin
import (
"github.com/zitadel/zitadel/internal/domain"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
policy_pb "github.com/zitadel/zitadel/pkg/grpc/policy"
)
func updateLabelPolicyToDomain(policy *admin_pb.UpdateLabelPolicyRequest) *domain.LabelPolicy {
return &domain.LabelPolicy{
PrimaryColor: policy.PrimaryColor,
BackgroundColor: policy.BackgroundColor,
WarnColor: policy.WarnColor,
FontColor: policy.FontColor,
PrimaryColorDark: policy.PrimaryColorDark,
BackgroundColorDark: policy.BackgroundColorDark,
WarnColorDark: policy.WarnColorDark,
FontColorDark: policy.FontColorDark,
HideLoginNameSuffix: policy.HideLoginNameSuffix,
DisableWatermark: policy.DisableWatermark,
ThemeMode: themeModeToDomain(policy.ThemeMode),
}
}
func themeModeToDomain(theme policy_pb.ThemeMode) domain.LabelPolicyThemeMode {
switch theme {
case policy_pb.ThemeMode_THEME_MODE_AUTO:
return domain.LabelPolicyThemeAuto
case policy_pb.ThemeMode_THEME_MODE_DARK:
return domain.LabelPolicyThemeDark
case policy_pb.ThemeMode_THEME_MODE_LIGHT:
return domain.LabelPolicyThemeLight
case policy_pb.ThemeMode_THEME_MODE_UNSPECIFIED:
return domain.LabelPolicyThemeAuto
default:
return domain.LabelPolicyThemeAuto
}
}

View File

@@ -0,0 +1,45 @@
package admin
import (
"context"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/object"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/i18n"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
)
func (s *Server) GetSupportedLanguages(ctx context.Context, req *admin_pb.GetSupportedLanguagesRequest) (*admin_pb.GetSupportedLanguagesResponse, error) {
return &admin_pb.GetSupportedLanguagesResponse{Languages: domain.LanguagesToStrings(i18n.SupportedLanguages())}, nil
}
func (s *Server) SetDefaultLanguage(ctx context.Context, req *admin_pb.SetDefaultLanguageRequest) (*admin_pb.SetDefaultLanguageResponse, error) {
lang, err := domain.ParseLanguage(req.Language)
if err != nil {
return nil, err
}
details, err := s.command.SetDefaultLanguage(ctx, lang[0])
if err != nil {
return nil, err
}
return &admin_pb.SetDefaultLanguageResponse{
Details: object.DomainToChangeDetailsPb(details),
}, nil
}
func (s *Server) GetDefaultLanguage(ctx context.Context, _ *admin_pb.GetDefaultLanguageRequest) (*admin_pb.GetDefaultLanguageResponse, error) {
return &admin_pb.GetDefaultLanguageResponse{Language: authz.GetInstance(ctx).DefaultLanguage().String()}, nil
}
func (s *Server) GetAllowedLanguages(ctx context.Context, _ *admin_pb.GetAllowedLanguagesRequest) (*admin_pb.GetAllowedLanguagesResponse, error) {
restrictions, err := s.query.GetInstanceRestrictions(ctx)
if err != nil {
return nil, err
}
allowed := restrictions.AllowedLanguages
if len(allowed) == 0 {
allowed = i18n.SupportedLanguages()
}
return &admin_pb.GetAllowedLanguagesResponse{Languages: domain.LanguagesToStrings(allowed)}, nil
}

View File

@@ -0,0 +1,19 @@
package admin
import (
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/pkg/grpc/admin"
)
func selectLanguagesToCommand(languages *admin.SelectLanguages) (tags []language.Tag, err error) {
allowedLanguages := languages.GetList()
if allowedLanguages == nil && languages != nil {
allowedLanguages = make([]string, 0)
}
if allowedLanguages == nil {
return nil, nil
}
return domain.ParseLanguage(allowedLanguages...)
}

View File

@@ -0,0 +1,31 @@
package admin
import (
"context"
"github.com/zitadel/zitadel/internal/api/grpc/object"
policy_grpc "github.com/zitadel/zitadel/internal/api/grpc/policy"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
)
func (s *Server) GetLockoutPolicy(ctx context.Context, req *admin_pb.GetLockoutPolicyRequest) (*admin_pb.GetLockoutPolicyResponse, error) {
policy, err := s.query.DefaultLockoutPolicy(ctx)
if err != nil {
return nil, err
}
return &admin_pb.GetLockoutPolicyResponse{Policy: policy_grpc.ModelLockoutPolicyToPb(policy)}, nil
}
func (s *Server) UpdateLockoutPolicy(ctx context.Context, req *admin_pb.UpdateLockoutPolicyRequest) (*admin_pb.UpdateLockoutPolicyResponse, error) {
policy, err := s.command.ChangeDefaultLockoutPolicy(ctx, UpdateLockoutPolicyToDomain(req))
if err != nil {
return nil, err
}
return &admin_pb.UpdateLockoutPolicyResponse{
Details: object.ChangeToDetailsPb(
policy.Sequence,
policy.ChangeDate,
policy.ResourceOwner,
),
}, nil
}

View File

@@ -0,0 +1,13 @@
package admin
import (
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/pkg/grpc/admin"
)
func UpdateLockoutPolicyToDomain(p *admin.UpdateLockoutPolicyRequest) *domain.LockoutPolicy {
return &domain.LockoutPolicy{
MaxPasswordAttempts: uint64(p.MaxPasswordAttempts),
MaxOTPAttempts: uint64(p.MaxOtpAttempts),
}
}

View File

@@ -0,0 +1,131 @@
package admin
import (
"context"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/idp"
"github.com/zitadel/zitadel/internal/api/grpc/object"
policy_grpc "github.com/zitadel/zitadel/internal/api/grpc/policy"
"github.com/zitadel/zitadel/internal/domain"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
)
func (s *Server) GetLoginPolicy(ctx context.Context, _ *admin_pb.GetLoginPolicyRequest) (*admin_pb.GetLoginPolicyResponse, error) {
policy, err := s.query.DefaultLoginPolicy(ctx)
if err != nil {
return nil, err
}
return &admin_pb.GetLoginPolicyResponse{Policy: policy_grpc.ModelLoginPolicyToPb(policy)}, nil
}
func (s *Server) UpdateLoginPolicy(ctx context.Context, p *admin_pb.UpdateLoginPolicyRequest) (*admin_pb.UpdateLoginPolicyResponse, error) {
policy, err := s.command.ChangeDefaultLoginPolicy(ctx, updateLoginPolicyToCommand(p))
if err != nil {
return nil, err
}
return &admin_pb.UpdateLoginPolicyResponse{
Details: object.ChangeToDetailsPb(
policy.Sequence,
policy.EventDate,
policy.ResourceOwner,
),
}, nil
}
func (s *Server) ListLoginPolicyIDPs(ctx context.Context, req *admin_pb.ListLoginPolicyIDPsRequest) (*admin_pb.ListLoginPolicyIDPsResponse, error) {
res, err := s.query.IDPLoginPolicyLinks(ctx, authz.GetInstance(ctx).InstanceID(), ListLoginPolicyIDPsRequestToQuery(req), false)
if err != nil {
return nil, err
}
return &admin_pb.ListLoginPolicyIDPsResponse{
Result: idp.IDPLoginPolicyLinksToPb(res.Links),
Details: object.ToListDetails(res.Count, res.Sequence, res.LastRun),
}, nil
}
func (s *Server) AddIDPToLoginPolicy(ctx context.Context, req *admin_pb.AddIDPToLoginPolicyRequest) (*admin_pb.AddIDPToLoginPolicyResponse, error) {
idp, err := s.command.AddIDPProviderToDefaultLoginPolicy(ctx, &domain.IDPProvider{IDPConfigID: req.IdpId})
if err != nil {
return nil, err
}
return &admin_pb.AddIDPToLoginPolicyResponse{
Details: object.AddToDetailsPb(
idp.Sequence,
idp.ChangeDate,
idp.ResourceOwner,
),
}, nil
}
func (s *Server) RemoveIDPFromLoginPolicy(ctx context.Context, req *admin_pb.RemoveIDPFromLoginPolicyRequest) (*admin_pb.RemoveIDPFromLoginPolicyResponse, error) {
objectDetails, err := s.command.RemoveIDPProviderFromDefaultLoginPolicy(ctx, &domain.IDPProvider{IDPConfigID: req.IdpId})
if err != nil {
return nil, err
}
return &admin_pb.RemoveIDPFromLoginPolicyResponse{
Details: object.DomainToChangeDetailsPb(objectDetails),
}, nil
}
func (s *Server) ListLoginPolicySecondFactors(ctx context.Context, req *admin_pb.ListLoginPolicySecondFactorsRequest) (*admin_pb.ListLoginPolicySecondFactorsResponse, error) {
result, err := s.query.DefaultSecondFactors(ctx)
if err != nil {
return nil, err
}
return &admin_pb.ListLoginPolicySecondFactorsResponse{
Details: object.ToListDetails(result.Count, result.Sequence, result.LastRun),
Result: policy_grpc.ModelSecondFactorTypesToPb(result.Factors),
}, nil
}
func (s *Server) AddSecondFactorToLoginPolicy(ctx context.Context, req *admin_pb.AddSecondFactorToLoginPolicyRequest) (*admin_pb.AddSecondFactorToLoginPolicyResponse, error) {
objectDetails, err := s.command.AddSecondFactorToDefaultLoginPolicy(ctx, policy_grpc.SecondFactorTypeToDomain(req.Type))
if err != nil {
return nil, err
}
return &admin_pb.AddSecondFactorToLoginPolicyResponse{
Details: object.DomainToAddDetailsPb(objectDetails),
}, nil
}
func (s *Server) RemoveSecondFactorFromLoginPolicy(ctx context.Context, req *admin_pb.RemoveSecondFactorFromLoginPolicyRequest) (*admin_pb.RemoveSecondFactorFromLoginPolicyResponse, error) {
objectDetails, err := s.command.RemoveSecondFactorFromDefaultLoginPolicy(ctx, policy_grpc.SecondFactorTypeToDomain(req.Type))
if err != nil {
return nil, err
}
return &admin_pb.RemoveSecondFactorFromLoginPolicyResponse{
Details: object.DomainToChangeDetailsPb(objectDetails),
}, nil
}
func (s *Server) ListLoginPolicyMultiFactors(ctx context.Context, req *admin_pb.ListLoginPolicyMultiFactorsRequest) (*admin_pb.ListLoginPolicyMultiFactorsResponse, error) {
res, err := s.query.DefaultMultiFactors(ctx)
if err != nil {
return nil, err
}
return &admin_pb.ListLoginPolicyMultiFactorsResponse{
Details: object.ToListDetails(res.Count, res.Sequence, res.LastRun),
Result: policy_grpc.ModelMultiFactorTypesToPb(res.Factors),
}, nil
}
func (s *Server) AddMultiFactorToLoginPolicy(ctx context.Context, req *admin_pb.AddMultiFactorToLoginPolicyRequest) (*admin_pb.AddMultiFactorToLoginPolicyResponse, error) {
objectDetails, err := s.command.AddMultiFactorToDefaultLoginPolicy(ctx, policy_grpc.MultiFactorTypeToDomain(req.Type))
if err != nil {
return nil, err
}
return &admin_pb.AddMultiFactorToLoginPolicyResponse{
Details: object.DomainToAddDetailsPb(objectDetails),
}, nil
}
func (s *Server) RemoveMultiFactorFromLoginPolicy(ctx context.Context, req *admin_pb.RemoveMultiFactorFromLoginPolicyRequest) (*admin_pb.RemoveMultiFactorFromLoginPolicyResponse, error) {
objectDetails, err := s.command.RemoveMultiFactorFromDefaultLoginPolicy(ctx, policy_grpc.MultiFactorTypeToDomain(req.Type))
if err != nil {
return nil, err
}
return &admin_pb.RemoveMultiFactorFromLoginPolicyResponse{
Details: object.DomainToChangeDetailsPb(objectDetails),
}, nil
}

View File

@@ -0,0 +1,42 @@
package admin
import (
"github.com/zitadel/zitadel/internal/api/grpc/object"
policy_grpc "github.com/zitadel/zitadel/internal/api/grpc/policy"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/query"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
)
func updateLoginPolicyToCommand(p *admin_pb.UpdateLoginPolicyRequest) *command.ChangeLoginPolicy {
return &command.ChangeLoginPolicy{
AllowUsernamePassword: p.AllowUsernamePassword,
AllowRegister: p.AllowRegister,
AllowExternalIDP: p.AllowExternalIdp,
ForceMFA: p.ForceMfa,
ForceMFALocalOnly: p.ForceMfaLocalOnly,
PasswordlessType: policy_grpc.PasswordlessTypeToDomain(p.PasswordlessType),
HidePasswordReset: p.HidePasswordReset,
IgnoreUnknownUsernames: p.IgnoreUnknownUsernames,
AllowDomainDiscovery: p.AllowDomainDiscovery,
DisableLoginWithEmail: p.DisableLoginWithEmail,
DisableLoginWithPhone: p.DisableLoginWithPhone,
DefaultRedirectURI: p.DefaultRedirectUri,
PasswordCheckLifetime: p.PasswordCheckLifetime.AsDuration(),
ExternalLoginCheckLifetime: p.ExternalLoginCheckLifetime.AsDuration(),
MFAInitSkipLifetime: p.MfaInitSkipLifetime.AsDuration(),
SecondFactorCheckLifetime: p.SecondFactorCheckLifetime.AsDuration(),
MultiFactorCheckLifetime: p.MultiFactorCheckLifetime.AsDuration(),
}
}
func ListLoginPolicyIDPsRequestToQuery(req *admin_pb.ListLoginPolicyIDPsRequest) *query.IDPLoginPolicyLinksSearchQuery {
offset, limit, asc := object.ListQueryToModel(req.Query)
return &query.IDPLoginPolicyLinksSearchQuery{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
},
}
}

View File

@@ -0,0 +1,24 @@
package admin
import (
"context"
"github.com/zitadel/zitadel/internal/api/authz"
object_pb "github.com/zitadel/zitadel/internal/api/grpc/object"
"github.com/zitadel/zitadel/pkg/grpc/admin"
)
func (s *Server) ListMilestones(ctx context.Context, req *admin.ListMilestonesRequest) (*admin.ListMilestonesResponse, error) {
queries, err := listMilestonesToModel(authz.GetInstance(ctx).InstanceID(), req)
if err != nil {
return nil, err
}
resp, err := s.query.SearchMilestones(ctx, []string{authz.GetInstance(ctx).InstanceID()}, queries)
if err != nil {
return nil, err
}
return &admin.ListMilestonesResponse{
Result: milestoneViewsToPb(resp.Milestones),
Details: object_pb.ToListDetails(resp.Count, resp.Sequence, resp.LastRun),
}, nil
}

View File

@@ -0,0 +1,100 @@
package admin
import (
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/grpc/object"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/repository/milestone"
"github.com/zitadel/zitadel/internal/zerrors"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
milestone_pb "github.com/zitadel/zitadel/pkg/grpc/milestone"
)
func listMilestonesToModel(instanceID string, req *admin_pb.ListMilestonesRequest) (*query.MilestonesSearchQueries, error) {
offset, limit, asc := object.ListQueryToModel(req.Query)
queries, err := milestoneQueriesToModel(req.GetQueries())
instanceIDQuery, err := query.NewTextQuery(query.MilestoneInstanceIDColID, instanceID, query.TextEquals)
if err != nil {
return nil, err
}
queries = append(queries, instanceIDQuery)
return &query.MilestonesSearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
SortingColumn: milestoneFieldNameToSortingColumn(req.SortingColumn),
},
Queries: queries,
}, nil
}
func milestoneQueriesToModel(queries []*milestone_pb.MilestoneQuery) (q []query.SearchQuery, err error) {
q = make([]query.SearchQuery, len(queries))
for i, query := range queries {
q[i], err = milestoneQueryToModel(query)
if err != nil {
return nil, err
}
}
return q, nil
}
func milestoneQueryToModel(milestoneQuery *milestone_pb.MilestoneQuery) (query.SearchQuery, error) {
switch q := milestoneQuery.Query.(type) {
case *milestone_pb.MilestoneQuery_IsReachedQuery:
if q.IsReachedQuery.GetReached() {
return query.NewNotNullQuery(query.MilestoneReachedDateColID)
}
return query.NewIsNullQuery(query.MilestoneReachedDateColID)
default:
return nil, zerrors.ThrowInvalidArgument(nil, "ADMIN-sE7pc", "List.Query.Invalid")
}
}
func milestoneFieldNameToSortingColumn(field milestone_pb.MilestoneFieldName) query.Column {
switch field {
case milestone_pb.MilestoneFieldName_MILESTONE_FIELD_NAME_REACHED_DATE:
return query.MilestoneReachedDateColID
default:
return query.MilestoneTypeColID
}
}
func milestoneViewsToPb(milestones []*query.Milestone) []*milestone_pb.Milestone {
resp := make([]*milestone_pb.Milestone, len(milestones))
for i, idp := range milestones {
resp[i] = modelMilestoneViewToPb(idp)
}
return resp
}
func modelMilestoneViewToPb(m *query.Milestone) *milestone_pb.Milestone {
mspb := &milestone_pb.Milestone{
Type: modelMilestoneTypeToPb(m.Type),
}
if !m.ReachedDate.IsZero() {
mspb.ReachedDate = timestamppb.New(m.ReachedDate)
}
return mspb
}
func modelMilestoneTypeToPb(t milestone.Type) milestone_pb.MilestoneType {
switch t {
case milestone.InstanceCreated:
return milestone_pb.MilestoneType_MILESTONE_TYPE_INSTANCE_CREATED
case milestone.AuthenticationSucceededOnInstance:
return milestone_pb.MilestoneType_MILESTONE_TYPE_AUTHENTICATION_SUCCEEDED_ON_INSTANCE
case milestone.ProjectCreated:
return milestone_pb.MilestoneType_MILESTONE_TYPE_PROJECT_CREATED
case milestone.ApplicationCreated:
return milestone_pb.MilestoneType_MILESTONE_TYPE_APPLICATION_CREATED
case milestone.AuthenticationSucceededOnApplication:
return milestone_pb.MilestoneType_MILESTONE_TYPE_AUTHENTICATION_SUCCEEDED_ON_APPLICATION
case milestone.InstanceDeleted:
return milestone_pb.MilestoneType_MILESTONE_TYPE_INSTANCE_DELETED
default:
return milestone_pb.MilestoneType_MILESTONE_TYPE_UNSPECIFIED
}
}

Some files were not shown because too many files have changed in this diff Show More