Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
@@ -0,0 +1,98 @@
// Package keycloak offers an invitation backend for the invitation service.
package keycloak
import (
"context"
"github.com/google/uuid"
"github.com/qsfera/server/pkg/keycloak"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/services/invitations/pkg/invitations"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
)
const (
userType = "Guest"
)
var userRequiredActions = []keycloak.UserAction{
keycloak.UserActionUpdatePassword,
keycloak.UserActionVerifyEmail,
}
// Backend represents the keycloak backend.
type Backend struct {
logger log.Logger
client keycloak.Client
userRealm string
}
// New instantiates a new keycloak.Backend with a default gocloak client.
func New(
logger log.Logger,
baseURL, clientID, clientSecret, clientRealm, userRealm string,
insecureSkipVerify bool,
) *Backend {
logger = log.Logger{
Logger: logger.With().
Str("invitationBackend", "keycloak").
Str("clientID", clientID).
Str("clientRealm", clientRealm).
Str("userRealm", userRealm).
Logger(),
}
client := keycloak.New(baseURL, clientID, clientSecret, clientRealm, insecureSkipVerify)
return NewWithClient(logger, client, userRealm)
}
// NewWithClient creates a new backend with the supplied keycloak client.
func NewWithClient(
logger log.Logger,
client keycloak.Client,
userRealm string,
) *Backend {
return &Backend{
logger: logger,
client: client,
userRealm: userRealm,
}
}
// CreateUser creates a user in the keycloak backend.
func (b Backend) CreateUser(ctx context.Context, invitation *invitations.Invitation) (string, error) {
u := uuid.New()
b.logger.Info().
Str("email", invitation.InvitedUserEmailAddress).
Msg("Creating new user")
user := &libregraph.User{
Mail: &invitation.InvitedUserEmailAddress,
AccountEnabled: boolP(true),
OnPremisesSamAccountName: invitation.InvitedUserEmailAddress,
Id: stringP(u.String()),
UserType: stringP(userType),
}
id, err := b.client.CreateUser(ctx, b.userRealm, user, userRequiredActions)
if err != nil {
b.logger.Error().
Str("userID", u.String()).
Str("email", invitation.InvitedUserEmailAddress).
Err(err).
Msg("Failed to create user")
return "", err
}
return id, nil
}
// CanSendMail returns true because keycloak does allow sending mail.
func (b Backend) CanSendMail() bool { return true }
// SendMail sends a mail to the user with details on how to redeem the invitation.
func (b Backend) SendMail(ctx context.Context, id string) error {
return b.client.SendActionsMail(ctx, b.userRealm, id, userRequiredActions)
}
func boolP(b bool) *bool { return &b }
func stringP(s string) *string { return &s }
@@ -0,0 +1,142 @@
// Package keycloak offers an invitation backend for the invitation service.
// TODO: Maybe move this outside of the invitation service and make it more generic?
package keycloak_test
import (
"context"
"testing"
kcpkg "github.com/qsfera/server/pkg/keycloak"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/services/invitations/pkg/backends/keycloak"
"github.com/qsfera/server/services/invitations/pkg/backends/keycloak/mocks"
"github.com/qsfera/server/services/invitations/pkg/invitations"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
const (
clientID = "test-id"
clientSecret = "test-secret"
clientRealm = "client-realm"
userRealm = "user-realm"
jwtToken = "test-token"
)
func TestBackend_CreateUser(t *testing.T) {
type args struct {
invitation *invitations.Invitation
}
type mockInputs struct {
funcName string
args []any
returns []any
}
tests := []struct {
name string
args args
want string
clientMocks []mockInputs
assertion assert.ErrorAssertionFunc
}{
{
name: "Test without diplay name",
args: args{
invitation: &invitations.Invitation{
InvitedUserEmailAddress: "test@example.org",
},
},
want: "test-id",
clientMocks: []mockInputs{
{
funcName: "CreateUser",
args: []any{
mock.Anything,
userRealm,
mock.Anything, // can't match on the user because it generates a UUID internally.
[]kcpkg.UserAction{
kcpkg.UserActionUpdatePassword,
kcpkg.UserActionVerifyEmail,
},
},
returns: []any{
"test-id",
nil,
},
},
},
assertion: func(t assert.TestingT, err error, args ...any) bool {
return assert.Nil(t, err, args...)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
c := &mocks.Client{}
for _, m := range tt.clientMocks {
c.On(m.funcName, m.args...).Return(m.returns...)
}
b := keycloak.NewWithClient(log.NopLogger(), c, userRealm)
got, err := b.CreateUser(ctx, tt.args.invitation)
tt.assertion(t, err)
assert.Equal(t, tt.want, got)
})
}
}
func TestBackend_SendMail(t *testing.T) {
type args struct {
id string
}
type mockInputs struct {
funcName string
args []any
returns []any
}
tests := []struct {
name string
args args
clientMocks []mockInputs
assertion assert.ErrorAssertionFunc
}{
{
name: "Mail successfully sent",
args: args{
id: "test-id",
},
clientMocks: []mockInputs{
{
funcName: "SendActionsMail",
args: []any{
mock.Anything,
userRealm,
"test-id",
[]kcpkg.UserAction{
kcpkg.UserActionUpdatePassword,
kcpkg.UserActionVerifyEmail,
},
},
returns: []any{
nil,
},
},
},
assertion: func(t assert.TestingT, err error, args ...any) bool {
return assert.Nil(t, err, args...)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
c := &mocks.Client{}
for _, m := range tt.clientMocks {
c.On(m.funcName, m.args...).Return(m.returns...)
}
b := keycloak.NewWithClient(log.NopLogger(), c, userRealm)
tt.assertion(t, b.SendMail(ctx, tt.args.id))
})
}
}
@@ -0,0 +1,15 @@
package keycloak
import (
"context"
"github.com/Nerzal/gocloak/v13"
)
// GoCloak represents the parts of gocloak.GoCloak that we use, mainly here for mockery.
type GoCloak interface {
CreateUser(ctx context.Context, token, realm string, user gocloak.User) (string, error)
ExecuteActionsEmail(ctx context.Context, token, realm string, params gocloak.ExecuteActionsEmail) error
LoginClient(ctx context.Context, clientID, clientSecret, realm string) (*gocloak.JWT, error)
RetrospectToken(ctx context.Context, accessToken, clientID, clientSecret, realm string) (*gocloak.IntroSpectTokenResult, error)
}
@@ -0,0 +1,335 @@
// Code generated by mockery; DO NOT EDIT.
// github.com/vektra/mockery
// template: testify
package mocks
import (
"context"
"github.com/opencloud-eu/libre-graph-api-go"
"github.com/qsfera/server/pkg/keycloak"
mock "github.com/stretchr/testify/mock"
)
// NewClient creates a new instance of Client. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewClient(t interface {
mock.TestingT
Cleanup(func())
}) *Client {
mock := &Client{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
// Client is an autogenerated mock type for the Client type
type Client struct {
mock.Mock
}
type Client_Expecter struct {
mock *mock.Mock
}
func (_m *Client) EXPECT() *Client_Expecter {
return &Client_Expecter{mock: &_m.Mock}
}
// CreateUser provides a mock function for the type Client
func (_mock *Client) CreateUser(ctx context.Context, realm string, user *libregraph.User, userActions []keycloak.UserAction) (string, error) {
ret := _mock.Called(ctx, realm, user, userActions)
if len(ret) == 0 {
panic("no return value specified for CreateUser")
}
var r0 string
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, *libregraph.User, []keycloak.UserAction) (string, error)); ok {
return returnFunc(ctx, realm, user, userActions)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, string, *libregraph.User, []keycloak.UserAction) string); ok {
r0 = returnFunc(ctx, realm, user, userActions)
} else {
r0 = ret.Get(0).(string)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, string, *libregraph.User, []keycloak.UserAction) error); ok {
r1 = returnFunc(ctx, realm, user, userActions)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Client_CreateUser_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateUser'
type Client_CreateUser_Call struct {
*mock.Call
}
// CreateUser is a helper method to define mock.On call
// - ctx context.Context
// - realm string
// - user *libregraph.User
// - userActions []keycloak.UserAction
func (_e *Client_Expecter) CreateUser(ctx interface{}, realm interface{}, user interface{}, userActions interface{}) *Client_CreateUser_Call {
return &Client_CreateUser_Call{Call: _e.mock.On("CreateUser", ctx, realm, user, userActions)}
}
func (_c *Client_CreateUser_Call) Run(run func(ctx context.Context, realm string, user *libregraph.User, userActions []keycloak.UserAction)) *Client_CreateUser_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
var arg2 *libregraph.User
if args[2] != nil {
arg2 = args[2].(*libregraph.User)
}
var arg3 []keycloak.UserAction
if args[3] != nil {
arg3 = args[3].([]keycloak.UserAction)
}
run(
arg0,
arg1,
arg2,
arg3,
)
})
return _c
}
func (_c *Client_CreateUser_Call) Return(s string, err error) *Client_CreateUser_Call {
_c.Call.Return(s, err)
return _c
}
func (_c *Client_CreateUser_Call) RunAndReturn(run func(ctx context.Context, realm string, user *libregraph.User, userActions []keycloak.UserAction) (string, error)) *Client_CreateUser_Call {
_c.Call.Return(run)
return _c
}
// GetPIIReport provides a mock function for the type Client
func (_mock *Client) GetPIIReport(ctx context.Context, realm string, username string) (*keycloak.PIIReport, error) {
ret := _mock.Called(ctx, realm, username)
if len(ret) == 0 {
panic("no return value specified for GetPIIReport")
}
var r0 *keycloak.PIIReport
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) (*keycloak.PIIReport, error)); ok {
return returnFunc(ctx, realm, username)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) *keycloak.PIIReport); ok {
r0 = returnFunc(ctx, realm, username)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*keycloak.PIIReport)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
r1 = returnFunc(ctx, realm, username)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Client_GetPIIReport_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPIIReport'
type Client_GetPIIReport_Call struct {
*mock.Call
}
// GetPIIReport is a helper method to define mock.On call
// - ctx context.Context
// - realm string
// - username string
func (_e *Client_Expecter) GetPIIReport(ctx interface{}, realm interface{}, username interface{}) *Client_GetPIIReport_Call {
return &Client_GetPIIReport_Call{Call: _e.mock.On("GetPIIReport", ctx, realm, username)}
}
func (_c *Client_GetPIIReport_Call) Run(run func(ctx context.Context, realm string, username string)) *Client_GetPIIReport_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
var arg2 string
if args[2] != nil {
arg2 = args[2].(string)
}
run(
arg0,
arg1,
arg2,
)
})
return _c
}
func (_c *Client_GetPIIReport_Call) Return(pIIReport *keycloak.PIIReport, err error) *Client_GetPIIReport_Call {
_c.Call.Return(pIIReport, err)
return _c
}
func (_c *Client_GetPIIReport_Call) RunAndReturn(run func(ctx context.Context, realm string, username string) (*keycloak.PIIReport, error)) *Client_GetPIIReport_Call {
_c.Call.Return(run)
return _c
}
// GetUserByUsername provides a mock function for the type Client
func (_mock *Client) GetUserByUsername(ctx context.Context, realm string, username string) (*libregraph.User, error) {
ret := _mock.Called(ctx, realm, username)
if len(ret) == 0 {
panic("no return value specified for GetUserByUsername")
}
var r0 *libregraph.User
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) (*libregraph.User, error)); ok {
return returnFunc(ctx, realm, username)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) *libregraph.User); ok {
r0 = returnFunc(ctx, realm, username)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*libregraph.User)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
r1 = returnFunc(ctx, realm, username)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Client_GetUserByUsername_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetUserByUsername'
type Client_GetUserByUsername_Call struct {
*mock.Call
}
// GetUserByUsername is a helper method to define mock.On call
// - ctx context.Context
// - realm string
// - username string
func (_e *Client_Expecter) GetUserByUsername(ctx interface{}, realm interface{}, username interface{}) *Client_GetUserByUsername_Call {
return &Client_GetUserByUsername_Call{Call: _e.mock.On("GetUserByUsername", ctx, realm, username)}
}
func (_c *Client_GetUserByUsername_Call) Run(run func(ctx context.Context, realm string, username string)) *Client_GetUserByUsername_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
var arg2 string
if args[2] != nil {
arg2 = args[2].(string)
}
run(
arg0,
arg1,
arg2,
)
})
return _c
}
func (_c *Client_GetUserByUsername_Call) Return(user *libregraph.User, err error) *Client_GetUserByUsername_Call {
_c.Call.Return(user, err)
return _c
}
func (_c *Client_GetUserByUsername_Call) RunAndReturn(run func(ctx context.Context, realm string, username string) (*libregraph.User, error)) *Client_GetUserByUsername_Call {
_c.Call.Return(run)
return _c
}
// SendActionsMail provides a mock function for the type Client
func (_mock *Client) SendActionsMail(ctx context.Context, realm string, userID string, userActions []keycloak.UserAction) error {
ret := _mock.Called(ctx, realm, userID, userActions)
if len(ret) == 0 {
panic("no return value specified for SendActionsMail")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, []keycloak.UserAction) error); ok {
r0 = returnFunc(ctx, realm, userID, userActions)
} else {
r0 = ret.Error(0)
}
return r0
}
// Client_SendActionsMail_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendActionsMail'
type Client_SendActionsMail_Call struct {
*mock.Call
}
// SendActionsMail is a helper method to define mock.On call
// - ctx context.Context
// - realm string
// - userID string
// - userActions []keycloak.UserAction
func (_e *Client_Expecter) SendActionsMail(ctx interface{}, realm interface{}, userID interface{}, userActions interface{}) *Client_SendActionsMail_Call {
return &Client_SendActionsMail_Call{Call: _e.mock.On("SendActionsMail", ctx, realm, userID, userActions)}
}
func (_c *Client_SendActionsMail_Call) Run(run func(ctx context.Context, realm string, userID string, userActions []keycloak.UserAction)) *Client_SendActionsMail_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
var arg2 string
if args[2] != nil {
arg2 = args[2].(string)
}
var arg3 []keycloak.UserAction
if args[3] != nil {
arg3 = args[3].([]keycloak.UserAction)
}
run(
arg0,
arg1,
arg2,
arg3,
)
})
return _c
}
func (_c *Client_SendActionsMail_Call) Return(err error) *Client_SendActionsMail_Call {
_c.Call.Return(err)
return _c
}
func (_c *Client_SendActionsMail_Call) RunAndReturn(run func(ctx context.Context, realm string, userID string, userActions []keycloak.UserAction) error) *Client_SendActionsMail_Call {
_c.Call.Return(run)
return _c
}