Files
opencloud/proxy/pkg/middleware/account_resolver.go
2020-11-17 12:23:40 +01:00

211 lines
6.4 KiB
Go

package middleware
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
revaUser "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
tokenPkg "github.com/cs3org/reva/pkg/token"
"github.com/cs3org/reva/pkg/token/manager/jwt"
accounts "github.com/owncloud/ocis/accounts/pkg/proto/v0"
"github.com/owncloud/ocis/ocis-pkg/log"
"github.com/owncloud/ocis/ocis-pkg/oidc"
settings "github.com/owncloud/ocis/settings/pkg/proto/v0"
)
// AccountResolver provides a middleware which mints a jwt and adds it to the proxied request based
// on the oidc-claims
func AccountResolver(optionSetters ...Option) func(next http.Handler) http.Handler {
options := newOptions(optionSetters...)
logger := options.Logger
return func(next http.Handler) http.Handler {
tokenManager, err := jwt.New(map[string]interface{}{
"secret": options.TokenManagerConfig.JWTSecret,
"expires": int64(60),
})
if err != nil {
logger.Fatal().Err(err).Msgf("Could not initialize token-manager")
}
return &accountResolver{
next: next,
logger: logger,
tokenManager: tokenManager,
accountsClient: options.AccountsClient,
oidcIss: options.OIDCIss,
autoprovisionAccounts: options.AutoprovisionAccounts,
settingsRoleService: options.SettingsRoleService,
}
}
}
type accountResolver struct {
oidcIss string
autoprovisionAccounts bool
next http.Handler
logger log.Logger
tokenManager tokenPkg.Manager
accountsClient accounts.AccountsService
settingsRoleService settings.RoleService
}
func (m accountResolver) ServeHTTP(w http.ResponseWriter, req *http.Request) {
var account *accounts.Account
var status int
claims := oidc.FromContext(req.Context())
if claims == nil {
m.next.ServeHTTP(w, req)
return
}
switch {
case claims.Email != "":
account, status = getAccount(m.logger, m.accountsClient, fmt.Sprintf("mail eq '%s'", strings.ReplaceAll(claims.Email, "'", "''")))
case claims.PreferredUsername != "":
account, status = getAccount(m.logger, m.accountsClient, fmt.Sprintf("preferred_name eq '%s'", strings.ReplaceAll(claims.PreferredUsername, "'", "''")))
case claims.OcisID != "":
account, status = getAccount(m.logger, m.accountsClient, fmt.Sprintf("id eq '%s'", strings.ReplaceAll(claims.OcisID, "'", "''")))
default:
// TODO allow lookup by custom claim, eg an id ... or sub
m.logger.Error().Msg("Could not lookup account, no mail or preferred_username claim set")
w.WriteHeader(http.StatusInternalServerError)
}
if m.autoprovisionAccounts && status == http.StatusNotFound {
account, status = createAccount(m.logger, claims, m.accountsClient)
}
if status != 0 || account == nil {
w.WriteHeader(status)
return
}
if !account.AccountEnabled {
m.logger.Debug().Interface("account", account).Msg("account is disabled")
w.WriteHeader(http.StatusUnauthorized)
return
}
groups := make([]string, len(account.MemberOf))
for i := range account.MemberOf {
// reva needs the unix group name
groups[i] = account.MemberOf[i].OnPremisesSamAccountName
}
// fetch active roles from ocis-settings
assignmentResponse, err := m.settingsRoleService.ListRoleAssignments(req.Context(), &settings.ListRoleAssignmentsRequest{AccountUuid: account.Id})
roleIDs := make([]string, 0)
if err != nil {
m.logger.Err(err).Str("accountID", account.Id).Msg("failed to fetch role assignments")
} else {
for _, assignment := range assignmentResponse.Assignments {
roleIDs = append(roleIDs, assignment.RoleId)
}
}
m.logger.Debug().Interface("claims", claims).Interface("account", account).Msgf("associated claims with uuid")
user := &revaUser.User{
Id: &revaUser.UserId{
OpaqueId: account.Id,
Idp: claims.Iss,
},
Username: account.OnPremisesSamAccountName,
DisplayName: account.DisplayName,
Mail: account.Mail,
MailVerified: account.ExternalUserState == "" || account.ExternalUserState == "Accepted",
Groups: groups,
Opaque: &types.Opaque{
Map: map[string]*types.OpaqueEntry{},
},
}
user.Opaque.Map["uid"] = &types.OpaqueEntry{
Decoder: "plain",
Value: []byte(strconv.FormatInt(account.UidNumber, 10)),
}
user.Opaque.Map["gid"] = &types.OpaqueEntry{
Decoder: "plain",
Value: []byte(strconv.FormatInt(account.GidNumber, 10)),
}
// encode roleIDs as json string
roleIDsJSON, jsonErr := json.Marshal(roleIDs)
if jsonErr != nil {
m.logger.Err(jsonErr).Str("accountID", account.Id).Msg("failed to marshal roleIDs into json")
} else {
user.Opaque.Map["roles"] = &types.OpaqueEntry{
Decoder: "json",
Value: roleIDsJSON,
}
}
token, err := m.tokenManager.MintToken(req.Context(), user)
if err != nil {
m.logger.Error().Err(err).Msgf("could not mint token")
w.WriteHeader(http.StatusInternalServerError)
return
}
req.Header.Set("x-access-token", token)
m.next.ServeHTTP(w, req)
}
func getAccount(logger log.Logger, ac accounts.AccountsService, query string) (account *accounts.Account, status int) {
resp, err := ac.ListAccounts(context.Background(), &accounts.ListAccountsRequest{
Query: query,
PageSize: 2,
})
if err != nil {
logger.Error().Err(err).Str("query", query).Msgf("error fetching from accounts-service")
status = http.StatusInternalServerError
return
}
if len(resp.Accounts) <= 0 {
logger.Error().Str("query", query).Msgf("account not found")
status = http.StatusNotFound
return
}
if len(resp.Accounts) > 1 {
logger.Error().Str("query", query).Msgf("more than one account found, aborting")
status = http.StatusForbidden
return
}
account = resp.Accounts[0]
return
}
func createAccount(l log.Logger, claims *oidc.StandardClaims, ac accounts.AccountsService) (*accounts.Account, int) {
// TODO check if fields are missing.
req := &accounts.CreateAccountRequest{
Account: &accounts.Account{
DisplayName: claims.DisplayName,
PreferredName: claims.PreferredUsername,
OnPremisesSamAccountName: claims.PreferredUsername,
Mail: claims.Email,
CreationType: "LocalAccount",
AccountEnabled: true,
},
}
created, err := ac.CreateAccount(context.Background(), req)
if err != nil {
l.Error().Err(err).Interface("account", req.Account).Msg("could not create account")
return nil, http.StatusInternalServerError
}
return created, 0
}