mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-05 19:59:37 -06:00
Use keycloak for invitations backend.
As keycloak already supports everything needed for the required invitation flow, it's ideal to use as the first backend to create users and to send them invitation mails. This PR implements that as the first and (for now) only backend.
This commit is contained in:
5
go.mod
5
go.mod
@@ -113,6 +113,7 @@ require (
|
||||
github.com/Masterminds/sprig v2.22.0+incompatible // indirect
|
||||
github.com/Microsoft/go-winio v0.6.0 // indirect
|
||||
github.com/OneOfOne/xxhash v1.2.8 // indirect
|
||||
github.com/Nerzal/gocloak/v13 v13.1.0 // indirect
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20220930113650-c6815a8c17ad // indirect
|
||||
github.com/RoaringBitmap/roaring v0.9.4 // indirect
|
||||
github.com/acomagu/bufpipe v1.0.3 // indirect
|
||||
@@ -185,6 +186,8 @@ require (
|
||||
github.com/go-logfmt/logfmt v0.5.1 // indirect
|
||||
github.com/go-logr/logr v1.2.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-redis/redis/v8 v8.11.5 // indirect
|
||||
github.com/go-resty/resty/v2 v2.7.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.6.0 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
@@ -260,6 +263,7 @@ require (
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/nxadm/tail v1.4.8 // indirect
|
||||
github.com/opencontainers/runtime-spec v1.0.2 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
@@ -276,6 +280,7 @@ require (
|
||||
github.com/russellhaering/goxmldsig v1.2.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/sciencemesh/meshdirectory-web v1.0.4 // indirect
|
||||
github.com/segmentio/ksuid v1.0.4 // indirect
|
||||
github.com/sergi/go-diff v1.2.0 // indirect
|
||||
github.com/sethvargo/go-password v0.2.0 // indirect
|
||||
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 // indirect
|
||||
|
||||
8
go.sum
8
go.sum
@@ -435,6 +435,8 @@ github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugX
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg=
|
||||
github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE=
|
||||
github.com/Nerzal/gocloak/v13 v13.1.0 h1:ret4pZTIsSQGZHURDMJ4jXnUmHyEoRykBqDTsAKoj8c=
|
||||
github.com/Nerzal/gocloak/v13 v13.1.0/go.mod h1:rRBtEdh5N0+JlZZEsrfZcB2sRMZWbgSxI2EIv9jpJp4=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
|
||||
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
|
||||
@@ -823,6 +825,8 @@ github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn
|
||||
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
||||
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
||||
github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48/go.mod h1:dZGr0i9PLlaaTD4H/hoZIDjQ+r6xq8mgbRzHZf7f2J8=
|
||||
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
|
||||
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
|
||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
@@ -1362,6 +1366,7 @@ github.com/open-policy-agent/opa v0.50.0/go.mod h1:9jKfDk0L5b9rnhH4M0nq10cGHbYOx
|
||||
github.com/opencontainers/runtime-spec v1.0.2 h1:UfAcuLBJB9Coz72x1hgl8O5RVzTdNiaglX6v2DM6FI0=
|
||||
github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
|
||||
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
|
||||
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
||||
github.com/oracle/oci-go-sdk v24.3.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35ukwStZIg5F66tcBccjip/j888=
|
||||
@@ -1489,6 +1494,8 @@ github.com/sciencemesh/meshdirectory-web v1.0.4 h1:1YSctF6PAXhoHUYCaeRTj7rHaF7b3
|
||||
github.com/sciencemesh/meshdirectory-web v1.0.4/go.mod h1:fJSThTS3xf+sTdL0iXQoaQJssLI7tn7DetHMHUl4SRk=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c=
|
||||
github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
|
||||
@@ -1812,6 +1819,7 @@ golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qx
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
|
||||
149
services/invitations/pkg/backends/keycloak/backend.go
Normal file
149
services/invitations/pkg/backends/keycloak/backend.go
Normal file
@@ -0,0 +1,149 @@
|
||||
// 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
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/Nerzal/gocloak/v13"
|
||||
"github.com/google/uuid"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/services/invitations/pkg/invitations"
|
||||
)
|
||||
|
||||
const (
|
||||
idAttr = "OWNCLOUD_ID"
|
||||
userTypeAttr = "OWNCLOUD_USER_TYPE"
|
||||
userTypeVal = "Guest"
|
||||
)
|
||||
|
||||
var userRequiredActions = []string{"UPDATE_PASSWORD", "VERIFY_EMAIL"}
|
||||
|
||||
// Backend represents the keycloak backend.
|
||||
type Backend struct {
|
||||
logger log.Logger
|
||||
client *gocloak.GoCloak
|
||||
clientID string
|
||||
clientSecret string
|
||||
clientRealm string
|
||||
userRealm string
|
||||
}
|
||||
|
||||
// New returns a new keycloak.Backend, with all the config options set.
|
||||
func New(
|
||||
logger log.Logger,
|
||||
baseURL, clientID, clientSecret, clientRealm, userRealm string,
|
||||
insecureSkipVerify bool,
|
||||
) *Backend {
|
||||
client := gocloak.NewClient(baseURL)
|
||||
restyClient := client.RestyClient()
|
||||
restyClient.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: insecureSkipVerify})
|
||||
return &Backend{
|
||||
logger: log.Logger{
|
||||
logger.With().
|
||||
Str("invitationBackend", "keycloak").
|
||||
Str("baseURL", baseURL).
|
||||
Str("clientID", clientID).
|
||||
Str("clientRealm", clientRealm).
|
||||
Str("userRealm", userRealm).
|
||||
Logger(),
|
||||
},
|
||||
client: client,
|
||||
clientID: clientID,
|
||||
clientSecret: clientSecret,
|
||||
clientRealm: clientRealm,
|
||||
userRealm: userRealm,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateUser creates a user in the keycloak backend.
|
||||
func (b Backend) CreateUser(ctx context.Context, invitation *invitations.Invitation) (string, error) {
|
||||
token, err := b.getToken(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
u := uuid.New()
|
||||
|
||||
firstName, lastName := splitDisplayName(invitation.InvitedUserDisplayName)
|
||||
b.logger.Info().
|
||||
Str(idAttr, u.String()).
|
||||
Str("email", invitation.InvitedUserEmailAddress).
|
||||
Msg("Creating new user")
|
||||
user := gocloak.User{
|
||||
FirstName: &firstName,
|
||||
LastName: &lastName,
|
||||
Email: &invitation.InvitedUserEmailAddress,
|
||||
Enabled: gocloak.BoolP(true),
|
||||
Username: &invitation.InvitedUserEmailAddress,
|
||||
Attributes: &map[string][]string{
|
||||
idAttr: {u.String()},
|
||||
userTypeAttr: {userTypeVal},
|
||||
},
|
||||
RequiredActions: &userRequiredActions,
|
||||
}
|
||||
|
||||
id, err := b.client.CreateUser(ctx, token.AccessToken, b.userRealm, user)
|
||||
if err != nil {
|
||||
b.logger.Error().
|
||||
Str(idAttr, 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 to send mail.
|
||||
func (b Backend) CanSendMail() bool { return true }
|
||||
|
||||
// SendMail sends a mail to the user with details on how to reedeem the invitation.
|
||||
func (b Backend) SendMail(ctx context.Context, id string) error {
|
||||
token, err := b.getToken(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
params := gocloak.ExecuteActionsEmail{
|
||||
UserID: &id,
|
||||
Actions: &userRequiredActions,
|
||||
}
|
||||
return b.client.ExecuteActionsEmail(ctx, token.AccessToken, b.userRealm, params)
|
||||
}
|
||||
|
||||
func (b Backend) getToken(ctx context.Context) (*gocloak.JWT, error) {
|
||||
b.logger.Debug().Msg("Logging into keycloak")
|
||||
token, err := b.client.LoginClient(ctx, b.clientID, b.clientSecret, b.clientRealm)
|
||||
if err != nil {
|
||||
b.logger.Error().Err(err).Msg("failed to get token")
|
||||
return nil, fmt.Errorf("failed to get token: %w", err)
|
||||
}
|
||||
|
||||
rRes, err := b.client.RetrospectToken(ctx, token.AccessToken, b.clientID, b.clientSecret, b.clientRealm)
|
||||
if err != nil {
|
||||
b.logger.Error().Err(err).Msg("failed to introspect token")
|
||||
return nil, fmt.Errorf("failed to retrospect token: %w", err)
|
||||
}
|
||||
|
||||
if !*rRes.Active {
|
||||
b.logger.Error().Msg("token not active")
|
||||
return nil, fmt.Errorf("token is not active")
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// Quick and dirty way to split the last name off from the first name(s), imperfect, because
|
||||
// every culture has a different conception of names.
|
||||
func splitDisplayName(displayName string) (string, string) {
|
||||
parts := strings.Split(displayName, " ")
|
||||
if len(parts) <= 1 {
|
||||
return parts[0], ""
|
||||
}
|
||||
|
||||
return strings.Join(parts[:len(parts)-1], " "), parts[len(parts)-1]
|
||||
}
|
||||
@@ -18,18 +18,18 @@ type Config struct {
|
||||
|
||||
HTTP HTTP `yaml:"http"`
|
||||
|
||||
Endpoint Endpoint `yaml:"enpoint"`
|
||||
|
||||
Keycloak Keycloak `yaml:"keycloak"`
|
||||
TokenManager *TokenManager `yaml:"token_manager"`
|
||||
|
||||
Context context.Context `yaml:"-"`
|
||||
}
|
||||
|
||||
// Endpoint to use
|
||||
type Endpoint struct {
|
||||
URL string `yaml:"url" env:"INVITATIONS_PROVISIONING_URL" desc:"The endpoint provisioning requests are sent to."`
|
||||
Method string `yaml:"method" env:"INVITATIONS_PROVISIONING_METHOD" desc:"The method to use when making provisioning requests."`
|
||||
BodyTemplate string `yaml:"body_template" env:"INVITATIONS_PROVISIONING_BODY_TEMPLATE" desc:"The template to use as body of a provisioning request."`
|
||||
Authorization string `yaml:"authorization" env:"INVITATIONS_PROVISIONING_AUTH" desc:"The authorization to use. Can be 'token' to reuse the access token or 'bearer' to send a static api token."`
|
||||
Token string `yaml:"authorization" env:"INVITATIONS_PROVISIONING_AUTH" desc:"The bearer token to send in provisioning requests."`
|
||||
// Keycloak configuration
|
||||
type Keycloak struct {
|
||||
BasePath string `yaml:"base_path" env:"INVITATIONS_KEYCLOAK_BASE_PATH" desc:"The URL to keycloak."`
|
||||
ClientID string `yaml:"client_id" env:"INVITATIONS_KEYCLOAK_CLIENT_ID" desc:"The client id to authenticate with keycloak."`
|
||||
ClientSecret string `yaml:"client_secret" env:"INVITATIONS_KEYCLOAK_CLIENT_SECRET" desc:"The client secret to use in authentication."`
|
||||
ClientRealm string `yaml:"client_realm" env:"INVITATIONS_KEYCLOAK_CLIENT_REALM" desc:"The realm the client is defined in."`
|
||||
UserRealm string `yaml:"user_realm" env:"INVITATIONS_KEYCLOAK_USER_REALM" desc:"The realm the users are in."`
|
||||
InsecureSkipVerify bool `yaml:"insecure_skip_verify" env:"INVITATIONS_KEYCLOAK_INSECURE_SKIP_VERIFY" desc:"Skip the check of the TLS certificate."`
|
||||
}
|
||||
|
||||
@@ -32,16 +32,12 @@ func DefaultConfig() *config.Config {
|
||||
Service: config.Service{
|
||||
Name: "invitations",
|
||||
},
|
||||
Endpoint: config.Endpoint{
|
||||
URL: "{{.OCIS_URL}}/graph/v1.0/users",
|
||||
Method: "POST",
|
||||
BodyTemplate: `{
|
||||
"inviteRedirectUrl": "{{.redirectUrl}}",
|
||||
"invitedUserEmailAddress": "{{.mail}}",
|
||||
"invitedUserDisplayName": "{{.displayName}}",
|
||||
"sendInvitationMessage": true
|
||||
}`,
|
||||
Authorization: "token", // reuse existing token
|
||||
Keycloak: config.Keycloak{
|
||||
BasePath: "https://keycloak.example.org/",
|
||||
ClientID: "invitations-service",
|
||||
ClientSecret: "fake-secret",
|
||||
ClientRealm: "someRealm",
|
||||
UserRealm: "someRealm",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ func InvitationHandler(service svc.Service) func(w http.ResponseWriter, r *http.
|
||||
i := &invitations.Invitation{}
|
||||
err := json.NewDecoder(r.Body).Decode(i)
|
||||
if err != nil {
|
||||
//logger.Debug().Err(err).Interface("body", r.Body).Msg("could not invite user: invalid request body")
|
||||
// logger.Debug().Err(err).Interface("body", r.Body).Msg("could not invite user: invalid request body")
|
||||
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, fmt.Sprintf("invalid request body: %v", err.Error()))
|
||||
return
|
||||
}
|
||||
@@ -103,7 +103,7 @@ func InvitationHandler(service svc.Service) func(w http.ResponseWriter, r *http.
|
||||
return
|
||||
}
|
||||
|
||||
//w.Header().Set("Content-type", "application/json")
|
||||
// w.Header().Set("Content-type", "application/json")
|
||||
render.Status(r, http.StatusCreated)
|
||||
render.JSON(w, r, res)
|
||||
}
|
||||
|
||||
@@ -6,4 +6,5 @@ var (
|
||||
ErrNotFound = errors.New("query target not found")
|
||||
ErrBadRequest = errors.New("bad request")
|
||||
ErrMissingEmail = errors.New("missing email address")
|
||||
ErrBackend = errors.New("backend error")
|
||||
)
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
libregraph "github.com/owncloud/libre-graph-api-go"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/services/invitations/pkg/backends/keycloak"
|
||||
"github.com/owncloud/ocis/v2/services/invitations/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/services/invitations/pkg/invitations"
|
||||
)
|
||||
@@ -40,33 +34,46 @@ type Service interface {
|
||||
Invite(ctx context.Context, invitation *invitations.Invitation) (*invitations.Invitation, error)
|
||||
}
|
||||
|
||||
// Backend defines the behaviour of a user backend.
|
||||
type Backend interface {
|
||||
// CreateUser creates a user in the backend and returns an identifier string.
|
||||
CreateUser(ctx context.Context, invitation *invitations.Invitation) (string, error)
|
||||
// CanSendMail should return true if the backend can send mail
|
||||
CanSendMail() bool
|
||||
// SendMail sends a mail to the user with details on how to reedeem the invitation.
|
||||
SendMail(ctx context.Context, identifier string) error
|
||||
}
|
||||
|
||||
// New returns a new instance of Service
|
||||
func New(opts ...Option) (Service, error) {
|
||||
options := newOptions(opts...)
|
||||
|
||||
urlTemplate, err := template.New("invitations-provisioning-endpoint-url").Parse(options.Config.Endpoint.URL)
|
||||
bodyTemplate, err := template.New("invitations-provisioning-endpoint-url").Parse(options.Config.Endpoint.BodyTemplate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Harcode keycloak backend for now, but this should be configurable in the future.
|
||||
backend := keycloak.New(
|
||||
options.Logger,
|
||||
options.Config.Keycloak.BasePath,
|
||||
options.Config.Keycloak.ClientID,
|
||||
options.Config.Keycloak.ClientSecret,
|
||||
options.Config.Keycloak.ClientRealm,
|
||||
options.Config.Keycloak.UserRealm,
|
||||
options.Config.Keycloak.InsecureSkipVerify,
|
||||
)
|
||||
|
||||
return svc{
|
||||
log: options.Logger,
|
||||
config: options.Config,
|
||||
urlTemplate: urlTemplate,
|
||||
bodyTemplate: bodyTemplate,
|
||||
log: options.Logger,
|
||||
config: options.Config,
|
||||
backend: backend,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type svc struct {
|
||||
config *config.Config
|
||||
log log.Logger
|
||||
urlTemplate *template.Template
|
||||
bodyTemplate *template.Template
|
||||
config *config.Config
|
||||
log log.Logger
|
||||
backend Backend
|
||||
}
|
||||
|
||||
// Invite implements the service interface
|
||||
func (s svc) Invite(ctx context.Context, invitation *invitations.Invitation) (*invitations.Invitation, error) {
|
||||
|
||||
if invitation == nil {
|
||||
return nil, ErrBadRequest
|
||||
}
|
||||
@@ -75,85 +82,19 @@ func (s svc) Invite(ctx context.Context, invitation *invitations.Invitation) (*i
|
||||
return nil, ErrMissingEmail
|
||||
}
|
||||
|
||||
user := &libregraph.User{
|
||||
Mail: &invitation.InvitedUserEmailAddress,
|
||||
// TODO we cannot set the user type here
|
||||
}
|
||||
|
||||
if invitation.InvitedUserDisplayName != "" {
|
||||
user.DisplayName = &invitation.InvitedUserDisplayName
|
||||
}
|
||||
// we don't really need a username as guests have to log in with their email address anyway
|
||||
// what if later a user is provisioned with a guest accounts email address?
|
||||
|
||||
templateVars := map[string]string{
|
||||
"redirectUrl": invitation.InviteRedirectUrl,
|
||||
// TODO message and other options
|
||||
"mail": invitation.InvitedUserEmailAddress,
|
||||
"displayName": invitation.InvitedUserDisplayName,
|
||||
"userType": invitation.InvitedUserType,
|
||||
}
|
||||
|
||||
var urlWriter strings.Builder
|
||||
if err := s.urlTemplate.Execute(&urlWriter, templateVars); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var bodyWriter strings.Builder
|
||||
if err := s.bodyTemplate.Execute(&bodyWriter, templateVars); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// send a request to the provisioning endpoint
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true /*TODO make configurable*/},
|
||||
}
|
||||
client := &http.Client{Transport: tr}
|
||||
|
||||
req, err := http.NewRequest(s.config.Endpoint.Method, urlWriter.String(), bytes.NewBufferString(bodyWriter.String()))
|
||||
id, err := s.backend.CreateUser(ctx, invitation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("%w: %s", ErrBackend, err)
|
||||
}
|
||||
|
||||
// TODO either forward current user token or use bearer token?
|
||||
switch s.config.Endpoint.Authorization {
|
||||
case "token":
|
||||
// TODO forward current reva access token
|
||||
case "bearer":
|
||||
req.Header.Set("Authorization", "Bearer "+s.config.Endpoint.Token)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown authorization: " + s.config.Endpoint.Authorization)
|
||||
// As we only have a single backend, and that backend supports email, we don't have
|
||||
// any code to handle mailing ourself yet.
|
||||
if s.backend.CanSendMail() {
|
||||
err := s.backend.SendMail(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrBackend, err)
|
||||
}
|
||||
}
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
// TODO hm ok so we expect the rosponse to be a libregraph user ... so much for a generic endpoint
|
||||
// we could try parsing into a map[string]interface{} .... hm ... maybe better to be specific about
|
||||
// the actual backend: libregraph, keycloak, scim or even oc10?
|
||||
|
||||
// Or we remember the mail of the user in memory and try to check if the user is already avilable via
|
||||
// a local user api ... hm ... graph or cs3 user backend now?
|
||||
|
||||
// in any case this will require an additional endpoint to keep track of the ongoing invitations
|
||||
|
||||
invitedUser := &libregraph.User{}
|
||||
err = json.NewDecoder(res.Body).Decode(invitedUser)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := &invitations.Invitation{
|
||||
InvitedUser: invitedUser,
|
||||
}
|
||||
if res.StatusCode == http.StatusCreated {
|
||||
response.Status = "Completed"
|
||||
}
|
||||
|
||||
// optionally send an email
|
||||
|
||||
return response, nil
|
||||
return invitation, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user