// 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 }