200 lines
6.9 KiB
Go
200 lines
6.9 KiB
Go
package oidc
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/url"
|
|
|
|
"github.com/hashicorp/boundary/internal/db"
|
|
"github.com/hashicorp/boundary/internal/errors"
|
|
"github.com/hashicorp/boundary/internal/kms"
|
|
"github.com/hashicorp/boundary/internal/oplog"
|
|
wrapping "github.com/hashicorp/go-kms-wrapping"
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
// Account must implement oplog.Replayable for upsertAccount to work
|
|
var _ oplog.ReplayableMessage = (*Account)(nil)
|
|
|
|
// Account must implement proto.Message for upsertAccount to work
|
|
var _ proto.Message = (*Account)(nil)
|
|
|
|
// upsertAccount will create/update account using claims from the user's ID and Access Tokens.
|
|
func (r *Repository) upsertAccount(ctx context.Context, am *AuthMethod, IdTokenClaims, AccessTokenClaims map[string]interface{}) (*Account, error) {
|
|
const op = "oidc.(Repository).upsertAccount"
|
|
if am == nil || am.AuthMethod == nil {
|
|
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method")
|
|
}
|
|
if IdTokenClaims == nil {
|
|
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing ID Token claims")
|
|
}
|
|
if AccessTokenClaims == nil {
|
|
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing Access Token claims")
|
|
}
|
|
|
|
fromSub, fromName, fromEmail := string(ToSubClaim), string(ToNameClaim), string(ToEmailClaim)
|
|
if len(am.AccountClaimMaps) > 0 {
|
|
acms, err := ParseAccountClaimMaps(ctx, am.AccountClaimMaps...)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
for _, m := range acms {
|
|
toClaim, err := ConvertToAccountToClaim(ctx, m.To)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
switch toClaim {
|
|
case ToSubClaim:
|
|
fromSub = m.From
|
|
case ToEmailClaim:
|
|
fromEmail = m.From
|
|
case ToNameClaim:
|
|
fromName = m.From
|
|
default:
|
|
// should never happen, but including it just in case.
|
|
return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("%s=%s is not a valid account claim map", m.From, m.To))
|
|
}
|
|
}
|
|
}
|
|
|
|
var iss, sub string
|
|
var ok bool
|
|
if iss, ok = IdTokenClaims["iss"].(string); !ok {
|
|
return nil, errors.New(ctx, errors.Unknown, op, "issuer is not present in ID Token, which should not be possible")
|
|
}
|
|
if sub, ok = IdTokenClaims[fromSub].(string); !ok {
|
|
return nil, errors.New(ctx, errors.Unknown, op, fmt.Sprintf("mapping 'claim' %s to account subject and it is not present in ID Token", fromSub))
|
|
}
|
|
pubId, err := newAccountId(ctx, am.GetPublicId(), iss, sub)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
issAsUrl, err := url.Parse(iss)
|
|
if err != nil {
|
|
return nil, errors.New(ctx, errors.Unknown, op, "unable to parse issuer", errors.WithWrap(err))
|
|
}
|
|
|
|
acct := AllocAccount()
|
|
acct.PublicId = pubId
|
|
acct.AuthMethodId = am.PublicId
|
|
acct.Issuer = issAsUrl.String()
|
|
acct.Subject = sub
|
|
|
|
oc := db.OnConflict{
|
|
Target: db.Constraint("auth_oidc_account_auth_method_id_issuer_subject_uq"),
|
|
Action: db.SetColumns([]string{"public_id", "auth_method_id", "issuer", "subject", "userinfo_claims", "token_claims"}),
|
|
}
|
|
|
|
marshaledTokenClaims, err := json.Marshal(IdTokenClaims)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
acct.TokenClaims = string(marshaledTokenClaims)
|
|
|
|
marshaledAccessTokenClaims, err := json.Marshal(AccessTokenClaims)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
acct.UserinfoClaims = string(marshaledAccessTokenClaims)
|
|
|
|
var foundName, foundEmail bool
|
|
switch {
|
|
case AccessTokenClaims[fromName] != nil:
|
|
foundName = true
|
|
acct.FullName = AccessTokenClaims[fromName].(string)
|
|
case IdTokenClaims[fromName] != nil:
|
|
foundName = true
|
|
acct.FullName = IdTokenClaims[fromName].(string)
|
|
}
|
|
|
|
switch {
|
|
case AccessTokenClaims[fromEmail] != nil:
|
|
foundEmail = true
|
|
acct.Email = AccessTokenClaims[fromEmail].(string)
|
|
case IdTokenClaims[fromEmail] != nil:
|
|
foundEmail = true
|
|
acct.Email = IdTokenClaims[fromEmail].(string)
|
|
}
|
|
if foundName {
|
|
oc.Action = append(oc.Action.([]db.ColumnValue), db.SetColumns([]string{"full_name"})...)
|
|
}
|
|
if foundEmail {
|
|
oc.Action = append(oc.Action.([]db.ColumnValue), db.SetColumns([]string{"email"})...)
|
|
}
|
|
|
|
oplogWrapper, err := r.kms.GetWrapper(ctx, am.ScopeId, kms.KeyPurposeOplog)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"))
|
|
}
|
|
|
|
_, err = r.writer.DoTx(
|
|
ctx,
|
|
db.StdRetryCnt,
|
|
db.ExpBackoff{},
|
|
func(reader db.Reader, w db.Writer) error {
|
|
var rowCnt int64
|
|
err := w.Create(ctx, acct, db.WithOnConflict(&oc), db.WithReturnRowsAffected(&rowCnt), db.WithOplog(oplogWrapper, acct.oplog(oplog.OpType_OP_TYPE_CREATE, am.ScopeId)))
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to insert/update auth oidc account"))
|
|
}
|
|
if rowCnt > 1 {
|
|
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("expected 1 row but got: %d", rowCnt))
|
|
}
|
|
return nil
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
return acct, nil
|
|
}
|
|
|
|
// upsertOplog will write oplog msgs for account upserts. The db.Writer needs to be the writer for the current
|
|
// transaction that's executing the upsert. Both fieldMasks and nullMasks are allowed to be nil for update operations.
|
|
func upsertOplog(ctx context.Context, w db.Writer, oplogWrapper wrapping.Wrapper, operation oplog.OpType, scopeId string, acct *Account, fieldMasks, nullMasks []string) error {
|
|
const op = "oidc.upsertOplog"
|
|
if w == nil {
|
|
return errors.New(ctx, errors.InvalidParameter, op, "missing db writer")
|
|
}
|
|
if oplogWrapper == nil {
|
|
return errors.New(ctx, errors.InvalidParameter, op, "missing oplog wrapper")
|
|
}
|
|
if operation != oplog.OpType_OP_TYPE_CREATE && operation != oplog.OpType_OP_TYPE_UPDATE {
|
|
return errors.New(ctx, errors.Internal, op, fmt.Sprintf("not a supported operation: %s", operation))
|
|
}
|
|
if scopeId == "" {
|
|
return errors.New(ctx, errors.InvalidParameter, op, "missing scope id")
|
|
}
|
|
if acct == nil || acct.Account == nil {
|
|
return errors.New(ctx, errors.InvalidParameter, op, "missing account")
|
|
}
|
|
if operation == oplog.OpType_OP_TYPE_UPDATE && len(fieldMasks) == 0 && len(nullMasks) == 0 {
|
|
return errors.New(ctx, errors.InvalidParameter, op, "update operations must specify field masks and/or null masks")
|
|
}
|
|
ticket, err := w.GetTicket(acct)
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket"))
|
|
}
|
|
metadata := acct.oplog(operation, scopeId)
|
|
acctAsReplayable, ok := interface{}(acct).(oplog.ReplayableMessage)
|
|
if !ok {
|
|
return errors.New(ctx, errors.Internal, op, "account is not replayable")
|
|
}
|
|
acctAsProto, ok := interface{}(acct).(proto.Message)
|
|
if !ok {
|
|
return errors.New(ctx, errors.Internal, op, "account is not a proto message")
|
|
}
|
|
msg := oplog.Message{
|
|
Message: acctAsProto,
|
|
TypeName: acctAsReplayable.TableName(),
|
|
OpType: oplog.OpType_OP_TYPE_CREATE,
|
|
FieldMaskPaths: fieldMasks,
|
|
SetToNullPaths: nullMasks,
|
|
}
|
|
if err := w.WriteOplogEntryWith(ctx, oplogWrapper, ticket, metadata, []*oplog.Message{&msg}); err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
return nil
|
|
}
|