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" "github.com/cs3org/reva/pkg/token/manager/jwt" acc "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" ) func getAccount(l log.Logger, ac acc.AccountsService, query string) (account *acc.Account, status int) { resp, err := ac.ListAccounts(context.Background(), &acc.ListAccountsRequest{ Query: query, PageSize: 2, }) if err != nil { l.Error().Err(err).Str("query", query).Msgf("Error fetching from accounts-service") status = http.StatusInternalServerError return } if len(resp.Accounts) <= 0 { l.Error().Str("query", query).Msgf("Account not found") status = http.StatusNotFound return } if len(resp.Accounts) > 1 { l.Error().Str("query", query).Msgf("More than one account found. Not logging user in.") status = http.StatusForbidden return } account = resp.Accounts[0] return } func createAccount(l log.Logger, claims *oidc.StandardClaims, ac acc.AccountsService) (*acc.Account, int) { // TODO check if fields are missing. req := &acc.CreateAccountRequest{ Account: &acc.Account{ DisplayName: claims.DisplayName, PreferredName: claims.PreferredUsername, OnPremisesSamAccountName: claims.PreferredUsername, Mail: claims.Email, CreationType: "LocalAccount", AccountEnabled: true, // TODO assign uidnumber and gidnumber? better do that in ocis-accounts as it can keep track of the next numbers }, } 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 } // AccountUUID provides a middleware which mints a jwt and adds it to the proxied request based // on the oidc-claims func AccountUUID(opts ...Option) func(next http.Handler) http.Handler { opt := newOptions(opts...) return func(next http.Handler) http.Handler { // TODO: handle error tokenManager, err := jwt.New(map[string]interface{}{ "secret": opt.TokenManagerConfig.JWTSecret, "expires": int64(60), }) if err != nil { opt.Logger.Fatal().Err(err).Msgf("Could not initialize token-manager") } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { l := opt.Logger claims := oidc.FromContext(r.Context()) if claims == nil { next.ServeHTTP(w, r) return } var account *acc.Account var status int if claims.Email != "" { account, status = getAccount(l, opt.AccountsClient, fmt.Sprintf("mail eq '%s'", strings.ReplaceAll(claims.Email, "'", "''"))) } else if claims.PreferredUsername != "" { account, status = getAccount(l, opt.AccountsClient, fmt.Sprintf("preferred_name eq '%s'", strings.ReplaceAll(claims.PreferredUsername, "'", "''"))) } else if claims.OcisID != "" { account, status = getAccount(l, opt.AccountsClient, fmt.Sprintf("id eq '%s'", strings.ReplaceAll(claims.OcisID, "'", "''"))) } else { // TODO allow lookup by custom claim, eg an id ... or sub l.Error().Err(err).Msgf("Could not lookup account, no mail or preferred_username claim set") w.WriteHeader(http.StatusInternalServerError) } if status != 0 || account == nil { if opt.AutoprovisionAccounts && status == http.StatusNotFound { account, status = createAccount(l, claims, opt.AccountsClient) if status != 0 { w.WriteHeader(status) return } } else { w.WriteHeader(status) return } } if !account.AccountEnabled { l.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 := opt.SettingsRoleService.ListRoleAssignments(r.Context(), &settings.ListRoleAssignmentsRequest{AccountUuid: account.Id}) roleIDs := make([]string, 0) if err != nil { l.Err(err).Str("accountID", account.Id).Msg("failed to fetch role assignments") } else { for _, assignment := range assignmentResponse.Assignments { roleIDs = append(roleIDs, assignment.RoleId) } } l.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 { l.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 := tokenManager.MintToken(r.Context(), user) if err != nil { l.Error().Err(err).Msgf("Could not mint token") w.WriteHeader(http.StatusInternalServerError) return } r.Header.Set("x-access-token", token) next.ServeHTTP(w, r) }) } }