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
+219
View File
@@ -0,0 +1,219 @@
// Package keycloak is a package for keycloak utility functions.
package keycloak
import (
"context"
"crypto/tls"
"fmt"
"github.com/Nerzal/gocloak/v13"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
)
// Some attribute constants.
// TODO: Make these configurable in the future.
const (
_idAttr = "QSFERA_ID"
_userTypeAttr = "QSFERA_USER_TYPE"
)
// ConcreteClient represents a concrete implementation of a keycloak client
type ConcreteClient struct {
keycloak GoCloak
clientID string
clientSecret string
realm string
baseURL string
}
// New instantiates a new keycloak.Backend with a default gocloak client.
func New(
baseURL, clientID, clientSecret, realm string,
insecureSkipVerify bool,
) *ConcreteClient {
gc := gocloak.NewClient(baseURL)
restyClient := gc.RestyClient()
restyClient.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: insecureSkipVerify}) //nolint:gosec
return NewWithClient(gc, baseURL, clientID, clientSecret, realm)
}
// NewWithClient instantiates a new keycloak.Backend with a custom
func NewWithClient(
gocloakClient GoCloak,
baseURL, clientID, clientSecret, realm string,
) *ConcreteClient {
return &ConcreteClient{
keycloak: gocloakClient,
baseURL: baseURL,
clientID: clientID,
clientSecret: clientSecret,
realm: realm,
}
}
// CreateUser creates a user from a libregraph user and returns its *keycloak* ID.
// TODO: For now we only call this from the invitation service where all the attributes are set correctly.
//
// For more wider use, do some sanity checking on the user instance.
func (c *ConcreteClient) CreateUser(ctx context.Context, realm string, user *libregraph.User, userActions []UserAction) (string, error) {
token, err := c.getToken(ctx)
if err != nil {
return "", err
}
req := gocloak.User{
Email: user.Mail,
Enabled: user.AccountEnabled,
Username: &user.OnPremisesSamAccountName,
FirstName: user.GivenName,
LastName: user.Surname,
Attributes: &map[string][]string{
_idAttr: {user.GetId()},
_userTypeAttr: {user.GetUserType()},
},
RequiredActions: convertUserActions(userActions),
}
return c.keycloak.CreateUser(ctx, token.AccessToken, realm, req)
}
// SendActionsMail sends a mail to the user with userID instructing them to do the actions defined in userActions.
func (c *ConcreteClient) SendActionsMail(ctx context.Context, realm, userID string, userActions []UserAction) error {
token, err := c.getToken(ctx)
if err != nil {
return err
}
params := gocloak.ExecuteActionsEmail{
UserID: &userID,
Actions: convertUserActions(userActions),
}
return c.keycloak.ExecuteActionsEmail(ctx, token.AccessToken, realm, params)
}
// getUserByParams looks up a user by the given parameters.
func (c *ConcreteClient) getUserByParams(ctx context.Context, realm string, params gocloak.GetUsersParams) (*libregraph.User, error) {
token, err := c.getToken(ctx)
if err != nil {
return nil, err
}
users, err := c.keycloak.GetUsers(ctx, token.AccessToken, realm, params)
if err != nil {
return nil, err
}
if len(users) == 0 {
return nil, fmt.Errorf("no users found")
}
if len(users) > 1 {
return nil, fmt.Errorf("%d users found", len(users))
}
return c.keycloakUserToLibregraph(users[0]), nil
}
// GetUserByUsername looks up a user by username.
func (c *ConcreteClient) GetUserByUsername(ctx context.Context, realm, username string) (*libregraph.User, error) {
return c.getUserByParams(ctx, realm, gocloak.GetUsersParams{
Username: &username,
})
}
// GetPIIReport returns a structure with all the PII for the user.
func (c *ConcreteClient) GetPIIReport(ctx context.Context, realm, username string) (*PIIReport, error) {
u, err := c.GetUserByUsername(ctx, realm, username)
if err != nil {
return nil, err
}
token, err := c.getToken(ctx)
if err != nil {
return nil, err
}
keycloakID, err := c.getKeyCloakID(u)
if err != nil {
return nil, err
}
sessions, err := c.keycloak.GetUserSessions(ctx, token.AccessToken, realm, keycloakID)
if err != nil {
return nil, err
}
return &PIIReport{
UserData: u,
Sessions: sessions,
}, nil
}
// getToken gets a fresh token for the request.
// TODO: set a token on the struct and check if it's still valid before requesting a new one.
func (c *ConcreteClient) getToken(ctx context.Context) (*gocloak.JWT, error) {
token, err := c.keycloak.LoginClient(ctx, c.clientID, c.clientSecret, c.realm)
if err != nil {
return nil, fmt.Errorf("failed to get token: %w", err)
}
rRes, err := c.keycloak.RetrospectToken(ctx, token.AccessToken, c.clientID, c.clientSecret, c.realm)
if err != nil {
return nil, fmt.Errorf("failed to retrospect token: %w", err)
}
if !*rRes.Active {
return nil, fmt.Errorf("token is not active")
}
return token, nil
}
func (c *ConcreteClient) keycloakUserToLibregraph(u *gocloak.User) *libregraph.User {
var ldapID string
var userType *string
if u.Attributes != nil {
attrs := *u.Attributes
ldapIDs, ok := attrs[_idAttr]
if ok {
ldapID = ldapIDs[0]
}
userTypes, ok := attrs[_userTypeAttr]
if ok {
userType = &userTypes[0]
}
}
return &libregraph.User{
Id: &ldapID,
Mail: u.Email,
GivenName: u.FirstName,
Surname: u.LastName,
AccountEnabled: u.Enabled,
UserType: userType,
Identities: []libregraph.ObjectIdentity{
{
Issuer: &c.baseURL,
IssuerAssignedId: u.ID,
},
},
}
}
func (c *ConcreteClient) getKeyCloakID(u *libregraph.User) (string, error) {
for _, i := range u.Identities {
if *i.Issuer == c.baseURL {
return *i.IssuerAssignedId, nil
}
}
return "", fmt.Errorf("could not find identity for issuer: %s", c.baseURL)
}
func convertUserActions(userActions []UserAction) *[]string {
stringActions := make([]string, len(userActions))
for i, a := range userActions {
stringActions[i] = userActionsToString[a]
}
return &stringActions
}
+18
View File
@@ -0,0 +1,18 @@
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)
GetUsers(ctx context.Context, token, realm string, params gocloak.GetUsersParams) ([]*gocloak.User, error)
ExecuteActionsEmail(ctx context.Context, token, realm string, params gocloak.ExecuteActionsEmail) error
LoginClient(ctx context.Context, clientID, clientSecret, realm string, scopes ...string) (*gocloak.JWT, error)
RetrospectToken(ctx context.Context, accessToken, clientID, clientSecret, realm string) (*gocloak.IntroSpectTokenResult, error)
GetCredentials(ctx context.Context, accessToken, realm, userID string) ([]*gocloak.CredentialRepresentation, error)
GetUserSessions(ctx context.Context, token, realm, userID string) ([]*gocloak.UserSessionRepresentation, error)
}
+39
View File
@@ -0,0 +1,39 @@
package keycloak
import (
"context"
"github.com/Nerzal/gocloak/v13"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
)
// UserAction defines a type for user actions
type UserAction int8
// An incomplete list of UserActions
const (
// UserActionUpdatePassword sets it that the user needs to change their password.
UserActionUpdatePassword UserAction = iota
// UserActionVerifyEmail sets it that the user needs to verify their email address.
UserActionVerifyEmail
)
// A lookup table to translate user actions to their string equivalents
var userActionsToString = map[UserAction]string{
UserActionUpdatePassword: "UPDATE_PASSWORD",
UserActionVerifyEmail: "VERIFY_EMAIL",
}
// PIIReport is a structure of all the PersonalIdentifiableInformation contained in keycloak.
type PIIReport struct {
UserData *libregraph.User
Sessions []*gocloak.UserSessionRepresentation
}
// Client represents a keycloak client.
type Client interface {
CreateUser(ctx context.Context, realm string, user *libregraph.User, userActions []UserAction) (string, error)
SendActionsMail(ctx context.Context, realm, userID string, userActions []UserAction) error
GetUserByUsername(ctx context.Context, realm, username string) (*libregraph.User, error)
GetPIIReport(ctx context.Context, realm, username string) (*PIIReport, error)
}