2022-07-19 16:58:03 -04:00
// Copyright (c) HashiCorp, Inc.
2022-05-18 20:20:45 -07:00
// SPDX-License-Identifier: BUSL-1.1
2023-02-02 17:15:17 -08:00
2022-05-18 20:20:45 -07:00
package credentials
import (
"context"
2022-08-08 15:15:03 -04:00
"crypto/x509"
2022-05-18 20:20:45 -07:00
"encoding/base64"
2022-09-16 15:00:04 -07:00
"encoding/json"
2022-05-18 20:20:45 -07:00
"fmt"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/credential"
"github.com/hashicorp/boundary/internal/credential/static"
"github.com/hashicorp/boundary/internal/credential/static/store"
"github.com/hashicorp/boundary/internal/daemon/controller/auth"
"github.com/hashicorp/boundary/internal/daemon/controller/common"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers"
"github.com/hashicorp/boundary/internal/errors"
pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services"
2023-11-29 16:29:55 -08:00
"github.com/hashicorp/boundary/internal/listtoken"
"github.com/hashicorp/boundary/internal/pagination"
2022-05-18 20:20:45 -07:00
"github.com/hashicorp/boundary/internal/perms"
"github.com/hashicorp/boundary/internal/requests"
"github.com/hashicorp/boundary/internal/types/action"
"github.com/hashicorp/boundary/internal/types/resource"
"github.com/hashicorp/boundary/internal/types/subtypes"
pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/credentials"
2022-07-19 16:58:03 -04:00
"golang.org/x/crypto/ssh"
2022-05-18 20:20:45 -07:00
"google.golang.org/grpc/codes"
"google.golang.org/protobuf/proto"
2022-09-16 15:00:04 -07:00
"google.golang.org/protobuf/types/known/structpb"
2022-05-18 20:20:45 -07:00
"google.golang.org/protobuf/types/known/wrapperspb"
)
const (
2022-08-08 15:15:03 -04:00
usernameField = "attributes.username"
passwordField = "attributes.password"
privateKeyField = "attributes.private_key"
privateKeyPassphraseField = "attributes.private_key_passphrase"
2022-09-16 15:00:04 -07:00
objectField = "attributes.object"
2022-08-08 15:15:03 -04:00
domain = "credential"
2022-05-18 20:20:45 -07:00
)
var (
2022-09-16 15:00:04 -07:00
upMaskManager handlers . MaskManager
spkMaskManager handlers . MaskManager
jsonMaskManager handlers . MaskManager
2022-05-18 20:20:45 -07:00
// IdActions contains the set of actions that can be performed on
// individual resources
2023-10-31 13:44:17 +00:00
IdActions = action . NewActionSet (
2022-05-18 20:20:45 -07:00
action . NoOp ,
action . Read ,
action . Update ,
action . Delete ,
2023-10-31 13:44:17 +00:00
)
2022-05-18 20:20:45 -07:00
// CollectionActions contains the set of actions that can be performed on
// this collection
2023-10-31 13:44:17 +00:00
CollectionActions = action . NewActionSet (
2022-05-18 20:20:45 -07:00
action . Create ,
action . List ,
2023-10-31 13:44:17 +00:00
)
2022-05-18 20:20:45 -07:00
)
func init () {
var err error
2023-06-06 17:09:08 -04:00
if upMaskManager , err = handlers . NewMaskManager (
context . Background (),
handlers . MaskDestination { & store . UsernamePasswordCredential {}},
handlers . MaskSource { & pb . Credential {}, & pb . UsernamePasswordAttributes {}},
); err != nil {
2022-05-18 20:20:45 -07:00
panic ( err )
}
2023-06-06 17:09:08 -04:00
if spkMaskManager , err = handlers . NewMaskManager (
context . Background (),
handlers . MaskDestination { & store . SshPrivateKeyCredential {}},
handlers . MaskSource { & pb . Credential {}, & pb . SshPrivateKeyAttributes {}},
); err != nil {
2022-07-18 19:16:11 -04:00
panic ( err )
}
2023-06-06 17:09:08 -04:00
if jsonMaskManager , err = handlers . NewMaskManager (
context . Background (),
handlers . MaskDestination { & store . JsonCredential {}},
handlers . MaskSource { & pb . Credential {}, & pb . JsonAttributes {}},
); err != nil {
2022-09-16 15:00:04 -07:00
panic ( err )
}
2023-10-11 18:26:16 +00:00
// TODO: refactor to remove IdActionsMap and CollectionActions package variables
action . RegisterResource ( resource . Credential , IdActions , CollectionActions )
2022-05-18 20:20:45 -07:00
}
// Service handles request as described by the pbs.CredentialServiceServer interface.
type Service struct {
2022-08-30 14:22:10 -04:00
pbs . UnsafeCredentialServiceServer
2022-05-18 20:20:45 -07:00
2023-11-29 16:29:55 -08:00
iamRepoFn common . IamRepoFactory
repoFn common . StaticCredentialRepoFactory
maxPageSize uint
2022-05-18 20:20:45 -07:00
}
2022-08-30 14:22:10 -04:00
var _ pbs . CredentialServiceServer = ( * Service )( nil )
2022-05-18 20:20:45 -07:00
// NewService returns a credential service which handles credential related requests to boundary.
2023-11-29 16:29:55 -08:00
func NewService (
ctx context . Context ,
iamRepo common . IamRepoFactory ,
repo common . StaticCredentialRepoFactory ,
maxPageSize uint ,
) ( Service , error ) {
2022-05-18 20:20:45 -07:00
const op = "credentials.NewService"
if iamRepo == nil {
2023-06-06 17:09:08 -04:00
return Service {}, errors . New ( ctx , errors . InvalidParameter , op , "missing iam repository" )
2022-05-18 20:20:45 -07:00
}
if repo == nil {
2023-06-06 17:09:08 -04:00
return Service {}, errors . New ( ctx , errors . InvalidParameter , op , "missing static credential repository" )
2022-05-18 20:20:45 -07:00
}
2023-11-29 16:29:55 -08:00
if maxPageSize == 0 {
maxPageSize = uint ( globals . DefaultMaxPageSize )
}
return Service { iamRepoFn : iamRepo , repoFn : repo , maxPageSize : maxPageSize }, nil
2022-05-18 20:20:45 -07:00
}
// ListCredentials implements the interface pbs.CredentialServiceServer
func ( s Service ) ListCredentials ( ctx context . Context , req * pbs . ListCredentialsRequest ) ( * pbs . ListCredentialsResponse , error ) {
2023-11-29 16:29:55 -08:00
const op = "credentials.(Service).ListCredentials"
2023-06-06 17:09:08 -04:00
if err := validateListRequest ( ctx , req ); err != nil {
2023-11-29 16:29:55 -08:00
return nil , errors . Wrap ( ctx , err , op )
2022-05-18 20:20:45 -07:00
}
authResults := s . authResult ( ctx , req . GetCredentialStoreId (), action . List )
if authResults . Error != nil {
return nil , authResults . Error
}
2023-11-29 16:29:55 -08:00
pageSize := int ( s . maxPageSize )
// Use the requested page size only if it is smaller than
// the configured max.
if req . GetPageSize () != 0 && uint ( req . GetPageSize ()) < s . maxPageSize {
pageSize = int ( req . GetPageSize ())
}
var filterItemFn func ( ctx context . Context , item credential . Static ) ( bool , error )
switch {
case req . GetFilter () != "" :
// Only use a filter if we need to
filter , err := handlers . NewFilter ( ctx , req . GetFilter ())
if err != nil {
return nil , err
}
// TODO: replace the need for this function with some way to convert the `filter`
// to a domain type. This would allow filtering to happen in the domain, and we could
// remove this callback altogether.
filterItemFn = func ( ctx context . Context , item credential . Static ) ( bool , error ) {
outputOpts , ok := newOutputOpts ( ctx , item , req . CredentialStoreId , authResults )
if ! ok {
return ok , nil
}
pbItem , err := toProto ( item , outputOpts ... )
if err != nil {
return false , err
}
filterable , err := subtypes . Filterable ( ctx , pbItem )
if err != nil {
return false , err
}
return filter . Match ( filterable ), nil
}
default :
filterItemFn = func ( ctx context . Context , item credential . Static ) ( bool , error ) {
return true , nil
}
2022-05-18 20:20:45 -07:00
}
2023-11-29 16:29:55 -08:00
repo , err := s . repoFn ()
2022-05-18 20:20:45 -07:00
if err != nil {
2023-11-29 16:29:55 -08:00
return nil , errors . Wrap ( ctx , err , op )
2022-05-18 20:20:45 -07:00
}
2023-11-29 16:29:55 -08:00
grantsHash , err := authResults . GrantsHash ( ctx )
if err != nil {
return nil , errors . Wrap ( ctx , err , op )
2022-05-18 20:20:45 -07:00
}
2023-11-29 16:29:55 -08:00
var listResp * pagination . ListResponse [ credential . Static ]
var sortBy string
if req . GetListToken () == "" {
sortBy = "created_time"
listResp , err = credential . List ( ctx , grantsHash , pageSize , filterItemFn , repo , req . GetCredentialStoreId ())
if err != nil {
return nil , err
}
} else {
listToken , err := handlers . ParseListToken ( ctx , req . GetListToken (), resource . Credential , grantsHash )
if err != nil {
return nil , err
2022-05-18 20:20:45 -07:00
}
2023-11-29 16:29:55 -08:00
switch st := listToken . Subtype .( type ) {
case * listtoken . PaginationToken :
sortBy = "created_time"
listResp , err = credential . ListPage ( ctx , grantsHash , pageSize , filterItemFn , listToken , repo , req . GetCredentialStoreId ())
if err != nil {
return nil , err
}
case * listtoken . StartRefreshToken :
sortBy = "updated_time"
listResp , err = credential . ListRefresh ( ctx , grantsHash , pageSize , filterItemFn , listToken , repo , req . GetCredentialStoreId ())
if err != nil {
return nil , err
}
case * listtoken . RefreshToken :
sortBy = "updated_time"
listResp , err = credential . ListRefreshPage ( ctx , grantsHash , pageSize , filterItemFn , listToken , repo , req . GetCredentialStoreId ())
if err != nil {
return nil , err
}
default :
return nil , handlers . ApiErrorWithCodeAndMessage ( codes . InvalidArgument , "unexpected list token subtype: %T" , st )
}
}
finalItems := make ([] * pb . Credential , 0 , len ( listResp . Items ))
for _ , item := range listResp . Items {
outputOpts , ok := newOutputOpts ( ctx , item , req . CredentialStoreId , authResults )
if ! ok {
continue
2022-05-18 20:20:45 -07:00
}
2023-11-29 16:29:55 -08:00
pbItem , err := toProto ( item , outputOpts ... )
2022-05-18 20:20:45 -07:00
if err != nil {
return nil , err
}
2023-11-29 16:29:55 -08:00
finalItems = append ( finalItems , pbItem )
}
respType := "delta"
if listResp . CompleteListing {
respType = "complete"
}
resp := & pbs . ListCredentialsResponse {
Items : finalItems ,
EstItemCount : uint32 ( listResp . EstimatedItemCount ),
RemovedIds : listResp . DeletedIds ,
ResponseType : respType ,
SortBy : sortBy ,
SortDir : "desc" ,
}
2022-05-18 20:20:45 -07:00
2023-11-29 16:29:55 -08:00
if listResp . ListToken != nil {
resp . ListToken , err = handlers . MarshalListToken ( ctx , listResp . ListToken , pbs . ResourceType_RESOURCE_TYPE_CREDENTIAL )
2022-05-18 20:20:45 -07:00
if err != nil {
return nil , err
}
}
2023-11-29 16:29:55 -08:00
return resp , nil
2022-05-18 20:20:45 -07:00
}
// GetCredential implements the interface pbs.CredentialServiceServer.
func ( s Service ) GetCredential ( ctx context . Context , req * pbs . GetCredentialRequest ) ( * pbs . GetCredentialResponse , error ) {
const op = "credentials.(Service).GetCredential"
if err := validateGetRequest ( req ); err != nil {
return nil , err
}
authResults := s . authResult ( ctx , req . GetId (), action . Read )
if authResults . Error != nil {
return nil , authResults . Error
}
c , err := s . getFromRepo ( ctx , req . GetId ())
if err != nil {
return nil , err
}
outputFields , ok := requests . OutputFields ( ctx )
if ! ok {
return nil , errors . New ( ctx , errors . Internal , op , "no request context found" )
}
outputOpts := make ([] handlers . Option , 0 , 3 )
2023-01-06 17:27:19 -06:00
outputOpts = append ( outputOpts , handlers . WithOutputFields ( outputFields ))
2022-05-18 20:20:45 -07:00
if outputFields . Has ( globals . ScopeField ) {
outputOpts = append ( outputOpts , handlers . WithScope ( authResults . Scope ))
}
if outputFields . Has ( globals . AuthorizedActionsField ) {
outputOpts = append ( outputOpts , handlers . WithAuthorizedActions ( authResults . FetchActionSetForId ( ctx , c . GetPublicId (), IdActions ). Strings ()))
}
item , err := toProto ( c , outputOpts ... )
if err != nil {
return nil , err
}
return & pbs . GetCredentialResponse { Item : item }, nil
}
// CreateCredential implements the interface pbs.CredentialServiceServer.
func ( s Service ) CreateCredential ( ctx context . Context , req * pbs . CreateCredentialRequest ) ( * pbs . CreateCredentialResponse , error ) {
const op = "credentials.(Service).CreateCredential"
if err := validateCreateRequest ( req ); err != nil {
return nil , err
}
authResults := s . authResult ( ctx , req . GetItem (). GetCredentialStoreId (), action . Create )
if authResults . Error != nil {
return nil , authResults . Error
}
cl , err := s . createInRepo ( ctx , authResults . Scope . GetId (), req . GetItem ())
if err != nil {
return nil , err
}
outputFields , ok := requests . OutputFields ( ctx )
if ! ok {
return nil , errors . New ( ctx , errors . Internal , op , "no request context found" )
}
outputOpts := make ([] handlers . Option , 0 , 3 )
2023-01-06 17:27:19 -06:00
outputOpts = append ( outputOpts , handlers . WithOutputFields ( outputFields ))
2022-05-18 20:20:45 -07:00
if outputFields . Has ( globals . ScopeField ) {
outputOpts = append ( outputOpts , handlers . WithScope ( authResults . Scope ))
}
if outputFields . Has ( globals . AuthorizedActionsField ) {
outputOpts = append ( outputOpts , handlers . WithAuthorizedActions ( authResults . FetchActionSetForId ( ctx , cl . GetPublicId (), IdActions ). Strings ()))
}
item , err := toProto ( cl , outputOpts ... )
if err != nil {
return nil , err
}
return & pbs . CreateCredentialResponse {
Item : item ,
Uri : fmt . Sprintf ( "credentials/%s" , item . GetId ()),
}, nil
}
// UpdateCredential implements the interface pbs.CredentialServiceServer.
func ( s Service ) UpdateCredential ( ctx context . Context , req * pbs . UpdateCredentialRequest ) ( * pbs . UpdateCredentialResponse , error ) {
const op = "credentials.(Service).UpdateCredential"
cur , err := s . getFromRepo ( ctx , req . GetId ())
if err != nil {
return nil , err
}
storeId := cur . GetStoreId ()
if err := validateUpdateRequest ( req ); err != nil {
return nil , err
}
authResults := s . authResult ( ctx , req . GetId (), action . Update )
if authResults . Error != nil {
return nil , authResults . Error
}
c , err := s . updateInRepo ( ctx , authResults . Scope . GetId (), storeId , req . GetId (), req . GetUpdateMask (). GetPaths (), req . GetItem ())
if err != nil {
return nil , err
}
outputFields , ok := requests . OutputFields ( ctx )
if ! ok {
return nil , errors . New ( ctx , errors . Internal , op , "no request context found" )
}
outputOpts := make ([] handlers . Option , 0 , 3 )
2023-01-06 17:27:19 -06:00
outputOpts = append ( outputOpts , handlers . WithOutputFields ( outputFields ))
2022-05-18 20:20:45 -07:00
if outputFields . Has ( globals . ScopeField ) {
outputOpts = append ( outputOpts , handlers . WithScope ( authResults . Scope ))
}
if outputFields . Has ( globals . AuthorizedActionsField ) {
outputOpts = append ( outputOpts , handlers . WithAuthorizedActions ( authResults . FetchActionSetForId ( ctx , c . GetPublicId (), IdActions ). Strings ()))
}
item , err := toProto ( c , outputOpts ... )
if err != nil {
return nil , err
}
return & pbs . UpdateCredentialResponse { Item : item }, nil
}
// DeleteCredential implements the interface pbs.CredentialServiceServer.
func ( s Service ) DeleteCredential ( ctx context . Context , req * pbs . DeleteCredentialRequest ) ( * pbs . DeleteCredentialResponse , error ) {
if err := validateDeleteRequest ( req ); err != nil {
return nil , err
}
authResults := s . authResult ( ctx , req . GetId (), action . Delete )
if authResults . Error != nil {
return nil , authResults . Error
}
_ , err := s . deleteFromRepo ( ctx , authResults . Scope . GetId (), req . GetId ())
if err != nil {
return nil , err
}
return nil , nil
}
func ( s Service ) getFromRepo ( ctx context . Context , id string ) ( credential . Static , error ) {
const op = "credentials.(Service).getFromRepo"
repo , err := s . repoFn ()
if err != nil {
return nil , errors . Wrap ( ctx , err , op )
}
cred , err := repo . LookupCredential ( ctx , id )
if err != nil && ! errors . IsNotFoundError ( err ) {
return nil , errors . Wrap ( ctx , err , op )
}
if cred == nil {
return nil , errors . New ( ctx , errors . InvalidParameter , op , fmt . Sprintf ( "credential %q not found" , id ))
}
return cred , err
}
func ( s Service ) createInRepo ( ctx context . Context , scopeId string , item * pb . Credential ) ( credential . Static , error ) {
const op = "credentials.(Service).createInRepo"
switch item . GetType () {
2023-11-02 16:03:47 -04:00
case credential . UsernamePasswordSubtype . String ():
2022-07-19 16:58:03 -04:00
cred , err := toUsernamePasswordStorageCredential ( ctx , item . GetCredentialStoreId (), item )
2022-05-18 20:20:45 -07:00
if err != nil {
return nil , errors . Wrap ( ctx , err , op )
}
repo , err := s . repoFn ()
if err != nil {
return nil , errors . Wrap ( ctx , err , op )
}
out , err := repo . CreateUsernamePasswordCredential ( ctx , scopeId , cred )
if err != nil {
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to create credential" ))
}
if out == nil {
return nil , handlers . ApiErrorWithCodeAndMessage ( codes . Internal , "Unable to create credential but no error returned from repository." )
}
return out , nil
2023-11-02 16:03:47 -04:00
case credential . SshPrivateKeySubtype . String ():
2022-07-18 19:16:11 -04:00
cred , err := toSshPrivateKeyStorageCredential ( ctx , item . GetCredentialStoreId (), item )
if err != nil {
return nil , errors . Wrap ( ctx , err , op )
}
repo , err := s . repoFn ()
if err != nil {
return nil , errors . Wrap ( ctx , err , op )
}
out , err := repo . CreateSshPrivateKeyCredential ( ctx , scopeId , cred )
if err != nil {
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to create credential" ))
}
if out == nil {
return nil , handlers . ApiErrorWithCodeAndMessage ( codes . Internal , "Unable to create credential but no error returned from repository." )
}
return out , nil
2023-11-02 16:03:47 -04:00
case credential . JsonSubtype . String ():
2022-09-16 15:00:04 -07:00
cred , err := toJsonStorageCredential ( ctx , item . GetCredentialStoreId (), item )
if err != nil {
return nil , errors . Wrap ( ctx , err , op )
}
repo , err := s . repoFn ()
if err != nil {
return nil , errors . Wrap ( ctx , err , op )
}
out , err := repo . CreateJsonCredential ( ctx , scopeId , cred )
if err != nil {
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to create credential" ))
}
if out == nil {
return nil , handlers . ApiErrorWithCodeAndMessage ( codes . Internal , "Unable to create credential but no error returned from repository." )
}
return out , nil
2022-05-18 20:20:45 -07:00
default :
return nil , handlers . ApiErrorWithCodeAndMessage ( codes . Internal , fmt . Sprintf ( "Unsupported credential type %q" , item . GetType ()))
}
}
func ( s Service ) updateInRepo (
ctx context . Context ,
scopeId , storeId , id string ,
masks [] string ,
in * pb . Credential ,
) ( credential . Static , error ) {
const op = "credentials.(Service).updateInRepo"
var dbMasks [] string
item := proto . Clone ( in ).( * pb . Credential )
2023-11-15 19:55:46 -05:00
switch globals . ResourceInfoFromPrefix ( id ). Subtype {
2023-11-02 16:03:47 -04:00
case credential . UsernamePasswordSubtype :
2022-07-18 19:16:11 -04:00
dbMasks = append ( dbMasks , upMaskManager . Translate ( masks ) ... )
if len ( dbMasks ) == 0 {
return nil , handlers . InvalidArgumentErrorf ( "No valid fields included in the update mask." , map [ string ] string { "update_mask" : "No valid fields provided in the update mask." })
}
2022-07-19 16:58:03 -04:00
cred , err := toUsernamePasswordStorageCredential ( ctx , storeId , in )
2022-07-15 13:09:55 -04:00
if err != nil {
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to convert to username/password storage credential" ))
}
2022-05-18 20:20:45 -07:00
cred . PublicId = id
repo , err := s . repoFn ()
if err != nil {
return nil , errors . Wrap ( ctx , err , op )
}
out , rowsUpdated , err := repo . UpdateUsernamePasswordCredential ( ctx , scopeId , cred , item . GetVersion (), dbMasks )
if err != nil {
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to update credential" ))
}
if rowsUpdated == 0 {
return nil , handlers . NotFoundErrorf ( "Credential %q doesn't exist or incorrect version provided." , id )
}
return out , nil
2022-07-18 19:16:11 -04:00
2023-11-02 16:03:47 -04:00
case credential . SshPrivateKeySubtype :
2022-07-18 19:16:11 -04:00
dbMasks = append ( dbMasks , spkMaskManager . Translate ( masks ) ... )
if len ( dbMasks ) == 0 {
return nil , handlers . InvalidArgumentErrorf ( "No valid fields included in the update mask." , map [ string ] string { "update_mask" : "No valid fields provided in the update mask." })
}
cred , err := toSshPrivateKeyStorageCredential ( ctx , storeId , in )
if err != nil {
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to convert to ssh private key storage credential" ))
}
2022-08-08 15:15:03 -04:00
if cred . PassphraseUnneeded {
// This happens when we have a private key given and no passphrase
// given and everything parses correctly. In that case we want to
// ensure that if a passphrase was in the database for the previous
// key that we get rid of it. We'll have nilled out several values
// above. Note that adding the passphrase field will, once we get to
// the repo, result in the mask for the other two related fields as
// well.
dbMasks = append ( dbMasks , static . PrivateKeyPassphraseField )
}
2022-07-18 19:16:11 -04:00
cred . PublicId = id
repo , err := s . repoFn ()
if err != nil {
return nil , errors . Wrap ( ctx , err , op )
}
out , rowsUpdated , err := repo . UpdateSshPrivateKeyCredential ( ctx , scopeId , cred , item . GetVersion (), dbMasks )
if err != nil {
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to update credential" ))
}
if rowsUpdated == 0 {
return nil , handlers . NotFoundErrorf ( "Credential %q doesn't exist or incorrect version provided." , id )
}
return out , nil
2023-11-02 16:03:47 -04:00
case credential . JsonSubtype :
2022-09-16 15:00:04 -07:00
dbMasks = append ( dbMasks , jsonMaskManager . Translate ( masks , "attributes" , "object" ) ... )
if len ( dbMasks ) == 0 {
return nil , handlers . InvalidArgumentErrorf ( "No valid fields included in the update mask." , map [ string ] string { "update_mask" : "No valid fields provided in the update mask." })
}
cred , err := toJsonStorageCredential ( ctx , storeId , in )
if err != nil {
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to convert to json storage credential" ))
}
cred . PublicId = id
repo , err := s . repoFn ()
if err != nil {
return nil , errors . Wrap ( ctx , err , op )
}
out , rowsUpdated , err := repo . UpdateJsonCredential ( ctx , scopeId , cred , item . GetVersion (), dbMasks )
if err != nil {
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to update credential" ))
}
if rowsUpdated == 0 {
return nil , handlers . NotFoundErrorf ( "Credential %q doesn't exist or incorrect version provided." , id )
}
return out , nil
2022-05-18 20:20:45 -07:00
default :
return nil , handlers . ApiErrorWithCodeAndMessage ( codes . Internal , fmt . Sprintf ( "Unsupported credential type %q" , item . GetType ()))
}
}
func ( s Service ) deleteFromRepo ( ctx context . Context , scopeId , id string ) ( bool , error ) {
const op = "credentials.(Service).deleteFromRepo"
repo , err := s . repoFn ()
if err != nil {
return false , err
}
rows , err := repo . DeleteCredential ( ctx , scopeId , id )
if err != nil {
if errors . IsNotFoundError ( err ) {
return false , nil
}
return false , errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to delete credential" ))
}
return rows > 0 , nil
}
func ( s Service ) authResult ( ctx context . Context , id string , a action . Type ) auth . VerifyResults {
const op = "credentials.(Service).authResult"
res := auth . VerifyResults {}
repo , err := s . repoFn ()
if err != nil {
res . Error = err
return res
}
var parentId string
opts := [] auth . Option { auth . WithType ( resource . Credential ), auth . WithAction ( a )}
switch a {
case action . List , action . Create :
parentId = id
default :
opts = append ( opts , auth . WithId ( id ))
cred , err := repo . LookupCredential ( ctx , id )
if err != nil {
res . Error = err
return res
}
if cred == nil {
res . Error = handlers . NotFoundError ()
return res
}
parentId = cred . GetStoreId ()
}
if parentId == "" {
res . Error = errors . New ( ctx , errors . RecordNotFound , op , "unable to find credential store for provided credential" )
return res
}
opts = append ( opts , auth . WithPin ( parentId ))
cs , err := repo . LookupCredentialStore ( ctx , parentId )
if err != nil {
res . Error = err
return res
}
if cs == nil {
res . Error = handlers . NotFoundError ()
return res
}
2022-07-20 16:24:46 -07:00
opts = append ( opts , auth . WithScopeId ( cs . GetProjectId ()))
2022-05-18 20:20:45 -07:00
return auth . Verify ( ctx , opts ... )
}
2023-11-29 16:29:55 -08:00
func newOutputOpts (
ctx context . Context ,
item credential . Static ,
credentialStoreId string ,
authResults auth . VerifyResults ,
) ([] handlers . Option , bool ) {
res := perms . Resource {
2025-01-24 10:23:33 -08:00
ScopeId : authResults . Scope . Id ,
ParentScopeId : authResults . Scope . ParentScopeId ,
Type : resource . Credential ,
Pin : credentialStoreId ,
2023-11-29 16:29:55 -08:00
}
res . Id = item . GetPublicId ()
authorizedActions := authResults . FetchActionSetForId ( ctx , item . GetPublicId (), IdActions , auth . WithResource ( & res )). Strings ()
if len ( authorizedActions ) == 0 {
return nil , false
}
outputFields := authResults . FetchOutputFields ( res , action . List ). SelfOrDefaults ( authResults . UserId )
outputOpts := make ([] handlers . Option , 0 , 3 )
outputOpts = append ( outputOpts , handlers . WithOutputFields ( outputFields ))
if outputFields . Has ( globals . ScopeField ) {
outputOpts = append ( outputOpts , handlers . WithScope ( authResults . Scope ))
}
if outputFields . Has ( globals . AuthorizedActionsField ) {
outputOpts = append ( outputOpts , handlers . WithAuthorizedActions ( authorizedActions ))
}
return outputOpts , true
}
2022-05-18 20:20:45 -07:00
func toProto ( in credential . Static , opt ... handlers . Option ) ( * pb . Credential , error ) {
opts := handlers . GetOpts ( opt ... )
if opts . WithOutputFields == nil {
return nil , handlers . ApiErrorWithCodeAndMessage ( codes . Internal , "output fields not found when building credential proto" )
}
outputFields := * opts . WithOutputFields
out := pb . Credential {}
if outputFields . Has ( globals . IdField ) {
out . Id = in . GetPublicId ()
}
if outputFields . Has ( globals . CredentialStoreIdField ) {
out . CredentialStoreId = in . GetStoreId ()
}
if outputFields . Has ( globals . TypeField ) {
switch in .( type ) {
case * static . UsernamePasswordCredential :
2023-11-02 16:03:47 -04:00
out . Type = credential . UsernamePasswordSubtype . String ()
2022-07-18 19:16:11 -04:00
case * static . SshPrivateKeyCredential :
2023-11-02 16:03:47 -04:00
out . Type = credential . SshPrivateKeySubtype . String ()
2022-09-16 15:00:04 -07:00
case * static . JsonCredential :
2023-11-02 16:03:47 -04:00
out . Type = credential . JsonSubtype . String ()
2022-05-18 20:20:45 -07:00
}
}
if outputFields . Has ( globals . DescriptionField ) && in . GetDescription () != "" {
out . Description = wrapperspb . String ( in . GetDescription ())
}
if outputFields . Has ( globals . NameField ) && in . GetName () != "" {
out . Name = wrapperspb . String ( in . GetName ())
}
if outputFields . Has ( globals . CreatedTimeField ) {
out . CreatedTime = in . GetCreateTime (). GetTimestamp ()
}
if outputFields . Has ( globals . UpdatedTimeField ) {
out . UpdatedTime = in . GetUpdateTime (). GetTimestamp ()
}
if outputFields . Has ( globals . VersionField ) {
out . Version = in . GetVersion ()
}
if outputFields . Has ( globals . ScopeField ) {
out . Scope = opts . WithScope
}
if outputFields . Has ( globals . AuthorizedActionsField ) {
out . AuthorizedActions = opts . WithAuthorizedActions
}
switch cred := in .( type ) {
case * static . UsernamePasswordCredential :
if outputFields . Has ( globals . AttributesField ) {
out . Attrs = & pb . Credential_UsernamePasswordAttributes {
UsernamePasswordAttributes : & pb . UsernamePasswordAttributes {
Username : wrapperspb . String ( cred . GetUsername ()),
PasswordHmac : base64 . RawURLEncoding . EncodeToString ( cred . GetPasswordHmac ()),
},
}
}
2022-07-18 19:16:11 -04:00
case * static . SshPrivateKeyCredential :
if outputFields . Has ( globals . AttributesField ) {
out . Attrs = & pb . Credential_SshPrivateKeyAttributes {
SshPrivateKeyAttributes : & pb . SshPrivateKeyAttributes {
2022-08-08 15:15:03 -04:00
Username : wrapperspb . String ( cred . GetUsername ()),
PrivateKeyHmac : base64 . RawURLEncoding . EncodeToString ( cred . GetPrivateKeyHmac ()),
PrivateKeyPassphraseHmac : base64 . RawURLEncoding . EncodeToString ( cred . GetPrivateKeyPassphraseHmac ()),
2022-07-18 19:16:11 -04:00
},
}
}
2022-09-16 15:00:04 -07:00
case * static . JsonCredential :
if outputFields . Has ( globals . AttributesField ) {
out . Attrs = & pb . Credential_JsonAttributes {
JsonAttributes : & pb . JsonAttributes {
ObjectHmac : base64 . RawURLEncoding . EncodeToString ( cred . GetObjectHmac ()),
},
}
}
2022-05-18 20:20:45 -07:00
}
return & out , nil
}
2022-07-19 16:58:03 -04:00
func toUsernamePasswordStorageCredential ( ctx context . Context , storeId string , in * pb . Credential ) ( out * static . UsernamePasswordCredential , err error ) {
2022-07-18 19:16:11 -04:00
const op = "credentials.toUsernamePasswordStorageCredential"
2022-05-18 20:20:45 -07:00
var opts [] static . Option
if in . GetName () != nil {
opts = append ( opts , static . WithName ( in . GetName (). GetValue ()))
}
if in . GetDescription () != nil {
opts = append ( opts , static . WithDescription ( in . GetDescription (). GetValue ()))
}
attrs := in . GetUsernamePasswordAttributes ()
cs , err := static . NewUsernamePasswordCredential (
storeId ,
attrs . GetUsername (). GetValue (),
credential . Password ( attrs . GetPassword (). GetValue ()),
opts ... )
if err != nil {
2022-07-19 16:58:03 -04:00
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to build credential" ))
2022-05-18 20:20:45 -07:00
}
return cs , err
}
2022-07-18 19:16:11 -04:00
func toSshPrivateKeyStorageCredential ( ctx context . Context , storeId string , in * pb . Credential ) ( out * static . SshPrivateKeyCredential , err error ) {
const op = "credentials.toSshPrivateKeyStorageCredential"
var opts [] static . Option
if in . GetName () != nil {
opts = append ( opts , static . WithName ( in . GetName (). GetValue ()))
}
if in . GetDescription () != nil {
opts = append ( opts , static . WithDescription ( in . GetDescription (). GetValue ()))
}
attrs := in . GetSshPrivateKeyAttributes ()
2022-08-08 15:15:03 -04:00
if attrs . GetPrivateKeyPassphrase () != nil {
opts = append ( opts , static . WithPrivateKeyPassphrase ([] byte ( attrs . GetPrivateKeyPassphrase (). GetValue ())))
}
2022-07-18 19:16:11 -04:00
cs , err := static . NewSshPrivateKeyCredential (
ctx ,
storeId ,
attrs . GetUsername (). GetValue (),
credential . PrivateKey ( attrs . GetPrivateKey (). GetValue ()),
opts ... )
if err != nil {
2022-07-19 16:58:03 -04:00
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to build credential" ))
2022-07-18 19:16:11 -04:00
}
return cs , err
}
2022-09-16 15:00:04 -07:00
func toJsonStorageCredential ( ctx context . Context , storeId string , in * pb . Credential ) ( out * static . JsonCredential , err error ) {
const op = "credentials.toJsonStorageCredential"
var opts [] static . Option
if in . GetName () != nil {
opts = append ( opts , static . WithName ( in . GetName (). GetValue ()))
}
if in . GetDescription () != nil {
opts = append ( opts , static . WithDescription ( in . GetDescription (). GetValue ()))
}
attrs := in . GetJsonAttributes ()
object := attrs . GetObject ()
if object == nil {
2022-10-25 18:29:38 -05:00
object , err = structpb . NewStruct ( map [ string ] any {})
2022-09-16 15:00:04 -07:00
if err != nil {
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to build credential" ))
}
}
cs , err := static . NewJsonCredential (
ctx ,
storeId ,
credential . JsonObject {
2023-09-19 09:17:42 -07:00
Struct : object ,
2022-09-16 15:00:04 -07:00
},
opts ... )
if err != nil {
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to build credential" ))
}
return cs , err
}
2022-05-18 20:20:45 -07:00
// A validateX method should exist for each method above. These methods do not make calls to any backing service but enforce
// requirements on the structure of the request. They verify that:
2022-08-25 09:12:24 -07:00
// - The path passed in is correctly formatted
// - All required parameters are set
// - There are no conflicting parameters provided
2022-05-18 20:20:45 -07:00
func validateGetRequest ( req * pbs . GetCredentialRequest ) error {
2022-07-18 19:16:11 -04:00
return handlers . ValidateGetRequest (
handlers . NoopValidatorFn ,
req ,
2023-03-10 15:06:39 -05:00
globals . UsernamePasswordCredentialPrefix ,
globals . UsernamePasswordCredentialPreviousPrefix ,
globals . SshPrivateKeyCredentialPrefix ,
globals . JsonCredentialPrefix ,
2022-07-18 19:16:11 -04:00
)
2022-05-18 20:20:45 -07:00
}
func validateCreateRequest ( req * pbs . CreateCredentialRequest ) error {
return handlers . ValidateCreateRequest ( req . GetItem (), func () map [ string ] string {
badFields := map [ string ] string {}
2023-03-10 15:06:39 -05:00
if ! handlers . ValidId ( handlers . Id ( req . Item . GetCredentialStoreId ()), globals . StaticCredentialStorePrefix , globals . StaticCredentialStorePreviousPrefix ) {
2022-05-18 20:20:45 -07:00
badFields [ globals . CredentialStoreIdField ] = "This field must be a valid credential store id."
}
2022-07-18 19:16:11 -04:00
switch req . Item . GetType () {
2023-11-02 16:03:47 -04:00
case credential . UsernamePasswordSubtype . String ():
2022-07-18 19:16:11 -04:00
if req . Item . GetUsernamePasswordAttributes (). GetUsername (). GetValue () == "" {
badFields [ usernameField ] = "Field required for creating a username-password credential."
}
if req . Item . GetUsernamePasswordAttributes (). GetPassword (). GetValue () == "" {
badFields [ passwordField ] = "Field required for creating a username-password credential."
}
2022-07-19 16:58:03 -04:00
2023-11-02 16:03:47 -04:00
case credential . SshPrivateKeySubtype . String ():
2022-07-18 19:16:11 -04:00
if req . Item . GetSshPrivateKeyAttributes (). GetUsername (). GetValue () == "" {
badFields [ usernameField ] = "Field required for creating an SSH private key credential."
}
2022-07-19 16:58:03 -04:00
privateKey := req . Item . GetSshPrivateKeyAttributes (). GetPrivateKey (). GetValue ()
2022-08-08 15:15:03 -04:00
passphrase := req . Item . GetSshPrivateKeyAttributes (). GetPrivateKeyPassphrase (). GetValue ()
2022-07-19 16:58:03 -04:00
if privateKey == "" {
2022-07-18 19:16:11 -04:00
badFields [ privateKeyField ] = "Field required for creating an SSH private key credential."
2022-07-19 16:58:03 -04:00
} else {
2022-08-08 15:15:03 -04:00
switch passphrase {
case "" :
if _ , err := ssh . ParsePrivateKey ([] byte ( privateKey )); err != nil {
badFields [ privateKeyField ] = fmt . Sprintf ( "Unable to parse given private key value: %v." , err )
}
2022-07-19 16:58:03 -04:00
default :
2022-08-08 15:15:03 -04:00
if _ , err := ssh . ParsePrivateKeyWithPassphrase ([] byte ( privateKey ), [] byte ( passphrase )); err != nil {
if errors . Is ( err , x509 . IncorrectPasswordError ) {
badFields [ privateKeyPassphraseField ] = "Incorrect private key passphrase."
} else {
if _ , err := ssh . ParsePrivateKey ([] byte ( privateKey )); err == nil {
badFields [ privateKeyPassphraseField ] = "Passphrase supplied for unencrypted key."
} else {
badFields [ privateKeyField ] = fmt . Sprintf ( "Unable to parse given private key value: %v." , err )
}
}
}
2022-07-19 16:58:03 -04:00
}
2022-07-18 19:16:11 -04:00
}
2022-07-19 16:58:03 -04:00
2023-11-02 16:03:47 -04:00
case credential . JsonSubtype . String ():
2022-09-16 15:00:04 -07:00
object := req . GetItem (). GetJsonAttributes (). GetObject ()
2023-04-19 10:17:07 -07:00
if object == nil || len ( object . AsMap ()) <= 0 {
badFields [ objectField ] = "This is a required field and cannot be set to empty."
} else if _ , err := json . Marshal ( object ); err != nil {
2022-09-16 15:00:04 -07:00
badFields [ objectField ] = "Unable to parse given json value"
}
2022-07-18 19:16:11 -04:00
default :
badFields [ globals . TypeField ] = fmt . Sprintf ( "Unsupported credential type %q" , req . Item . GetType ())
2022-05-18 20:20:45 -07:00
}
return badFields
})
}
func validateUpdateRequest ( req * pbs . UpdateCredentialRequest ) error {
return handlers . ValidateUpdateRequest ( req , req . GetItem (), func () map [ string ] string {
badFields := map [ string ] string {}
2023-11-15 19:55:46 -05:00
switch globals . ResourceInfoFromPrefix ( req . GetId ()). Subtype {
2023-11-02 16:03:47 -04:00
case credential . UsernamePasswordSubtype :
2022-07-18 19:16:11 -04:00
attrs := req . GetItem (). GetUsernamePasswordAttributes ()
if handlers . MaskContains ( req . GetUpdateMask (). GetPaths (), usernameField ) && attrs . GetUsername (). GetValue () == "" {
badFields [ usernameField ] = "This is a required field and cannot be set to empty."
}
if handlers . MaskContains ( req . GetUpdateMask (). GetPaths (), passwordField ) && attrs . GetPassword (). GetValue () == "" {
badFields [ passwordField ] = "This is a required field and cannot be set to empty."
}
2022-07-19 16:58:03 -04:00
2023-11-02 16:03:47 -04:00
case credential . SshPrivateKeySubtype :
2022-07-18 19:16:11 -04:00
attrs := req . GetItem (). GetSshPrivateKeyAttributes ()
if handlers . MaskContains ( req . GetUpdateMask (). GetPaths (), usernameField ) && attrs . GetUsername (). GetValue () == "" {
badFields [ usernameField ] = "This is a required field and cannot be set to empty."
}
2022-07-19 16:58:03 -04:00
privateKey := attrs . GetPrivateKey (). GetValue ()
2022-08-08 15:15:03 -04:00
passphrase := attrs . GetPrivateKeyPassphrase (). GetValue ()
if handlers . MaskContains ( req . GetUpdateMask (). GetPaths (), privateKeyField ) {
if privateKey == "" {
badFields [ privateKeyField ] = "This is a required field and cannot be set to empty."
} else {
switch passphrase {
case "" :
if _ , err := ssh . ParsePrivateKey ([] byte ( privateKey )); err != nil {
badFields [ privateKeyField ] = fmt . Sprintf ( "Unable to parse given private key value: %v." , err )
}
default :
if _ , err := ssh . ParsePrivateKeyWithPassphrase ([] byte ( privateKey ), [] byte ( passphrase )); err != nil {
if errors . Is ( err , x509 . IncorrectPasswordError ) {
badFields [ privateKeyPassphraseField ] = "Incorrect private key passphrase."
} else {
if _ , err := ssh . ParsePrivateKey ([] byte ( privateKey )); err == nil {
badFields [ privateKeyPassphraseField ] = "Passphrase supplied for unencrypted key."
} else {
badFields [ privateKeyField ] = fmt . Sprintf ( "Unable to parse given private key value: %v." , err )
}
}
}
}
2022-07-19 16:58:03 -04:00
}
2022-07-18 19:16:11 -04:00
}
2022-07-19 16:58:03 -04:00
2023-11-02 16:03:47 -04:00
case credential . JsonSubtype :
2023-04-19 10:17:07 -07:00
if handlers . MaskContainsPrefix ( req . GetUpdateMask (). GetPaths (), objectField ) {
object := req . GetItem (). GetJsonAttributes (). GetObject ()
if object == nil || len ( object . AsMap ()) <= 0 {
badFields [ objectField ] = "This is a required field and cannot be set to empty"
} else if _ , err := json . Marshal ( object ); err != nil {
2022-09-16 15:00:04 -07:00
badFields [ objectField ] = "Unable to parse given json value"
}
}
2022-07-18 19:16:11 -04:00
default :
2022-08-08 15:15:03 -04:00
badFields [ globals . IdField ] = "Unknown credential type."
2022-05-18 20:20:45 -07:00
}
return badFields
2022-07-18 19:16:11 -04:00
},
2023-03-10 15:06:39 -05:00
globals . UsernamePasswordCredentialPrefix ,
globals . UsernamePasswordCredentialPreviousPrefix ,
globals . SshPrivateKeyCredentialPrefix ,
globals . JsonCredentialPrefix ,
2022-07-18 19:16:11 -04:00
)
2022-05-18 20:20:45 -07:00
}
func validateDeleteRequest ( req * pbs . DeleteCredentialRequest ) error {
2022-07-18 19:16:11 -04:00
return handlers . ValidateDeleteRequest (
handlers . NoopValidatorFn ,
req ,
2023-03-10 15:06:39 -05:00
globals . UsernamePasswordCredentialPrefix ,
globals . UsernamePasswordCredentialPreviousPrefix ,
globals . SshPrivateKeyCredentialPrefix ,
globals . JsonCredentialPrefix ,
2022-07-18 19:16:11 -04:00
)
2022-05-18 20:20:45 -07:00
}
2023-06-06 17:09:08 -04:00
func validateListRequest ( ctx context . Context , req * pbs . ListCredentialsRequest ) error {
2022-05-18 20:20:45 -07:00
badFields := map [ string ] string {}
2023-03-10 15:06:39 -05:00
if ! handlers . ValidId ( handlers . Id ( req . GetCredentialStoreId ()), globals . StaticCredentialStorePrefix , globals . StaticCredentialStorePreviousPrefix ) {
2022-05-18 20:20:45 -07:00
badFields [ globals . CredentialStoreIdField ] = "This field must be a valid credential store id."
}
2023-06-06 17:09:08 -04:00
if _ , err := handlers . NewFilter ( ctx , req . GetFilter ()); err != nil {
2022-05-18 20:20:45 -07:00
badFields [ "filter" ] = fmt . Sprintf ( "This field could not be parsed. %v" , err )
}
if len ( badFields ) > 0 {
return handlers . InvalidArgumentErrorf ( "Improperly formatted identifier." , badFields )
}
return nil
}