mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2025-12-23 22:29:59 -05:00
220 lines
5.8 KiB
Go
220 lines
5.8 KiB
Go
// 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 = "OPENCLOUD_ID"
|
|
_userTypeAttr = "OPENCLOUD_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
|
|
}
|