-
Notifications
You must be signed in to change notification settings - Fork 289
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Uruemu Aganbi
committed
Feb 23, 2024
1 parent
3ad4a9a
commit ef2adbd
Showing
1 changed file
with
319 additions
and
0 deletions.
There are no files selected for viewing
319 changes: 319 additions & 0 deletions
319
internal/daemon/controller/handlers/apptokens/app_token_service.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,319 @@ | ||
// Copyright (c) HashiCorp, Inc. | ||
// SPDX-License-Identifier: BUSL-1.1 | ||
|
||
package apptokens | ||
|
||
import ( | ||
"context" | ||
"strings" | ||
"time" | ||
|
||
"github.com/hashicorp/boundary/globals" | ||
"github.com/hashicorp/boundary/internal/apptoken" | ||
"github.com/hashicorp/boundary/internal/apptoken/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" | ||
"github.com/hashicorp/boundary/internal/types/action" | ||
"github.com/hashicorp/boundary/internal/types/resource" | ||
apptokens "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/apptokens" | ||
pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/apptokens" | ||
"google.golang.org/grpc/codes" | ||
"google.golang.org/protobuf/types/known/wrapperspb" | ||
) | ||
|
||
var ( | ||
// IdActions contains the set of actions that can be performed on | ||
// individual resources | ||
IdActions = action.NewActionSet( | ||
action.NoOp, | ||
action.Read, | ||
action.Delete, | ||
) | ||
|
||
// CollectionActions contains the set of actions that can be performed on | ||
// this collection | ||
CollectionActions = action.NewActionSet( | ||
action.Create, | ||
action.List, | ||
) | ||
) | ||
|
||
type Service struct { | ||
repoFn apptoken.RepositoryFactory | ||
iamRepoFn common.IamRepoFactory | ||
} | ||
|
||
func NewService(ctx context.Context, repoFn apptoken.RepositoryFactory, iamRepoFn common.IamRepoFactory) (*Service, error) { | ||
const op = "apptokens.NewService" | ||
|
||
if repoFn == nil { | ||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing apptoken repository") | ||
} | ||
if iamRepoFn == nil { | ||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing iam repository") | ||
} | ||
|
||
return &Service{ | ||
repoFn: repoFn, | ||
iamRepoFn: iamRepoFn, | ||
}, nil | ||
} | ||
|
||
func (s *Service) CreateAppToken(ctx context.Context, req *pbs.CreateAppTokenRequest) (*pbs.CreateAppTokenResponse, error) { | ||
const op = "apptokens.(Service).CreateAppToken" | ||
|
||
if err := validateCreateRequest(ctx, req); err != nil { | ||
return nil, err | ||
} | ||
|
||
authResults := s.authResult(ctx, req.GetItem().GetScopeId(), action.Create) | ||
if authResults.Error != nil { | ||
return nil, authResults.Error | ||
} | ||
|
||
appToken, err := s.createInRepo(ctx, req.Item) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
pbsAppToken, err := toProto(ctx, *appToken, handlers.WithScope(authResults.Scope)) | ||
|
||
return &pbs.CreateAppTokenResponse{ | ||
Item: pbsAppToken, | ||
}, nil | ||
} | ||
|
||
func (s Service) createInRepo(ctx context.Context, item *pb.AppToken) (*apptoken.AppToken, error) { | ||
const op = "apptokens.(Service).createInRepo" | ||
opts := []apptoken.Option{} | ||
if item.GetDescription() != nil { | ||
opts = append(opts, apptoken.WithDescription(ctx, item.GetDescription().GetValue())) | ||
} | ||
if item.GetName() != nil { | ||
opts = append(opts, apptoken.WithName(ctx, item.GetDescription().GetValue())) | ||
} | ||
|
||
repo, err := s.repoFn() | ||
if err != nil { | ||
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "internal error") | ||
} | ||
out, err := repo.CreateAppToken(ctx, | ||
item.GetScopeId(), | ||
item.GetExpirationTime().AsTime(), | ||
item.GetCreatedByUserId(), | ||
item.GetGrantStrings(), | ||
opts...) | ||
if err != nil { | ||
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "unable to create apptoken") | ||
} | ||
|
||
if out == nil { | ||
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "unable to create apptoken but no error returned from repository.") | ||
} | ||
return out, nil | ||
} | ||
|
||
func validateCreateRequest(ctx context.Context, req *pbs.CreateAppTokenRequest) error { | ||
const op = "apptokens.validateCreateRequest" | ||
if req == nil { | ||
return errors.New(ctx, errors.InvalidParameter, op, "nil request") | ||
} | ||
badFields := map[string]string{} | ||
|
||
now := time.Now() | ||
|
||
i := req.GetItem() | ||
if i == nil { | ||
badFields["item"] = "This field is required." | ||
} | ||
if i.GetId() != "" { | ||
badFields["id"] = "This is a read only field." | ||
} | ||
if i.GetName() != nil { | ||
trimmed := strings.TrimSpace(i.GetName().GetValue()) | ||
switch { | ||
case trimmed == "": | ||
badFields["name"] = "Cannot set empty string as name" | ||
case !handlers.ValidNameDescription(trimmed): | ||
badFields["name"] = "Name contains unprintable characters" | ||
default: | ||
i.GetName().Value = trimmed | ||
} | ||
} | ||
if i.GetDescription() != nil { | ||
trimmed := strings.TrimSpace(i.GetDescription().GetValue()) | ||
switch { | ||
case trimmed == "": | ||
badFields["description"] = "Cannot set empty string as description" | ||
case !handlers.ValidNameDescription(trimmed): | ||
badFields["description"] = "Description contains unprintable characters" | ||
default: | ||
i.GetDescription().Value = trimmed | ||
} | ||
} | ||
if i.GetCreatedTime() != nil { | ||
badFields["created_time"] = "This is a read only field." | ||
} | ||
if i.GetScopeId() == "" { | ||
badFields["item.scope"] = "This field is required." | ||
} | ||
if i.GetGrantStrings() == nil && len(req.GetItem().GetGrantStrings()) == 0 { | ||
badFields["item.grants"] = "This field is required." | ||
} | ||
if i.GetExpirationTime() == nil { | ||
badFields["item.expiration_time"] = "This field is required." | ||
} | ||
if i.GetExpirationTime() != nil { | ||
exp := i.GetExpirationTime().AsTime() | ||
switch { | ||
case exp.IsZero(): | ||
badFields["expiration_time"] = "Expiration time cannot be zero." | ||
case exp.Before(now): | ||
badFields["expiration_time"] = "Expiration time cannot be in the past." | ||
case exp.After(now.Add(time.Hour * 24 * 365 * 3)): | ||
badFields["expiration_time"] = "Expiration time cannot be more than 3 years in the future." | ||
case i.ExpirationInterval != 0: | ||
// The validation for the expiration time must be done before this check | ||
timeToExpire := i.GetExpirationTime().AsTime().Sub(now).Round(time.Second) | ||
if i.ExpirationInterval > uint32(timeToExpire) { | ||
badFields["expiration_interval"] = "Expiration interval cannot be greater than the time to expire." | ||
} | ||
} | ||
} | ||
|
||
if len(badFields) > 0 { | ||
return handlers.InvalidArgumentErrorf("Error in provided request.", badFields) | ||
} | ||
return nil | ||
} | ||
|
||
func (s Service) authResult(ctx context.Context, scopeID string, a action.Type) auth.VerifyResults { | ||
res := auth.VerifyResults{} | ||
|
||
var parentId string | ||
var at *apptoken.AppToken | ||
opts := []auth.Option{auth.WithType(resource.Target), auth.WithAction(a)} | ||
switch a { | ||
case action.List, action.Create: | ||
parentId = scopeID | ||
iamRepo, err := s.iamRepoFn() | ||
if err != nil { | ||
res.Error = err | ||
return res | ||
} | ||
scp, err := iamRepo.LookupScope(ctx, parentId) | ||
if err != nil { | ||
res.Error = err | ||
return res | ||
} | ||
if scp == nil { | ||
res.Error = handlers.NotFoundError() | ||
return res | ||
} | ||
default: | ||
repo, err := s.repoFn() | ||
if err != nil { | ||
res.Error = err | ||
return res | ||
} | ||
at, err = repo.LookupAppToken(ctx, id) | ||
Check failure on line 222 in internal/daemon/controller/handlers/apptokens/app_token_service.go GitHub Actions / test
|
||
if err != nil { | ||
res.Error = err | ||
return res | ||
} | ||
if at == nil { | ||
res.Error = handlers.NotFoundError() | ||
return res | ||
} | ||
scopeID = at.GetScopeId() | ||
opts = append(opts, auth.WithId(scopeID)) | ||
} | ||
opts = append(opts, auth.WithScopeId(parentId)) | ||
ret := auth.Verify(ctx, opts...) | ||
return ret | ||
} | ||
|
||
func toProto(ctx context.Context, in apptoken.AppToken, opt ...handlers.Option) (*apptokens.AppToken, error) { | ||
const op = "apptoken_service.toProto" | ||
opts := handlers.GetOpts(opt...) | ||
if opts.WithOutputFields == nil { | ||
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "output fields not found when building apptoken proto") | ||
} | ||
outputFields := *opts.WithOutputFields | ||
|
||
out := apptokens.AppToken{} | ||
if outputFields.Has(globals.IdField) { | ||
out.Id = in.GetPublicId() | ||
} | ||
if outputFields.Has(globals.CreatedTimeField) { | ||
out.CreatedTime = in.GetCreateTime().GetTimestamp() | ||
} | ||
if outputFields.Has(globals.NameField) { | ||
out.Name = wrapperspb.String(in.GetName()) | ||
} | ||
if outputFields.Has(globals.DescriptionField) { | ||
out.Description = wrapperspb.String(in.GetDescription()) | ||
} | ||
if outputFields.Has(globals.ScopeIdField) { | ||
out.ScopeId = in.GetScopeId() | ||
} | ||
if outputFields.Has(globals.ExpirationTimeField) { | ||
out.ExpirationTime = in.GetExpirationTime().GetTimestamp() | ||
} | ||
if outputFields.Has(globals.CreatedByField) { | ||
out.CreatedByUserId = in.GetCreatedBy() | ||
} | ||
if outputFields.Has(globals.ExpirationIntervalField) { | ||
out.ExpirationInterval = in.GetExpirationIntervalInMaxSeconds() | ||
} | ||
if outputFields.Has(globals.GrantStringsField) { | ||
for _, g := range in.GetGrants() { | ||
out.GrantStrings = append(out.GrantStrings, g.GetRawGrant()) | ||
} | ||
} | ||
if outputFields.Has(globals.GrantsField) { | ||
for _, g := range in.GetGrants() { | ||
grant, err := grantToProto(ctx, g, opt...) | ||
if err != nil { | ||
return nil, err | ||
} | ||
out.Grants = append(out.Grants, grant) | ||
|
||
} | ||
} | ||
if outputFields.Has(globals.GrantScopeIdField) { | ||
// TODO: regenerate proto to include GrantScopeId | ||
out.GrantScopeId = wrapperspb.String(in.GetGrantScopeId()) | ||
Check failure on line 289 in internal/daemon/controller/handlers/apptokens/app_token_service.go GitHub Actions / test
|
||
} | ||
if outputFields.Has(globals.ScopeField) { | ||
out.Scope = opts.WithScope | ||
} | ||
|
||
return &out, nil | ||
} | ||
|
||
// TODO: convert all grants and return an array of grants | ||
func grantToProto(ctx context.Context, in *store.AppTokenGrant, opt ...handlers.Option) (*apptokens.Grant, error) { | ||
const op = "apptoken_service.grantToProto" | ||
opts := handlers.GetOpts(opt...) | ||
if opts.WithOutputFields == nil { | ||
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "output fields not found when building apptoken_grant proto") | ||
} | ||
outputFields := *opts.WithOutputFields | ||
|
||
out := apptokens.Grant{} | ||
if outputFields.Has(globals.CanonicalGrantField) { | ||
out.Canonical = in.GetCanonicalGrant() | ||
} | ||
if outputFields.Has(globals.JsonGrantField) { | ||
// TODO: replicate https://github.com/hashicorp/boundary/blob/590228d956fe4099eac1ae43d0d8d0bc3869d009/internal/daemon/controller/handlers/roles/role_service.go#L942-L947 | ||
// out.Json = in. | ||
} | ||
if outputFields.Has(globals.RawGrantField) { | ||
out.Raw = in.GetRawGrant() | ||
} | ||
return &out, nil | ||
} |