diff --git a/CHANGELOG.md b/CHANGELOG.md index ecee40912..146522faf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,14 +9,14 @@ Canonical reference for changes, improvements, and bugfixes for Boundary. * API `delete` actions now result in a `204` status code and no body when successful. This was not the case previously due to a technical limitation which has now been solved. -* When using a `delete` command we now either show success or treat the `404` - error the same as any other `404` error, that is, it results in a non-zero - status code and an error message. This makes `delete` actions behave the same - as other commands, all of which pass through errors to the CLI. Given `-format - json` capability, it's relatively easy to perform a check to see whether an - error was `404` or something else from within scripts, in conjunction with - checking that the returned status code matches the API error status code - (`1`). +* When using a `delete` command within the CLI we now either show success or + treat the `404` error the same as any other `404` error, that is, it results + in a non-zero status code and an error message. This makes `delete` actions + behave the same as other commands, all of which pass through errors to the + CLI. Given `-format json` capability, it's relatively easy to perform a check + to see whether an error was `404` or something else from within scripts, in + conjunction with checking that the returned status code matches the API error + status code (`1`). * When outputting from the CLI in JSON format, the resource information under `item` or `items` (depending on the action) now exactly matches the JSON sent across the wire by the controller, as opposed to matching the Go SDK @@ -34,6 +34,20 @@ Canonical reference for changes, improvements, and bugfixes for Boundary. ### New and Improved +* permissions: Improving upon the work put into 0.2.0 to limit the fields that + are returned when listing as the anonymous user, grants now support a new + `output_fields` section. This takes in a comma-delimited (or in JSON format, + array) set of values that correspond to the JSON fields returned from an API + call (for listing, this will be applied to each resource under the `items` + field). If specified for a given ID or resource type (and scoped to specific + actions, if included), only the given values will be returned in the output. + If no `output_fields` are specified, the defaults are used. For authenticated + users this defaults to all fields; for `u_anon` this defaults to the fields + useful for navigating to and authenticating to the system. In either case, + this is overridable. See the [permissions + documentation](https://www.boundaryproject.io/docs/concepts/security/permissions) + for more information on why and when to use this. This currently only applies + to top-level fields in the response. * cli/api/sdk: Add support to request additional OIDC claims scope values from the OIDC provider when making an authentication request. ([PR](https://github.com/hashicorp/boundary/pull/1175)). @@ -50,7 +64,7 @@ Canonical reference for changes, improvements, and bugfixes for Boundary. Boundary now provides a new OIDC auth method parameter `claims_scopes` which allows you to add multiple additional claims scope values to an OIDC auth - method configuration. + method configuration. For information on claims scope values see: [Scope Claims in the OIDC specification](https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims) @@ -65,7 +79,7 @@ Canonical reference for changes, improvements, and bugfixes for Boundary. used to allow principals to see values in list results without also giving `read` or other capabilities on the resources. The default scope permissions have been updated to convey `no-op,list` instead of `read,list`. - [PR](https://github.com/hashicorp/boundary/pull/1138) + ([PR](https://github.com/hashicorp/boundary/pull/1138)) * cli/api/sdk: User resources have new attributes for: * Primary Account ID * Login Name @@ -79,10 +93,10 @@ Canonical reference for changes, improvements, and bugfixes for Boundary. * cli: Support for reading and deleting the user's own token via the new `read:self` and `delete:self` actions on auth tokens. If no token ID is provided, the stored token's ID will be used (after prompting), or `"self"` - can be set to the ID to trigger this behavior without prompting. - ([PR](https://github.com/hashicorp/boundary/pull/1162)) + can be set as the value of the `-id` parameter to trigger this behavior + without prompting. ([PR](https://github.com/hashicorp/boundary/pull/1162)) * cli: New `logout` command deletes the current token in Boundary and forgets it - from the local system credential store + from the local system credential store, respecting `-token-name` ([PR](https://github.com/hashicorp/boundary/pull/1134)) * config: The `name` field for workers and controllers now supports being set from environment variables or a file on disk @@ -91,7 +105,7 @@ Canonical reference for changes, improvements, and bugfixes for Boundary. ### Bug Fixes * cors: Fix allowing all origins by default - [PR](https://github.com/hashicorp/boundary/pull/1134) + ([PR](https://github.com/hashicorp/boundary/pull/1134)) * cli: It is now an error to run `boundary database migrate` on an uninitalized db. Use `boundary database init` instead. ([PR](https://github.com/hashicorp/boundary/pull/1184)) @@ -141,19 +155,19 @@ Canonical reference for changes, improvements, and bugfixes for Boundary. update, delete, and list (see new cli `oidc` subcommands available on CRUDL operations for examples), as well as the ability to authenticate against it via the SDK, CLI, admin UI, and desktop client. - [PR](https://github.com/hashicorp/boundary/pull/1090) + ([PR](https://github.com/hashicorp/boundary/pull/1090)) * server: When performing recursive listing, `list` action is no longer required to be granted to the calling user. Instead, the given scope acts as the root point (so only results under that scope will be shown), and `list` grant is - evaluated per-scope. [PR](https://github.com/hashicorp/boundary/pull/1016) + evaluated per-scope. ([PR](https://github.com/hashicorp/boundary/pull/1016)) * database init: If the database is already initialized, return 0 as the exit code. This matches how the `database migrate` command works. - [PR](https://github.com/hashicorp/boundary/pull/1033) + ([PR](https://github.com/hashicorp/boundary/pull/1033)) ### Bug Fixes * server: Roles for auto generated scopes are now generated at database init. - [PR](https://github.com/hashicorp/boundary/pull/996) + ([PR](https://github.com/hashicorp/boundary/pull/996)) * cli: Don't panic on certain commands when outputting in `json` format ([Issue](https://github.com/hashicorp/boundary/pull/992), [PR](https://github.com/hashicorp/boundary/pull/1095)) @@ -190,25 +204,25 @@ to call out in this changelog. The full set of open issues is on GitHub. `ResponseMap()` and `ResponseBody()`, resources simply expose `Response()`. This higher-level response object contains the map and body, and also exposes `StatusCode()` in place of indivdidual resources. - [PR](https://github.com/hashicorp/boundary/pull/962) + ([PR](https://github.com/hashicorp/boundary/pull/962)) * cli: In `json` output format, a resource item is now an object under the top-level key `item`; a list of resource items is now an list of objects under the top-level key `items`. This preserves the top level for putting in other useful information later on (and the HTTP status code is included now). - [PR](https://github.com/hashicorp/boundary/pull/962) + ([PR](https://github.com/hashicorp/boundary/pull/962)) * cli: In `json` output format, errors are now serialized as a JSON object with an `error` key instead of outputting normal text - [PR](https://github.com/hashicorp/boundary/pull/962) + ([PR](https://github.com/hashicorp/boundary/pull/962)) * cli: All errors, including API errors, are now written to `stderr`. Previously in the default table format, API errors would be written to `stdout`. - [PR](https://github.com/hashicorp/boundary/pull/962) + ([PR](https://github.com/hashicorp/boundary/pull/962)) * cli: Error return codes have been standardized across CLI commands. An error code of `1` indicates an error generated from the actual controller API; an error code of `2` is an error encountered due to the CLI command's logic; and an error code of `3` indicates an error that was caused due to user input to the command. (There is some nuance sometimes whether an error is really due to user input or not, but we attempt to be consistent.) - [PR](https://github.com/hashicorp/boundary/pull/976) + ([PR](https://github.com/hashicorp/boundary/pull/976)) ### New and Improved diff --git a/globals/fields.go b/globals/fields.go new file mode 100644 index 000000000..56d8fd652 --- /dev/null +++ b/globals/fields.go @@ -0,0 +1,56 @@ +package globals + +// This set of consts is intended to be a place to collect commonly-used field +// names. Since these might be used outside of the internal code (e.g. by SDK or +// CLI), it's outside internal/ here in globals. Some of these are already const +// elsewhere in the code but we should migrate to using a single source. +const ( + IdField = "id" + VersionField = "version" + NameField = "name" + DescriptionField = "description" + CreatedTimeField = "created_time" + UpdatedTimeField = "updated_time" + TypeField = "type" + AttributesField = "attributes" + ScopeIdField = "scope_id" + ScopeField = "scope" + AuthMethodIdField = "auth_method_id" + AccountIdField = "account_id" + UserIdField = "user_id" + IsPrimaryField = "is_primary" + AuthorizedActionsField = "authorized_actions" + AuthorizedCollectionActionsField = "authorized_collection_actions" + ExpirationTimeField = "expiration_time" + ApproximateLastUsedTimeField = "approximate_last_used_time" + MembersField = "members" + MemberIdsField = "member_ids" + HostCatalogIdField = "host_catalog_id" + HostSetIdsField = "host_set_ids" + HostIdsField = "host_ids" + PrincipalIdsField = "principal_ids" + PrincipalsField = "principals" + GrantScopeIdField = "grant_scope_id" + GrantsField = "grants" + GrantStringsField = "grant_strings" + PrimaryAuthMethodId = "primary_auth_method_id" + TargetIdField = "target_id" + HostIdField = "host_id" + HostSetIdField = "host_set_id" + HostSetsField = "host_sets" + AuthTokenIdField = "auth_token_id" + EndpointField = "endpoint" + CertificateField = "certificate" + TerminationReasonField = "termination_reason" + StatusField = "status" + StatesField = "states" + SessionConnectionLimitField = "session_connection_limit" + SessionMaxSecondsField = "session_max_seconds" + WorkerFilterField = "worker_filter" + AccountIdsField = "account_ids" + AccountsField = "accounts" + LoginNameField = "login_name" + FullNameField = "full_name" + PrimaryAccountIdField = "primary_account_id" + EmailField = "email" +) diff --git a/globals/globals.go b/globals/globals.go index 6c261bc6a..b73da4cee 100644 --- a/globals/globals.go +++ b/globals/globals.go @@ -15,8 +15,8 @@ const ( ) type ( - ContextMaxRequestSizeType int - ContextOriginalRequestPathType int + ContextMaxRequestSizeType struct{} + ContextOriginalRequestPathType struct{} ) var ( diff --git a/internal/auth/additional_verification_test.go b/internal/auth/additional_verification_test.go index 9c7a8a67b..afc6c8cce 100644 --- a/internal/auth/additional_verification_test.go +++ b/internal/auth/additional_verification_test.go @@ -2,16 +2,23 @@ package auth_test import ( "context" + "fmt" + "sort" "strings" "testing" + "github.com/hashicorp/boundary/api/authmethods" + "github.com/hashicorp/boundary/api/authtokens" "github.com/hashicorp/boundary/internal/auth" + "github.com/hashicorp/boundary/internal/auth/password" "github.com/hashicorp/boundary/internal/authtoken" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/servers" "github.com/hashicorp/boundary/internal/servers/controller" + authmethodsservice "github.com/hashicorp/boundary/internal/servers/controller/handlers/authmethods" "github.com/hashicorp/boundary/internal/types/action" "github.com/hashicorp/boundary/internal/types/resource" + "github.com/hashicorp/boundary/internal/types/scope" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -106,3 +113,182 @@ func TestFetchActionSetForId(t *testing.T) { }) } } + +func TestRecursiveListingDifferentOutputFields(t *testing.T) { + require, assert := require.New(t), assert.New(t) + tc := controller.NewTestController(t, nil) + defer tc.Shutdown() + + conn := tc.DbConn() + client := tc.Client() + token := tc.Token() + client.SetToken(token.Token) + + // Set some global permissions so we can read the auth method there. Here we + // will expect the defaults. + globalRole := iam.TestRole(t, conn, scope.Global.String()) + iam.TestUserRole(t, conn, globalRole.PublicId, token.UserId) + iam.TestUserRole(t, conn, globalRole.PublicId, auth.AnonymousUserId) + iam.TestRoleGrant(t, conn, globalRole.PublicId, "id=*;type=auth-method;actions=list,no-op") + + // Create some users at the org level, and some role grants for them + org, _ := iam.TestScopes(t, tc.IamRepo(), iam.WithUserId(token.UserId), iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true)) + orgAm1 := password.TestAuthMethod(t, conn, org.GetPublicId(), password.WithName("orgam1"), password.WithDescription("orgam1")) + orgAm2 := password.TestAuthMethod(t, conn, org.GetPublicId(), password.WithName("orgam2"), password.WithDescription("orgam2")) + orgRole := iam.TestRole(t, conn, org.GetPublicId()) + iam.TestUserRole(t, conn, orgRole.PublicId, token.UserId) + // The first and second will actually not take effect for output + // grantsbecause it's a list and output fields are scoped by action. So we + // expect only name and scope_id for the auth methods in the scope, using + // two patterns below. However, since you need an action on the resource for + // list to return anything, those grants allow us to list the items, while + // also verifying that those output fields don't take effect for the wrong + // action. + iam.TestRoleGrant(t, conn, orgRole.PublicId, fmt.Sprintf("id=%s;actions=read;output_fields=id,version", orgAm1.GetPublicId())) + iam.TestRoleGrant(t, conn, orgRole.PublicId, fmt.Sprintf("id=%s;actions=read;output_fields=description", orgAm2.GetPublicId())) + iam.TestRoleGrant(t, conn, orgRole.PublicId, "id=*;type=auth-method;output_fields=name") + iam.TestRoleGrant(t, conn, orgRole.PublicId, "id=*;type=auth-method;actions=list;output_fields=scope_id") + + amClient := authmethods.NewClient(tc.Client()) + resp, err := amClient.List(tc.Context(), scope.Global.String(), authmethods.WithRecursive(true)) + // Basic sanity checks + require.NoError(err) + require.NotNil(resp) + require.NotNil(resp.GetItems()) + assert.Len(resp.GetItems().([]*authmethods.AuthMethod), 3) + items := resp.GetResponse().Map["items"].([]interface{}) + require.NotNil(items) + + // The default generated roles don't have output field definitions for them, + // so we look for "scope" and if it's there we skip. Otherwise, we look for + // "version" and if found expect that we will see a global auth method also + // containing name and description. Otherwise it's an org role and we expect + // to see name and scope_id. At the end we verify we've seen the exact + // numbers of each we expect. Note that we expect 3 at global because of the + // auto generated one. + var globalAms, orgAms int + for _, item := range items { + m := item.(map[string]interface{}) + require.NotNil(m) + switch m["scope_id"].(string) { + case scope.Global.String(): + // Validate that it contains fields anon shouldn't; we'll check anon + // later + assert.Contains(m, "created_time") + assert.Contains(m, "attributes") + globalAms++ + + default: + assert.Len(m, 2) + assert.Contains(m, "name") + assert.Contains(m, "scope_id") + orgAms++ + } + } + assert.Equal(1, globalAms) + assert.Equal(2, orgAms) + + // Now act as the anonymous user and ensure that the fields we checked for + // before are not available + tc.Client().SetToken("") + amClient = authmethods.NewClient(tc.Client()) + resp, err = amClient.List(tc.Context(), scope.Global.String(), authmethods.WithRecursive(true)) + require.NoError(err) + require.NotNil(resp) + require.NotNil(resp.GetItems()) + assert.Len(resp.GetItems().([]*authmethods.AuthMethod), 1) + item := resp.GetResponse().Map["items"].([]interface{})[0].(map[string]interface{}) + assert.NotContains(item, "created_time") + assert.NotContains(item, "attributes") +} + +func TestSelfReadingDifferentOutputFields(t *testing.T) { + tc := controller.NewTestController(t, nil) + defer tc.Shutdown() + + conn := tc.DbConn() + + s, err := authmethodsservice.NewService(tc.Kms(), + tc.Controller().PasswordAuthRepoFn, + tc.Controller().OidcRepoFn, + tc.Controller().IamRepoFn, + tc.Controller().AuthTokenRepoFn) + require.NoError(t, err) + + // Create two auth tokens belonging to different users in the org. Each will + // have grants below and should be able to read the other token but see more + // fields for its own. + org, _ := iam.TestScopes(t, tc.IamRepo(), iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true)) + am := password.TestAuthMethod(t, conn, org.GetPublicId()) + acct1 := password.TestAccount(t, conn, am.GetPublicId(), "acct1") + acct2 := password.TestAccount(t, conn, am.GetPublicId(), "acct2") + user1 := iam.TestUser(t, tc.IamRepo(), org.GetPublicId(), iam.WithAccountIds(acct1.GetPublicId())) + user2 := iam.TestUser(t, tc.IamRepo(), org.GetPublicId(), iam.WithAccountIds(acct2.GetPublicId())) + at1, err := tc.AuthTokenRepo().CreateAuthToken(tc.Context(), user1, acct1.GetPublicId()) + require.NoError(t, err) + token1, err := s.ConvertInternalAuthTokenToApiAuthToken(tc.Context(), at1) + require.NoError(t, err) + at2, err := tc.AuthTokenRepo().CreateAuthToken(tc.Context(), user2, acct2.GetPublicId()) + require.NoError(t, err) + token2, err := s.ConvertInternalAuthTokenToApiAuthToken(tc.Context(), at2) + require.NoError(t, err) + + orgRole := iam.TestRole(t, conn, org.GetPublicId()) + iam.TestUserRole(t, conn, orgRole.PublicId, user1.GetPublicId()) + iam.TestUserRole(t, conn, orgRole.PublicId, user2.GetPublicId()) + iam.TestRoleGrant(t, conn, orgRole.PublicId, "id=*;type=auth-token;actions=read:self;output_fields=account_id") + iam.TestRoleGrant(t, conn, orgRole.PublicId, "id=*;type=auth-token;actions=read;output_fields=id,scope_id") + + cases := []struct { + name string + token string + lookupId string + keys []string + }{ + { + name: "at1 self", + token: token1.GetToken(), + lookupId: token1.GetId(), + keys: []string{"id", "scope_id", "account_id"}, + }, + { + name: "at2 self", + token: token2.GetToken(), + lookupId: token2.GetId(), + keys: []string{"id", "scope_id", "account_id"}, + }, + { + name: "at1 other", + token: token1.GetToken(), + lookupId: token2.GetId(), + keys: []string{"id", "scope_id"}, + }, + { + name: "at2 other", + token: token2.GetToken(), + lookupId: token1.GetId(), + keys: []string{"id", "scope_id"}, + }, + } + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + client := tc.Client().Clone() + client.SetToken(test.token) + atClient := authtokens.NewClient(client) + resp, err := atClient.Read(tc.Context(), test.lookupId) + require.NoError(err) + require.NotNil(resp) + require.NotNil(resp.GetItem()) + item := resp.GetResponse().Map + require.NotNil(item) + keys := make([]string, 0, len(item)) + for k := range item { + keys = append(keys, k) + } + sort.Strings(test.keys) + sort.Strings(keys) + assert.Equal(test.keys, keys) + }) + } +} diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 0799a0d2d..1f0661b13 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/boundary/internal/gen/controller/tokens" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/perms" + "github.com/hashicorp/boundary/internal/requests" "github.com/hashicorp/boundary/internal/servers/controller/common" "github.com/hashicorp/boundary/internal/servers/controller/handlers" "github.com/hashicorp/boundary/internal/types/action" @@ -158,6 +159,19 @@ func Verify(ctx context.Context, opt ...Option) (ret VerifyResults) { ret.Scope = new(scopes.ScopeInfo) + // Note: we don't call RequestContextFromCtx here because that performs a + // SelfOrDefault. That's useful in the handlers, but here we don't want to + // do anything if it's nil. That's mainly useful in tests where + // DisableAuthEntirely is set. Later, if we don't have the value set at all, + // we have some safeguards to ensure we fail safe (e.g. no output fields at + // all). So it provides a good indication as well whether we have set this + // where and when needed. + var reqInfo *requests.RequestContext + reqInfoRaw := ctx.Value(requests.ContextRequestInformationKey) + if reqInfoRaw != nil { + reqInfo = reqInfoRaw.(*requests.RequestContext) + } + // In tests we often simply disable auth so we can test the service handlers // without fuss if v.requestInfo.DisableAuthEntirely { @@ -203,6 +217,9 @@ func Verify(ctx context.Context, opt ...Option) (ret VerifyResults) { } } ret.UserId = v.requestInfo.userIdOverride + if reqInfo != nil { + reqInfo.UserId = ret.UserId + } ret.Error = nil return } @@ -249,6 +266,11 @@ func Verify(ctx context.Context, opt ...Option) (ret VerifyResults) { } } + if reqInfo != nil { + reqInfo.UserId = ret.UserId + reqInfo.OutputFields = authResults.OutputFields + } + ret.Error = nil return } @@ -513,16 +535,16 @@ func (v verifier) performAuthCheck() (aclResults perms.ACLResults, userId string // FetchActionSetForId returns the allowed actions for a given ID using the // current set of ACLs and all other parameters the same (user, etc.) func (r *VerifyResults) FetchActionSetForId(ctx context.Context, id string, availableActions action.ActionSet, opt ...Option) action.ActionSet { - return r.fetchActions(ctx, id, resource.Unknown, availableActions, opt...) + return r.fetchActions(id, resource.Unknown, availableActions, opt...) } // FetchActionSetForType returns the allowed actions for a given collection type // using the current set of ACLs and all other parameters the same (user, etc.) func (r *VerifyResults) FetchActionSetForType(ctx context.Context, typ resource.Type, availableActions action.ActionSet, opt ...Option) action.ActionSet { - return r.fetchActions(ctx, "", typ, availableActions, opt...) + return r.fetchActions("", typ, availableActions, opt...) } -func (r *VerifyResults) fetchActions(ctx context.Context, id string, typ resource.Type, availableActions action.ActionSet, opt ...Option) action.ActionSet { +func (r *VerifyResults) fetchActions(id string, typ resource.Type, availableActions action.ActionSet, opt ...Option) action.ActionSet { switch { case r.v.requestInfo.DisableAuthEntirely, r.v.requestInfo.TokenFormat == AuthTokenTypeRecoveryKms: @@ -560,6 +582,17 @@ func (r *VerifyResults) fetchActions(ctx context.Context, id string, typ resourc return ret } +func (r *VerifyResults) FetchOutputFields(res perms.Resource, act action.Type) perms.OutputFieldsMap { + switch { + case r.v.requestInfo.TokenFormat == AuthTokenTypeRecoveryKms: + return perms.OutputFieldsMap{"*": true} + case r.v.requestInfo.DisableAuthEntirely: + return nil + } + + return r.v.acl.Allowed(res, act).OutputFields +} + // GetTokenFromRequest pulls the token from either the Authorization header or // split cookies and parses it. If it cannot be parsed successfully, the issue // is logged and we return blank, so logic will continue as the anonymous user. diff --git a/internal/auth/authorized_actions.go b/internal/auth/authorized_actions.go new file mode 100644 index 000000000..3ffe8ea2c --- /dev/null +++ b/internal/auth/authorized_actions.go @@ -0,0 +1,45 @@ +package auth + +import ( + "context" + + "github.com/hashicorp/boundary/internal/perms" + "github.com/hashicorp/boundary/internal/types/action" + "github.com/hashicorp/boundary/internal/types/resource" + "github.com/hashicorp/boundary/sdk/strutil" + "google.golang.org/protobuf/types/known/structpb" +) + +// CalculateAuthorizedCollectionActions returns authorized actions for the given +// inputs. +// +// NOTE: Eventually we should unit test this, but for now every service handler +// is validating the results of this (as it was pulled out of the service +// handlers). +func CalculateAuthorizedCollectionActions(ctx context.Context, + authResults VerifyResults, + mapToRange map[resource.Type]action.ActionSet, + scopeId, pin string) (map[string]*structpb.ListValue, error) { + res := &perms.Resource{ + ScopeId: scopeId, + Pin: pin, + } + // Range over the defined collections and check permissions against those + // collections. + var ret map[string]*structpb.ListValue + for k, v := range mapToRange { + res.Type = k + acts := authResults.FetchActionSetForType(ctx, k, v, WithResource(res)).Strings() + if len(acts) > 0 { + if ret == nil { + ret = make(map[string]*structpb.ListValue) + } + lv, err := structpb.NewList(strutil.StringListToInterfaceList(acts)) + if err != nil { + return nil, err + } + ret[k.String()+"s"] = lv + } + } + return ret, nil +} diff --git a/internal/auth/option.go b/internal/auth/option.go index 9b55b7576..939a2f0d0 100644 --- a/internal/auth/option.go +++ b/internal/auth/option.go @@ -33,9 +33,7 @@ type options struct { } func getDefaultOptions() options { - return options{ - withUserId: AnonymousUserId, - } + return options{} } func WithScopeId(id string) Option { diff --git a/internal/auth/options_test.go b/internal/auth/options_test.go index fe1b3a7d4..4f26d994e 100644 --- a/internal/auth/options_test.go +++ b/internal/auth/options_test.go @@ -13,15 +13,11 @@ import ( // Test_GetOpts provides unit tests for GetOpts and all the options func Test_GetOpts(t *testing.T) { t.Parallel() - opts := getOpts() - assert.Equal(t, options{ - withUserId: AnonymousUserId, - }, opts) withKms := new(kms.Kms) res := new(perms.Resource) - opts = getOpts( + opts := getOpts( WithScopeId("foo"), WithPin("bar"), WithId("zip"), diff --git a/internal/auth/password/testing.go b/internal/auth/password/testing.go index f81be1413..8b71bda3f 100644 --- a/internal/auth/password/testing.go +++ b/internal/auth/password/testing.go @@ -11,40 +11,48 @@ import ( "github.com/stretchr/testify/require" ) +// TestAuthMethod creates a password auth methods to the provided DB with the +// provided scope id. If any errors are encountered during the creation of the +// auth methods, the test will fail. +func TestAuthMethod(t *testing.T, conn *gorm.DB, scopeId string, opt ...Option) *AuthMethod { + t.Helper() + assert, require := assert.New(t), require.New(t) + w := db.New(conn) + cat, err := NewAuthMethod(scopeId, opt...) + assert.NoError(err) + require.NotNil(cat) + id, err := newAuthMethodId() + assert.NoError(err) + require.NotEmpty(id) + cat.PublicId = id + + conf := NewArgon2Configuration() + require.NotNil(conf) + conf.PrivateId, err = newArgon2ConfigurationId() + require.NoError(err) + conf.PasswordMethodId = cat.PublicId + cat.PasswordConfId = conf.PrivateId + + ctx := context.Background() + _, err2 := w.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, + func(_ db.Reader, iw db.Writer) error { + require.NoError(iw.Create(ctx, conf)) + return iw.Create(ctx, cat) + }, + ) + + require.NoError(err2) + return cat +} + // TestAuthMethods creates count number of password auth methods to the provided DB // with the provided scope id. If any errors are encountered during the creation of // the auth methods, the test will fail. func TestAuthMethods(t *testing.T, conn *gorm.DB, scopeId string, count int) []*AuthMethod { t.Helper() - assert, require := assert.New(t), require.New(t) - w := db.New(conn) var auts []*AuthMethod for i := 0; i < count; i++ { - cat, err := NewAuthMethod(scopeId) - assert.NoError(err) - require.NotNil(cat) - id, err := newAuthMethodId() - assert.NoError(err) - require.NotEmpty(id) - cat.PublicId = id - - conf := NewArgon2Configuration() - require.NotNil(conf) - conf.PrivateId, err = newArgon2ConfigurationId() - require.NoError(err) - conf.PasswordMethodId = cat.PublicId - cat.PasswordConfId = conf.PrivateId - - ctx := context.Background() - _, err2 := w.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, - func(_ db.Reader, iw db.Writer) error { - require.NoError(iw.Create(ctx, conf)) - return iw.Create(ctx, cat) - }, - ) - - require.NoError(err2) - auts = append(auts, cat) + auts = append(auts, TestAuthMethod(t, conn, scopeId)) } return auts } diff --git a/internal/auth/testing.go b/internal/auth/testing.go index fcfc8f012..9c2a68001 100644 --- a/internal/auth/testing.go +++ b/internal/auth/testing.go @@ -3,10 +3,13 @@ package auth import ( "context" + "github.com/hashicorp/boundary/internal/requests" "github.com/hashicorp/boundary/internal/servers/controller/common" ) -// DisabledAuthTestContext is meant for testing, and uses a context that has auth checking entirely disabled +// DisabledAuthTestContext is meant for testing, and uses a context that has +// auth checking entirely disabled. Supported options: WithScopeId an WithUserId +// are used directly; WithKms is passed through into the verifier context. func DisabledAuthTestContext(iamRepoFn common.IamRepoFactory, scopeId string, opt ...Option) context.Context { reqInfo := RequestInfo{DisableAuthEntirely: true} opts := getOpts(opt...) @@ -15,5 +18,9 @@ func DisabledAuthTestContext(iamRepoFn common.IamRepoFactory, scopeId string, op reqInfo.scopeIdOverride = scopeId } reqInfo.userIdOverride = opts.withUserId - return NewVerifierContext(context.Background(), nil, iamRepoFn, nil, nil, opts.withKms, reqInfo) + if reqInfo.userIdOverride == "" { + reqInfo.userIdOverride = "u_auth" + } + requestContext := context.WithValue(context.Background(), requests.ContextRequestInformationKey, &requests.RequestContext{}) + return NewVerifierContext(requestContext, nil, iamRepoFn, nil, nil, opts.withKms, reqInfo) } diff --git a/internal/cmd/base/format.go b/internal/cmd/base/format.go index 6280fd980..94192e98f 100644 --- a/internal/cmd/base/format.go +++ b/internal/cmd/base/format.go @@ -20,6 +20,9 @@ import ( var asciiSpace = [256]uint8{'\t': 1, '\n': 1, '\v': 1, '\f': 1, '\r': 1, ' ': 1} func ScopeInfoForOutput(scp *scopes.ScopeInfo, maxLength int) string { + if scp == nil { + return " " + } vals := map[string]interface{}{ "ID": scp.Id, "Type": scp.Type, diff --git a/internal/cmd/commands/accountscmd/accounts.gen.go b/internal/cmd/commands/accountscmd/accounts.gen.go index e75f879b0..7f951afcb 100644 --- a/internal/cmd/commands/accountscmd/accounts.gen.go +++ b/internal/cmd/commands/accountscmd/accounts.gen.go @@ -275,8 +275,7 @@ func (c *Command) Run(args []string) int { switch base.Format(c.UI) { case "table": - item := result.GetItem().(*accounts.Account) - c.UI.Output(printItemTable(item)) + c.UI.Output(printItemTable(result)) case "json": if ok := c.PrintJsonItem(result); !ok { diff --git a/internal/cmd/commands/accountscmd/funcs.go b/internal/cmd/commands/accountscmd/funcs.go index 50e709981..54a2889fa 100644 --- a/internal/cmd/commands/accountscmd/funcs.go +++ b/internal/cmd/commands/accountscmd/funcs.go @@ -193,31 +193,43 @@ func (c *Command) printListTable(items []*accounts.Account) string { "", "Account information:", } - for i, m := range items { + for i, item := range items { if i > 0 { output = append(output, "") } - if true { + if item.Id != "" { output = append(output, - fmt.Sprintf(" ID: %s", m.Id), - fmt.Sprintf(" Version: %d", m.Version), - fmt.Sprintf(" Type: %s", m.Type), + fmt.Sprintf(" ID: %s", item.Id), + ) + } else { + output = append(output, + fmt.Sprintf(" ID: %s", "(not available)"), ) } - if m.Name != "" { + if item.Version > 0 { output = append(output, - fmt.Sprintf(" Name: %s", m.Name), + fmt.Sprintf(" Version: %d", item.Version), ) } - if m.Description != "" { + if item.Type != "" { output = append(output, - fmt.Sprintf(" Description: %s", m.Description), + fmt.Sprintf(" Type: %s", item.Type), ) } - if len(m.AuthorizedActions) > 0 { + if item.Name != "" { + output = append(output, + fmt.Sprintf(" Name: %s", item.Name), + ) + } + if item.Description != "" { + output = append(output, + fmt.Sprintf(" Description: %s", item.Description), + ) + } + if len(item.AuthorizedActions) > 0 { output = append(output, " Authorized Actions:", - base.WrapSlice(6, m.AuthorizedActions), + base.WrapSlice(6, item.AuthorizedActions), ) } } @@ -225,16 +237,27 @@ func (c *Command) printListTable(items []*accounts.Account) string { return base.WrapForHelpText(output) } -func printItemTable(item *accounts.Account) string { - nonAttributeMap := map[string]interface{}{ - "ID": item.Id, - "Version": item.Version, - "Type": item.Type, - "Created Time": item.CreatedTime.Local().Format(time.RFC1123), - "Updated Time": item.UpdatedTime.Local().Format(time.RFC1123), - "Auth Method ID": item.AuthMethodId, +func printItemTable(result api.GenericResult) string { + item := result.GetItem().(*accounts.Account) + nonAttributeMap := map[string]interface{}{} + if item.Id != "" { + nonAttributeMap["ID"] = item.Id + } + if item.Version != 0 { + nonAttributeMap["Version"] = item.Version + } + if item.Type != "" { + nonAttributeMap["Type"] = item.Type + } + if !item.CreatedTime.IsZero() { + nonAttributeMap["Created Time"] = item.CreatedTime.Local().Format(time.RFC1123) + } + if !item.UpdatedTime.IsZero() { + nonAttributeMap["Updated Time"] = item.UpdatedTime.Local().Format(time.RFC1123) + } + if item.AuthMethodId != "" { + nonAttributeMap["Auth Method ID"] = item.AuthMethodId } - if item.Name != "" { nonAttributeMap["Name"] = item.Name } @@ -248,9 +271,14 @@ func printItemTable(item *accounts.Account) string { "", "Account information:", base.WrapMap(2, maxLength+2, nonAttributeMap), - "", - " Scope:", - base.ScopeInfoForOutput(item.Scope, maxLength), + } + + if item.Scope != nil { + ret = append(ret, + "", + " Scope:", + base.ScopeInfoForOutput(item.Scope, maxLength), + ) } if len(item.AuthorizedActions) > 0 { diff --git a/internal/cmd/commands/accountscmd/oidc_accounts.gen.go b/internal/cmd/commands/accountscmd/oidc_accounts.gen.go index aa8724eb2..659d4a8c0 100644 --- a/internal/cmd/commands/accountscmd/oidc_accounts.gen.go +++ b/internal/cmd/commands/accountscmd/oidc_accounts.gen.go @@ -216,8 +216,7 @@ func (c *OidcCommand) Run(args []string) int { switch base.Format(c.UI) { case "table": - item := result.GetItem().(*accounts.Account) - c.UI.Output(printItemTable(item)) + c.UI.Output(printItemTable(result)) case "json": if ok := c.PrintJsonItem(result); !ok { diff --git a/internal/cmd/commands/accountscmd/password_accounts.gen.go b/internal/cmd/commands/accountscmd/password_accounts.gen.go index c5ceb8f09..49ada7e0e 100644 --- a/internal/cmd/commands/accountscmd/password_accounts.gen.go +++ b/internal/cmd/commands/accountscmd/password_accounts.gen.go @@ -216,8 +216,7 @@ func (c *PasswordCommand) Run(args []string) int { switch base.Format(c.UI) { case "table": - item := result.GetItem().(*accounts.Account) - c.UI.Output(printItemTable(item)) + c.UI.Output(printItemTable(result)) case "json": if ok := c.PrintJsonItem(result); !ok { diff --git a/internal/cmd/commands/authmethodscmd/authmethods.gen.go b/internal/cmd/commands/authmethodscmd/authmethods.gen.go index 6ce403e35..186a291a8 100644 --- a/internal/cmd/commands/authmethodscmd/authmethods.gen.go +++ b/internal/cmd/commands/authmethodscmd/authmethods.gen.go @@ -242,8 +242,7 @@ func (c *Command) Run(args []string) int { switch base.Format(c.UI) { case "table": - item := result.GetItem().(*authmethods.AuthMethod) - c.UI.Output(printItemTable(item)) + c.UI.Output(printItemTable(result)) case "json": if ok := c.PrintJsonItem(result); !ok { diff --git a/internal/cmd/commands/authmethodscmd/funcs.go b/internal/cmd/commands/authmethodscmd/funcs.go index d1e18073d..7ed22b1d0 100644 --- a/internal/cmd/commands/authmethodscmd/funcs.go +++ b/internal/cmd/commands/authmethodscmd/funcs.go @@ -5,7 +5,9 @@ import ( "sort" "time" + "github.com/hashicorp/boundary/api" "github.com/hashicorp/boundary/api/authmethods" + "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/cmd/base" ) @@ -76,46 +78,54 @@ func (c *Command) printListTable(items []*authmethods.AuthMethod) string { "", "Auth Method information:", } - for i, m := range items { + for i, item := range items { if i > 0 { output = append(output, "") } - if true { + if item.Id != "" { output = append(output, - fmt.Sprintf(" ID: %s", m.Id), + fmt.Sprintf(" ID: %s", item.Id), + ) + } else { + output = append(output, + fmt.Sprintf(" ID: %s", "(not available)"), ) } - if c.FlagRecursive { + if c.FlagRecursive && item.ScopeId != "" { output = append(output, - fmt.Sprintf(" Scope ID: %s", m.Scope.Id), + fmt.Sprintf(" Scope ID: %s", item.ScopeId), ) } - if true { + if item.Version > 0 { output = append(output, - fmt.Sprintf(" Version: %d", m.Version), - fmt.Sprintf(" Type: %s", m.Type), + fmt.Sprintf(" Version: %d", item.Version), ) } - if m.Name != "" { + if item.Type != "" { output = append(output, - fmt.Sprintf(" Name: %s", m.Name), + fmt.Sprintf(" Type: %s", item.Type), ) } - if m.Description != "" { + if item.Name != "" { output = append(output, - fmt.Sprintf(" Description: %s", m.Description), + fmt.Sprintf(" Name: %s", item.Name), ) } - if true { + if item.Description != "" { output = append(output, - fmt.Sprintf(" Is Primary For Scope: %t", m.IsPrimary), + fmt.Sprintf(" Description: %s", item.Description), + ) + } + if item.IsPrimary { + output = append(output, + fmt.Sprintf(" Is Primary For Scope: %t", item.IsPrimary), ) } - if len(m.AuthorizedActions) > 0 { + if len(item.AuthorizedActions) > 0 { output = append(output, " Authorized Actions:", - base.WrapSlice(6, m.AuthorizedActions), + base.WrapSlice(6, item.AuthorizedActions), ) } } @@ -123,45 +133,63 @@ func (c *Command) printListTable(items []*authmethods.AuthMethod) string { return base.WrapForHelpText(output) } -func printItemTable(in *authmethods.AuthMethod) string { - nonAttributeMap := map[string]interface{}{ - "ID": in.Id, - "Version": in.Version, - "Type": in.Type, - "Created Time": in.CreatedTime.Local().Format(time.RFC1123), - "Updated Time": in.UpdatedTime.Local().Format(time.RFC1123), - "Is Primary For Scope": in.IsPrimary, +func printItemTable(result api.GenericResult) string { + item := result.GetItem().(*authmethods.AuthMethod) + nonAttributeMap := map[string]interface{}{} + if item.Id != "" { + nonAttributeMap["ID"] = item.Id + } + if item.Version != 0 { + nonAttributeMap["Version"] = item.Version + } + if item.Type != "" { + nonAttributeMap["Type"] = item.Type + } + if !item.CreatedTime.IsZero() { + nonAttributeMap["Created Time"] = item.CreatedTime.Local().Format(time.RFC1123) + } + if !item.UpdatedTime.IsZero() { + nonAttributeMap["Updated Time"] = item.UpdatedTime.Local().Format(time.RFC1123) + } + if item.Name != "" { + nonAttributeMap["Name"] = item.Name + } + if item.Description != "" { + nonAttributeMap["Description"] = item.Description + } + if result.GetResponse() != nil && result.GetResponse().Map != nil { + if result.GetResponse().Map[globals.IsPrimaryField] != nil { + nonAttributeMap["Is Primary For Scope"] = item.IsPrimary + } } - if in.Name != "" { - nonAttributeMap["Name"] = in.Name - } - if in.Description != "" { - nonAttributeMap["Description"] = in.Description - } - - maxLength := base.MaxAttributesLength(nonAttributeMap, in.Attributes, keySubstMap) + maxLength := base.MaxAttributesLength(nonAttributeMap, item.Attributes, keySubstMap) ret := []string{ "", "Auth Method information:", base.WrapMap(2, maxLength+2, nonAttributeMap), - "", - " Scope:", - base.ScopeInfoForOutput(in.Scope, maxLength), } - if len(in.AuthorizedActions) > 0 { + if item.Scope != nil { ret = append(ret, "", - " Authorized Actions:", - base.WrapSlice(4, in.AuthorizedActions), + " Scope:", + base.ScopeInfoForOutput(item.Scope, maxLength), ) } - if len(in.AuthorizedCollectionActions) > 0 { - keys := make([]string, 0, len(in.AuthorizedCollectionActions)) - for k := range in.AuthorizedCollectionActions { + if len(item.AuthorizedActions) > 0 { + ret = append(ret, + "", + " Authorized Actions:", + base.WrapSlice(4, item.AuthorizedActions), + ) + } + + if len(item.AuthorizedCollectionActions) > 0 { + keys := make([]string, 0, len(item.AuthorizedCollectionActions)) + for k := range item.AuthorizedCollectionActions { keys = append(keys, k) } sort.Strings(keys) @@ -172,16 +200,16 @@ func printItemTable(in *authmethods.AuthMethod) string { for _, key := range keys { ret = append(ret, fmt.Sprintf(" %s:", key), - base.WrapSlice(6, in.AuthorizedCollectionActions[key]), + base.WrapSlice(6, item.AuthorizedCollectionActions[key]), ) } } - if len(in.Attributes) > 0 { + if len(item.Attributes) > 0 { ret = append(ret, "", " Attributes:", - base.WrapMap(4, maxLength, in.Attributes), + base.WrapMap(4, maxLength, item.Attributes), ) } diff --git a/internal/cmd/commands/authmethodscmd/oidc_authmethods.gen.go b/internal/cmd/commands/authmethodscmd/oidc_authmethods.gen.go index 1adc3e08f..eeabfd01c 100644 --- a/internal/cmd/commands/authmethodscmd/oidc_authmethods.gen.go +++ b/internal/cmd/commands/authmethodscmd/oidc_authmethods.gen.go @@ -231,8 +231,7 @@ func (c *OidcCommand) Run(args []string) int { switch base.Format(c.UI) { case "table": - item := result.GetItem().(*authmethods.AuthMethod) - c.UI.Output(printItemTable(item)) + c.UI.Output(printItemTable(result)) case "json": if ok := c.PrintJsonItem(result); !ok { diff --git a/internal/cmd/commands/authmethodscmd/password_authmethods.gen.go b/internal/cmd/commands/authmethodscmd/password_authmethods.gen.go index 83b2d72ab..41aa2fb24 100644 --- a/internal/cmd/commands/authmethodscmd/password_authmethods.gen.go +++ b/internal/cmd/commands/authmethodscmd/password_authmethods.gen.go @@ -221,8 +221,7 @@ func (c *PasswordCommand) Run(args []string) int { switch base.Format(c.UI) { case "table": - item := result.GetItem().(*authmethods.AuthMethod) - c.UI.Output(printItemTable(item)) + c.UI.Output(printItemTable(result)) case "json": if ok := c.PrintJsonItem(result); !ok { diff --git a/internal/cmd/commands/authtokenscmd/authtokens.gen.go b/internal/cmd/commands/authtokenscmd/authtokens.gen.go index 1f30377f3..e2bcbd5fb 100644 --- a/internal/cmd/commands/authtokenscmd/authtokens.gen.go +++ b/internal/cmd/commands/authtokenscmd/authtokens.gen.go @@ -233,8 +233,7 @@ func (c *Command) Run(args []string) int { switch base.Format(c.UI) { case "table": - item := result.GetItem().(*authtokens.AuthToken) - c.UI.Output(printItemTable(item)) + c.UI.Output(printItemTable(result)) case "json": if ok := c.PrintJsonItem(result); !ok { diff --git a/internal/cmd/commands/authtokenscmd/funcs.go b/internal/cmd/commands/authtokenscmd/funcs.go index f84fafdcd..cba50379b 100644 --- a/internal/cmd/commands/authtokenscmd/funcs.go +++ b/internal/cmd/commands/authtokenscmd/funcs.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + "github.com/hashicorp/boundary/api" "github.com/hashicorp/boundary/api/authtokens" "github.com/hashicorp/boundary/internal/cmd/base" "github.com/hashicorp/boundary/sdk/strutil" @@ -69,23 +70,43 @@ func (c *Command) printListTable(items []*authtokens.AuthToken) string { if i > 0 { output = append(output, "") } - if true { + if t.Id != "" { output = append(output, fmt.Sprintf(" ID: %s", t.Id), ) } - if c.FlagRecursive { + if c.FlagRecursive && t.ScopeId != "" { output = append(output, - fmt.Sprintf(" Scope ID: %s", t.Scope.Id), + fmt.Sprintf(" Scope ID: %s", t.ScopeId), ) } - if true { + if !t.ApproximateLastUsedTime.IsZero() { output = append(output, fmt.Sprintf(" Approximate Last Used Time: %s", t.ApproximateLastUsedTime.Local().Format(time.RFC1123)), + ) + } + if t.AuthMethodId != "" { + output = append(output, fmt.Sprintf(" Auth Method ID: %s", t.AuthMethodId), + ) + } + if !t.CreatedTime.IsZero() { + output = append(output, fmt.Sprintf(" Created Time: %s", t.CreatedTime.Local().Format(time.RFC1123)), + ) + } + if !t.ExpirationTime.IsZero() { + output = append(output, fmt.Sprintf(" Expiration Time: %s", t.ExpirationTime.Local().Format(time.RFC1123)), + ) + } + if !t.UpdatedTime.IsZero() { + output = append(output, fmt.Sprintf(" Updated Time: %s", t.UpdatedTime.Local().Format(time.RFC1123)), + ) + } + if t.UserId != "" { + output = append(output, fmt.Sprintf(" User ID: %s", t.UserId), ) } @@ -100,15 +121,16 @@ func (c *Command) printListTable(items []*authtokens.AuthToken) string { return base.WrapForHelpText(output) } -func printItemTable(in *authtokens.AuthToken) string { +func printItemTable(result api.GenericResult) string { + item := result.GetItem().(*authtokens.AuthToken) nonAttributeMap := map[string]interface{}{ - "ID": in.Id, - "Auth Method ID": in.AuthMethodId, - "User ID": in.UserId, - "Created Time": in.CreatedTime.Local().Format(time.RFC1123), - "Updated Time": in.UpdatedTime.Local().Format(time.RFC1123), - "Expiration Time": in.ExpirationTime.Local().Format(time.RFC1123), - "Approximate Last Used Time": in.ApproximateLastUsedTime.Local().Format(time.RFC1123), + "ID": item.Id, + "Auth Method ID": item.AuthMethodId, + "User ID": item.UserId, + "Created Time": item.CreatedTime.Local().Format(time.RFC1123), + "Updated Time": item.UpdatedTime.Local().Format(time.RFC1123), + "Expiration Time": item.ExpirationTime.Local().Format(time.RFC1123), + "Approximate Last Used Time": item.ApproximateLastUsedTime.Local().Format(time.RFC1123), } maxLength := base.MaxAttributesLength(nonAttributeMap, nil, nil) @@ -119,14 +141,14 @@ func printItemTable(in *authtokens.AuthToken) string { base.WrapMap(2, maxLength+2, nonAttributeMap), "", " Scope:", - base.ScopeInfoForOutput(in.Scope, maxLength), + base.ScopeInfoForOutput(item.Scope, maxLength), } - if len(in.AuthorizedActions) > 0 { + if len(item.AuthorizedActions) > 0 { ret = append(ret, "", " Authorized Actions:", - base.WrapSlice(4, in.AuthorizedActions), + base.WrapSlice(4, item.AuthorizedActions), ) } diff --git a/internal/cmd/commands/groupscmd/funcs.go b/internal/cmd/commands/groupscmd/funcs.go index 97bbf7f80..53f8ce17f 100644 --- a/internal/cmd/commands/groupscmd/funcs.go +++ b/internal/cmd/commands/groupscmd/funcs.go @@ -149,39 +149,43 @@ func (c *Command) printListTable(items []*groups.Group) string { "", "Group information:", } - for i, g := range items { + for i, item := range items { if i > 0 { output = append(output, "") } - if true { + if item.Id != "" { output = append(output, - fmt.Sprintf(" ID: %s", g.Id), + fmt.Sprintf(" ID: %s", item.Id), + ) + } else { + output = append(output, + fmt.Sprintf(" ID: %s", "(not available)"), ) } - if c.FlagRecursive { + if c.FlagRecursive && item.ScopeId != "" { output = append(output, - fmt.Sprintf(" Scope ID: %s", g.Scope.Id), + fmt.Sprintf(" Scope ID: %s", item.ScopeId), ) } - if true { + if item.Version > 0 { output = append(output, - fmt.Sprintf(" Version: %d", g.Version), + fmt.Sprintf(" Version: %d", item.Version), ) } - if g.Name != "" { + if item.Name != "" { output = append(output, - fmt.Sprintf(" Name: %s", g.Name), + fmt.Sprintf(" Name: %s", item.Name), ) } - if g.Description != "" { + if item.Description != "" { output = append(output, - fmt.Sprintf(" Description: %s", g.Description), + fmt.Sprintf(" Description: %s", item.Description), ) } - if len(g.AuthorizedActions) > 0 { + if len(item.AuthorizedActions) > 0 { output = append(output, " Authorized Actions:", - base.WrapSlice(6, g.AuthorizedActions), + base.WrapSlice(6, item.AuthorizedActions), ) } } @@ -189,14 +193,21 @@ func (c *Command) printListTable(items []*groups.Group) string { return base.WrapForHelpText(output) } -func printItemTable(item *groups.Group) string { - nonAttributeMap := map[string]interface{}{ - "ID": item.Id, - "Version": item.Version, - "Created Time": item.CreatedTime.Local().Format(time.RFC1123), - "Updated Time": item.UpdatedTime.Local().Format(time.RFC1123), +func printItemTable(result api.GenericResult) string { + item := result.GetItem().(*groups.Group) + nonAttributeMap := map[string]interface{}{} + if item.Id != "" { + nonAttributeMap["ID"] = item.Id + } + if item.Version != 0 { + nonAttributeMap["Version"] = item.Version + } + if !item.CreatedTime.IsZero() { + nonAttributeMap["Created Time"] = item.CreatedTime.Local().Format(time.RFC1123) + } + if !item.UpdatedTime.IsZero() { + nonAttributeMap["Updated Time"] = item.UpdatedTime.Local().Format(time.RFC1123) } - if item.Name != "" { nonAttributeMap["Name"] = item.Name } @@ -224,9 +235,14 @@ func printItemTable(item *groups.Group) string { "", "Group information:", base.WrapMap(2, maxLength+2, nonAttributeMap), - "", - " Scope:", - base.ScopeInfoForOutput(item.Scope, maxLength), + } + + if item.Scope != nil { + ret = append(ret, + "", + " Scope:", + base.ScopeInfoForOutput(item.Scope, maxLength), + ) } if len(item.AuthorizedActions) > 0 { diff --git a/internal/cmd/commands/groupscmd/groups.gen.go b/internal/cmd/commands/groupscmd/groups.gen.go index 10e5252ae..92b452eac 100644 --- a/internal/cmd/commands/groupscmd/groups.gen.go +++ b/internal/cmd/commands/groupscmd/groups.gen.go @@ -316,8 +316,7 @@ func (c *Command) Run(args []string) int { switch base.Format(c.UI) { case "table": - item := result.GetItem().(*groups.Group) - c.UI.Output(printItemTable(item)) + c.UI.Output(printItemTable(result)) case "json": if ok := c.PrintJsonItem(result); !ok { diff --git a/internal/cmd/commands/hostcatalogscmd/funcs.go b/internal/cmd/commands/hostcatalogscmd/funcs.go index 88524d057..e6022239b 100644 --- a/internal/cmd/commands/hostcatalogscmd/funcs.go +++ b/internal/cmd/commands/hostcatalogscmd/funcs.go @@ -5,6 +5,7 @@ import ( "sort" "time" + "github.com/hashicorp/boundary/api" "github.com/hashicorp/boundary/api/hostcatalogs" "github.com/hashicorp/boundary/internal/cmd/base" ) @@ -68,19 +69,27 @@ func (c *Command) printListTable(items []*hostcatalogs.HostCatalog) string { if i > 0 { output = append(output, "") } - if true { + if m.Id != "" { output = append(output, fmt.Sprintf(" ID: %s", m.Id), ) - } - if c.FlagRecursive { + } else { output = append(output, - fmt.Sprintf(" Scope ID: %s", m.Scope.Id), + fmt.Sprintf(" ID: %s", "(not available)"), ) } - if true { + if c.FlagRecursive && m.ScopeId != "" { + output = append(output, + fmt.Sprintf(" Scope ID: %s", m.ScopeId), + ) + } + if m.Version > 0 { output = append(output, fmt.Sprintf(" Version: %d", m.Version), + ) + } + if m.Type != "" { + output = append(output, fmt.Sprintf(" Type: %s", m.Type), ) } @@ -105,44 +114,58 @@ func (c *Command) printListTable(items []*hostcatalogs.HostCatalog) string { return base.WrapForHelpText(output) } -func printItemTable(in *hostcatalogs.HostCatalog) string { - nonAttributeMap := map[string]interface{}{ - "ID": in.Id, - "Version": in.Version, - "Type": in.Type, - "Created Time": in.CreatedTime.Local().Format(time.RFC1123), - "Updated Time": in.UpdatedTime.Local().Format(time.RFC1123), +func printItemTable(result api.GenericResult) string { + item := result.GetItem().(*hostcatalogs.HostCatalog) + nonAttributeMap := map[string]interface{}{} + if item.Id != "" { + nonAttributeMap["ID"] = item.Id + } + if item.Version != 0 { + nonAttributeMap["Version"] = item.Version + } + if !item.CreatedTime.IsZero() { + nonAttributeMap["Created Time"] = item.CreatedTime.Local().Format(time.RFC1123) + } + if !item.UpdatedTime.IsZero() { + nonAttributeMap["Updated Time"] = item.UpdatedTime.Local().Format(time.RFC1123) + } + if item.Name != "" { + nonAttributeMap["Name"] = item.Name + } + if item.Description != "" { + nonAttributeMap["Description"] = item.Description + } + if item.Type != "" { + nonAttributeMap["Type"] = item.Type } - if in.Name != "" { - nonAttributeMap["Name"] = in.Name - } - if in.Description != "" { - nonAttributeMap["Description"] = in.Description - } - - maxLength := base.MaxAttributesLength(nonAttributeMap, in.Attributes, keySubstMap) + maxLength := base.MaxAttributesLength(nonAttributeMap, item.Attributes, keySubstMap) ret := []string{ "", "Host Catalog information:", base.WrapMap(2, maxLength+2, nonAttributeMap), - "", - " Scope:", - base.ScopeInfoForOutput(in.Scope, maxLength), } - if len(in.AuthorizedActions) > 0 { + if item.Scope != nil { ret = append(ret, "", - " Authorized Actions:", - base.WrapSlice(4, in.AuthorizedActions), + " Scope:", + base.ScopeInfoForOutput(item.Scope, maxLength), ) } - if len(in.AuthorizedCollectionActions) > 0 { - keys := make([]string, 0, len(in.AuthorizedCollectionActions)) - for k := range in.AuthorizedCollectionActions { + if len(item.AuthorizedActions) > 0 { + ret = append(ret, + "", + " Authorized Actions:", + base.WrapSlice(4, item.AuthorizedActions), + ) + } + + if len(item.AuthorizedCollectionActions) > 0 { + keys := make([]string, 0, len(item.AuthorizedCollectionActions)) + for k := range item.AuthorizedCollectionActions { keys = append(keys, k) } sort.Strings(keys) @@ -152,16 +175,16 @@ func printItemTable(in *hostcatalogs.HostCatalog) string { for _, key := range keys { ret = append(ret, fmt.Sprintf(" %s:", key), - base.WrapSlice(6, in.AuthorizedCollectionActions[key]), + base.WrapSlice(6, item.AuthorizedCollectionActions[key]), ) } } - if len(in.Attributes) > 0 { + if len(item.Attributes) > 0 { ret = append(ret, "", " Attributes:", - base.WrapMap(4, maxLength, in.Attributes), + base.WrapMap(4, maxLength, item.Attributes), ) } diff --git a/internal/cmd/commands/hostcatalogscmd/hostcatalogs.gen.go b/internal/cmd/commands/hostcatalogscmd/hostcatalogs.gen.go index 96b348c35..2b9b14ab7 100644 --- a/internal/cmd/commands/hostcatalogscmd/hostcatalogs.gen.go +++ b/internal/cmd/commands/hostcatalogscmd/hostcatalogs.gen.go @@ -242,8 +242,7 @@ func (c *Command) Run(args []string) int { switch base.Format(c.UI) { case "table": - item := result.GetItem().(*hostcatalogs.HostCatalog) - c.UI.Output(printItemTable(item)) + c.UI.Output(printItemTable(result)) case "json": if ok := c.PrintJsonItem(result); !ok { diff --git a/internal/cmd/commands/hostcatalogscmd/static_hostcatalogs.gen.go b/internal/cmd/commands/hostcatalogscmd/static_hostcatalogs.gen.go index f5d4edd9c..708153ce2 100644 --- a/internal/cmd/commands/hostcatalogscmd/static_hostcatalogs.gen.go +++ b/internal/cmd/commands/hostcatalogscmd/static_hostcatalogs.gen.go @@ -219,8 +219,7 @@ func (c *StaticCommand) Run(args []string) int { switch base.Format(c.UI) { case "table": - item := result.GetItem().(*hostcatalogs.HostCatalog) - c.UI.Output(printItemTable(item)) + c.UI.Output(printItemTable(result)) case "json": if ok := c.PrintJsonItem(result); !ok { diff --git a/internal/cmd/commands/hostscmd/funcs.go b/internal/cmd/commands/hostscmd/funcs.go index 313267c3a..ed6c292d9 100644 --- a/internal/cmd/commands/hostscmd/funcs.go +++ b/internal/cmd/commands/hostscmd/funcs.go @@ -4,6 +4,7 @@ import ( "fmt" "time" + "github.com/hashicorp/boundary/api" "github.com/hashicorp/boundary/api/hosts" "github.com/hashicorp/boundary/internal/cmd/base" ) @@ -67,10 +68,22 @@ func (c *Command) printListTable(items []*hosts.Host) string { if i > 0 { output = append(output, "") } - if true { + if item.Id != "" { output = append(output, fmt.Sprintf(" ID: %s", item.Id), + ) + } else { + output = append(output, + fmt.Sprintf(" ID: %s", "(not available)"), + ) + } + if item.Version > 0 { + output = append(output, fmt.Sprintf(" Version: %d", item.Version), + ) + } + if item.Type != "" { + output = append(output, fmt.Sprintf(" Type: %s", item.Type), ) } @@ -95,55 +108,71 @@ func (c *Command) printListTable(items []*hosts.Host) string { return base.WrapForHelpText(output) } -func printItemTable(in *hosts.Host) string { - nonAttributeMap := map[string]interface{}{ - "ID": in.Id, - "Version": in.Version, - "Type": in.Type, - "Created Time": in.CreatedTime.Local().Format(time.RFC1123), - "Updated Time": in.UpdatedTime.Local().Format(time.RFC1123), - "Host Catalog ID": in.HostCatalogId, +func printItemTable(result api.GenericResult) string { + item := result.GetItem().(*hosts.Host) + nonAttributeMap := map[string]interface{}{} + if item.Id != "" { + nonAttributeMap["ID"] = item.Id + } + if item.Version != 0 { + nonAttributeMap["Version"] = item.Version + } + if item.Type != "" { + nonAttributeMap["Type"] = item.Type + } + if !item.CreatedTime.IsZero() { + nonAttributeMap["Created Time"] = item.CreatedTime.Local().Format(time.RFC1123) + } + if !item.UpdatedTime.IsZero() { + nonAttributeMap["Updated Time"] = item.UpdatedTime.Local().Format(time.RFC1123) + } + if item.Name != "" { + nonAttributeMap["Name"] = item.Name + } + if item.Description != "" { + nonAttributeMap["Description"] = item.Description + } + if item.HostCatalogId != "" { + nonAttributeMap["Host Catalog ID"] = item.HostCatalogId } - if in.Name != "" { - nonAttributeMap["Name"] = in.Name - } - if in.Description != "" { - nonAttributeMap["Description"] = in.Description - } - - maxLength := base.MaxAttributesLength(nonAttributeMap, in.Attributes, keySubstMap) + maxLength := base.MaxAttributesLength(nonAttributeMap, item.Attributes, keySubstMap) ret := []string{ "", "Host information:", base.WrapMap(2, maxLength+2, nonAttributeMap), - "", - " Scope:", - base.ScopeInfoForOutput(in.Scope, maxLength), } - if len(in.AuthorizedActions) > 0 { + if item.Scope != nil { + ret = append(ret, + "", + " Scope:", + base.ScopeInfoForOutput(item.Scope, maxLength), + ) + } + + if len(item.AuthorizedActions) > 0 { ret = append(ret, "", " Authorized Actions:", - base.WrapSlice(4, in.AuthorizedActions), + base.WrapSlice(4, item.AuthorizedActions), ) } - if len(in.HostSetIds) > 0 { + if len(item.HostSetIds) > 0 { ret = append(ret, "", " Host Set IDs:", - base.WrapSlice(4, in.HostSetIds), + base.WrapSlice(4, item.HostSetIds), ) } - if len(in.Attributes) > 0 { + if len(item.Attributes) > 0 { ret = append(ret, "", " Attributes:", - base.WrapMap(4, maxLength, in.Attributes), + base.WrapMap(4, maxLength, item.Attributes), ) } diff --git a/internal/cmd/commands/hostscmd/hosts.gen.go b/internal/cmd/commands/hostscmd/hosts.gen.go index ace10fff0..c741b89ab 100644 --- a/internal/cmd/commands/hostscmd/hosts.gen.go +++ b/internal/cmd/commands/hostscmd/hosts.gen.go @@ -253,8 +253,7 @@ func (c *Command) Run(args []string) int { switch base.Format(c.UI) { case "table": - item := result.GetItem().(*hosts.Host) - c.UI.Output(printItemTable(item)) + c.UI.Output(printItemTable(result)) case "json": if ok := c.PrintJsonItem(result); !ok { diff --git a/internal/cmd/commands/hostscmd/static_hosts.gen.go b/internal/cmd/commands/hostscmd/static_hosts.gen.go index 0332c0f58..44c96265c 100644 --- a/internal/cmd/commands/hostscmd/static_hosts.gen.go +++ b/internal/cmd/commands/hostscmd/static_hosts.gen.go @@ -216,8 +216,7 @@ func (c *StaticCommand) Run(args []string) int { switch base.Format(c.UI) { case "table": - item := result.GetItem().(*hosts.Host) - c.UI.Output(printItemTable(item)) + c.UI.Output(printItemTable(result)) case "json": if ok := c.PrintJsonItem(result); !ok { diff --git a/internal/cmd/commands/hostsetscmd/funcs.go b/internal/cmd/commands/hostsetscmd/funcs.go index dffb0c4c7..b3064c8af 100644 --- a/internal/cmd/commands/hostsetscmd/funcs.go +++ b/internal/cmd/commands/hostsetscmd/funcs.go @@ -182,31 +182,43 @@ func (c *Command) printListTable(items []*hostsets.HostSet) string { "", "Host Set information:", } - for i, m := range items { + for i, item := range items { if i > 0 { output = append(output, "") } - if true { + if item.Id != "" { output = append(output, - fmt.Sprintf(" ID: %s", m.Id), - fmt.Sprintf(" Version: %d", m.Version), - fmt.Sprintf(" Type: %s", m.Type), + fmt.Sprintf(" ID: %s", item.Id), + ) + } else { + output = append(output, + fmt.Sprintf(" ID: %s", "(not available)"), ) } - if m.Name != "" { + if item.Version > 0 { output = append(output, - fmt.Sprintf(" Name: %s", m.Name), + fmt.Sprintf(" Version: %d", item.Version), ) } - if m.Description != "" { + if item.Type != "" { output = append(output, - fmt.Sprintf(" Description: %s", m.Description), + fmt.Sprintf(" Type: %s", item.Type), ) } - if len(m.AuthorizedActions) > 0 { + if item.Name != "" { + output = append(output, + fmt.Sprintf(" Name: %s", item.Name), + ) + } + if item.Description != "" { + output = append(output, + fmt.Sprintf(" Description: %s", item.Description), + ) + } + if len(item.AuthorizedActions) > 0 { output = append(output, " Authorized Actions:", - base.WrapSlice(6, m.AuthorizedActions), + base.WrapSlice(6, item.AuthorizedActions), ) } } @@ -214,55 +226,71 @@ func (c *Command) printListTable(items []*hostsets.HostSet) string { return base.WrapForHelpText(output) } -func printItemTable(in *hostsets.HostSet) string { - nonAttributeMap := map[string]interface{}{ - "ID": in.Id, - "Version": in.Version, - "Type": in.Type, - "Created Time": in.CreatedTime.Local().Format(time.RFC1123), - "Updated Time": in.UpdatedTime.Local().Format(time.RFC1123), - "Host Catalog ID": in.HostCatalogId, +func printItemTable(result api.GenericResult) string { + item := result.GetItem().(*hostsets.HostSet) + nonAttributeMap := map[string]interface{}{} + if item.Id != "" { + nonAttributeMap["ID"] = item.Id + } + if item.Version != 0 { + nonAttributeMap["Version"] = item.Version + } + if item.Type != "" { + nonAttributeMap["Type"] = item.Type + } + if !item.CreatedTime.IsZero() { + nonAttributeMap["Created Time"] = item.CreatedTime.Local().Format(time.RFC1123) + } + if !item.UpdatedTime.IsZero() { + nonAttributeMap["Updated Time"] = item.UpdatedTime.Local().Format(time.RFC1123) + } + if item.Name != "" { + nonAttributeMap["Name"] = item.Name + } + if item.Description != "" { + nonAttributeMap["Description"] = item.Description + } + if item.HostCatalogId != "" { + nonAttributeMap["Host Catalog ID"] = item.HostCatalogId } - if in.Name != "" { - nonAttributeMap["Name"] = in.Name - } - if in.Description != "" { - nonAttributeMap["Description"] = in.Description - } - - maxLength := base.MaxAttributesLength(nonAttributeMap, in.Attributes, keySubstMap) + maxLength := base.MaxAttributesLength(nonAttributeMap, item.Attributes, keySubstMap) ret := []string{ "", "Host Set information:", base.WrapMap(2, maxLength+2, nonAttributeMap), - "", - " Scope:", - base.ScopeInfoForOutput(in.Scope, maxLength), } - if len(in.AuthorizedActions) > 0 { + if item.Scope != nil { + ret = append(ret, + "", + " Scope:", + base.ScopeInfoForOutput(item.Scope, maxLength), + ) + } + + if len(item.AuthorizedActions) > 0 { ret = append(ret, "", " Authorized Actions:", - base.WrapSlice(4, in.AuthorizedActions), + base.WrapSlice(4, item.AuthorizedActions), ) } - if len(in.HostIds) > 0 { + if len(item.HostIds) > 0 { ret = append(ret, "", " Host IDs:", - base.WrapSlice(4, in.HostIds), + base.WrapSlice(4, item.HostIds), ) } - if len(in.Attributes) > 0 { + if len(item.Attributes) > 0 { ret = append(ret, "", " Attributes:", - base.WrapMap(4, maxLength, in.Attributes), + base.WrapMap(4, maxLength, item.Attributes), ) } diff --git a/internal/cmd/commands/hostsetscmd/hostsets.gen.go b/internal/cmd/commands/hostsetscmd/hostsets.gen.go index b7c630ecc..d21f2b078 100644 --- a/internal/cmd/commands/hostsetscmd/hostsets.gen.go +++ b/internal/cmd/commands/hostsetscmd/hostsets.gen.go @@ -283,8 +283,7 @@ func (c *Command) Run(args []string) int { switch base.Format(c.UI) { case "table": - item := result.GetItem().(*hostsets.HostSet) - c.UI.Output(printItemTable(item)) + c.UI.Output(printItemTable(result)) case "json": if ok := c.PrintJsonItem(result); !ok { diff --git a/internal/cmd/commands/hostsetscmd/static_hostsets.gen.go b/internal/cmd/commands/hostsetscmd/static_hostsets.gen.go index 19c5eaed9..ecf825633 100644 --- a/internal/cmd/commands/hostsetscmd/static_hostsets.gen.go +++ b/internal/cmd/commands/hostsetscmd/static_hostsets.gen.go @@ -214,8 +214,7 @@ func (c *StaticCommand) Run(args []string) int { switch base.Format(c.UI) { case "table": - item := result.GetItem().(*hostsets.HostSet) - c.UI.Output(printItemTable(item)) + c.UI.Output(printItemTable(result)) case "json": if ok := c.PrintJsonItem(result); !ok { diff --git a/internal/cmd/commands/rolescmd/funcs.go b/internal/cmd/commands/rolescmd/funcs.go index 74a52adaf..fb104148c 100644 --- a/internal/cmd/commands/rolescmd/funcs.go +++ b/internal/cmd/commands/rolescmd/funcs.go @@ -257,17 +257,21 @@ func (c *Command) printListTable(items []*roles.Role) string { if i > 0 { output = append(output, "") } - if true { + if item.Id != "" { output = append(output, fmt.Sprintf(" ID: %s", item.Id), ) - } - if c.FlagRecursive { + } else { output = append(output, - fmt.Sprintf(" Scope ID: %s", item.Scope.Id), + fmt.Sprintf(" ID: %s", "(not available)"), ) } - if true { + if c.FlagRecursive && item.ScopeId != "" { + output = append(output, + fmt.Sprintf(" Scope ID: %s", item.ScopeId), + ) + } + if item.Version > 0 { output = append(output, fmt.Sprintf(" Version: %d", item.Version), ) @@ -293,22 +297,29 @@ func (c *Command) printListTable(items []*roles.Role) string { return base.WrapForHelpText(output) } -func printItemTable(in *roles.Role) string { - nonAttributeMap := map[string]interface{}{ - "ID": in.Id, - "Version": in.Version, - "Created Time": in.CreatedTime.Local().Format(time.RFC1123), - "Updated Time": in.UpdatedTime.Local().Format(time.RFC1123), +func printItemTable(result api.GenericResult) string { + item := result.GetItem().(*roles.Role) + nonAttributeMap := map[string]interface{}{} + if item.Id != "" { + nonAttributeMap["ID"] = item.Id } - - if in.Name != "" { - nonAttributeMap["Name"] = in.Name + if item.Version != 0 { + nonAttributeMap["Version"] = item.Version } - if in.Description != "" { - nonAttributeMap["Description"] = in.Description + if !item.CreatedTime.IsZero() { + nonAttributeMap["Created Time"] = item.CreatedTime.Local().Format(time.RFC1123) } - if in.GrantScopeId != "" { - nonAttributeMap["Grant Scope ID"] = in.GrantScopeId + if !item.UpdatedTime.IsZero() { + nonAttributeMap["Updated Time"] = item.UpdatedTime.Local().Format(time.RFC1123) + } + if item.Name != "" { + nonAttributeMap["Name"] = item.Name + } + if item.Description != "" { + nonAttributeMap["Description"] = item.Description + } + if item.GrantScopeId != "" { + nonAttributeMap["Grant Scope ID"] = item.GrantScopeId } maxLength := base.MaxAttributesLength(nonAttributeMap, nil, nil) @@ -317,39 +328,44 @@ func printItemTable(in *roles.Role) string { "", "Role information:", base.WrapMap(2, maxLength+2, nonAttributeMap), - "", - " Scope:", - base.ScopeInfoForOutput(in.Scope, maxLength), } - if len(in.AuthorizedActions) > 0 { + if item.Scope != nil { ret = append(ret, "", - " Authorized Actions:", - base.WrapSlice(4, in.AuthorizedActions), + " Scope:", + base.ScopeInfoForOutput(item.Scope, maxLength), ) } - if len(in.Principals) > 0 { + if len(item.AuthorizedActions) > 0 { + ret = append(ret, + "", + " Authorized Actions:", + base.WrapSlice(4, item.AuthorizedActions), + ) + } + + if len(item.Principals) > 0 { ret = append(ret, "", fmt.Sprintf(" Principals: %s", ""), ) } - for _, principal := range in.Principals { + for _, principal := range item.Principals { ret = append(ret, fmt.Sprintf(" ID: %s", principal.Id), fmt.Sprintf(" Type: %s", principal.Type), fmt.Sprintf(" Scope ID: %s", principal.ScopeId), ) } - if len(in.Grants) > 0 { + if len(item.Grants) > 0 { ret = append(ret, "", fmt.Sprintf(" Canonical Grants: %s", ""), ) } - for _, grant := range in.Grants { + for _, grant := range item.Grants { ret = append(ret, fmt.Sprintf(" %s", grant.Canonical), ) diff --git a/internal/cmd/commands/rolescmd/roles.gen.go b/internal/cmd/commands/rolescmd/roles.gen.go index 4cb779638..b36f0cbfd 100644 --- a/internal/cmd/commands/rolescmd/roles.gen.go +++ b/internal/cmd/commands/rolescmd/roles.gen.go @@ -340,8 +340,7 @@ func (c *Command) Run(args []string) int { switch base.Format(c.UI) { case "table": - item := result.GetItem().(*roles.Role) - c.UI.Output(printItemTable(item)) + c.UI.Output(printItemTable(result)) case "json": if ok := c.PrintJsonItem(result); !ok { diff --git a/internal/cmd/commands/scopescmd/funcs.go b/internal/cmd/commands/scopescmd/funcs.go index 33d1ac081..b26c8a28d 100644 --- a/internal/cmd/commands/scopescmd/funcs.go +++ b/internal/cmd/commands/scopescmd/funcs.go @@ -5,6 +5,7 @@ import ( "sort" "time" + "github.com/hashicorp/boundary/api" "github.com/hashicorp/boundary/api/scopes" "github.com/hashicorp/boundary/internal/cmd/base" ) @@ -96,7 +97,7 @@ func (c *Command) printListTable(items []*scopes.Scope) string { fmt.Sprintf(" Scope ID: %s", item.Scope.Id), ) } - if true { + if item.Version > 0 { output = append(output, fmt.Sprintf(" Version: %d", item.Version), ) @@ -127,22 +128,29 @@ func (c *Command) printListTable(items []*scopes.Scope) string { return base.WrapForHelpText(output) } -func printItemTable(in *scopes.Scope) string { - nonAttributeMap := map[string]interface{}{ - "ID": in.Id, - "Version": in.Version, - "Created Time": in.CreatedTime.Local().Format(time.RFC1123), - "Updated Time": in.UpdatedTime.Local().Format(time.RFC1123), +func printItemTable(result api.GenericResult) string { + item := result.GetItem().(*scopes.Scope) + nonAttributeMap := map[string]interface{}{} + if item.Id != "" { + nonAttributeMap["ID"] = item.Id } - - if in.Name != "" { - nonAttributeMap["Name"] = in.Name + if item.Version != 0 { + nonAttributeMap["Version"] = item.Version } - if in.Description != "" { - nonAttributeMap["Description"] = in.Description + if !item.CreatedTime.IsZero() { + nonAttributeMap["Created Time"] = item.CreatedTime.Local().Format(time.RFC1123) } - if in.PrimaryAuthMethodId != "" { - nonAttributeMap[flagPrimaryAuthMethodIdName] = in.PrimaryAuthMethodId + if !item.UpdatedTime.IsZero() { + nonAttributeMap["Updated Time"] = item.UpdatedTime.Local().Format(time.RFC1123) + } + if item.Name != "" { + nonAttributeMap["Name"] = item.Name + } + if item.Description != "" { + nonAttributeMap["Description"] = item.Description + } + if item.PrimaryAuthMethodId != "" { + nonAttributeMap[flagPrimaryAuthMethodIdName] = item.PrimaryAuthMethodId } maxLength := base.MaxAttributesLength(nonAttributeMap, nil, nil) @@ -151,23 +159,28 @@ func printItemTable(in *scopes.Scope) string { "", "Scope information:", base.WrapMap(2, maxLength+2, nonAttributeMap), - "", - " Scope (parent):", - base.ScopeInfoForOutput(in.Scope, maxLength), } - if len(in.AuthorizedActions) > 0 { + if item.Scope != nil { + ret = append(ret, + "", + " Scope (parent):", + base.ScopeInfoForOutput(item.Scope, maxLength), + ) + } + + if len(item.AuthorizedActions) > 0 { ret = append(ret, "", " Authorized Actions:", - base.WrapSlice(4, in.AuthorizedActions), + base.WrapSlice(4, item.AuthorizedActions), "", ) } - if len(in.AuthorizedCollectionActions) > 0 { - keys := make([]string, 0, len(in.AuthorizedCollectionActions)) - for k := range in.AuthorizedCollectionActions { + if len(item.AuthorizedCollectionActions) > 0 { + keys := make([]string, 0, len(item.AuthorizedCollectionActions)) + for k := range item.AuthorizedCollectionActions { keys = append(keys, k) } sort.Strings(keys) @@ -175,7 +188,7 @@ func printItemTable(in *scopes.Scope) string { for _, key := range keys { ret = append(ret, fmt.Sprintf(" %s:", key), - base.WrapSlice(6, in.AuthorizedCollectionActions[key]), + base.WrapSlice(6, item.AuthorizedCollectionActions[key]), ) } } diff --git a/internal/cmd/commands/scopescmd/scopes.gen.go b/internal/cmd/commands/scopescmd/scopes.gen.go index 68b832aa5..dc09630a3 100644 --- a/internal/cmd/commands/scopescmd/scopes.gen.go +++ b/internal/cmd/commands/scopescmd/scopes.gen.go @@ -290,8 +290,7 @@ func (c *Command) Run(args []string) int { switch base.Format(c.UI) { case "table": - item := result.GetItem().(*scopes.Scope) - c.UI.Output(printItemTable(item)) + c.UI.Output(printItemTable(result)) case "json": if ok := c.PrintJsonItem(result); !ok { diff --git a/internal/cmd/commands/sessionscmd/funcs.go b/internal/cmd/commands/sessionscmd/funcs.go index 68e31fd11..cccfecb51 100644 --- a/internal/cmd/commands/sessionscmd/funcs.go +++ b/internal/cmd/commands/sessionscmd/funcs.go @@ -76,23 +76,47 @@ func (c *Command) printListTable(items []*sessions.Session) string { if i > 0 { output = append(output, "") } - if true { + if item.Id != "" { output = append(output, fmt.Sprintf(" ID: %s", item.Id), ) - } - if c.FlagRecursive { + } else { output = append(output, - fmt.Sprintf(" Scope ID: %s", item.Scope.Id), + fmt.Sprintf(" ID: %s", "(not available)"), ) } - if true { + if c.FlagRecursive && item.ScopeId != "" { + output = append(output, + fmt.Sprintf(" Scope ID: %s", item.ScopeId), + ) + } + if item.Status != "" { output = append(output, fmt.Sprintf(" Status: %s", item.Status), + ) + } + if !item.CreatedTime.IsZero() { + output = append(output, fmt.Sprintf(" Created Time: %s", item.CreatedTime.Local().Format(time.RFC1123)), + ) + } + if !item.ExpirationTime.IsZero() { + output = append(output, fmt.Sprintf(" Expiration Time: %s", item.ExpirationTime.Local().Format(time.RFC1123)), + ) + } + if !item.UpdatedTime.IsZero() { + output = append(output, fmt.Sprintf(" Updated Time: %s", item.UpdatedTime.Local().Format(time.RFC1123)), + ) + } + if item.UserId != "" { + output = append(output, fmt.Sprintf(" User ID: %s", item.UserId), + ) + } + if item.TargetId != "" { + output = append(output, fmt.Sprintf(" Target ID: %s", item.TargetId), ) } @@ -107,31 +131,57 @@ func (c *Command) printListTable(items []*sessions.Session) string { return base.WrapForHelpText(output) } -func printItemTable(in *sessions.Session) string { - nonAttributeMap := map[string]interface{}{ - "ID": in.Id, - "Target ID": in.TargetId, - "Created Time": in.CreatedTime.Local().Format(time.RFC1123), - "Updated Time": in.UpdatedTime.Local().Format(time.RFC1123), - "Expiration Time": in.ExpirationTime.Local().Format(time.RFC1123), - "Version": in.Version, - "Type": in.Type, - "Auth Token ID": in.AuthTokenId, - "User ID": in.UserId, - "Host Set ID": in.HostSetId, - "Host ID": in.HostId, - "Endpoint": in.Endpoint, - "Status": in.Status, +func printItemTable(result api.GenericResult) string { + item := result.GetItem().(*sessions.Session) + nonAttributeMap := map[string]interface{}{} + if item.Id != "" { + nonAttributeMap["ID"] = item.Id } - if len(strings.TrimSpace(in.TerminationReason)) > 0 { - nonAttributeMap["Termination Reason"] = in.TerminationReason + if item.Version != 0 { + nonAttributeMap["Version"] = item.Version + } + if item.Type != "" { + nonAttributeMap["Type"] = item.Type + } + if !item.CreatedTime.IsZero() { + nonAttributeMap["Created Time"] = item.CreatedTime.Local().Format(time.RFC1123) + } + if !item.UpdatedTime.IsZero() { + nonAttributeMap["Updated Time"] = item.UpdatedTime.Local().Format(time.RFC1123) + } + if !item.ExpirationTime.IsZero() { + nonAttributeMap["Expiration Time"] = item.ExpirationTime.Local().Format(time.RFC1123) + } + if item.TargetId != "" { + nonAttributeMap["Target ID"] = item.TargetId + } + if item.AuthTokenId != "" { + nonAttributeMap["Auth Token ID"] = item.AuthTokenId + } + if item.UserId != "" { + nonAttributeMap["User ID"] = item.UserId + } + if item.HostSetId != "" { + nonAttributeMap["Host Set ID"] = item.HostSetId + } + if item.HostId != "" { + nonAttributeMap["Host ID"] = item.HostId + } + if item.Endpoint != "" { + nonAttributeMap["Endpoint"] = item.Endpoint + } + if item.Status != "" { + nonAttributeMap["Status"] = item.Status + } + if len(strings.TrimSpace(item.TerminationReason)) > 0 { + nonAttributeMap["Termination Reason"] = item.TerminationReason } maxLength := base.MaxAttributesLength(nonAttributeMap, nil, nil) var statesMaps []map[string]interface{} - if len(in.States) > 0 { - for _, state := range in.States { + if len(item.States) > 0 { + for _, state := range item.States { m := map[string]interface{}{ "Status": state.Status, "Start Time": state.StartTime.Local().Format(time.RFC1123), @@ -147,8 +197,8 @@ func printItemTable(in *sessions.Session) string { } var workerInfoMaps []map[string]interface{} - if len(in.WorkerInfo) > 0 { - for _, wi := range in.WorkerInfo { + if len(item.WorkerInfo) > 0 { + for _, wi := range item.WorkerInfo { m := map[string]interface{}{ "Address": wi.Address, } @@ -163,20 +213,25 @@ func printItemTable(in *sessions.Session) string { "", "Session information:", base.WrapMap(2, maxLength+2, nonAttributeMap), - "", - " Scope:", - base.ScopeInfoForOutput(in.Scope, maxLength), } - if len(in.AuthorizedActions) > 0 { + if item.Scope != nil { ret = append(ret, "", - " Authorized Actions:", - base.WrapSlice(4, in.AuthorizedActions), + " Scope:", + base.ScopeInfoForOutput(item.Scope, maxLength), ) } - if len(in.States) > 0 { + if len(item.AuthorizedActions) > 0 { + ret = append(ret, + "", + " Authorized Actions:", + base.WrapSlice(4, item.AuthorizedActions), + ) + } + + if len(item.States) > 0 { ret = append(ret, "", " States:", @@ -189,7 +244,7 @@ func printItemTable(in *sessions.Session) string { } } - if len(in.WorkerInfo) > 0 { + if len(item.WorkerInfo) > 0 { ret = append(ret, fmt.Sprintf(" Worker Info: %s", ""), ) diff --git a/internal/cmd/commands/sessionscmd/sessions.gen.go b/internal/cmd/commands/sessionscmd/sessions.gen.go index a0fe32fa6..bfd254d6e 100644 --- a/internal/cmd/commands/sessionscmd/sessions.gen.go +++ b/internal/cmd/commands/sessionscmd/sessions.gen.go @@ -225,8 +225,7 @@ func (c *Command) Run(args []string) int { switch base.Format(c.UI) { case "table": - item := result.GetItem().(*sessions.Session) - c.UI.Output(printItemTable(item)) + c.UI.Output(printItemTable(result)) case "json": if ok := c.PrintJsonItem(result); !ok { diff --git a/internal/cmd/commands/targetscmd/funcs.go b/internal/cmd/commands/targetscmd/funcs.go index 0813f7a57..ac69499ea 100644 --- a/internal/cmd/commands/targetscmd/funcs.go +++ b/internal/cmd/commands/targetscmd/funcs.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/boundary/api" "github.com/hashicorp/boundary/api/scopes" "github.com/hashicorp/boundary/api/targets" + "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/cmd/base" "github.com/hashicorp/boundary/sdk/strutil" "github.com/mitchellh/go-wordwrap" @@ -286,40 +287,48 @@ func (c *Command) printListTable(items []*targets.Target) string { "", "Target information:", } - for i, m := range items { + for i, item := range items { if i > 0 { output = append(output, "") } - if true { + if item.Id != "" { output = append(output, - fmt.Sprintf(" ID: %s", m.Id), + fmt.Sprintf(" ID: %s", item.Id), + ) + } else { + output = append(output, + fmt.Sprintf(" ID: %s", "(not available)"), ) } - if c.FlagRecursive { + if c.FlagRecursive && item.ScopeId != "" { output = append(output, - fmt.Sprintf(" Scope ID: %s", m.Scope.Id), + fmt.Sprintf(" Scope ID: %s", item.ScopeId), ) } - if true { + if item.Version > 0 { output = append(output, - fmt.Sprintf(" Version: %d", m.Version), - fmt.Sprintf(" Type: %s", m.Type), + fmt.Sprintf(" Version: %d", item.Version), ) } - if m.Name != "" { + if item.Type != "" { output = append(output, - fmt.Sprintf(" Name: %s", m.Name), + fmt.Sprintf(" Type: %s", item.Type), ) } - if m.Description != "" { + if item.Name != "" { output = append(output, - fmt.Sprintf(" Description: %s", m.Description), + fmt.Sprintf(" Name: %s", item.Name), ) } - if len(m.AuthorizedActions) > 0 { + if item.Description != "" { + output = append(output, + fmt.Sprintf(" Description: %s", item.Description), + ) + } + if len(item.AuthorizedActions) > 0 { output = append(output, " Authorized Actions:", - base.WrapSlice(6, m.AuthorizedActions), + base.WrapSlice(6, item.AuthorizedActions), ) } } @@ -327,17 +336,24 @@ func (c *Command) printListTable(items []*targets.Target) string { return base.WrapForHelpText(output) } -func printItemTable(item *targets.Target) string { - nonAttributeMap := map[string]interface{}{ - "ID": item.Id, - "Version": item.Version, - "Type": item.Type, - "Created Time": item.CreatedTime.Local().Format(time.RFC1123), - "Updated Time": item.UpdatedTime.Local().Format(time.RFC1123), - "Session Connection Limit": item.SessionConnectionLimit, - "Session Max Seconds": item.SessionMaxSeconds, +func printItemTable(result api.GenericResult) string { + item := result.GetItem().(*targets.Target) + nonAttributeMap := map[string]interface{}{} + if item.Id != "" { + nonAttributeMap["ID"] = item.Id + } + if item.Version != 0 { + nonAttributeMap["Version"] = item.Version + } + if item.Type != "" { + nonAttributeMap["Type"] = item.Type + } + if !item.CreatedTime.IsZero() { + nonAttributeMap["Created Time"] = item.CreatedTime.Local().Format(time.RFC1123) + } + if !item.UpdatedTime.IsZero() { + nonAttributeMap["Updated Time"] = item.UpdatedTime.Local().Format(time.RFC1123) } - if item.Name != "" { nonAttributeMap["Name"] = item.Name } @@ -347,6 +363,14 @@ func printItemTable(item *targets.Target) string { if item.WorkerFilter != "" { nonAttributeMap["Worker Filter"] = item.WorkerFilter } + if result.GetResponse() != nil && result.GetResponse().Map != nil { + if result.GetResponse().Map[globals.SessionConnectionLimitField] != nil { + nonAttributeMap["Session Connection Limit"] = item.SessionConnectionLimit + } + if result.GetResponse().Map[globals.SessionMaxSecondsField] != nil { + nonAttributeMap["Session Max Seconds"] = item.SessionMaxSeconds + } + } maxLength := base.MaxAttributesLength(nonAttributeMap, item.Attributes, keySubstMap) @@ -368,9 +392,14 @@ func printItemTable(item *targets.Target) string { "", "Target information:", base.WrapMap(2, maxLength+2, nonAttributeMap), - "", - " Scope:", - base.ScopeInfoForOutput(item.Scope, maxLength), + } + + if item.Scope != nil { + ret = append(ret, + "", + " Scope:", + base.ScopeInfoForOutput(item.Scope, maxLength), + ) } if len(item.AuthorizedActions) > 0 { @@ -459,7 +488,7 @@ var keySubstMap = map[string]string{ } func exampleOutput() string { - in := &targets.Target{ + item := &targets.Target{ Id: "ttcp_1234567890", ScopeId: "global", Scope: &scopes.ScopeInfo{ @@ -486,5 +515,7 @@ func exampleOutput() string { "default_port": 22, }, } - return printItemTable(in) + target := new(targets.TargetReadResult) + target.Item = item + return printItemTable(target) } diff --git a/internal/cmd/commands/targetscmd/targets.gen.go b/internal/cmd/commands/targetscmd/targets.gen.go index f4fc9a1ed..a8ac4ebe7 100644 --- a/internal/cmd/commands/targetscmd/targets.gen.go +++ b/internal/cmd/commands/targetscmd/targets.gen.go @@ -289,8 +289,7 @@ func (c *Command) Run(args []string) int { switch base.Format(c.UI) { case "table": - item := result.GetItem().(*targets.Target) - c.UI.Output(printItemTable(item)) + c.UI.Output(printItemTable(result)) case "json": if ok := c.PrintJsonItem(result); !ok { diff --git a/internal/cmd/commands/targetscmd/tcp_targets.gen.go b/internal/cmd/commands/targetscmd/tcp_targets.gen.go index aa0ebf382..aaf06169e 100644 --- a/internal/cmd/commands/targetscmd/tcp_targets.gen.go +++ b/internal/cmd/commands/targetscmd/tcp_targets.gen.go @@ -221,8 +221,7 @@ func (c *TcpCommand) Run(args []string) int { switch base.Format(c.UI) { case "table": - item := result.GetItem().(*targets.Target) - c.UI.Output(printItemTable(item)) + c.UI.Output(printItemTable(result)) case "json": if ok := c.PrintJsonItem(result); !ok { diff --git a/internal/cmd/commands/userscmd/funcs.go b/internal/cmd/commands/userscmd/funcs.go index fd1578811..ff9d6f671 100644 --- a/internal/cmd/commands/userscmd/funcs.go +++ b/internal/cmd/commands/userscmd/funcs.go @@ -150,60 +150,64 @@ func (c *Command) printListTable(items []*users.User) string { "User information:", } - for i, u := range items { + for i, item := range items { if i > 0 { output = append(output, "") } - if true { + if item.Id != "" { output = append(output, - fmt.Sprintf(" ID: %s", u.Id), + fmt.Sprintf(" ID: %s", item.Id), + ) + } else { + output = append(output, + fmt.Sprintf(" ID: %s", "(not available)"), ) } - if c.FlagRecursive { + if c.FlagRecursive && item.ScopeId != "" { output = append(output, - fmt.Sprintf(" Scope ID: %s", u.Scope.Id), + fmt.Sprintf(" Scope ID: %s", item.ScopeId), ) } - if true { + if item.Version > 0 { output = append(output, - fmt.Sprintf(" Version: %d", u.Version), + fmt.Sprintf(" Version: %d", item.Version), ) } - if u.Name != "" { + if item.Name != "" { output = append(output, - fmt.Sprintf(" Name: %s", u.Name), + fmt.Sprintf(" Name: %s", item.Name), ) } - if u.Description != "" { + if item.Description != "" { output = append(output, - fmt.Sprintf(" Description: %s", u.Description), + fmt.Sprintf(" Description: %s", item.Description), ) } - if u.PrimaryAccountId != "" { + if item.PrimaryAccountId != "" { output = append(output, - fmt.Sprintf(" Primary Account ID: %s", u.PrimaryAccountId), + fmt.Sprintf(" Primary Account ID: %s", item.PrimaryAccountId), ) } - if u.LoginName != "" { + if item.LoginName != "" { output = append(output, - fmt.Sprintf(" Login Name: %s", u.LoginName), + fmt.Sprintf(" Login Name: %s", item.LoginName), ) } - if u.FullName != "" { + if item.FullName != "" { output = append(output, - fmt.Sprintf(" Full Name: %s", u.FullName), + fmt.Sprintf(" Full Name: %s", item.FullName), ) } - if u.Email != "" { + if item.Email != "" { output = append(output, - fmt.Sprintf(" Email: %s", u.Email), + fmt.Sprintf(" Email: %s", item.Email), ) } - if len(u.AuthorizedActions) > 0 { + if len(item.AuthorizedActions) > 0 { output = append(output, " Authorized Actions:", - base.WrapSlice(6, u.AuthorizedActions), + base.WrapSlice(6, item.AuthorizedActions), ) } } @@ -211,38 +215,45 @@ func (c *Command) printListTable(items []*users.User) string { return base.WrapForHelpText(output) } -func printItemTable(in *users.User) string { - nonAttributeMap := map[string]interface{}{ - "ID": in.Id, - "Version": in.Version, - "Created Time": in.CreatedTime.Local().Format(time.RFC1123), - "Updated Time": in.UpdatedTime.Local().Format(time.RFC1123), +func printItemTable(result api.GenericResult) string { + item := result.GetItem().(*users.User) + nonAttributeMap := map[string]interface{}{} + if item.Id != "" { + nonAttributeMap["ID"] = item.Id } - - if in.Name != "" { - nonAttributeMap["Name"] = in.Name + if item.Version != 0 { + nonAttributeMap["Version"] = item.Version } - if in.Description != "" { - nonAttributeMap["Description"] = in.Description + if !item.CreatedTime.IsZero() { + nonAttributeMap["Created Time"] = item.CreatedTime.Local().Format(time.RFC1123) } - if in.PrimaryAccountId != "" { - nonAttributeMap["PrimaryAccountId"] = in.PrimaryAccountId + if !item.UpdatedTime.IsZero() { + nonAttributeMap["Updated Time"] = item.UpdatedTime.Local().Format(time.RFC1123) } - if in.LoginName != "" { - nonAttributeMap["LoginName"] = in.LoginName + if item.Name != "" { + nonAttributeMap["Name"] = item.Name } - if in.FullName != "" { - nonAttributeMap["FullName"] = in.FullName + if item.Description != "" { + nonAttributeMap["Description"] = item.Description } - if in.Email != "" { - nonAttributeMap["Email"] = in.Email + if item.PrimaryAccountId != "" { + nonAttributeMap["Primary Account Id"] = item.PrimaryAccountId + } + if item.LoginName != "" { + nonAttributeMap["Login Name"] = item.LoginName + } + if item.FullName != "" { + nonAttributeMap["Full Name"] = item.FullName + } + if item.Email != "" { + nonAttributeMap["Email"] = item.Email } maxLength := base.MaxAttributesLength(nonAttributeMap, nil, nil) var userMaps []map[string]interface{} - if len(in.Accounts) > 0 { - for _, account := range in.Accounts { + if len(item.Accounts) > 0 { + for _, account := range item.Accounts { a := map[string]interface{}{ "ID": account.Id, "Scope ID": account.ScopeId, @@ -258,20 +269,25 @@ func printItemTable(in *users.User) string { "", "User information:", base.WrapMap(2, maxLength+2, nonAttributeMap), - "", - " Scope:", - base.ScopeInfoForOutput(in.Scope, maxLength), } - if len(in.AuthorizedActions) > 0 { + if item.Scope != nil { ret = append(ret, "", - " Authorized Actions:", - base.WrapSlice(4, in.AuthorizedActions), + " Scope:", + base.ScopeInfoForOutput(item.Scope, maxLength), ) } - if len(in.Accounts) > 0 { + if len(item.AuthorizedActions) > 0 { + ret = append(ret, + "", + " Authorized Actions:", + base.WrapSlice(4, item.AuthorizedActions), + ) + } + + if len(item.Accounts) > 0 { ret = append(ret, "", " Accounts:", diff --git a/internal/cmd/commands/userscmd/users.gen.go b/internal/cmd/commands/userscmd/users.gen.go index 5b93677d3..ae7fff215 100644 --- a/internal/cmd/commands/userscmd/users.gen.go +++ b/internal/cmd/commands/userscmd/users.gen.go @@ -316,8 +316,7 @@ func (c *Command) Run(args []string) int { switch base.Format(c.UI) { case "table": - item := result.GetItem().(*users.User) - c.UI.Output(printItemTable(item)) + c.UI.Output(printItemTable(result)) case "json": if ok := c.PrintJsonItem(result); !ok { diff --git a/internal/cmd/gencli/templates.go b/internal/cmd/gencli/templates.go index c72e6ac22..d4bb519b3 100644 --- a/internal/cmd/gencli/templates.go +++ b/internal/cmd/gencli/templates.go @@ -413,8 +413,7 @@ func (c *{{ camelCase .SubActionPrefix }}Command) Run(args []string) int { switch base.Format(c.UI) { case "table": - item := result.GetItem().(*{{ .Pkg }}.{{ camelCase .ResourceType }}) - c.UI.Output(printItemTable(item)) + c.UI.Output(printItemTable(result)) case "json": if ok := c.PrintJsonItem(result); !ok { diff --git a/internal/perms/acl.go b/internal/perms/acl.go index 1f7bb5cac..98243f34e 100644 --- a/internal/perms/acl.go +++ b/internal/perms/acl.go @@ -20,6 +20,8 @@ import ( "github.com/hashicorp/boundary/internal/types/resource" ) +const AnonymousUserId = "u_anon" + // ACL provides an entry point into the permissions engine for determining if an // action is allowed on a resource based on a principal's (user or group) grants. type ACL struct { @@ -32,6 +34,7 @@ type ACL struct { type ACLResults struct { AuthenticationFinished bool Authorized bool + OutputFields OutputFieldsMap // This is included but unexported for testing/debugging scopeMap map[string][]Grant @@ -80,7 +83,17 @@ func (a ACL) Allowed(r Resource, aType action.Type) (results ACLResults) { } // Now, go through and check the cases indicated above for _, grant := range grants { + var outputFieldsOnly bool switch { + case len(grant.actions) == 0: + // Continue with the next grant, unless we have output fields + // specified in which case we continue to be able to apply the + // output fields depending on ID and type. + if len(grant.OutputFields) > 0 { + outputFieldsOnly = true + } else { + continue + } case grant.actions[aType]: // We have this action case grant.actions[parentAction]: @@ -94,8 +107,18 @@ func (a ACL) Allowed(r Resource, aType action.Type) (results ACLResults) { // with the next grant continue } + + // We step through all grants, to fetch the full list of output fields. + // However, we shortcut if we find *. + // + // If the action was not found above but we did find output fields in + // patterns that match, we do not authorize the request, but we do build + // up the output fields patterns. + var found bool switch { - // id=;actions= where ID cannot be a wildcard + // id=;actions= where ID cannot be a wildcard; or + // id=;output_fields= where fields cannot be a + // wildcard. case grant.id == r.Id && grant.id != "" && grant.id != "*" && @@ -103,12 +126,12 @@ func (a ACL) Allowed(r Resource, aType action.Type) (results ACLResults) { aType != action.List && aType != action.Create: - results.Authorized = true - return + found = true - // type=;actions= when action is list(:self) or - // create. Must be a top level collection, otherwise must be one of the - // two formats specified below. + // type=;actions= when action is list or create. + // Must be a top level collection, otherwise must be one of the two + // formats specified below. Or, + // type=resource.type;output_fields= and no action. case grant.id == "" && r.Id == "" && grant.typ == r.Type && @@ -117,29 +140,37 @@ func (a ACL) Allowed(r Resource, aType action.Type) (results ACLResults) { (aType == action.List || aType == action.Create): - results.Authorized = true - return + found = true // id=*;type=;actions= where type cannot be - // unknown but can be a wildcard to allow any resource at all + // unknown but can be a wildcard to allow any resource at all; or + // id=*;type=;output_fields= with no action. case grant.id == "*" && grant.typ != resource.Unknown && (grant.typ == r.Type || grant.typ == resource.All): - results.Authorized = true - return + found = true // id=;type=;actions= where type can be a - // wildcard and this this is operating on a non-top-level type + // wildcard and this this is operating on a non-top-level type. Same for + // output fields only. case grant.id != "" && grant.id == r.Pin && grant.typ != resource.Unknown && (grant.typ == r.Type || grant.typ == resource.All) && !topLevelType(r.Type): - results.Authorized = true - return + found = true + } + + if found { + if !outputFieldsOnly { + results.Authorized = true + } + if results.OutputFields = results.OutputFields.AddFields(grant.OutputFields.Fields()); results.OutputFields.HasAll() && results.Authorized { + return + } } } return diff --git a/internal/perms/acl_test.go b/internal/perms/acl_test.go index 2a1d96562..a09e45590 100644 --- a/internal/perms/acl_test.go +++ b/internal/perms/acl_test.go @@ -17,8 +17,9 @@ func Test_ACLAllowed(t *testing.T) { grants []string } type actionAuthorized struct { - action action.Type - authorized bool + action action.Type + authorized bool + outputFields []string } type input struct { name string @@ -45,8 +46,9 @@ func Test_ACLAllowed(t *testing.T) { scope: "o_b", grants: []string{ "id=*;type=host-set;actions=list,create", - "id=mypin;type=host;actions=*", + "id=mypin;type=host;actions=*;output_fields=name,description", "id=*;type=*;actions=authenticate", + "id=*;type=*;output_fields=id", }, }, { @@ -61,6 +63,7 @@ func Test_ACLAllowed(t *testing.T) { grants: []string{ "id=*;type=*;actions=create,update", "id=*;type=session;actions=*", + "id=*;type=account;actions=update;output_fields=id,version", }, }, } @@ -118,9 +121,9 @@ func Test_ACLAllowed(t *testing.T) { resource: Resource{ScopeId: "o_b", Pin: "mypin", Type: resource.Host}, scopeGrants: commonGrants, actionsAuthorized: []actionAuthorized{ - {action: action.Read, authorized: true}, - {action: action.Update, authorized: true}, - {action: action.Delete, authorized: true}, + {action: action.Read, authorized: true, outputFields: []string{"description", "id", "name"}}, + {action: action.Update, authorized: true, outputFields: []string{"description", "id", "name"}}, + {action: action.Delete, authorized: true, outputFields: []string{"description", "id", "name"}}, }, }, { @@ -128,9 +131,9 @@ func Test_ACLAllowed(t *testing.T) { resource: Resource{ScopeId: "o_b", Pin: "notmypin", Type: resource.Host}, scopeGrants: commonGrants, actionsAuthorized: []actionAuthorized{ - {action: action.Read}, - {action: action.Update}, - {action: action.Delete}, + {action: action.Read, outputFields: []string{"id"}}, + {action: action.Update, outputFields: []string{"id"}}, + {action: action.Delete, outputFields: []string{"id"}}, }, }, { @@ -138,9 +141,9 @@ func Test_ACLAllowed(t *testing.T) { resource: Resource{ScopeId: "o_b", Id: "myhost", Type: resource.HostSet}, scopeGrants: commonGrants, actionsAuthorized: []actionAuthorized{ - {action: action.List, authorized: true}, - {action: action.Create, authorized: true}, - {action: action.AddHosts}, + {action: action.List, authorized: true, outputFields: []string{"id"}}, + {action: action.Create, authorized: true, outputFields: []string{"id"}}, + {action: action.AddHosts, outputFields: []string{"id"}}, }, }, { @@ -148,9 +151,9 @@ func Test_ACLAllowed(t *testing.T) { resource: Resource{ScopeId: "o_b", Id: "id_g"}, scopeGrants: commonGrants, actionsAuthorized: []actionAuthorized{ - {action: action.Read}, - {action: action.Update}, - {action: action.Delete}, + {action: action.Read, outputFields: []string{"id"}}, + {action: action.Update, outputFields: []string{"id"}}, + {action: action.Delete, outputFields: []string{"id"}}, }, }, { @@ -197,9 +200,9 @@ func Test_ACLAllowed(t *testing.T) { resource: Resource{ScopeId: "o_b", Type: resource.AuthMethod}, scopeGrants: commonGrants, actionsAuthorized: []actionAuthorized{ - {action: action.List}, - {action: action.Authenticate, authorized: true}, - {action: action.Delete}, + {action: action.List, outputFields: []string{"id"}}, + {action: action.Authenticate, authorized: true, outputFields: []string{"id"}}, + {action: action.Delete, outputFields: []string{"id"}}, }, }, { @@ -235,7 +238,7 @@ func Test_ACLAllowed(t *testing.T) { accountId: "apw_1234567890", }, { - name: "good templated user id", + name: "good templated account id", resource: Resource{ScopeId: "o_c", Id: "apw_1234567890"}, scopeGrants: commonGrants, actionsAuthorized: []actionAuthorized{ @@ -250,7 +253,7 @@ func Test_ACLAllowed(t *testing.T) { scopeGrants: commonGrants, actionsAuthorized: []actionAuthorized{ {action: action.Create, authorized: true}, - {action: action.Update, authorized: true}, + {action: action.Update, authorized: true, outputFields: []string{"id", "version"}}, }, userId: "u_abcd1234", }, @@ -302,7 +305,9 @@ func Test_ACLAllowed(t *testing.T) { } acl := NewACL(grants...) for _, aa := range test.actionsAuthorized { - assert.True(t, acl.Allowed(test.resource, aa.action).Authorized == aa.authorized, "action: %s, acl authorized: %t, test action authorized: %t", aa.action, acl.Allowed(test.resource, aa.action).Authorized, aa.authorized) + result := acl.Allowed(test.resource, aa.action) + assert.True(t, result.Authorized == aa.authorized, "action: %s, acl authorized: %t, test action authorized: %t", aa.action, acl.Allowed(test.resource, aa.action).Authorized, aa.authorized) + assert.ElementsMatch(t, result.OutputFields.Fields(), aa.outputFields) } }) } diff --git a/internal/perms/grants.go b/internal/perms/grants.go index 1e3b78ee6..c9dc66b4c 100644 --- a/internal/perms/grants.go +++ b/internal/perms/grants.go @@ -43,6 +43,9 @@ type Grant struct { // The set of actions being granted actions map[action.Type]bool + // The set of output fields granted + OutputFields OutputFieldsMap + // This is used as a temporary staging area before validating permissions to // allow the same validation code across grant string formats actionsBeingParsed []string @@ -85,6 +88,12 @@ func (g Grant) clone() *Grant { ret.actions[action] = true } } + if g.OutputFields != nil { + ret.OutputFields = make(OutputFieldsMap, len(g.OutputFields)) + for k, v := range g.OutputFields { + ret.OutputFields[k] = v + } + } return ret } @@ -109,6 +118,10 @@ func (g Grant) CanonicalString() string { builder = append(builder, fmt.Sprintf("actions=%s", strings.Join(actions, ","))) } + if len(g.OutputFields) > 0 { + builder = append(builder, fmt.Sprintf("output_fields=%s", strings.Join(g.OutputFields.Fields(), ","))) + } + return strings.Join(builder, ";") } @@ -130,6 +143,9 @@ func (g Grant) MarshalJSON() ([]byte, error) { sort.Strings(actions) res["actions"] = actions } + if len(g.OutputFields) > 0 { + res["output_fields"] = g.OutputFields.Fields() + } b, err := json.Marshal(res) if err != nil { return nil, errors.Wrap(err, op, errors.WithCode(errors.Encode)) @@ -183,6 +199,26 @@ func (g *Grant) unmarshalJSON(data []byte) error { } } } + if rawOutputFields, ok := raw["output_fields"]; ok { + interfaceOutputFields, ok := rawOutputFields.([]interface{}) + if !ok { + return errors.New(errors.InvalidParameter, op, fmt.Sprintf("unable to interpret %q as array", "output_fields")) + } + // We do the make here because we detect later if the field was set but + // no values given + g.OutputFields = make(OutputFieldsMap, len(interfaceOutputFields)) + if len(interfaceOutputFields) > 0 { + for _, v := range interfaceOutputFields { + field, ok := v.(string) + switch { + case !ok: + return errors.New(errors.InvalidParameter, op, fmt.Sprintf("unable to interpret %v in output_fields array as string", v)) + default: + g.OutputFields[field] = true + } + } + } + } return nil } @@ -224,6 +260,9 @@ func (g *Grant) unmarshalText(grantString string) error { g.actionsBeingParsed = append(g.actionsBeingParsed, strings.ToLower(action)) } } + + case "output_fields": + g.OutputFields = g.OutputFields.AddFields(strings.Split(kv[1], ",")) } } @@ -348,26 +387,33 @@ func Parse(scopeId, grantString string, opt ...Option) (Grant, error) { } } } - // Create a dummy resource and pass it through Allowed and ensure that - // we get allowed. - acl := NewACL(grant) - r := Resource{ - ScopeId: scopeId, - Id: grant.id, - Type: grant.typ, + // Set but empty output fields... + if grant.OutputFields != nil && len(grant.OutputFields) == 0 { + return Grant{}, errors.New(errors.InvalidParameter, op, "parsed grant string has output_fields set but empty") } - if !topLevelType(grant.typ) { - r.Pin = grant.id - } - var allowed bool - for k := range grant.actions { - results := acl.Allowed(r, k) - if results.Authorized { - allowed = true + // This might be zero if output fields is populated + if len(grant.actions) > 0 { + // Create a dummy resource and pass it through Allowed and ensure that + // we get allowed. + acl := NewACL(grant) + r := Resource{ + ScopeId: scopeId, + Id: grant.id, + Type: grant.typ, + } + if !topLevelType(grant.typ) { + r.Pin = grant.id + } + var allowed bool + for k := range grant.actions { + results := acl.Allowed(r, k) + if results.Authorized { + allowed = true + } + } + if !allowed { + return Grant{}, errors.New(errors.InvalidParameter, op, "parsed grant string would not result in any action being authorized") } - } - if !allowed { - return Grant{}, errors.New(errors.InvalidParameter, op, "parsed grant string would not result in any action being authorized") } } @@ -399,6 +445,12 @@ func (g Grant) validateType() error { func (g *Grant) parseAndValidateActions() error { const op = "perms.(Grant).parseAndValidateActions" if len(g.actionsBeingParsed) == 0 { + g.actionsBeingParsed = nil + // If there are no actions it's fine if the grant is just used to + // specify output fields + if len(g.OutputFields) > 0 { + return nil + } return errors.New(errors.InvalidParameter, op, "missing actions") } diff --git a/internal/perms/grants_test.go b/internal/perms/grants_test.go index 644ef080b..91573188a 100644 --- a/internal/perms/grants_test.go +++ b/internal/perms/grants_test.go @@ -32,6 +32,19 @@ func Test_ActionParsingValidation(t *testing.T) { }, errResult: "perms.(Grant).parseAndValidateActions: empty action found: parameter violation: error #100", }, + { + name: "empty action with output fields", + input: Grant{ + OutputFields: OutputFieldsMap{ + "id": true, + }, + }, + result: Grant{ + OutputFields: OutputFieldsMap{ + "id": true, + }, + }, + }, { name: "unknown action", input: Grant{ @@ -157,6 +170,23 @@ func Test_MarshalingAndCloning(t *testing.T) { jsonOutput: `{"id":"baz","type":"group"}`, canonicalString: `id=baz;type=group`, }, + { + name: "output fields", + input: Grant{ + id: "baz", + scope: Scope{ + Type: scope.Project, + }, + typ: resource.Group, + OutputFields: OutputFieldsMap{ + "name": true, + "version": true, + "id": true, + }, + }, + jsonOutput: `{"id":"baz","output_fields":["id","name","version"],"type":"group"}`, + canonicalString: `id=baz;type=group;output_fields=id,name,version`, + }, { name: "everything", input: Grant{ @@ -170,9 +200,14 @@ func Test_MarshalingAndCloning(t *testing.T) { action.Read: true, }, actionsBeingParsed: []string{"create", "read"}, + OutputFields: OutputFieldsMap{ + "name": true, + "version": true, + "id": true, + }, }, - jsonOutput: `{"actions":["create","read"],"id":"baz","type":"group"}`, - canonicalString: `id=baz;type=group;actions=create,read`, + jsonOutput: `{"actions":["create","read"],"id":"baz","output_fields":["id","name","version"],"type":"group"}`, + canonicalString: `id=baz;type=group;actions=create,read;output_fields=id,name,version`, }, } @@ -249,6 +284,25 @@ func Test_Unmarshaling(t *testing.T) { textInput: `type=host-catalog=id`, textErr: `perms.(Grant).unmarshalText: segment "type=host-catalog=id" not formatted correctly, wrong number of equal signs: parameter violation: error #100`, }, + { + name: "good output fields", + expected: Grant{ + OutputFields: OutputFieldsMap{ + "name": true, + "version": true, + "id": true, + }, + }, + jsonInput: `{"output_fields":["id","name","version"]}`, + textInput: `output_fields=id,version,name`, + }, + { + name: "bad output fields", + jsonInput: `{"output_fields":true}`, + jsonErr: `perms.(Grant).unmarshalJSON: unable to interpret "output_fields" as array: parameter violation: error #100`, + textInput: `output_fields=id=version,name`, + textErr: `perms.(Grant).unmarshalText: segment "output_fields=id=version,name" not formatted correctly, wrong number of equal signs: parameter violation: error #100`, + }, { name: "good actions", expected: Grant{ @@ -386,6 +440,16 @@ func Test_Parse(t *testing.T) { input: "actions=create", err: `perms.Parse: parsed grant string contains no id or type: parameter violation: error #100`, }, + { + name: "empty output fields", + input: "id=*;type=*;actions=read,list;output_fields=", + err: `perms.Parse: unable to parse grant string: perms.(Grant).unmarshalText: segment "output_fields=" not formatted correctly, missing value: parameter violation: error #100`, + }, + { + name: "empty output fields json", + input: `{"id": "*", "type": "*", "actions": ["read", "list"], "output_fields": []}`, + err: "perms.Parse: parsed grant string has output_fields set but empty: parameter violation: error #100", + }, { name: "wildcard id and type and actions with list", input: "id=*;type=*;actions=read,list", @@ -431,6 +495,43 @@ func Test_Parse(t *testing.T) { }, }, }, + { + name: "good json output fields", + input: `{"id":"foobar","actions":["read"],"output_fields":["version","id","name"]}`, + expected: Grant{ + scope: Scope{ + Id: "o_scope", + Type: scope.Org, + }, + id: "foobar", + typ: resource.Unknown, + actions: map[action.Type]bool{ + action.Read: true, + }, + OutputFields: OutputFieldsMap{ + "version": true, + "id": true, + "name": true, + }, + }, + }, + { + name: "good json output fields no action", + input: `{"id":"foobar","output_fields":["version","id","name"]}`, + expected: Grant{ + scope: Scope{ + Id: "o_scope", + Type: scope.Org, + }, + id: "foobar", + typ: resource.Unknown, + OutputFields: OutputFieldsMap{ + "version": true, + "id": true, + "name": true, + }, + }, + }, { name: "good text type", input: `type=host-catalog;actions=create`, @@ -460,6 +561,26 @@ func Test_Parse(t *testing.T) { }, }, }, + { + name: "good output fields", + input: `id=foobar;actions=read;output_fields=version,id,name`, + expected: Grant{ + scope: Scope{ + Id: "o_scope", + Type: scope.Org, + }, + id: "foobar", + typ: resource.Unknown, + actions: map[action.Type]bool{ + action.Read: true, + }, + OutputFields: OutputFieldsMap{ + "version": true, + "id": true, + "name": true, + }, + }, + }, { name: "default project scope", input: `id=foobar;actions=read`, diff --git a/internal/perms/output_fields.go b/internal/perms/output_fields.go new file mode 100644 index 000000000..31871cb44 --- /dev/null +++ b/internal/perms/output_fields.go @@ -0,0 +1,96 @@ +package perms + +import ( + "sort" + + "github.com/hashicorp/boundary/globals" +) + +// OutputFieldsMap is used to store information about allowed output fields in +// grants +type OutputFieldsMap map[string]bool + +// AddFields adds the given fields and returns the map. +func (o OutputFieldsMap) AddFields(input []string) (ret OutputFieldsMap) { + switch { + case len(input) == 0: + if o == nil { + return o + } + return o + case o == nil: + ret = make(OutputFieldsMap, len(input)) + case len(o) == 1 && o["*"]: + return o + default: + ret = o + } + for _, k := range input { + if k == "*" { + ret = OutputFieldsMap{k: true} + return + } + ret[k] = true + } + return +} + +func (o OutputFieldsMap) HasAll() bool { + return o["*"] +} + +// Fields returns an alphabetical string slice of the fields in the map +func (o OutputFieldsMap) Fields() (ret []string) { + if o == nil { + return nil + } + if len(o) == 0 { + return []string{} + } + ret = make([]string, 0, len(o)) + for f := range o { + ret = append(ret, f) + } + sort.Strings(ret) + return +} + +// SelfOrDefaults returns either the fields map itself or the defaults for the +// given user +func (o OutputFieldsMap) SelfOrDefaults(userId string) OutputFieldsMap { + switch { + case o != nil: + // We have values set (which may be empty) so use those + return o + case userId == "": + // This shouldn't happen, and if it does, don't allow anything to be + // output + return OutputFieldsMap{} + case userId == AnonymousUserId: + return OutputFieldsMap{ + globals.IdField: true, + globals.ScopeField: true, + globals.ScopeIdField: true, + globals.NameField: true, + globals.DescriptionField: true, + globals.TypeField: true, + globals.IsPrimaryField: true, + globals.AuthorizedActionsField: true, + globals.AuthorizedCollectionActionsField: true, + } + default: + return OutputFieldsMap{ + "*": true, + } + } +} + +// Has returns true if the value exists; that is, it is directly in the map, or +// the map contains * +func (o OutputFieldsMap) Has(in string) bool { + // Handle nil or empty case + if len(o) == 0 { + return false + } + return o.HasAll() || o[in] +} diff --git a/internal/perms/output_fields_test.go b/internal/perms/output_fields_test.go new file mode 100644 index 000000000..ed7d9297e --- /dev/null +++ b/internal/perms/output_fields_test.go @@ -0,0 +1,267 @@ +package perms + +import ( + "testing" + + "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/types/action" + "github.com/hashicorp/boundary/internal/types/resource" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_OutputFields(t *testing.T) { + t.Parallel() + + type input struct { + name string + fields []string + startMap OutputFieldsMap + resMap OutputFieldsMap + resStar bool + } + tests := []input{ + { + name: "nil map, add nil", + }, + { + name: "nil map, add fields", + fields: []string{"id", "version"}, + resMap: OutputFieldsMap{"id": true, "version": true}, + }, + { + name: "existing map, add nil", + startMap: OutputFieldsMap{"id": true, "version": true}, + resMap: OutputFieldsMap{"id": true, "version": true}, + }, + { + name: "existing with star, add nil", + startMap: OutputFieldsMap{"*": true}, + resMap: OutputFieldsMap{"*": true}, + resStar: true, + }, + { + name: "existing with star, add new", + fields: []string{"id", "version"}, + startMap: OutputFieldsMap{"*": true}, + resMap: OutputFieldsMap{"*": true}, + resStar: true, + }, + { + name: "existing without star, add new", + fields: []string{"id", "version"}, + startMap: OutputFieldsMap{"name": true}, + resMap: OutputFieldsMap{"id": true, "version": true, "name": true}, + }, + { + name: "existing without star, add star", + fields: []string{"id", "*"}, + startMap: OutputFieldsMap{"name": true}, + resMap: OutputFieldsMap{"*": true}, + resStar: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert := assert.New(t) + out := test.startMap.AddFields(test.fields) + assert.True(out.HasAll() == test.resStar) + assert.Equal(test.resMap, out) + }) + } +} + +func Test_ACLOutputFields(t *testing.T) { + t.Parallel() + + type input struct { + name string + grants []string + resource Resource + action action.Type + fields []string + authorized bool + } + tests := []input{ + { + name: "default", + resource: Resource{ScopeId: "o_myorg", Id: "bar", Type: resource.Role}, + action: action.Read, + grants: []string{"id=bar;actions=read,update"}, + authorized: true, + }, + { + name: "single value", + resource: Resource{ScopeId: "o_myorg", Id: "bar", Type: resource.Role}, + grants: []string{"id=bar;actions=read,update;output_fields=id"}, + action: action.Read, + fields: []string{"id"}, + authorized: true, + }, + { + name: "compound no overlap", + resource: Resource{ScopeId: "o_myorg", Id: "bar", Type: resource.Role}, + grants: []string{ + "id=bar;actions=read,update;output_fields=id", + "id=*;type=host-catalog;actions=read,update;output_fields=version", + }, + action: action.Read, + fields: []string{"id"}, + authorized: true, + }, + { + name: "compound", + resource: Resource{ScopeId: "o_myorg", Id: "bar", Type: resource.Role}, + grants: []string{ + "id=bar;actions=read,update;output_fields=id", + "id=*;type=role;output_fields=version", + }, + action: action.Read, + fields: []string{"id", "version"}, + authorized: true, + }, + { + name: "wildcard with type", + resource: Resource{ScopeId: "o_myorg", Id: "bar", Type: resource.Role}, + grants: []string{ + "id=bar;actions=read,update;output_fields=read", + "id=*;type=role;output_fields=*", + }, + action: action.Read, + fields: []string{"*"}, + authorized: true, + }, + { + name: "wildcard with wildcard type", + resource: Resource{ScopeId: "o_myorg", Id: "bar", Type: resource.Role}, + grants: []string{ + "id=bar;actions=read,update;output_fields=read", + "id=*;type=*;output_fields=*", + }, + action: action.Read, + fields: []string{"*"}, + authorized: true, + }, + { + name: "subaction exact", + resource: Resource{ScopeId: "o_myorg", Id: "bar", Type: resource.Role}, + grants: []string{ + "id=bar;actions=read:self,update;output_fields=version", + }, + action: action.ReadSelf, + fields: []string{"version"}, + authorized: true, + }, + { + // If the action is a subaction, parent output fields will apply, in + // addition to subaction. This matches authorization. + name: "subaction parent action", + resource: Resource{ScopeId: "o_myorg", Id: "bar", Type: resource.Role}, + grants: []string{ + "id=bar;actions=read,update;output_fields=version", + "id=bar;actions=read:self;output_fields=id", + }, + action: action.ReadSelf, + fields: []string{"id", "version"}, + authorized: true, + }, + { + // The inverse isn't true. Similarly to authorization, if you have + // specific output fields on a self action, they don't apply to + // non-self actions. This is useful to allow more visibility to self + // actions and less in the general case. + name: "subaction child action", + resource: Resource{ScopeId: "o_myorg", Id: "bar", Type: resource.Role}, + grants: []string{ + "id=bar;actions=read:self,update;output_fields=version", + "id=bar;actions=read;output_fields=id", + }, + action: action.Read, + fields: []string{"id"}, + authorized: true, + }, + { + name: "initial grant unauthorized with star", + resource: Resource{ScopeId: "o_myorg", Id: "bar", Type: resource.Role}, + grants: []string{ + "id=bar;output_fields=*", + "id=bar;actions=delete;output_fields=id", + }, + action: action.Delete, + fields: []string{"*"}, + authorized: true, + }, + { + name: "unauthorized", + resource: Resource{ScopeId: "o_myorg", Id: "bar", Type: resource.Role}, + grants: []string{ + "id=bar;output_fields=name", + }, + action: action.Delete, + fields: []string{"name"}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var grants []Grant + for _, g := range test.grants { + grant, err := Parse("o_myorg", g) + require.NoError(t, err) + grants = append(grants, grant) + } + acl := NewACL(grants...) + results := acl.Allowed(test.resource, test.action) + assert.ElementsMatch(t, results.OutputFields.Fields(), test.fields) + assert.True(t, test.authorized == results.Authorized) + }) + } +} + +func Test_ACLSelfOrDefault(t *testing.T) { + t.Parallel() + + type input struct { + name string + input OutputFieldsMap + output OutputFieldsMap + userId string + } + tests := []input{ + { + name: "nil, no user ID", + output: OutputFieldsMap{}, + }, + { + name: "nil, non anon id", + output: OutputFieldsMap{"*": true}, + userId: "u_abc123", + }, + { + name: "nil, anon id", + output: OutputFieldsMap{ + globals.IdField: true, + globals.ScopeField: true, + globals.ScopeIdField: true, + globals.NameField: true, + globals.DescriptionField: true, + globals.TypeField: true, + globals.IsPrimaryField: true, + globals.AuthorizedActionsField: true, + globals.AuthorizedCollectionActionsField: true, + }, + userId: AnonymousUserId, + }, + { + name: "not nil", + input: OutputFieldsMap{"foo": true}, + output: OutputFieldsMap{"foo": true}, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.output, test.input.SelfOrDefaults(test.userId)) + }) + } +} diff --git a/internal/requests/options.go b/internal/requests/options.go new file mode 100644 index 000000000..4a81c7fe3 --- /dev/null +++ b/internal/requests/options.go @@ -0,0 +1,31 @@ +package requests + +func getOpts(opt ...Option) options { + opts := getDefaultOptions() + for _, o := range opt { + o(&opts) + } + return opts +} + +// Option - how Options are passed as arguments. Some of these are used for +// types within the handlers package, and some are for handlers to re-use across +// the various handler types. +type Option func(*options) + +// options = how options are represented +type options struct { + withUserId string +} + +func getDefaultOptions() options { + return options{} +} + +// WithUserId specifies a user ID with which to populate the returned +// RequestContext +func WithUserId(userId string) Option { + return func(o *options) { + o.withUserId = userId + } +} diff --git a/internal/requests/requests.go b/internal/requests/requests.go new file mode 100644 index 000000000..2c50a3aea --- /dev/null +++ b/internal/requests/requests.go @@ -0,0 +1,73 @@ +package requests + +import ( + "context" + + "github.com/hashicorp/boundary/internal/perms" +) + +// ContextRequestInforation is a type used solely for context keys -- see the +// variable below +type ContextRequestInformation struct{} + +// ContextRequestInformationKey is a value to keep linters from complaining +// about clashing identifiers +var ContextRequestInformationKey ContextRequestInformation + +// RequestContext is used to propagate request information. It can be updated at +// various points, e.g. UserId would be updated via the result of auth.Verify. +// +// It serves as a struct to gather information we learn about the request as it +// goes along. This is distinct from options handling; none of the fields here +// are "optional" and should all eventually be populated as the request moves +// through the system. +type RequestContext struct { + // Method is the method of the request + Method string + + // Path is the path of the request + Path string + + // UserId contains the final discovered user ID + UserId string + + // OutputFields is the set of fields authorized for output for the + // authorized action, if not the default + OutputFields perms.OutputFieldsMap +} + +// NewRequestContext returns a derived context with a new RequestContext value +// added in. +func NewRequestContext(parent context.Context, opt ...Option) context.Context { + opts := getOpts(opt...) + ret := &RequestContext{ + UserId: opts.withUserId, + } + return context.WithValue(parent, ContextRequestInformationKey, ret) +} + +// RequestContextFromCtx pulls out RequestContext and returns it and an +// indication it was found. If it's not found, nil will be returned and the bool +// will be false. +func RequestContextFromCtx(ctx context.Context) (*RequestContext, bool) { + reqCtxRaw := ctx.Value(ContextRequestInformationKey) + if reqCtxRaw == nil { + return nil, false + } + reqCtx, ok := reqCtxRaw.(*RequestContext) + if !ok { + return nil, false + } + return reqCtx, true +} + +// OutputFields returns output fields from the given context and calls +// SelfOrDefaults on it. If the context does not contain a RequestContext, +// this will return nil, false. +func OutputFields(ctx context.Context) (perms.OutputFieldsMap, bool) { + reqCtx, ok := RequestContextFromCtx(ctx) + if !ok { + return nil, false + } + return reqCtx.OutputFields.SelfOrDefaults(reqCtx.UserId), true +} diff --git a/internal/requests/requests_test.go b/internal/requests/requests_test.go new file mode 100644 index 000000000..3a200ff31 --- /dev/null +++ b/internal/requests/requests_test.go @@ -0,0 +1,47 @@ +package requests + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_RequestContextFromCtx(t *testing.T) { + t.Parallel() + + type input struct { + name string + input context.Context + output *RequestContext + } + tests := []input{ + { + name: "not added", + input: context.Background(), + }, + { + name: "added but empty", + input: NewRequestContext(context.Background()), + output: &RequestContext{}, + }, + { + name: "non-empty", + input: func() context.Context { + ctx := context.WithValue(context.Background(), ContextRequestInformationKey, &RequestContext{ + UserId: "u_foo", + }) + return ctx + }(), + output: &RequestContext{UserId: "u_foo"}, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + reqCtx, ok := RequestContextFromCtx(test.input) + require.Equal(t, test.output == nil, !ok) + assert.Equal(t, test.output, reqCtx) + }) + } +} diff --git a/internal/servers/controller/handler.go b/internal/servers/controller/handler.go index 653d80b82..af4e79ee2 100644 --- a/internal/servers/controller/handler.go +++ b/internal/servers/controller/handler.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/boundary/internal/auth" "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/gen/controller/api/services" + "github.com/hashicorp/boundary/internal/requests" "github.com/hashicorp/boundary/internal/servers/controller/handlers/accounts" "github.com/hashicorp/boundary/internal/servers/controller/handlers/authmethods" "github.com/hashicorp/boundary/internal/servers/controller/handlers/host_sets" @@ -228,6 +229,18 @@ func wrapHandlerWithCommonFuncs(h http.Handler, c *Controller, props HandlerProp requestInfo.PublicId, requestInfo.EncryptedToken, requestInfo.TokenFormat = auth.GetTokenFromRequest(c.logger, c.kms, r) ctx = auth.NewVerifierContext(ctx, c.logger, c.IamRepoFn, c.AuthTokenRepoFn, c.ServersRepoFn, c.kms, requestInfo) + // Add general request information to the context. The information from + // the auth verifier context is pretty specifically curated to + // authentication/authorization verification so this is more + // general-purpose. + // + // We could use requests.NewRequestContext but this saves an immediate + // lookup. + ctx = context.WithValue(ctx, requests.ContextRequestInformationKey, &requests.RequestContext{ + Path: r.URL.Path, + Method: r.Method, + }) + // Set the context back on the request r = r.WithContext(ctx) diff --git a/internal/servers/controller/handlers/accounts/account_service.go b/internal/servers/controller/handlers/accounts/account_service.go index 2d228faf3..35e22a0d9 100644 --- a/internal/servers/controller/handlers/accounts/account_service.go +++ b/internal/servers/controller/handlers/accounts/account_service.go @@ -6,6 +6,7 @@ import ( "net/url" "strings" + "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/auth" "github.com/hashicorp/boundary/internal/auth/oidc" oidcstore "github.com/hashicorp/boundary/internal/auth/oidc/store" @@ -15,6 +16,7 @@ import ( pb "github.com/hashicorp/boundary/internal/gen/controller/api/resources/accounts" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" "github.com/hashicorp/boundary/internal/perms" + "github.com/hashicorp/boundary/internal/requests" "github.com/hashicorp/boundary/internal/servers/controller/common" "github.com/hashicorp/boundary/internal/servers/controller/handlers" "github.com/hashicorp/boundary/internal/types/action" @@ -120,22 +122,45 @@ func (s Service) ListAccounts(ctx context.Context, req *pbs.ListAccountsRequest) if err != nil { return nil, err } + if len(ul) == 0 { + return &pbs.ListAccountsResponse{}, nil + } + filter, err := handlers.NewFilter(req.GetFilter()) if err != nil { return nil, err } finalItems := make([]*pb.Account, 0, len(ul)) - res := &perms.Resource{ + + res := perms.Resource{ ScopeId: authResults.Scope.Id, Type: resource.Account, Pin: req.GetAuthMethodId(), } - for _, item := range ul { - item.Scope = authResults.Scope - item.AuthorizedActions = authResults.FetchActionSetForId(ctx, item.Id, IdActions[auth.SubtypeFromId(item.Id)], auth.WithResource(res)).Strings() - if len(item.AuthorizedActions) == 0 { + for _, acct := range ul { + res.Id = acct.GetPublicId() + authorizedActions := authResults.FetchActionSetForId(ctx, acct.GetPublicId(), IdActions[auth.SubtypeFromId(acct.GetPublicId())], auth.WithResource(&res)).Strings() + if len(authorizedActions) == 0 { continue } + + 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)) + } + + item, err := toProto(ctx, acct, outputOpts...) + if err != nil { + return nil, err + } + + // This comes last so that we can use item fields in the filter after + // the allowed fields are populated above if filter.Match(item) { finalItems = append(finalItems, item) } @@ -145,56 +170,119 @@ func (s Service) ListAccounts(ctx context.Context, req *pbs.ListAccountsRequest) // GetAccount implements the interface pbs.AccountServiceServer. func (s Service) GetAccount(ctx context.Context, req *pbs.GetAccountRequest) (*pbs.GetAccountResponse, error) { + const op = "accounts.(Service).GetAccount" + if err := validateGetRequest(req); err != nil { return nil, err } + _, authResults := s.parentAndAuthResult(ctx, req.GetId(), action.Read) if authResults.Error != nil { return nil, authResults.Error } - u, err := s.getFromRepo(ctx, req.GetId()) + acct, err := s.getFromRepo(ctx, req.GetId()) if err != nil { return nil, err } - u.Scope = authResults.Scope - u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions[auth.SubtypeFromId(u.Id)]).Strings() - return &pbs.GetAccountResponse{Item: u}, nil + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, acct.GetPublicId(), IdActions[auth.SubtypeFromId(acct.GetPublicId())]).Strings())) + } + + item, err := toProto(ctx, acct, outputOpts...) + if err != nil { + return nil, err + } + + return &pbs.GetAccountResponse{Item: item}, nil } // CreateAccount implements the interface pbs.AccountServiceServer. func (s Service) CreateAccount(ctx context.Context, req *pbs.CreateAccountRequest) (*pbs.CreateAccountResponse, error) { + const op = "accounts.(Service).CreateAccount" + if err := validateCreateRequest(req); err != nil { return nil, err } + authMeth, authResults := s.parentAndAuthResult(ctx, req.GetItem().GetAuthMethodId(), action.Create) if authResults.Error != nil { return nil, authResults.Error } - u, err := s.createInRepo(ctx, authMeth, req.GetItem()) + acct, err := s.createInRepo(ctx, authMeth, req.GetItem()) if err != nil { return nil, err } - u.Scope = authResults.Scope - u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions[auth.SubtypeFromId(req.GetItem().GetAuthMethodId())]).Strings() - return &pbs.CreateAccountResponse{Item: u, Uri: fmt.Sprintf("accounts/%s", u.GetId())}, nil + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, acct.GetPublicId(), IdActions[auth.SubtypeFromId(acct.GetPublicId())]).Strings())) + } + + item, err := toProto(ctx, acct, outputOpts...) + if err != nil { + return nil, err + } + + return &pbs.CreateAccountResponse{Item: item, Uri: fmt.Sprintf("accounts/%s", item.GetId())}, nil } // UpdateAccount implements the interface pbs.AccountServiceServer. func (s Service) UpdateAccount(ctx context.Context, req *pbs.UpdateAccountRequest) (*pbs.UpdateAccountResponse, error) { + const op = "accounts.(Service).UpdateAccount" + if err := validateUpdateRequest(req); err != nil { return nil, err } + authMeth, authResults := s.parentAndAuthResult(ctx, req.GetId(), action.Update) if authResults.Error != nil { return nil, authResults.Error } - u, err := s.updateInRepo(ctx, authResults.Scope.GetId(), authMeth.GetPublicId(), req) + acct, err := s.updateInRepo(ctx, authResults.Scope.GetId(), authMeth.GetPublicId(), req) if err != nil { return nil, err } - u.Scope = authResults.Scope - u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions[auth.SubtypeFromId(u.Id)]).Strings() - return &pbs.UpdateAccountResponse{Item: u}, nil + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, acct.GetPublicId(), IdActions[auth.SubtypeFromId(acct.GetPublicId())]).Strings())) + } + + item, err := toProto(ctx, acct, outputOpts...) + if err != nil { + return nil, err + } + + return &pbs.UpdateAccountResponse{Item: item}, nil } // DeleteAccount implements the interface pbs.AccountServiceServer. @@ -215,41 +303,83 @@ func (s Service) DeleteAccount(ctx context.Context, req *pbs.DeleteAccountReques // ChangePassword implements the interface pbs.AccountServiceServer. func (s Service) ChangePassword(ctx context.Context, req *pbs.ChangePasswordRequest) (*pbs.ChangePasswordResponse, error) { + const op = "accounts.(Service).ChangePassword" + if err := validateChangePasswordRequest(req); err != nil { return nil, err } + _, authResults := s.parentAndAuthResult(ctx, req.GetId(), action.ChangePassword) if authResults.Error != nil { return nil, authResults.Error } - u, err := s.changePasswordInRepo(ctx, authResults.Scope.GetId(), req.GetId(), req.GetVersion(), req.GetCurrentPassword(), req.GetNewPassword()) + acct, err := s.changePasswordInRepo(ctx, authResults.Scope.GetId(), req.GetId(), req.GetVersion(), req.GetCurrentPassword(), req.GetNewPassword()) if err != nil { return nil, err } - u.Scope = authResults.Scope - u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions[auth.SubtypeFromId(u.Id)]).Strings() - return &pbs.ChangePasswordResponse{Item: u}, nil + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, acct.GetPublicId(), IdActions[auth.SubtypeFromId(acct.GetPublicId())]).Strings())) + } + + item, err := toProto(ctx, acct, outputOpts...) + if err != nil { + return nil, err + } + + return &pbs.ChangePasswordResponse{Item: item}, nil } // SetPassword implements the interface pbs.AccountServiceServer. func (s Service) SetPassword(ctx context.Context, req *pbs.SetPasswordRequest) (*pbs.SetPasswordResponse, error) { + const op = "accounts.(Service).SetPassword" + if err := validateSetPasswordRequest(req); err != nil { return nil, err } + _, authResults := s.parentAndAuthResult(ctx, req.GetId(), action.SetPassword) if authResults.Error != nil { return nil, authResults.Error } - u, err := s.setPasswordInRepo(ctx, authResults.Scope.GetId(), req.GetId(), req.GetVersion(), req.GetPassword()) + acct, err := s.setPasswordInRepo(ctx, authResults.Scope.GetId(), req.GetId(), req.GetVersion(), req.GetPassword()) if err != nil { return nil, err } - u.Scope = authResults.Scope - u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions[auth.SubtypeFromId(u.Id)]).Strings() - return &pbs.SetPasswordResponse{Item: u}, nil + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, acct.GetPublicId(), IdActions[auth.SubtypeFromId(acct.GetPublicId())]).Strings())) + } + + item, err := toProto(ctx, acct, outputOpts...) + if err != nil { + return nil, err + } + + return &pbs.SetPasswordResponse{Item: item}, nil } -func (s Service) getFromRepo(ctx context.Context, id string) (*pb.Account, error) { +func (s Service) getFromRepo(ctx context.Context, id string) (auth.Account, error) { var acct auth.Account switch auth.SubtypeFromId(id) { case auth.PasswordSubtype: @@ -281,7 +411,7 @@ func (s Service) getFromRepo(ctx context.Context, id string) (*pb.Account, error default: return nil, handlers.NotFoundErrorf("Unrecognized id.") } - return toProto(acct) + return acct, nil } func (s Service) createPwInRepo(ctx context.Context, am auth.AuthMethod, item *pb.Account) (*password.Account, error) { @@ -367,7 +497,7 @@ func (s Service) createOidcInRepo(ctx context.Context, am auth.AuthMethod, item return out, nil } -func (s Service) createInRepo(ctx context.Context, am auth.AuthMethod, item *pb.Account) (*pb.Account, error) { +func (s Service) createInRepo(ctx context.Context, am auth.AuthMethod, item *pb.Account) (auth.Account, error) { const op = "accounts.(Service).createInRepo" if item == nil { return nil, errors.New(errors.InvalidParameter, op, "missing item") @@ -393,7 +523,7 @@ func (s Service) createInRepo(ctx context.Context, am auth.AuthMethod, item *pb. } out = am } - return toProto(out) + return out, nil } func (s Service) updatePwInRepo(ctx context.Context, scopeId, authMethId, id string, mask []string, item *pb.Account) (*password.Account, error) { @@ -463,7 +593,7 @@ func (s Service) updateOidcInRepo(ctx context.Context, scopeId, amId, id string, return out, nil } -func (s Service) updateInRepo(ctx context.Context, scopeId, authMethodId string, req *pbs.UpdateAccountRequest) (*pb.Account, error) { +func (s Service) updateInRepo(ctx context.Context, scopeId, authMethodId string, req *pbs.UpdateAccountRequest) (auth.Account, error) { const op = "accounts.(Service).updateInRepo" var out auth.Account switch auth.SubtypeFromId(req.GetId()) { @@ -486,7 +616,7 @@ func (s Service) updateInRepo(ctx context.Context, scopeId, authMethodId string, } out = a } - return toProto(out) + return out, nil } func (s Service) deleteFromRepo(ctx context.Context, scopeId, id string) (bool, error) { @@ -516,46 +646,40 @@ func (s Service) deleteFromRepo(ctx context.Context, scopeId, id string) (bool, return rows > 0, nil } -func (s Service) listFromRepo(ctx context.Context, authMethodId string) ([]*pb.Account, error) { - var outUl []*pb.Account +func (s Service) listFromRepo(ctx context.Context, authMethodId string) ([]auth.Account, error) { + const op = "accounts.(Service).listFromRepo" + + var outUl []auth.Account switch auth.SubtypeFromId(authMethodId) { case auth.PasswordSubtype: pwRepo, err := s.pwRepoFn() if err != nil { - return nil, err + return nil, errors.Wrap(err, op) } pwl, err := pwRepo.ListAccounts(ctx, authMethodId) if err != nil { - return nil, err + return nil, errors.Wrap(err, op) } - for _, u := range pwl { - ou, err := toProto(u) - if err != nil { - return nil, err - } - outUl = append(outUl, ou) + for _, a := range pwl { + outUl = append(outUl, a) } case auth.OidcSubtype: oidcRepo, err := s.oidcRepoFn() if err != nil { - return nil, err + return nil, errors.Wrap(err, op) } oidcl, err := oidcRepo.ListAccounts(ctx, authMethodId) if err != nil { - return nil, err + return nil, errors.Wrap(err, op) } - for _, u := range oidcl { - ou, err := toProto(u) - if err != nil { - return nil, err - } - outUl = append(outUl, ou) + for _, a := range oidcl { + outUl = append(outUl, a) } } return outUl, nil } -func (s Service) changePasswordInRepo(ctx context.Context, scopeId, id string, version uint32, currentPassword, newPassword string) (*pb.Account, error) { +func (s Service) changePasswordInRepo(ctx context.Context, scopeId, id string, version uint32, currentPassword, newPassword string) (auth.Account, error) { const op = "account.(Service).changePasswordInRepo" repo, err := s.pwRepoFn() if err != nil { @@ -578,10 +702,10 @@ func (s Service) changePasswordInRepo(ctx context.Context, scopeId, id string, v if out == nil { return nil, handlers.ApiErrorWithCodeAndMessage(codes.PermissionDenied, "Failed to change password.") } - return toProto(out) + return out, nil } -func (s Service) setPasswordInRepo(ctx context.Context, scopeId, id string, version uint32, pw string) (*pb.Account, error) { +func (s Service) setPasswordInRepo(ctx context.Context, scopeId, id string, version uint32, pw string) (auth.Account, error) { const op = "accounts.(Service).setPasswordInRepo" repo, err := s.pwRepoFn() @@ -599,7 +723,7 @@ func (s Service) setPasswordInRepo(ctx context.Context, scopeId, id string, vers } return nil, errors.Wrap(err, op) } - return toProto(out) + return out, nil } func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Type) (auth.AuthMethod, auth.VerifyResults) { @@ -677,30 +801,61 @@ func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Ty return authMeth, auth.Verify(ctx, opts...) } -func toProto(in auth.Account) (*pb.Account, error) { - out := pb.Account{ - Id: in.GetPublicId(), - CreatedTime: in.GetCreateTime().GetTimestamp(), - UpdatedTime: in.GetUpdateTime().GetTimestamp(), - AuthMethodId: in.GetAuthMethodId(), - Version: in.GetVersion(), +func toProto(ctx context.Context, in auth.Account, opt ...handlers.Option) (*pb.Account, error) { + opts := handlers.GetOpts(opt...) + if opts.WithOutputFields == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "output fields not found when building account proto") } - if in.GetDescription() != "" { + outputFields := *opts.WithOutputFields + + out := pb.Account{} + if outputFields.Has(globals.IdField) { + out.Id = in.GetPublicId() + } + if outputFields.Has(globals.AuthMethodIdField) { + out.AuthMethodId = in.GetAuthMethodId() + } + if outputFields.Has(globals.DescriptionField) && in.GetDescription() != "" { out.Description = &wrapperspb.StringValue{Value: in.GetDescription()} } - if in.GetName() != "" { + if outputFields.Has(globals.NameField) && in.GetName() != "" { out.Name = &wrapperspb.StringValue{Value: 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 i := in.(type) { case *password.Account: - out.Type = auth.PasswordSubtype.String() + if outputFields.Has(globals.TypeField) { + out.Type = auth.PasswordSubtype.String() + } + if !outputFields.Has(globals.AttributesField) { + break + } st, err := handlers.ProtoToStruct(&pb.PasswordAccountAttributes{LoginName: i.GetLoginName()}) if err != nil { return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "failed building password attribute struct: %v", err) } out.Attributes = st case *oidc.Account: - out.Type = auth.OidcSubtype.String() + if outputFields.Has(globals.TypeField) { + out.Type = auth.OidcSubtype.String() + } + if !outputFields.Has(globals.AttributesField) { + break + } attrs := &pb.OidcAccountAttributes{ Issuer: i.GetIssuer(), Subject: i.GetSubject(), diff --git a/internal/servers/controller/handlers/accounts/account_service_test.go b/internal/servers/controller/handlers/accounts/account_service_test.go index 444d77808..d5db6cc83 100644 --- a/internal/servers/controller/handlers/accounts/account_service_test.go +++ b/internal/servers/controller/handlers/accounts/account_service_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/auth" "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/auth/password" @@ -268,10 +269,11 @@ func TestListPassword(t *testing.T) { } cases := []struct { - name string - req *pbs.ListAccountsRequest - res *pbs.ListAccountsResponse - err error + name string + req *pbs.ListAccountsRequest + res *pbs.ListAccountsResponse + err error + skipAnon bool }{ { name: "List Some Accounts", @@ -299,7 +301,8 @@ func TestListPassword(t *testing.T) { AuthMethodId: amSomeAccounts.GetPublicId(), Filter: fmt.Sprintf(`"/item/attributes/login_name"==%q`, wantSomeAccounts[1].Attributes.AsMap()["login_name"]), }, - res: &pbs.ListAccountsResponse{Items: wantSomeAccounts[1:2]}, + res: &pbs.ListAccountsResponse{Items: wantSomeAccounts[1:2]}, + skipAnon: true, }, { name: "Filter All Accounts", @@ -321,6 +324,7 @@ func TestListPassword(t *testing.T) { s, err := accounts.NewService(pwRepoFn, oidcRepoFn) require.NoError(err, "Couldn't create new user service.") + // Test non-anon first got, gErr := s.ListAccounts(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.req) if tc.err != nil { require.Error(gErr) @@ -330,6 +334,20 @@ func TestListPassword(t *testing.T) { require.NoError(gErr) } assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform(), protocmp.SortRepeatedFields(got)), "ListAccounts() with scope %q got response %q, wanted %q", tc.req, got, tc.res) + + // Now test with anon + if tc.skipAnon { + return + } + got, gErr = s.ListAccounts(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId(), auth.WithUserId(auth.AnonymousUserId)), tc.req) + require.NoError(gErr) + assert.Len(got.Items, len(tc.res.Items)) + for _, g := range got.GetItems() { + assert.Nil(g.Attributes) + assert.Nil(g.CreatedTime) + assert.Nil(g.UpdatedTime) + assert.Empty(g.Version) + } }) } } @@ -401,10 +419,11 @@ func TestListOidc(t *testing.T) { } cases := []struct { - name string - req *pbs.ListAccountsRequest - res *pbs.ListAccountsResponse - err error + name string + req *pbs.ListAccountsRequest + res *pbs.ListAccountsResponse + err error + skipAnon bool }{ { name: "List Some Accounts", @@ -432,7 +451,8 @@ func TestListOidc(t *testing.T) { AuthMethodId: amSomeAccounts.GetPublicId(), Filter: fmt.Sprintf(`"/item/attributes/subject"==%q`, wantSomeAccounts[1].Attributes.AsMap()["subject"]), }, - res: &pbs.ListAccountsResponse{Items: wantSomeAccounts[1:2]}, + res: &pbs.ListAccountsResponse{Items: wantSomeAccounts[1:2]}, + skipAnon: true, }, { name: "Filter All Accounts", @@ -467,6 +487,20 @@ func TestListOidc(t *testing.T) { got.Items[j].GetAttributes().GetFields()["subject"].GetStringValue()) < 0 }) assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListAccounts() with scope %q got response %q, wanted %q", tc.req, got, tc.res) + + // Now test with anon + if tc.skipAnon { + return + } + got, gErr = s.ListAccounts(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId(), auth.WithUserId(auth.AnonymousUserId)), tc.req) + require.NoError(gErr) + assert.Len(got.Items, len(tc.res.Items)) + for _, g := range got.GetItems() { + assert.Nil(g.Attributes) + assert.Nil(g.CreatedTime) + assert.Nil(g.UpdatedTime) + assert.Empty(g.Version) + } }) } } @@ -1067,7 +1101,7 @@ func TestUpdatePassword(t *testing.T) { name: "Update an Existing AuthMethod", req: &pbs.UpdateAccountRequest{ UpdateMask: &field_mask.FieldMask{ - Paths: []string{"name", "description"}, + Paths: []string{globals.NameField, globals.DescriptionField}, }, Item: &pb.Account{ Name: &wrapperspb.StringValue{Value: "new"}, @@ -1126,7 +1160,7 @@ func TestUpdatePassword(t *testing.T) { name: "Cant change type", req: &pbs.UpdateAccountRequest{ UpdateMask: &field_mask.FieldMask{ - Paths: []string{"name"}, + Paths: []string{globals.NameField}, }, Item: &pb.Account{ Name: &wrapperspb.StringValue{Value: ""}, @@ -1163,7 +1197,7 @@ func TestUpdatePassword(t *testing.T) { name: "Unset Name", req: &pbs.UpdateAccountRequest{ UpdateMask: &field_mask.FieldMask{ - Paths: []string{"name"}, + Paths: []string{globals.NameField}, }, Item: &pb.Account{ Description: &wrapperspb.StringValue{Value: "ignored"}, @@ -1185,7 +1219,7 @@ func TestUpdatePassword(t *testing.T) { name: "Update Only Name", req: &pbs.UpdateAccountRequest{ UpdateMask: &field_mask.FieldMask{ - Paths: []string{"name"}, + Paths: []string{globals.NameField}, }, Item: &pb.Account{ Name: &wrapperspb.StringValue{Value: "updated"}, @@ -1209,7 +1243,7 @@ func TestUpdatePassword(t *testing.T) { name: "Update Only Description", req: &pbs.UpdateAccountRequest{ UpdateMask: &field_mask.FieldMask{ - Paths: []string{"description"}, + Paths: []string{globals.DescriptionField}, }, Item: &pb.Account{ Name: &wrapperspb.StringValue{Value: "ignored"}, @@ -1258,7 +1292,7 @@ func TestUpdatePassword(t *testing.T) { req: &pbs.UpdateAccountRequest{ Id: password.AccountPrefix + "_DoesntExis", UpdateMask: &field_mask.FieldMask{ - Paths: []string{"description"}, + Paths: []string{globals.DescriptionField}, }, Item: &pb.Account{ Name: &wrapperspb.StringValue{Value: "new"}, @@ -1434,7 +1468,7 @@ func TestUpdateOidc(t *testing.T) { name: "Update an Existing AuthMethod", req: &pbs.UpdateAccountRequest{ UpdateMask: &field_mask.FieldMask{ - Paths: []string{"name", "description"}, + Paths: []string{globals.NameField, globals.DescriptionField}, }, Item: &pb.Account{ Name: &wrapperspb.StringValue{Value: "new"}, @@ -1493,7 +1527,7 @@ func TestUpdateOidc(t *testing.T) { name: "Cant change type", req: &pbs.UpdateAccountRequest{ UpdateMask: &field_mask.FieldMask{ - Paths: []string{"name"}, + Paths: []string{globals.NameField}, }, Item: &pb.Account{ Name: &wrapperspb.StringValue{Value: ""}, @@ -1530,7 +1564,7 @@ func TestUpdateOidc(t *testing.T) { name: "Unset Name", req: &pbs.UpdateAccountRequest{ UpdateMask: &field_mask.FieldMask{ - Paths: []string{"name"}, + Paths: []string{globals.NameField}, }, Item: &pb.Account{ Description: &wrapperspb.StringValue{Value: "ignored"}, @@ -1552,7 +1586,7 @@ func TestUpdateOidc(t *testing.T) { name: "Update Only Name", req: &pbs.UpdateAccountRequest{ UpdateMask: &field_mask.FieldMask{ - Paths: []string{"name"}, + Paths: []string{globals.NameField}, }, Item: &pb.Account{ Name: &wrapperspb.StringValue{Value: "updated"}, @@ -1576,7 +1610,7 @@ func TestUpdateOidc(t *testing.T) { name: "Update Only Description", req: &pbs.UpdateAccountRequest{ UpdateMask: &field_mask.FieldMask{ - Paths: []string{"description"}, + Paths: []string{globals.DescriptionField}, }, Item: &pb.Account{ Name: &wrapperspb.StringValue{Value: "ignored"}, @@ -1615,7 +1649,7 @@ func TestUpdateOidc(t *testing.T) { req: &pbs.UpdateAccountRequest{ Id: password.AccountPrefix + "_DoesntExis", UpdateMask: &field_mask.FieldMask{ - Paths: []string{"description"}, + Paths: []string{globals.DescriptionField}, }, Item: &pb.Account{ Name: &wrapperspb.StringValue{Value: "new"}, @@ -1864,7 +1898,7 @@ func TestSetPassword(t *testing.T) { password: "anewpassword", }, { - name: "password to short", + name: "password too short", accountId: defaultAcct.GetId(), version: defaultAcct.GetVersion(), password: "123", @@ -2027,7 +2061,7 @@ func TestChangePassword(t *testing.T) { newPW: "anewpassword", }, { - name: "new password to short", + name: "new password too short", authMethodId: defaultAcct.GetAuthMethodId(), accountId: defaultAcct.GetId(), version: defaultAcct.GetVersion(), diff --git a/internal/servers/controller/handlers/authmethods/authmethod_service.go b/internal/servers/controller/handlers/authmethods/authmethod_service.go index 1d4f0ac71..85d405ab3 100644 --- a/internal/servers/controller/handlers/authmethods/authmethod_service.go +++ b/internal/servers/controller/handlers/authmethods/authmethod_service.go @@ -6,6 +6,7 @@ import ( "net/url" "strings" + "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/auth" "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/auth/password" @@ -17,6 +18,7 @@ import ( pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/perms" + "github.com/hashicorp/boundary/internal/requests" "github.com/hashicorp/boundary/internal/servers/controller/common" "github.com/hashicorp/boundary/internal/servers/controller/common/scopeids" "github.com/hashicorp/boundary/internal/servers/controller/handlers" @@ -61,33 +63,6 @@ var ( } ) -func populateCollectionAuthorizedActions(ctx context.Context, - authResults auth.VerifyResults, - item *pb.AuthMethod) error { - res := &perms.Resource{ - ScopeId: authResults.Scope.Id, - Pin: item.Id, - } - // Range over the defined collections and check permissions against those - // collections. We use the ID of this scope being returned, not its parent, - // hence passing in a resource here. - for k, v := range collectionTypeMap { - res.Type = k - acts := authResults.FetchActionSetForType(ctx, k, v, auth.WithResource(res)).Strings() - if len(acts) > 0 { - if item.AuthorizedCollectionActions == nil { - item.AuthorizedCollectionActions = make(map[string]*structpb.ListValue) - } - lv, err := structpb.NewList(strutil.StringListToInterfaceList(acts)) - if err != nil { - return err - } - item.AuthorizedCollectionActions[k.String()+"s"] = lv - } - } - return nil -} - // Service handles request as described by the pbs.AuthMethodServiceServer interface. type Service struct { pbs.UnimplementedAuthMethodServiceServer @@ -157,30 +132,54 @@ func (s Service) ListAuthMethods(ctx context.Context, req *pbs.ListAuthMethodsRe return &pbs.ListAuthMethodsResponse{}, nil } - ul, err := s.listFromRepo(ctx, scopeIds, authResults.UserId == auth.AnonymousUserId) + ul, err := s.listFromRepo(ctx, scopeIds, authResults) if err != nil { return nil, err } + if len(ul) == 0 { + return &pbs.ListAuthMethodsResponse{}, nil + } + filter, err := handlers.NewFilter(req.GetFilter()) if err != nil { return nil, err } finalItems := make([]*pb.AuthMethod, 0, len(ul)) - res := &perms.Resource{ + res := perms.Resource{ Type: resource.AuthMethod, } - for _, item := range ul { - item.Scope = scopeInfoMap[item.GetScopeId()] - res.ScopeId = item.Scope.Id - item.AuthorizedActions = authResults.FetchActionSetForId(ctx, item.Id, IdActions[auth.SubtypeFromId(item.Id)], auth.WithResource(res)).Strings() - if len(item.AuthorizedActions) == 0 { + for _, am := range ul { + res.Id = am.GetPublicId() + res.ScopeId = am.GetScopeId() + authorizedActions := authResults.FetchActionSetForId(ctx, am.GetPublicId(), IdActions[auth.SubtypeFromId(am.GetPublicId())], auth.WithResource(&res)).Strings() + if len(authorizedActions) == 0 { continue } - if filter.Match(item) { - finalItems = append(finalItems, item) - if err := populateCollectionAuthorizedActions(ctx, authResults, item); err != nil { + + 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(scopeInfoMap[am.GetScopeId()])) + } + if outputFields.Has(globals.AuthorizedActionsField) { + outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authorizedActions)) + } + if outputFields.Has(globals.AuthorizedCollectionActionsField) { + collectionActions, err := auth.CalculateAuthorizedCollectionActions(ctx, authResults, collectionTypeMap, authResults.Scope.Id, am.GetPublicId()) + if err != nil { return nil, err } + outputOpts = append(outputOpts, handlers.WithAuthorizedCollectionActions(collectionActions)) + } + + item, err := toAuthMethodProto(ctx, am, outputOpts...) + if err != nil { + return nil, err + } + + if filter.Match(item) { + finalItems = append(finalItems, item) } } return &pbs.ListAuthMethodsResponse{Items: finalItems}, nil @@ -188,6 +187,8 @@ func (s Service) ListAuthMethods(ctx context.Context, req *pbs.ListAuthMethodsRe // GetAuthMethod implements the interface pbs.AuthMethodServiceServer. func (s Service) GetAuthMethod(ctx context.Context, req *pbs.GetAuthMethodRequest) (*pbs.GetAuthMethodResponse, error) { + const op = "authmethods.(Service).GetAuthMethod" + if err := validateGetRequest(req); err != nil { return nil, err } @@ -195,20 +196,44 @@ func (s Service) GetAuthMethod(ctx context.Context, req *pbs.GetAuthMethodReques if authResults.Error != nil { return nil, authResults.Error } - u, err := s.getFromRepo(ctx, req.GetId()) + am, err := s.getFromRepo(ctx, req.GetId()) if err != nil { return nil, err } - u.Scope = authResults.Scope - u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions[auth.SubtypeFromId(u.Id)]).Strings() - if err := populateCollectionAuthorizedActions(ctx, authResults, u); err != nil { + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, am.GetPublicId(), IdActions[auth.SubtypeFromId(am.GetPublicId())]).Strings())) + } + if outputFields.Has(globals.AuthorizedCollectionActionsField) { + collectionActions, err := auth.CalculateAuthorizedCollectionActions(ctx, authResults, collectionTypeMap, authResults.Scope.Id, am.GetPublicId()) + if err != nil { + return nil, err + } + outputOpts = append(outputOpts, handlers.WithAuthorizedCollectionActions(collectionActions)) + } + + item, err := toAuthMethodProto(ctx, am, outputOpts...) + if err != nil { return nil, err } - return &pbs.GetAuthMethodResponse{Item: u}, nil + + return &pbs.GetAuthMethodResponse{Item: item}, nil } // CreateAuthMethod implements the interface pbs.AuthMethodServiceServer. func (s Service) CreateAuthMethod(ctx context.Context, req *pbs.CreateAuthMethodRequest) (*pbs.CreateAuthMethodResponse, error) { + const op = "authmethods.(Service).CreateAuthMethod" + if err := validateCreateRequest(req); err != nil { return nil, err } @@ -216,20 +241,44 @@ func (s Service) CreateAuthMethod(ctx context.Context, req *pbs.CreateAuthMethod if authResults.Error != nil { return nil, authResults.Error } - u, err := s.createInRepo(ctx, authResults.Scope.GetId(), req.GetItem()) + am, err := s.createInRepo(ctx, authResults.Scope.GetId(), req.GetItem()) if err != nil { return nil, err } - u.Scope = authResults.Scope - u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions[auth.SubtypeFromId(u.Id)]).Strings() - if err := populateCollectionAuthorizedActions(ctx, authResults, u); err != nil { + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, am.GetPublicId(), IdActions[auth.SubtypeFromId(am.GetPublicId())]).Strings())) + } + if outputFields.Has(globals.AuthorizedCollectionActionsField) { + collectionActions, err := auth.CalculateAuthorizedCollectionActions(ctx, authResults, collectionTypeMap, authResults.Scope.Id, am.GetPublicId()) + if err != nil { + return nil, err + } + outputOpts = append(outputOpts, handlers.WithAuthorizedCollectionActions(collectionActions)) + } + + item, err := toAuthMethodProto(ctx, am, outputOpts...) + if err != nil { return nil, err } - return &pbs.CreateAuthMethodResponse{Item: u, Uri: fmt.Sprintf("auth-methods/%s", u.GetId())}, nil + + return &pbs.CreateAuthMethodResponse{Item: item, Uri: fmt.Sprintf("auth-methods/%s", item.GetId())}, nil } // UpdateAuthMethod implements the interface pbs.AuthMethodServiceServer. func (s Service) UpdateAuthMethod(ctx context.Context, req *pbs.UpdateAuthMethodRequest) (*pbs.UpdateAuthMethodResponse, error) { + const op = "authmethods.(Service).UpdateAuthMethod" + if err := validateUpdateRequest(req); err != nil { return nil, err } @@ -237,7 +286,7 @@ func (s Service) UpdateAuthMethod(ctx context.Context, req *pbs.UpdateAuthMethod if authResults.Error != nil { return nil, authResults.Error } - u, err := s.updateInRepo(ctx, authResults.Scope.GetId(), req) + am, dryRun, err := s.updateInRepo(ctx, authResults.Scope.GetId(), req) if err != nil { switch { case errors.Match(errors.T(errors.InvalidParameter), err): @@ -246,16 +295,44 @@ func (s Service) UpdateAuthMethod(ctx context.Context, req *pbs.UpdateAuthMethod return nil, err } } - u.Scope = authResults.Scope - u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions[auth.SubtypeFromId(u.Id)]).Strings() - if err := populateCollectionAuthorizedActions(ctx, authResults, u); err != nil { + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, am.GetPublicId(), IdActions[auth.SubtypeFromId(am.GetPublicId())]).Strings())) + } + if outputFields.Has(globals.AuthorizedCollectionActionsField) { + collectionActions, err := auth.CalculateAuthorizedCollectionActions(ctx, authResults, collectionTypeMap, authResults.Scope.Id, am.GetPublicId()) + if err != nil { + return nil, err + } + outputOpts = append(outputOpts, handlers.WithAuthorizedCollectionActions(collectionActions)) + } + + item, err := toAuthMethodProto(ctx, am, outputOpts...) + if err != nil { return nil, err } - return &pbs.UpdateAuthMethodResponse{Item: u}, nil + + if item.GetAttributes() != nil && dryRun { + item.GetAttributes().Fields["dry_run"] = structpb.NewBoolValue(true) + } + + return &pbs.UpdateAuthMethodResponse{Item: item}, nil } // ChangeState implements the interface pbs.AuthMethodServiceServer. func (s Service) ChangeState(ctx context.Context, req *pbs.ChangeStateRequest) (*pbs.ChangeStateResponse, error) { + const op = "authmethods.(Service).ChangeState" + if err := validateChangeStateRequest(req); err != nil { return nil, err } @@ -272,12 +349,34 @@ func (s Service) ChangeState(ctx context.Context, req *pbs.ChangeStateRequest) ( return nil, err } } - am.Scope = authResults.Scope - am.AuthorizedActions = authResults.FetchActionSetForId(ctx, am.Id, IdActions[auth.OidcSubtype]).Strings() - if err := populateCollectionAuthorizedActions(ctx, authResults, am); err != nil { + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, am.GetPublicId(), IdActions[auth.SubtypeFromId(am.GetPublicId())]).Strings())) + } + if outputFields.Has(globals.AuthorizedCollectionActionsField) { + collectionActions, err := auth.CalculateAuthorizedCollectionActions(ctx, authResults, collectionTypeMap, authResults.Scope.Id, am.GetPublicId()) + if err != nil { + return nil, err + } + outputOpts = append(outputOpts, handlers.WithAuthorizedCollectionActions(collectionActions)) + } + + item, err := toAuthMethodProto(ctx, am, outputOpts...) + if err != nil { return nil, err } - return &pbs.ChangeStateResponse{Item: am}, nil + + return &pbs.ChangeStateResponse{Item: item}, nil } // DeleteAuthMethod implements the interface pbs.AuthMethodServiceServer. @@ -351,7 +450,7 @@ func (s Service) AuthenticateLogin(ctx context.Context, req *pbs.AuthenticateLog return &pbs.AuthenticateLoginResponse{Item: tok, TokenType: req.GetTokenType()}, nil } -func (s Service) getFromRepo(ctx context.Context, id string) (*pb.AuthMethod, error) { +func (s Service) getFromRepo(ctx context.Context, id string) (auth.AuthMethod, error) { var lookupErr error var am auth.AuthMethod switch auth.SubtypeFromId(id) { @@ -383,26 +482,28 @@ func (s Service) getFromRepo(ctx context.Context, id string) (*pb.AuthMethod, er return nil, handlers.NotFoundErrorf("AuthMethod %q doesn't exist.", id) } - return toAuthMethodProto(am) + return am, nil } -func (s Service) listFromRepo(ctx context.Context, scopeIds []string, anonUser bool) ([]*pb.AuthMethod, error) { +func (s Service) listFromRepo(ctx context.Context, scopeIds []string, authResults auth.VerifyResults) ([]auth.AuthMethod, error) { const op = "authmethods.(Service).listFromRepo" + reqCtx, ok := requests.RequestContextFromCtx(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + var outUl []auth.AuthMethod + oidcRepo, err := s.oidcRepoFn() if err != nil { - return nil, err + return nil, errors.Wrap(err, op) } - ol, err := oidcRepo.ListAuthMethods(ctx, scopeIds, oidc.WithUnauthenticatedUser(anonUser)) + ol, err := oidcRepo.ListAuthMethods(ctx, scopeIds, oidc.WithUnauthenticatedUser(reqCtx.UserId == auth.AnonymousUserId)) if err != nil { - return nil, err + return nil, errors.Wrap(err, op) } - var outUl []*pb.AuthMethod - for _, u := range ol { - ou, err := toAuthMethodProto(u, handlers.WithAnonymousListing(anonUser)) - if err != nil { - return nil, err - } - outUl = append(outUl, ou) + for _, item := range ol { + outUl = append(outUl, item) } repo, err := s.pwRepoFn() @@ -413,17 +514,13 @@ func (s Service) listFromRepo(ctx context.Context, scopeIds []string, anonUser b if err != nil { return nil, errors.Wrap(err, op) } - for _, u := range pl { - ou, err := toAuthMethodProto(u, handlers.WithAnonymousListing(anonUser)) - if err != nil { - return nil, errors.Wrap(err, op) - } - outUl = append(outUl, ou) + for _, item := range pl { + outUl = append(outUl, item) } return outUl, nil } -func (s Service) createInRepo(ctx context.Context, scopeId string, item *pb.AuthMethod) (*pb.AuthMethod, error) { +func (s Service) createInRepo(ctx context.Context, scopeId string, item *pb.AuthMethod) (auth.AuthMethod, error) { const op = "authmethods.(Service).createInRepo" var out auth.AuthMethod switch auth.SubtypeFromType(item.GetType()) { @@ -446,51 +543,39 @@ func (s Service) createInRepo(ctx context.Context, scopeId string, item *pb.Auth } out = am } - return toAuthMethodProto(out) + return out, nil } -func (s Service) updateInRepo(ctx context.Context, scopeId string, req *pbs.UpdateAuthMethodRequest) (*pb.AuthMethod, error) { +func (s Service) updateInRepo(ctx context.Context, scopeId string, req *pbs.UpdateAuthMethodRequest) (auth.AuthMethod, bool, error) { const op = "authmethods.(Service).updateInRepo" - storageToWire := toAuthMethodProto - var out auth.AuthMethod + + var am auth.AuthMethod + var dryRun bool + switch auth.SubtypeFromId(req.GetId()) { case auth.PasswordSubtype: - am, err := s.updatePwInRepo(ctx, scopeId, req.GetId(), req.GetUpdateMask().GetPaths(), req.GetItem()) + pam, err := s.updatePwInRepo(ctx, scopeId, req.GetId(), req.GetUpdateMask().GetPaths(), req.GetItem()) if err != nil { - return nil, errors.Wrap(err, op) + return nil, false, errors.Wrap(err, op) } - if am == nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to update auth method but no error returned from repository.") + if pam == nil { + return nil, false, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to update auth method but no error returned from repository.") } - out = am + am = pam + case auth.OidcSubtype: - am, dryRun, err := s.updateOidcInRepo(ctx, scopeId, req) + oam, dr, err := s.updateOidcInRepo(ctx, scopeId, req) if err != nil { - return nil, errors.Wrap(err, op) + return nil, false, errors.Wrap(err, op) } - if am == nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to update auth method but no error returned from repository.") + if oam == nil { + return nil, false, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to update auth method but no error returned from repository.") } - if dryRun { - storageToWire = func(in auth.AuthMethod, opt ...handlers.Option) (*pb.AuthMethod, error) { - am, err := toAuthMethodProto(in) - if err != nil { - return nil, errors.Wrap(err, op) - } - attrs := &pb.OidcAuthMethodAttributes{} - if err := handlers.StructToProto(am.GetAttributes(), attrs); err != nil { - return nil, errors.Wrap(err, op, errors.WithMsg("can't convert from attribute struct to proto")) - } - attrs.DryRun = true - if am.Attributes, err = handlers.ProtoToStruct(attrs); err != nil { - return nil, errors.Wrap(err, op, errors.WithMsg("can't convert from attribute proto to struct")) - } - return am, nil - } - } - out = am + am = oam + dryRun = dr } - return storageToWire(out) + + return am, dryRun, nil } func (s Service) deleteFromRepo(ctx context.Context, scopeId, id string) (bool, error) { @@ -523,7 +608,7 @@ func (s Service) deleteFromRepo(ctx context.Context, scopeId, id string) (bool, return rows > 0, nil } -func (s Service) changeStateInRepo(ctx context.Context, req *pbs.ChangeStateRequest) (*pb.AuthMethod, error) { +func (s Service) changeStateInRepo(ctx context.Context, req *pbs.ChangeStateRequest) (auth.AuthMethod, error) { const op = "authmethod_service.(Service).changeStateInRepo" switch auth.SubtypeFromId(req.GetId()) { @@ -558,7 +643,7 @@ func (s Service) changeStateInRepo(ctx context.Context, req *pbs.ChangeStateRequ return nil, err } - return toAuthMethodProto(am) + return am, nil } return nil, errors.New(errors.InvalidParameter, op, "Given auth method type does not support changing state") @@ -633,28 +718,53 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type) auth. return auth.Verify(ctx, opts...) } -func toAuthMethodProto(in auth.AuthMethod, opt ...handlers.Option) (*pb.AuthMethod, error) { - anonListing := handlers.GetOpts(opt...).WithAnonymousListing - out := &pb.AuthMethod{ - Id: in.GetPublicId(), - ScopeId: in.GetScopeId(), - IsPrimary: in.GetIsPrimaryAuthMethod(), +func toAuthMethodProto(ctx context.Context, in auth.AuthMethod, opt ...handlers.Option) (*pb.AuthMethod, error) { + opts := handlers.GetOpts(opt...) + if opts.WithOutputFields == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "output fields not found when building auth method proto") } - if in.GetDescription() != "" { + outputFields := *opts.WithOutputFields + + out := pb.AuthMethod{} + if outputFields.Has(globals.IdField) { + out.Id = in.GetPublicId() + } + if outputFields.Has(globals.ScopeIdField) { + out.ScopeId = in.GetScopeId() + } + if outputFields.Has(globals.IsPrimaryField) { + out.IsPrimary = in.GetIsPrimaryAuthMethod() + } + if outputFields.Has(globals.DescriptionField) && in.GetDescription() != "" { out.Description = wrapperspb.String(in.GetDescription()) } - if in.GetName() != "" { + if outputFields.Has(globals.NameField) && in.GetName() != "" { out.Name = wrapperspb.String(in.GetName()) } - if !anonListing { + 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 + } + if outputFields.Has(globals.AuthorizedCollectionActionsField) { + out.AuthorizedCollectionActions = opts.WithAuthorizedCollectionActions + } switch i := in.(type) { case *password.AuthMethod: - out.Type = auth.PasswordSubtype.String() - if anonListing { + if outputFields.Has(globals.TypeField) { + out.Type = auth.PasswordSubtype.String() + } + if !outputFields.Has(globals.AttributesField) { break } st, err := handlers.ProtoToStruct(&pb.PasswordAuthMethodAttributes{ @@ -666,8 +776,10 @@ func toAuthMethodProto(in auth.AuthMethod, opt ...handlers.Option) (*pb.AuthMeth } out.Attributes = st case *oidc.AuthMethod: - out.Type = auth.OidcSubtype.String() - if anonListing { + if outputFields.Has(globals.TypeField) { + out.Type = auth.OidcSubtype.String() + } + if !outputFields.Has(globals.AttributesField) { break } attrs := &pb.OidcAuthMethodAttributes{ @@ -704,7 +816,7 @@ func toAuthMethodProto(in auth.AuthMethod, opt ...handlers.Option) (*pb.AuthMeth } out.Attributes = st } - return out, nil + return &out, nil } func toAuthTokenProto(t *authtoken.AuthToken) *pba.AuthToken { @@ -1080,8 +1192,8 @@ func validateAuthenticateLoginRequest(req *pbs.AuthenticateLoginRequest) error { return nil } -func (s Service) convertInternalAuthTokenToApiAuthToken(ctx context.Context, tok *authtoken.AuthToken) (*pba.AuthToken, error) { - const op = "authmethod.convertInternalAuthTokenToApiAuthToken" +func (s Service) ConvertInternalAuthTokenToApiAuthToken(ctx context.Context, tok *authtoken.AuthToken) (*pba.AuthToken, error) { + const op = "authmethod.ConvertInternalAuthTokenToApiAuthToken" iamRepo, err := s.iamRepoFn() if err != nil { return nil, err diff --git a/internal/servers/controller/handlers/authmethods/authmethod_service_test.go b/internal/servers/controller/handlers/authmethods/authmethod_service_test.go index dd0a87486..5ea556f41 100644 --- a/internal/servers/controller/handlers/authmethods/authmethod_service_test.go +++ b/internal/servers/controller/handlers/authmethods/authmethod_service_test.go @@ -353,7 +353,7 @@ func TestList(t *testing.T) { require.NoError(err, "Couldn't create new auth_method service.") // First check with non-anonymous user - got, gErr := s.ListAuthMethods(auth.DisabledAuthTestContext(iamRepoFn, tc.req.GetScopeId(), auth.WithUserId("u_auth")), tc.req) + got, gErr := s.ListAuthMethods(auth.DisabledAuthTestContext(iamRepoFn, tc.req.GetScopeId()), tc.req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "ListAuthMethods() for scope %q got error %v, wanted %v", tc.req.GetScopeId(), gErr, tc.err) @@ -370,8 +370,9 @@ func TestList(t *testing.T) { assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListAuthMethods() for scope %q got response %q, wanted %q", tc.req.GetScopeId(), got, tc.res) // Now check with anonymous user - got, gErr = s.ListAuthMethods(auth.DisabledAuthTestContext(iamRepoFn, tc.req.GetScopeId()), tc.req) + got, gErr = s.ListAuthMethods(auth.DisabledAuthTestContext(iamRepoFn, tc.req.GetScopeId(), auth.WithUserId(auth.AnonymousUserId)), tc.req) require.NoError(gErr) + assert.Len(got.Items, len(tc.res.Items)) for _, g := range got.GetItems() { assert.Nil(g.Attributes) assert.Nil(g.CreatedTime) diff --git a/internal/servers/controller/handlers/authmethods/oidc.go b/internal/servers/controller/handlers/authmethods/oidc.go index 581bb516c..5151724c0 100644 --- a/internal/servers/controller/handlers/authmethods/oidc.go +++ b/internal/servers/controller/handlers/authmethods/oidc.go @@ -324,7 +324,7 @@ func (s Service) authenticateOidcToken(ctx context.Context, req *pbs.Authenticat }, nil } - responseToken, err := s.convertInternalAuthTokenToApiAuthToken( + responseToken, err := s.ConvertInternalAuthTokenToApiAuthToken( ctx, token, ) diff --git a/internal/servers/controller/handlers/authmethods/oidc_test.go b/internal/servers/controller/handlers/authmethods/oidc_test.go index e790fd54d..bcafd774b 100644 --- a/internal/servers/controller/handlers/authmethods/oidc_test.go +++ b/internal/servers/controller/handlers/authmethods/oidc_test.go @@ -24,6 +24,7 @@ import ( pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/requests" "github.com/hashicorp/boundary/internal/servers" "github.com/hashicorp/boundary/internal/servers/controller/common" "github.com/hashicorp/boundary/internal/servers/controller/handlers" @@ -153,6 +154,7 @@ func TestList_FilterNonPublic(t *testing.T) { serversRepoFn := func() (*servers.Repository, error) { return servers.NewRepository(rw, rw, kmsCache) } + iamRepo := iam.TestRepo(t, conn, wrapper) o, _ := iam.TestScopes(t, iamRepo) @@ -194,14 +196,14 @@ func TestList_FilterNonPublic(t *testing.T) { }{ { name: "unauthenticated", - reqCtx: auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), + reqCtx: auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId(), auth.WithUserId(auth.AnonymousUserId)), respCount: 1, }, { name: "authenticated", reqCtx: func() context.Context { at := authtoken.TestAuthToken(t, conn, kmsCache, o.GetPublicId()) - return auth.NewVerifierContext(context.Background(), + return auth.NewVerifierContext(requests.NewRequestContext(context.Background()), nil, iamRepoFn, authTokenRepoFn, diff --git a/internal/servers/controller/handlers/authmethods/password.go b/internal/servers/controller/handlers/authmethods/password.go index 56309c137..79390e59e 100644 --- a/internal/servers/controller/handlers/authmethods/password.go +++ b/internal/servers/controller/handlers/authmethods/password.go @@ -127,7 +127,7 @@ func (s Service) authenticateWithPwRepo(ctx context.Context, scopeId, authMethod return nil, err } - return s.convertInternalAuthTokenToApiAuthToken( + return s.ConvertInternalAuthTokenToApiAuthToken( ctx, tok, ) diff --git a/internal/servers/controller/handlers/authtokens/authtoken_service.go b/internal/servers/controller/handlers/authtokens/authtoken_service.go index 8d5386e1f..8a620675b 100644 --- a/internal/servers/controller/handlers/authtokens/authtoken_service.go +++ b/internal/servers/controller/handlers/authtokens/authtoken_service.go @@ -4,18 +4,21 @@ import ( "context" "fmt" + "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/auth" "github.com/hashicorp/boundary/internal/authtoken" "github.com/hashicorp/boundary/internal/errors" pb "github.com/hashicorp/boundary/internal/gen/controller/api/resources/authtokens" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" "github.com/hashicorp/boundary/internal/perms" + "github.com/hashicorp/boundary/internal/requests" "github.com/hashicorp/boundary/internal/servers/controller/common" "github.com/hashicorp/boundary/internal/servers/controller/common/scopeids" "github.com/hashicorp/boundary/internal/servers/controller/handlers" "github.com/hashicorp/boundary/internal/types/action" "github.com/hashicorp/boundary/internal/types/resource" "github.com/hashicorp/boundary/internal/types/scope" + "google.golang.org/grpc/codes" ) var ( @@ -90,37 +93,57 @@ func (s Service) ListAuthTokens(ctx context.Context, req *pbs.ListAuthTokensRequ if err != nil { return nil, err } + if len(ul) == 0 { + return &pbs.ListAuthTokensResponse{}, nil + } + filter, err := handlers.NewFilter(req.GetFilter()) if err != nil { return nil, err } finalItems := make([]*pb.AuthToken, 0, len(ul)) - res := &perms.Resource{ + res := perms.Resource{ Type: resource.AuthToken, } - for _, item := range ul { - item.Scope = scopeInfoMap[item.GetScopeId()] - res.ScopeId = item.Scope.Id - authorizedActions := authResults.FetchActionSetForId(ctx, item.Id, IdActions, auth.WithResource(res)) + for _, at := range ul { + res.Id = at.GetPublicId() + res.ScopeId = at.GetScopeId() + authorizedActions := authResults.FetchActionSetForId(ctx, at.GetPublicId(), IdActions, auth.WithResource(&res)) if len(authorizedActions) == 0 { continue } - if authorizedActions.OnlySelf() && item.GetUserId() != authResults.UserId { + if authorizedActions.OnlySelf() && at.GetIamUserId() != authResults.UserId { continue } - item.AuthorizedActions = authorizedActions.Strings() + 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(scopeInfoMap[at.GetScopeId()])) + } + if outputFields.Has(globals.AuthorizedActionsField) { + outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authorizedActions.Strings())) + } + + item, err := toProto(ctx, at, outputOpts...) + if err != nil { + return nil, err + } if filter.Match(item) { finalItems = append(finalItems, item) } } + return &pbs.ListAuthTokensResponse{Items: finalItems}, nil } // GetAuthToken implements the interface pbs.AuthTokenServiceServer. func (s Service) GetAuthToken(ctx context.Context, req *pbs.GetAuthTokenRequest) (*pbs.GetAuthTokenResponse, error) { + const op = "authtokens.(Service).GetAuthToken" + if err := validateGetRequest(req); err != nil { return nil, err } @@ -128,22 +151,47 @@ func (s Service) GetAuthToken(ctx context.Context, req *pbs.GetAuthTokenRequest) if authResults.Error != nil { return nil, authResults.Error } - u, err := s.getFromRepo(ctx, req.GetId()) + at, err := s.getFromRepo(ctx, req.GetId()) if err != nil { return nil, err } - authzdActions := authResults.FetchActionSetForId(ctx, u.Id, IdActions) + var outputFields perms.OutputFieldsMap + authorizedActions := authResults.FetchActionSetForId(ctx, at.GetPublicId(), IdActions) + // Check to see if we need to verify Read vs. just ReadSelf - if u.GetUserId() != authResults.UserId { - if !authzdActions.HasAction(action.Read) { + if at.GetIamUserId() != authResults.UserId { + if !authorizedActions.HasAction(action.Read) { return nil, handlers.ForbiddenError() } + outputFields = authResults.FetchOutputFields(perms.Resource{ + Id: at.GetPublicId(), + ScopeId: at.GetScopeId(), + Type: resource.AuthToken, + }, action.Read).SelfOrDefaults(authResults.UserId) + } else { + var ok bool + outputFields, ok = requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } } - u.Scope = authResults.Scope - u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions).Strings() - return &pbs.GetAuthTokenResponse{Item: u}, nil + 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.Strings())) + } + + item, err := toProto(ctx, at, outputOpts...) + if err != nil { + return nil, err + } + + return &pbs.GetAuthTokenResponse{Item: item}, nil } // DeleteAuthToken implements the interface pbs.AuthTokenServiceServer. @@ -160,10 +208,12 @@ func (s Service) DeleteAuthToken(ctx context.Context, req *pbs.DeleteAuthTokenRe if err != nil { return nil, err } - authzdActions := authResults.FetchActionSetForId(ctx, at.Id, IdActions) + + authorizedActions := authResults.FetchActionSetForId(ctx, at.GetPublicId(), IdActions) + // Check to see if we need to verify Delete vs. just DeleteSelf - if at.GetUserId() != authResults.UserId { - if !authzdActions.HasAction(action.Delete) { + if at.GetIamUserId() != authResults.UserId { + if !authorizedActions.HasAction(action.Delete) { return nil, handlers.ForbiddenError() } } @@ -175,20 +225,20 @@ func (s Service) DeleteAuthToken(ctx context.Context, req *pbs.DeleteAuthTokenRe return nil, nil } -func (s Service) getFromRepo(ctx context.Context, id string) (*pb.AuthToken, error) { +func (s Service) getFromRepo(ctx context.Context, id string) (*authtoken.AuthToken, error) { const op = "authtokens.(Service).getFromRepo" repo, err := s.repoFn() if err != nil { return nil, errors.Wrap(err, op) } - u, err := repo.LookupAuthToken(ctx, id) + at, err := repo.LookupAuthToken(ctx, id) if err != nil && !errors.IsNotFoundError(err) { return nil, errors.Wrap(err, op) } - if u == nil { + if at == nil { return nil, errors.New(errors.InvalidParameter, op, fmt.Sprintf("AuthToken %q not found", id)) } - return toProto(u), nil + return at, nil } func (s Service) deleteFromRepo(ctx context.Context, id string) (bool, error) { @@ -207,7 +257,7 @@ func (s Service) deleteFromRepo(ctx context.Context, id string) (bool, error) { return rows > 0, nil } -func (s Service) listFromRepo(ctx context.Context, scopeIds []string) ([]*pb.AuthToken, error) { +func (s Service) listFromRepo(ctx context.Context, scopeIds []string) ([]*authtoken.AuthToken, error) { repo, err := s.repoFn() _ = repo if err != nil { @@ -217,11 +267,7 @@ func (s Service) listFromRepo(ctx context.Context, scopeIds []string) ([]*pb.Aut if err != nil { return nil, err } - var outUl []*pb.AuthToken - for _, u := range ul { - outUl = append(outUl, toProto(u)) - } - return outUl, nil + return ul, nil } func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.VerifyResults { @@ -268,19 +314,49 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type) auth. return auth.Verify(ctx, opts...) } -func toProto(in *authtoken.AuthToken) *pb.AuthToken { - out := pb.AuthToken{ - Id: in.GetPublicId(), - ScopeId: in.GetScopeId(), - CreatedTime: in.GetCreateTime().GetTimestamp(), - UpdatedTime: in.GetUpdateTime().GetTimestamp(), - ApproximateLastUsedTime: in.GetApproximateLastAccessTime().GetTimestamp(), - ExpirationTime: in.GetExpirationTime().GetTimestamp(), - UserId: in.GetIamUserId(), - AuthMethodId: in.GetAuthMethodId(), - AccountId: in.GetAuthAccountId(), +func toProto(ctx context.Context, in *authtoken.AuthToken, opt ...handlers.Option) (*pb.AuthToken, error) { + opts := handlers.GetOpts(opt...) + if opts.WithOutputFields == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "output fields not found when building auth token proto") } - return &out + outputFields := *opts.WithOutputFields + + out := pb.AuthToken{} + if outputFields.Has(globals.IdField) { + out.Id = in.GetPublicId() + } + if outputFields.Has(globals.ScopeIdField) { + out.ScopeId = in.GetScopeId() + } + if outputFields.Has(globals.UserIdField) { + out.UserId = in.GetIamUserId() + } + if outputFields.Has(globals.AuthMethodIdField) { + out.AuthMethodId = in.GetAuthMethodId() + } + if outputFields.Has(globals.AccountIdField) { + out.AccountId = in.GetAuthAccountId() + } + if outputFields.Has(globals.CreatedTimeField) { + out.CreatedTime = in.GetCreateTime().GetTimestamp() + } + if outputFields.Has(globals.UpdatedTimeField) { + out.UpdatedTime = in.GetUpdateTime().GetTimestamp() + } + if outputFields.Has(globals.ApproximateLastUsedTimeField) { + out.ApproximateLastUsedTime = in.GetApproximateLastAccessTime().GetTimestamp() + } + if outputFields.Has(globals.ExpirationTimeField) { + out.ExpirationTime = in.GetExpirationTime().GetTimestamp() + } + if outputFields.Has(globals.ScopeField) { + out.Scope = opts.WithScope + } + if outputFields.Has(globals.AuthorizedActionsField) { + out.AuthorizedActions = opts.WithAuthorizedActions + } + + return &out, nil } // A validateX method should exist for each method above. These methods do not make calls to any backing service but enforce diff --git a/internal/servers/controller/handlers/authtokens/authtoken_service_test.go b/internal/servers/controller/handlers/authtokens/authtoken_service_test.go index d68cae9ad..e1a6c8948 100644 --- a/internal/servers/controller/handlers/authtokens/authtoken_service_test.go +++ b/internal/servers/controller/handlers/authtokens/authtoken_service_test.go @@ -16,6 +16,7 @@ import ( pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/requests" "github.com/hashicorp/boundary/internal/servers" "github.com/hashicorp/boundary/internal/servers/controller/handlers" "github.com/hashicorp/boundary/internal/servers/controller/handlers/authtokens" @@ -97,6 +98,7 @@ func TestGetSelf(t *testing.T) { } ctx := auth.NewVerifierContext(context.Background(), logger, iamRepoFn, tokenRepoFn, serversRepoFn, kms, requestInfo) + ctx = context.WithValue(ctx, requests.ContextRequestInformationKey, &requests.RequestContext{}) got, err := a.GetAuthToken(ctx, &pbs.GetAuthTokenRequest{Id: tc.readId}) if tc.err != nil { require.EqualError(err, tc.err.Error()) @@ -105,9 +107,9 @@ func TestGetSelf(t *testing.T) { } require.NoError(err) require.NotNil(got) - assert.Equal(got.GetItem().GetId(), tc.token.GetPublicId()) + assert.Equal(tc.token.GetPublicId(), got.GetItem().GetId()) // Ensure we didn't simply have e.g. read on all tokens - assert.Equal(got.Item.GetAuthorizedActions(), []string{"read:self", "delete:self"}) + assert.Equal([]string{"read:self", "delete:self"}, got.Item.GetAuthorizedActions()) }) } } @@ -384,16 +386,30 @@ func TestList(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { s, err := authtokens.NewService(repoFn, iamRepoFn) - require.NoError(t, err, "Couldn't create new user service.") + assert, require := assert.New(t), require.New(t) + require.NoError(err, "Couldn't create new user service.") - got, gErr := s.ListAuthTokens(auth.DisabledAuthTestContext(iamRepoFn, tc.req.GetScopeId()), tc.req) + // Check non-anon listing + got, gErr := s.ListAuthTokens(auth.DisabledAuthTestContext(iamRepoFn, tc.req.GetScopeId(), auth.WithUserId("u_auth")), tc.req) if tc.err != nil { - require.Error(t, gErr) - assert.True(t, errors.Is(gErr, tc.err), "ListAuthTokens() with scope %q got error %v, wanted %v", tc.req.GetScopeId(), gErr, tc.err) + require.Error(gErr) + assert.True(errors.Is(gErr, tc.err), "ListAuthTokens() with scope %q got error %v, wanted %v", tc.req.GetScopeId(), gErr, tc.err) + return } else { - require.NoError(t, gErr) + require.NoError(gErr) + } + assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform(), protocmp.SortRepeatedFields(got)), "ListAuthTokens() with scope %q got response %q, wanted %q", tc.req.GetScopeId(), got, tc.res) + + // Now check anon listing + got, gErr = s.ListAuthTokens(auth.DisabledAuthTestContext(iamRepoFn, tc.req.GetScopeId(), auth.WithUserId(auth.AnonymousUserId)), tc.req) + require.NoError(gErr) + assert.Len(got.Items, len(tc.res.Items)) + for _, item := range got.GetItems() { + require.Nil(item.CreatedTime) + require.Nil(item.ExpirationTime) + require.Nil(item.UpdatedTime) + require.Nil(item.ApproximateLastUsedTime) } - assert.Empty(t, cmp.Diff(got, tc.res, protocmp.Transform(), protocmp.SortRepeatedFields(got)), "ListAuthTokens() with scope %q got response %q, wanted %q", tc.req.GetScopeId(), got, tc.res) }) } } diff --git a/internal/servers/controller/handlers/groups/group_service.go b/internal/servers/controller/handlers/groups/group_service.go index f56c84f72..f538c8e7b 100644 --- a/internal/servers/controller/handlers/groups/group_service.go +++ b/internal/servers/controller/handlers/groups/group_service.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/auth" "github.com/hashicorp/boundary/internal/errors" pb "github.com/hashicorp/boundary/internal/gen/controller/api/resources/groups" @@ -11,6 +12,7 @@ import ( "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/iam/store" "github.com/hashicorp/boundary/internal/perms" + "github.com/hashicorp/boundary/internal/requests" "github.com/hashicorp/boundary/internal/servers/controller/common" "github.com/hashicorp/boundary/internal/servers/controller/common/scopeids" "github.com/hashicorp/boundary/internal/servers/controller/handlers" @@ -102,21 +104,41 @@ func (s Service) ListGroups(ctx context.Context, req *pbs.ListGroupsRequest) (*p if err != nil { return nil, err } + if len(gl) == 0 { + return &pbs.ListGroupsResponse{}, nil + } + filter, err := handlers.NewFilter(req.GetFilter()) if err != nil { return nil, err } finalItems := make([]*pb.Group, 0, len(gl)) - res := &perms.Resource{ + res := perms.Resource{ Type: resource.Group, } for _, item := range gl { - item.Scope = scopeInfoMap[item.GetScopeId()] - res.ScopeId = item.Scope.Id - item.AuthorizedActions = authResults.FetchActionSetForId(ctx, item.Id, IdActions, auth.WithResource(res)).Strings() - if len(item.AuthorizedActions) == 0 { + res.Id = item.GetPublicId() + res.ScopeId = item.GetScopeId() + authorizedActions := authResults.FetchActionSetForId(ctx, item.GetPublicId(), IdActions, auth.WithResource(&res)).Strings() + if len(authorizedActions) == 0 { continue } + + 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(scopeInfoMap[item.GetScopeId()])) + } + if outputFields.Has(globals.AuthorizedActionsField) { + outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authorizedActions)) + } + + item, err := toProto(ctx, item, nil, outputOpts...) + if err != nil { + return nil, err + } + if filter.Match(item) { finalItems = append(finalItems, item) } @@ -126,6 +148,8 @@ func (s Service) ListGroups(ctx context.Context, req *pbs.ListGroupsRequest) (*p // GetGroups implements the interface pbs.GroupServiceServer. func (s Service) GetGroup(ctx context.Context, req *pbs.GetGroupRequest) (*pbs.GetGroupResponse, error) { + const op = "groups.(Service).GetGroup" + if err := validateGetRequest(req); err != nil { return nil, err } @@ -133,17 +157,37 @@ func (s Service) GetGroup(ctx context.Context, req *pbs.GetGroupRequest) (*pbs.G if authResults.Error != nil { return nil, authResults.Error } - u, err := s.getFromRepo(ctx, req.GetId()) + u, members, err := s.getFromRepo(ctx, req.GetId()) if err != nil { return nil, err } - u.Scope = authResults.Scope - u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions).Strings() - return &pbs.GetGroupResponse{Item: u}, nil + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, u.GetPublicId(), IdActions).Strings())) + } + + item, err := toProto(ctx, u, members, outputOpts...) + if err != nil { + return nil, err + } + + return &pbs.GetGroupResponse{Item: item}, nil } // CreateGroup implements the interface pbs.GroupServiceServer. func (s Service) CreateGroup(ctx context.Context, req *pbs.CreateGroupRequest) (*pbs.CreateGroupResponse, error) { + const op = "groups.(Service).CreateGroup" + if err := validateCreateRequest(req); err != nil { return nil, err } @@ -155,13 +199,33 @@ func (s Service) CreateGroup(ctx context.Context, req *pbs.CreateGroupRequest) ( if err != nil { return nil, err } - u.Scope = authResults.Scope - u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions).Strings() - return &pbs.CreateGroupResponse{Item: u, Uri: fmt.Sprintf("groups/%s", u.GetId())}, nil + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, u.GetPublicId(), IdActions).Strings())) + } + + item, err := toProto(ctx, u, nil, outputOpts...) + if err != nil { + return nil, err + } + + return &pbs.CreateGroupResponse{Item: item, Uri: fmt.Sprintf("groups/%s", item.GetId())}, nil } // UpdateGroup implements the interface pbs.GroupServiceServer. func (s Service) UpdateGroup(ctx context.Context, req *pbs.UpdateGroupRequest) (*pbs.UpdateGroupResponse, error) { + const op = "groups.(Service).UpdateGroup" + if err := validateUpdateRequest(req); err != nil { return nil, err } @@ -169,13 +233,31 @@ func (s Service) UpdateGroup(ctx context.Context, req *pbs.UpdateGroupRequest) ( if authResults.Error != nil { return nil, authResults.Error } - u, err := s.updateInRepo(ctx, authResults.Scope.GetId(), req.GetId(), req.GetUpdateMask().GetPaths(), req.GetItem()) + u, members, err := s.updateInRepo(ctx, authResults.Scope.GetId(), req.GetId(), req.GetUpdateMask().GetPaths(), req.GetItem()) if err != nil { return nil, err } - u.Scope = authResults.Scope - u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions).Strings() - return &pbs.UpdateGroupResponse{Item: u}, nil + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, u.GetPublicId(), IdActions).Strings())) + } + + item, err := toProto(ctx, u, members, outputOpts...) + if err != nil { + return nil, err + } + + return &pbs.UpdateGroupResponse{Item: item}, nil } // DeleteGroup implements the interface pbs.GroupServiceServer. @@ -196,6 +278,8 @@ func (s Service) DeleteGroup(ctx context.Context, req *pbs.DeleteGroupRequest) ( // AddGroupMembers implements the interface pbs.GroupServiceServer. func (s Service) AddGroupMembers(ctx context.Context, req *pbs.AddGroupMembersRequest) (*pbs.AddGroupMembersResponse, error) { + const op = "groups.(Service).AddGroupMembers" + if err := validateAddGroupMembersRequest(req); err != nil { return nil, err } @@ -203,17 +287,37 @@ func (s Service) AddGroupMembers(ctx context.Context, req *pbs.AddGroupMembersRe if authResults.Error != nil { return nil, authResults.Error } - g, err := s.addMembersInRepo(ctx, req.GetId(), req.GetMemberIds(), req.GetVersion()) + g, members, err := s.addMembersInRepo(ctx, req.GetId(), req.GetMemberIds(), req.GetVersion()) if err != nil { return nil, err } - g.Scope = authResults.Scope - g.AuthorizedActions = authResults.FetchActionSetForId(ctx, g.Id, IdActions).Strings() - return &pbs.AddGroupMembersResponse{Item: g}, nil + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, g.GetPublicId(), IdActions).Strings())) + } + + item, err := toProto(ctx, g, members, outputOpts...) + if err != nil { + return nil, err + } + + return &pbs.AddGroupMembersResponse{Item: item}, nil } // SetGroupMembers implements the interface pbs.GroupServiceServer. func (s Service) SetGroupMembers(ctx context.Context, req *pbs.SetGroupMembersRequest) (*pbs.SetGroupMembersResponse, error) { + const op = "groups.(Service).SetGroupMembers" + if err := validateSetGroupMembersRequest(req); err != nil { return nil, err } @@ -221,17 +325,37 @@ func (s Service) SetGroupMembers(ctx context.Context, req *pbs.SetGroupMembersRe if authResults.Error != nil { return nil, authResults.Error } - g, err := s.setMembersInRepo(ctx, req.GetId(), req.GetMemberIds(), req.GetVersion()) + g, members, err := s.setMembersInRepo(ctx, req.GetId(), req.GetMemberIds(), req.GetVersion()) if err != nil { return nil, err } - g.Scope = authResults.Scope - g.AuthorizedActions = authResults.FetchActionSetForId(ctx, g.Id, IdActions).Strings() - return &pbs.SetGroupMembersResponse{Item: g}, nil + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, g.GetPublicId(), IdActions).Strings())) + } + + item, err := toProto(ctx, g, members, outputOpts...) + if err != nil { + return nil, err + } + + return &pbs.SetGroupMembersResponse{Item: item}, nil } // RemoveGroupMembers implements the interface pbs.GroupServiceServer. func (s Service) RemoveGroupMembers(ctx context.Context, req *pbs.RemoveGroupMembersRequest) (*pbs.RemoveGroupMembersResponse, error) { + const op = "groups.(Service).RemoveGroupMembers" + if err := validateRemoveGroupMembersRequest(req); err != nil { return nil, err } @@ -239,32 +363,50 @@ func (s Service) RemoveGroupMembers(ctx context.Context, req *pbs.RemoveGroupMem if authResults.Error != nil { return nil, authResults.Error } - g, err := s.removeMembersInRepo(ctx, req.GetId(), req.GetMemberIds(), req.GetVersion()) + g, members, err := s.removeMembersInRepo(ctx, req.GetId(), req.GetMemberIds(), req.GetVersion()) if err != nil { return nil, err } - g.Scope = authResults.Scope - g.AuthorizedActions = authResults.FetchActionSetForId(ctx, g.Id, IdActions).Strings() - return &pbs.RemoveGroupMembersResponse{Item: g}, nil + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, g.GetPublicId(), IdActions).Strings())) + } + + item, err := toProto(ctx, g, members, outputOpts...) + if err != nil { + return nil, err + } + + return &pbs.RemoveGroupMembersResponse{Item: item}, nil } -func (s Service) getFromRepo(ctx context.Context, id string) (*pb.Group, error) { +func (s Service) getFromRepo(ctx context.Context, id string) (*iam.Group, []*iam.GroupMember, error) { const op = "groups.(Service).getFromRepo" repo, err := s.repoFn() if err != nil { - return nil, errors.Wrap(err, op) + return nil, nil, errors.Wrap(err, op) } g, m, err := repo.LookupGroup(ctx, id) if err != nil && !errors.IsNotFoundError(err) { - return nil, errors.Wrap(err, op) + return nil, nil, errors.Wrap(err, op) } if g == nil { - return nil, errors.New(errors.InvalidParameter, op, fmt.Sprintf("group %q not found", id)) + return nil, nil, errors.New(errors.InvalidParameter, op, fmt.Sprintf("group %q not found", id)) } - return toProto(g, m), nil + return g, m, err } -func (s Service) createInRepo(ctx context.Context, scopeId string, item *pb.Group) (*pb.Group, error) { +func (s Service) createInRepo(ctx context.Context, scopeId string, item *pb.Group) (*iam.Group, error) { const op = "groups.(Service).createInRepo" var opts []iam.Option if item.GetName() != nil { @@ -288,10 +430,10 @@ func (s Service) createInRepo(ctx context.Context, scopeId string, item *pb.Grou if out == nil { return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create group but no error returned from repository.") } - return toProto(out, nil), nil + return out, nil } -func (s Service) updateInRepo(ctx context.Context, scopeId, id string, mask []string, item *pb.Group) (*pb.Group, error) { +func (s Service) updateInRepo(ctx context.Context, scopeId, id string, mask []string, item *pb.Group) (*iam.Group, []*iam.GroupMember, error) { const op = "groups.(Service).updateInRepo" var opts []iam.Option if desc := item.GetDescription(); desc != nil { @@ -303,25 +445,25 @@ func (s Service) updateInRepo(ctx context.Context, scopeId, id string, mask []st version := item.GetVersion() g, err := iam.NewGroup(scopeId, opts...) if err != nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to build group for update: %v.", err) + return nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to build group for update: %v.", err) } g.PublicId = id dbMask := maskManager.Translate(mask) if len(dbMask) == 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."}) + return nil, nil, handlers.InvalidArgumentErrorf("No valid fields included in the update mask.", map[string]string{"update_mask": "No valid fields provided in the update mask."}) } repo, err := s.repoFn() if err != nil { - return nil, errors.Wrap(err, op) + return nil, nil, errors.Wrap(err, op) } out, m, rowsUpdated, err := repo.UpdateGroup(ctx, g, version, dbMask) if err != nil { - return nil, errors.Wrap(err, op) + return nil, nil, errors.Wrap(err, op) } if rowsUpdated == 0 { - return nil, handlers.NotFoundErrorf("Group %q doesn't exist or incorrect version provided.", id) + return nil, nil, handlers.NotFoundErrorf("Group %q doesn't exist or incorrect version provided.", id) } - return toProto(out, m), nil + return out, m, nil } func (s Service) deleteFromRepo(ctx context.Context, id string) (bool, error) { @@ -340,7 +482,7 @@ func (s Service) deleteFromRepo(ctx context.Context, id string) (bool, error) { return rows > 0, nil } -func (s Service) listFromRepo(ctx context.Context, scopeIds []string) ([]*pb.Group, error) { +func (s Service) listFromRepo(ctx context.Context, scopeIds []string) ([]*iam.Group, error) { const op = "groups.(Service).listFromRepo" repo, err := s.repoFn() if err != nil { @@ -350,74 +492,70 @@ func (s Service) listFromRepo(ctx context.Context, scopeIds []string) ([]*pb.Gro if err != nil { return nil, errors.Wrap(err, op) } - var outGl []*pb.Group - for _, g := range gl { - outGl = append(outGl, toProto(g, nil)) - } - return outGl, nil + return gl, nil } -func (s Service) addMembersInRepo(ctx context.Context, groupId string, userIds []string, version uint32) (*pb.Group, error) { +func (s Service) addMembersInRepo(ctx context.Context, groupId string, userIds []string, version uint32) (*iam.Group, []*iam.GroupMember, error) { const op = "groups.(Service).addMembersInRepo" repo, err := s.repoFn() if err != nil { - return nil, errors.Wrap(err, op) + return nil, nil, errors.Wrap(err, op) } _, err = repo.AddGroupMembers(ctx, groupId, version, strutil.RemoveDuplicates(userIds, false)) if err != nil { // TODO: Figure out a way to surface more helpful error info beyond the Internal error. - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to add members to group: %v.", err) + return nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to add members to group: %v.", err) } out, m, err := repo.LookupGroup(ctx, groupId) if err != nil { - return nil, errors.Wrap(err, op, errors.WithMsg("unable to look up group after adding memebers")) + return nil, nil, errors.Wrap(err, op, errors.WithMsg("unable to look up group after adding memebers")) } if out == nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup group after adding member to it.") + return nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup group after adding member to it.") } - return toProto(out, m), nil + return out, m, nil } -func (s Service) setMembersInRepo(ctx context.Context, groupId string, userIds []string, version uint32) (*pb.Group, error) { +func (s Service) setMembersInRepo(ctx context.Context, groupId string, userIds []string, version uint32) (*iam.Group, []*iam.GroupMember, error) { const op = "groups.(Service).setMembersInRepo" repo, err := s.repoFn() if err != nil { - return nil, errors.Wrap(err, op) + return nil, nil, errors.Wrap(err, op) } _, _, err = repo.SetGroupMembers(ctx, groupId, version, strutil.RemoveDuplicates(userIds, false)) if err != nil { // TODO: Figure out a way to surface more helpful error info beyond the Internal error. - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to set members on group: %v.", err) + return nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to set members on group: %v.", err) } out, m, err := repo.LookupGroup(ctx, groupId) if err != nil { - return nil, errors.Wrap(err, op, errors.WithMsg("unable to look up group after setting members")) + return nil, nil, errors.Wrap(err, op, errors.WithMsg("unable to look up group after setting members")) } if out == nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup group after setting members for it.") + return nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup group after setting members for it.") } - return toProto(out, m), nil + return out, m, nil } -func (s Service) removeMembersInRepo(ctx context.Context, groupId string, userIds []string, version uint32) (*pb.Group, error) { +func (s Service) removeMembersInRepo(ctx context.Context, groupId string, userIds []string, version uint32) (*iam.Group, []*iam.GroupMember, error) { const op = "groups.(Service).removeMembersInRepo" repo, err := s.repoFn() if err != nil { - return nil, errors.Wrap(err, op) + return nil, nil, errors.Wrap(err, op) } _, err = repo.DeleteGroupMembers(ctx, groupId, version, strutil.RemoveDuplicates(userIds, false)) if err != nil { // TODO: Figure out a way to surface more helpful error info beyond the Internal error. - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to remove members from group: %v.", err) + return nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to remove members from group: %v.", err) } out, m, err := repo.LookupGroup(ctx, groupId) if err != nil { - return nil, errors.Wrap(err, op, errors.WithMsg("unable to look up group after removing members")) + return nil, nil, errors.Wrap(err, op, errors.WithMsg("unable to look up group after removing members")) } if out == nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup group after removing members from it.") + return nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup group after removing members from it.") } - return toProto(out, m), nil + return out, m, nil } func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.VerifyResults { @@ -459,28 +597,55 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type) auth. return auth.Verify(ctx, opts...) } -func toProto(in *iam.Group, members []*iam.GroupMember) *pb.Group { - out := pb.Group{ - Id: in.GetPublicId(), - ScopeId: in.GetScopeId(), - CreatedTime: in.GetCreateTime().GetTimestamp(), - UpdatedTime: in.GetUpdateTime().GetTimestamp(), - Version: in.Version, +func toProto(ctx context.Context, in *iam.Group, members []*iam.GroupMember, opt ...handlers.Option) (*pb.Group, error) { + opts := handlers.GetOpts(opt...) + if opts.WithOutputFields == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "output fields not found when building group proto") } - if in.GetDescription() != "" { - out.Description = &wrapperspb.StringValue{Value: in.GetDescription()} + outputFields := *opts.WithOutputFields + + out := pb.Group{} + if outputFields.Has(globals.IdField) { + out.Id = in.GetPublicId() } - if in.GetName() != "" { - out.Name = &wrapperspb.StringValue{Value: in.GetName()} + if outputFields.Has(globals.ScopeIdField) { + out.ScopeId = in.GetScopeId() } - for _, m := range members { - out.MemberIds = append(out.MemberIds, m.GetMemberId()) - out.Members = append(out.Members, &pb.Member{ - Id: m.GetMemberId(), - ScopeId: m.GetMemberScopeId(), - }) + if outputFields.Has(globals.DescriptionField) && in.GetDescription() != "" { + out.Description = wrapperspb.String(in.GetDescription()) } - return &out + 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 + } + if outputFields.Has(globals.MemberIdsField) { + for _, m := range members { + out.MemberIds = append(out.MemberIds, m.GetMemberId()) + } + } + if outputFields.Has(globals.MembersField) { + for _, m := range members { + out.Members = append(out.Members, &pb.Member{ + Id: m.GetMemberId(), + ScopeId: m.GetMemberScopeId(), + }) + } + } + return &out, nil } // A validateX method should exist for each method above. These methods do not make calls to any backing service but enforce diff --git a/internal/servers/controller/handlers/groups/group_service_test.go b/internal/servers/controller/handlers/groups/group_service_test.go index 4ad49b887..b22e1f530 100644 --- a/internal/servers/controller/handlers/groups/group_service_test.go +++ b/internal/servers/controller/handlers/groups/group_service_test.go @@ -319,12 +319,26 @@ func TestList(t *testing.T) { s, err := groups.NewService(repoFn) require.NoError(err, "Couldn't create new group service.") + // Test with a non-anon user got, gErr := s.ListGroups(auth.DisabledAuthTestContext(repoFn, tc.req.GetScopeId()), tc.req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "ListGroups(%q) got error %v, wanted %v", tc.req.GetScopeId(), gErr, tc.err) + return } + require.NoError(gErr) assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListGroups(%q) got response %q, wanted %q", tc.req.GetScopeId(), got, tc.res) + + // Test the anon case + got, gErr = s.ListGroups(auth.DisabledAuthTestContext(repoFn, tc.req.GetScopeId(), auth.WithUserId(auth.AnonymousUserId)), tc.req) + require.NoError(gErr) + assert.Len(got.Items, len(tc.res.Items)) + for _, item := range got.GetItems() { + require.Nil(item.CreatedTime) + require.Nil(item.Members) + require.Nil(item.UpdatedTime) + require.Zero(item.Version) + } }) } } diff --git a/internal/servers/controller/handlers/host_catalogs/host_catalog_service.go b/internal/servers/controller/handlers/host_catalogs/host_catalog_service.go index e552dedd6..3348f4d13 100644 --- a/internal/servers/controller/handlers/host_catalogs/host_catalog_service.go +++ b/internal/servers/controller/handlers/host_catalogs/host_catalog_service.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/auth" "github.com/hashicorp/boundary/internal/errors" pb "github.com/hashicorp/boundary/internal/gen/controller/api/resources/hostcatalogs" @@ -12,6 +13,7 @@ import ( "github.com/hashicorp/boundary/internal/host/static" "github.com/hashicorp/boundary/internal/host/static/store" "github.com/hashicorp/boundary/internal/perms" + "github.com/hashicorp/boundary/internal/requests" "github.com/hashicorp/boundary/internal/servers/controller/common" "github.com/hashicorp/boundary/internal/servers/controller/common/scopeids" "github.com/hashicorp/boundary/internal/servers/controller/handlers" @@ -20,9 +22,7 @@ import ( "github.com/hashicorp/boundary/internal/types/action" "github.com/hashicorp/boundary/internal/types/resource" "github.com/hashicorp/boundary/internal/types/scope" - "github.com/hashicorp/boundary/sdk/strutil" "google.golang.org/grpc/codes" - "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/wrapperspb" ) @@ -67,33 +67,6 @@ type Service struct { var _ pbs.HostCatalogServiceServer = Service{} -func populateCollectionAuthorizedActions(ctx context.Context, - authResults auth.VerifyResults, - item *pb.HostCatalog) error { - res := &perms.Resource{ - ScopeId: authResults.Scope.Id, - Pin: item.Id, - } - // Range over the defined collections and check permissions against those - // collections. We use the ID of this scope being returned, not its parent, - // hence passing in a resource here. - for k, v := range collectionTypeMap { - res.Type = k - acts := authResults.FetchActionSetForType(ctx, k, v, auth.WithResource(res)).Strings() - if len(acts) > 0 { - if item.AuthorizedCollectionActions == nil { - item.AuthorizedCollectionActions = make(map[string]*structpb.ListValue) - } - lv, err := structpb.NewList(strutil.StringListToInterfaceList(acts)) - if err != nil { - return err - } - item.AuthorizedCollectionActions[k.String()+"s"] = lv - } - } - return nil -} - // NewService returns a host catalog Service which handles host catalog related requests to boundary and uses the provided // repositories for storage and retrieval. func NewService(repoFn common.StaticRepoFactory, iamRepoFn common.IamRepoFactory) (Service, error) { @@ -138,27 +111,50 @@ func (s Service) ListHostCatalogs(ctx context.Context, req *pbs.ListHostCatalogs if err != nil { return nil, err } + if len(ul) == 0 { + return &pbs.ListHostCatalogsResponse{}, nil + } filter, err := handlers.NewFilter(req.GetFilter()) if err != nil { return nil, err } finalItems := make([]*pb.HostCatalog, 0, len(ul)) - res := &perms.Resource{ + res := perms.Resource{ Type: resource.HostCatalog, } for _, item := range ul { - item.Scope = scopeInfoMap[item.GetScopeId()] - res.ScopeId = item.Scope.Id - item.AuthorizedActions = authResults.FetchActionSetForId(ctx, item.Id, IdActions, auth.WithResource(res)).Strings() - if len(item.AuthorizedActions) == 0 { + res.Id = item.GetPublicId() + res.ScopeId = item.GetScopeId() + authorizedActions := authResults.FetchActionSetForId(ctx, item.GetPublicId(), IdActions, auth.WithResource(&res)).Strings() + if len(authorizedActions) == 0 { continue } - if filter.Match(item) { - finalItems = append(finalItems, item) - if err := populateCollectionAuthorizedActions(ctx, authResults, item); err != nil { + + 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(scopeInfoMap[item.GetScopeId()])) + } + if outputFields.Has(globals.AuthorizedActionsField) { + outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authorizedActions)) + } + if outputFields.Has(globals.AuthorizedCollectionActionsField) { + collectionActions, err := auth.CalculateAuthorizedCollectionActions(ctx, authResults, collectionTypeMap, authResults.Scope.Id, item.GetPublicId()) + if err != nil { return nil, err } + outputOpts = append(outputOpts, handlers.WithAuthorizedCollectionActions(collectionActions)) + } + + item, err := toProto(ctx, item, outputOpts...) + if err != nil { + return nil, err + } + + if filter.Match(item) { + finalItems = append(finalItems, item) } } return &pbs.ListHostCatalogsResponse{Items: finalItems}, nil @@ -166,6 +162,8 @@ func (s Service) ListHostCatalogs(ctx context.Context, req *pbs.ListHostCatalogs // GetHostCatalog implements the interface pbs.HostCatalogServiceServer. func (s Service) GetHostCatalog(ctx context.Context, req *pbs.GetHostCatalogRequest) (*pbs.GetHostCatalogResponse, error) { + const op = "host_catalogs.(Service).GetostCatalog" + if err := validateGetRequest(req); err != nil { return nil, err } @@ -177,16 +175,40 @@ func (s Service) GetHostCatalog(ctx context.Context, req *pbs.GetHostCatalogRequ if err != nil { return nil, err } - hc.Scope = authResults.Scope - hc.AuthorizedActions = authResults.FetchActionSetForId(ctx, hc.Id, IdActions).Strings() - if err := populateCollectionAuthorizedActions(ctx, authResults, hc); err != nil { + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, hc.GetPublicId(), IdActions).Strings())) + } + if outputFields.Has(globals.AuthorizedCollectionActionsField) { + collectionActions, err := auth.CalculateAuthorizedCollectionActions(ctx, authResults, collectionTypeMap, authResults.Scope.Id, hc.GetPublicId()) + if err != nil { + return nil, err + } + outputOpts = append(outputOpts, handlers.WithAuthorizedCollectionActions(collectionActions)) + } + + item, err := toProto(ctx, hc, outputOpts...) + if err != nil { return nil, err } - return &pbs.GetHostCatalogResponse{Item: hc}, nil + + return &pbs.GetHostCatalogResponse{Item: item}, nil } // CreateHostCatalog implements the interface pbs.HostCatalogServiceServer. func (s Service) CreateHostCatalog(ctx context.Context, req *pbs.CreateHostCatalogRequest) (*pbs.CreateHostCatalogResponse, error) { + const op = "host_catalogs.(Service).CreateHostCatalog" + if err := validateCreateRequest(req); err != nil { return nil, err } @@ -198,19 +220,43 @@ func (s Service) CreateHostCatalog(ctx context.Context, req *pbs.CreateHostCatal if err != nil { return nil, err } - hc.Scope = authResults.Scope - hc.AuthorizedActions = authResults.FetchActionSetForId(ctx, hc.Id, IdActions).Strings() - if err := populateCollectionAuthorizedActions(ctx, authResults, hc); err != nil { + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, hc.GetPublicId(), IdActions).Strings())) + } + if outputFields.Has(globals.AuthorizedCollectionActionsField) { + collectionActions, err := auth.CalculateAuthorizedCollectionActions(ctx, authResults, collectionTypeMap, authResults.Scope.Id, hc.GetPublicId()) + if err != nil { + return nil, err + } + outputOpts = append(outputOpts, handlers.WithAuthorizedCollectionActions(collectionActions)) + } + + item, err := toProto(ctx, hc, outputOpts...) + if err != nil { return nil, err } + return &pbs.CreateHostCatalogResponse{ - Item: hc, - Uri: fmt.Sprintf("host-catalogs/%s", hc.GetId()), + Item: item, + Uri: fmt.Sprintf("host-catalogs/%s", item.GetId()), }, nil } // UpdateHostCatalog implements the interface pbs.HostCatalogServiceServer. func (s Service) UpdateHostCatalog(ctx context.Context, req *pbs.UpdateHostCatalogRequest) (*pbs.UpdateHostCatalogResponse, error) { + const op = "host_catalogs.(Service).UpdateHostCatalog" + if err := validateUpdateRequest(req); err != nil { return nil, err } @@ -222,12 +268,34 @@ func (s Service) UpdateHostCatalog(ctx context.Context, req *pbs.UpdateHostCatal if err != nil { return nil, err } - hc.Scope = authResults.Scope - hc.AuthorizedActions = authResults.FetchActionSetForId(ctx, hc.Id, IdActions).Strings() - if err := populateCollectionAuthorizedActions(ctx, authResults, hc); err != nil { + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, hc.GetPublicId(), IdActions).Strings())) + } + if outputFields.Has(globals.AuthorizedCollectionActionsField) { + collectionActions, err := auth.CalculateAuthorizedCollectionActions(ctx, authResults, collectionTypeMap, authResults.Scope.Id, hc.GetPublicId()) + if err != nil { + return nil, err + } + outputOpts = append(outputOpts, handlers.WithAuthorizedCollectionActions(collectionActions)) + } + + item, err := toProto(ctx, hc, outputOpts...) + if err != nil { return nil, err } - return &pbs.UpdateHostCatalogResponse{Item: hc}, nil + + return &pbs.UpdateHostCatalogResponse{Item: item}, nil } // DeleteHostCatalog implements the interface pbs.HostCatalogServiceServer. @@ -246,7 +314,7 @@ func (s Service) DeleteHostCatalog(ctx context.Context, req *pbs.DeleteHostCatal return nil, nil } -func (s Service) getFromRepo(ctx context.Context, id string) (*pb.HostCatalog, error) { +func (s Service) getFromRepo(ctx context.Context, id string) (*static.HostCatalog, error) { repo, err := s.staticRepoFn() if err != nil { return nil, err @@ -258,10 +326,10 @@ func (s Service) getFromRepo(ctx context.Context, id string) (*pb.HostCatalog, e if hc == nil { return nil, handlers.NotFoundErrorf("Host Catalog %q doesn't exist.", id) } - return toProto(hc), nil + return hc, nil } -func (s Service) listFromRepo(ctx context.Context, scopeIds []string) ([]*pb.HostCatalog, error) { +func (s Service) listFromRepo(ctx context.Context, scopeIds []string) ([]*static.HostCatalog, error) { repo, err := s.staticRepoFn() if err != nil { return nil, err @@ -270,14 +338,10 @@ func (s Service) listFromRepo(ctx context.Context, scopeIds []string) ([]*pb.Hos if err != nil { return nil, err } - var outUl []*pb.HostCatalog - for _, u := range ul { - outUl = append(outUl, toProto(u)) - } - return outUl, nil + return ul, nil } -func (s Service) createInRepo(ctx context.Context, projId string, item *pb.HostCatalog) (*pb.HostCatalog, error) { +func (s Service) createInRepo(ctx context.Context, projId string, item *pb.HostCatalog) (*static.HostCatalog, error) { const op = "host_catalogs.(Servivce).createInRepo" var opts []static.Option if item.GetName() != nil { @@ -301,11 +365,11 @@ func (s Service) createInRepo(ctx context.Context, projId string, item *pb.HostC if out == nil { return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create host catalog but no error returned from repository.") } - return toProto(out), nil + return out, nil } -func (s Service) updateInRepo(ctx context.Context, projId, id string, mask []string, item *pb.HostCatalog) (*pb.HostCatalog, error) { - const op = "service.(Service).updateInRepo" +func (s Service) updateInRepo(ctx context.Context, projId, id string, mask []string, item *pb.HostCatalog) (*static.HostCatalog, error) { + const op = "host_catalogs.(Service).updateInRepo" var opts []static.Option if desc := item.GetDescription(); desc != nil { opts = append(opts, static.WithDescription(desc.GetValue())) @@ -334,11 +398,11 @@ func (s Service) updateInRepo(ctx context.Context, projId, id string, mask []str if rowsUpdated == 0 { return nil, handlers.NotFoundErrorf("Host Catalog %q doesn't exist or incorrect version provided.", id) } - return toProto(out), nil + return out, nil } func (s Service) deleteFromRepo(ctx context.Context, id string) (bool, error) { - const op = "service.(Service).deleteFromRepo" + const op = "host_catalogs.(Service).deleteFromRepo" repo, err := s.staticRepoFn() if err != nil { return false, errors.Wrap(err, op) @@ -394,22 +458,48 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type) auth. return auth.Verify(ctx, opts...) } -func toProto(in *static.HostCatalog) *pb.HostCatalog { - out := pb.HostCatalog{ - Id: in.GetPublicId(), - ScopeId: in.GetScopeId(), - CreatedTime: in.GetCreateTime().GetTimestamp(), - UpdatedTime: in.GetUpdateTime().GetTimestamp(), - Version: in.GetVersion(), - Type: host.StaticSubtype.String(), +func toProto(ctx context.Context, in *static.HostCatalog, opt ...handlers.Option) (*pb.HostCatalog, error) { + opts := handlers.GetOpts(opt...) + if opts.WithOutputFields == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "output fields not found when building auth method proto") } - if in.GetDescription() != "" { - out.Description = &wrapperspb.StringValue{Value: in.GetDescription()} + outputFields := *opts.WithOutputFields + + out := pb.HostCatalog{} + if outputFields.Has(globals.IdField) { + out.Id = in.GetPublicId() } - if in.GetName() != "" { - out.Name = &wrapperspb.StringValue{Value: in.GetName()} + if outputFields.Has(globals.ScopeIdField) { + out.ScopeId = in.GetScopeId() } - return &out + if outputFields.Has(globals.TypeField) { + out.Type = host.StaticSubtype.String() + } + 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 + } + if outputFields.Has(globals.AuthorizedCollectionActionsField) { + out.AuthorizedCollectionActions = opts.WithAuthorizedCollectionActions + } + return &out, nil } // A validateX method should exist for each method above. These methods do not make calls to any backing service but enforce diff --git a/internal/servers/controller/handlers/host_catalogs/host_catalog_service_test.go b/internal/servers/controller/handlers/host_catalogs/host_catalog_service_test.go index d7b341bda..7cba7f117 100644 --- a/internal/servers/controller/handlers/host_catalogs/host_catalog_service_test.go +++ b/internal/servers/controller/handlers/host_catalogs/host_catalog_service_test.go @@ -259,12 +259,24 @@ func TestList(t *testing.T) { s, err := host_catalogs.NewService(repoFn, iamRepoFn) require.NoError(err, "Couldn't create new auth_method service.") + // Test with non-anon user got, gErr := s.ListHostCatalogs(auth.DisabledAuthTestContext(iamRepoFn, tc.req.GetScopeId()), tc.req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "ListHostCatalogs() for scope %q got error %v, wanted %v", tc.req.GetScopeId(), gErr, tc.err) + return } assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListHostCatalogs() for scope %q got response %q, wanted %q", tc.req.GetScopeId(), got, tc.res) + + // Test with anon user + got, gErr = s.ListHostCatalogs(auth.DisabledAuthTestContext(iamRepoFn, tc.req.GetScopeId(), auth.WithUserId("u_anon")), tc.req) + require.NoError(gErr) + assert.Len(got.Items, len(tc.res.Items)) + for _, item := range got.GetItems() { + require.Nil(item.CreatedTime) + require.Nil(item.UpdatedTime) + require.Zero(item.Version) + } }) } } diff --git a/internal/servers/controller/handlers/host_sets/host_set_service.go b/internal/servers/controller/handlers/host_sets/host_set_service.go index 816e99cda..6fff324e2 100644 --- a/internal/servers/controller/handlers/host_sets/host_set_service.go +++ b/internal/servers/controller/handlers/host_sets/host_set_service.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/auth" "github.com/hashicorp/boundary/internal/errors" pb "github.com/hashicorp/boundary/internal/gen/controller/api/resources/hostsets" @@ -12,6 +13,7 @@ import ( "github.com/hashicorp/boundary/internal/host/static" "github.com/hashicorp/boundary/internal/host/static/store" "github.com/hashicorp/boundary/internal/perms" + "github.com/hashicorp/boundary/internal/requests" "github.com/hashicorp/boundary/internal/servers/controller/common" "github.com/hashicorp/boundary/internal/servers/controller/handlers" "github.com/hashicorp/boundary/internal/types/action" @@ -81,22 +83,43 @@ func (s Service) ListHostSets(ctx context.Context, req *pbs.ListHostSetsRequest) if err != nil { return nil, err } + if len(hl) == 0 { + return &pbs.ListHostSetsResponse{}, nil + } + filter, err := handlers.NewFilter(req.GetFilter()) if err != nil { return nil, err } finalItems := make([]*pb.HostSet, 0, len(hl)) - res := &perms.Resource{ + + res := perms.Resource{ ScopeId: authResults.Scope.Id, Type: resource.HostSet, Pin: req.GetHostCatalogId(), } for _, item := range hl { - item.Scope = authResults.Scope - item.AuthorizedActions = authResults.FetchActionSetForId(ctx, item.Id, IdActions, auth.WithResource(res)).Strings() - if len(item.AuthorizedActions) == 0 { + res.Id = item.GetPublicId() + authorizedActions := authResults.FetchActionSetForId(ctx, item.GetPublicId(), IdActions, auth.WithResource(&res)).Strings() + if len(authorizedActions) == 0 { continue } + + 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)) + } + + item, err := toProto(ctx, item, nil, outputOpts...) + if err != nil { + return nil, err + } + if filter.Match(item) { finalItems = append(finalItems, item) } @@ -106,6 +129,8 @@ func (s Service) ListHostSets(ctx context.Context, req *pbs.ListHostSetsRequest) // GetHostSet implements the interface pbs.HostSetServiceServer. func (s Service) GetHostSet(ctx context.Context, req *pbs.GetHostSetRequest) (*pbs.GetHostSetResponse, error) { + const op = "host_sets.(Service).GetHostSet" + if err := validateGetRequest(req); err != nil { return nil, err } @@ -113,17 +138,37 @@ func (s Service) GetHostSet(ctx context.Context, req *pbs.GetHostSetRequest) (*p if authResults.Error != nil { return nil, authResults.Error } - hc, err := s.getFromRepo(ctx, req.GetId()) + hs, hosts, err := s.getFromRepo(ctx, req.GetId()) if err != nil { return nil, err } - hc.Scope = authResults.Scope - hc.AuthorizedActions = authResults.FetchActionSetForId(ctx, hc.Id, IdActions).Strings() - return &pbs.GetHostSetResponse{Item: hc}, nil + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, hs.GetPublicId(), IdActions).Strings())) + } + + item, err := toProto(ctx, hs, hosts, outputOpts...) + if err != nil { + return nil, err + } + + return &pbs.GetHostSetResponse{Item: item}, nil } // CreateHostSet implements the interface pbs.HostSetServiceServer. func (s Service) CreateHostSet(ctx context.Context, req *pbs.CreateHostSetRequest) (*pbs.CreateHostSetResponse, error) { + const op = "host_sets.(Service).CreateHostSet" + if err := validateCreateRequest(req); err != nil { return nil, err } @@ -131,20 +176,40 @@ func (s Service) CreateHostSet(ctx context.Context, req *pbs.CreateHostSetReques if authResults.Error != nil { return nil, authResults.Error } - h, err := s.createInRepo(ctx, authResults.Scope.GetId(), cat.GetPublicId(), req.GetItem()) + hs, err := s.createInRepo(ctx, authResults.Scope.GetId(), cat.GetPublicId(), req.GetItem()) if err != nil { return nil, err } - h.Scope = authResults.Scope - h.AuthorizedActions = authResults.FetchActionSetForId(ctx, h.Id, IdActions).Strings() + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, hs.GetPublicId(), IdActions).Strings())) + } + + item, err := toProto(ctx, hs, nil, outputOpts...) + if err != nil { + return nil, err + } + return &pbs.CreateHostSetResponse{ - Item: h, - Uri: fmt.Sprintf("host-sets/%s", h.GetId()), + Item: item, + Uri: fmt.Sprintf("host-sets/%s", item.GetId()), }, nil } // UpdateHostSet implements the interface pbs.HostSetServiceServer. func (s Service) UpdateHostSet(ctx context.Context, req *pbs.UpdateHostSetRequest) (*pbs.UpdateHostSetResponse, error) { + const op = "host_sets.(Service).UpdateHostSet" + if err := validateUpdateRequest(req); err != nil { return nil, err } @@ -152,13 +217,31 @@ func (s Service) UpdateHostSet(ctx context.Context, req *pbs.UpdateHostSetReques if authResults.Error != nil { return nil, authResults.Error } - hc, err := s.updateInRepo(ctx, authResults.Scope.GetId(), cat.GetPublicId(), req.GetId(), req.GetUpdateMask().GetPaths(), req.GetItem()) + hs, hosts, err := s.updateInRepo(ctx, authResults.Scope.GetId(), cat.GetPublicId(), req.GetId(), req.GetUpdateMask().GetPaths(), req.GetItem()) if err != nil { return nil, err } - hc.Scope = authResults.Scope - hc.AuthorizedActions = authResults.FetchActionSetForId(ctx, hc.Id, IdActions).Strings() - return &pbs.UpdateHostSetResponse{Item: hc}, nil + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, hs.GetPublicId(), IdActions).Strings())) + } + + item, err := toProto(ctx, hs, hosts, outputOpts...) + if err != nil { + return nil, err + } + + return &pbs.UpdateHostSetResponse{Item: item}, nil } // DeleteHostSet implements the interface pbs.HostSetServiceServer. @@ -179,6 +262,8 @@ func (s Service) DeleteHostSet(ctx context.Context, req *pbs.DeleteHostSetReques // AddHostSetHosts implements the interface pbs.HostSetServiceServer. func (s Service) AddHostSetHosts(ctx context.Context, req *pbs.AddHostSetHostsRequest) (*pbs.AddHostSetHostsResponse, error) { + const op = "host_sets.(Service).AddHostSetHosts" + if err := validateAddRequest(req); err != nil { return nil, err } @@ -186,17 +271,37 @@ func (s Service) AddHostSetHosts(ctx context.Context, req *pbs.AddHostSetHostsRe if authResults.Error != nil { return nil, authResults.Error } - g, err := s.addInRepo(ctx, authResults.Scope.GetId(), req.GetId(), req.GetHostIds(), req.GetVersion()) + hs, hosts, err := s.addInRepo(ctx, authResults.Scope.GetId(), req.GetId(), req.GetHostIds(), req.GetVersion()) if err != nil { return nil, err } - g.Scope = authResults.Scope - g.AuthorizedActions = authResults.FetchActionSetForId(ctx, g.Id, IdActions).Strings() - return &pbs.AddHostSetHostsResponse{Item: g}, nil + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, hs.GetPublicId(), IdActions).Strings())) + } + + item, err := toProto(ctx, hs, hosts, outputOpts...) + if err != nil { + return nil, err + } + + return &pbs.AddHostSetHostsResponse{Item: item}, nil } // SetHostSetHosts implements the interface pbs.HostSetServiceServer. func (s Service) SetHostSetHosts(ctx context.Context, req *pbs.SetHostSetHostsRequest) (*pbs.SetHostSetHostsResponse, error) { + const op = "host_sets.(Service).SetHostSetHosts" + if err := validateSetRequest(req); err != nil { return nil, err } @@ -204,17 +309,36 @@ func (s Service) SetHostSetHosts(ctx context.Context, req *pbs.SetHostSetHostsRe if authResults.Error != nil { return nil, authResults.Error } - g, err := s.setInRepo(ctx, authResults.Scope.GetId(), req.GetId(), req.GetHostIds(), req.GetVersion()) + hs, hosts, err := s.setInRepo(ctx, authResults.Scope.GetId(), req.GetId(), req.GetHostIds(), req.GetVersion()) if err != nil { return nil, err } - g.Scope = authResults.Scope - g.AuthorizedActions = authResults.FetchActionSetForId(ctx, g.Id, IdActions).Strings() - return &pbs.SetHostSetHostsResponse{Item: g}, nil + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, hs.GetPublicId(), IdActions).Strings())) + } + + item, err := toProto(ctx, hs, hosts, outputOpts...) + if err != nil { + return nil, err + } + + return &pbs.SetHostSetHostsResponse{Item: item}, nil } // RemoveHostSetHosts implements the interface pbs.HostSetServiceServer. func (s Service) RemoveHostSetHosts(ctx context.Context, req *pbs.RemoveHostSetHostsRequest) (*pbs.RemoveHostSetHostsResponse, error) { + const op = "host_sets.(Service).RemoveHostSetHosts" + if err := validateRemoveRequest(req); err != nil { return nil, err } @@ -222,31 +346,49 @@ func (s Service) RemoveHostSetHosts(ctx context.Context, req *pbs.RemoveHostSetH if authResults.Error != nil { return nil, authResults.Error } - g, err := s.removeInRepo(ctx, authResults.Scope.GetId(), req.GetId(), req.GetHostIds(), req.GetVersion()) + hs, hosts, err := s.removeInRepo(ctx, authResults.Scope.GetId(), req.GetId(), req.GetHostIds(), req.GetVersion()) if err != nil { return nil, err } - g.Scope = authResults.Scope - g.AuthorizedActions = authResults.FetchActionSetForId(ctx, g.Id, IdActions).Strings() - return &pbs.RemoveHostSetHostsResponse{Item: g}, nil -} -func (s Service) getFromRepo(ctx context.Context, id string) (*pb.HostSet, error) { - repo, err := s.staticRepoFn() + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, hs.GetPublicId(), IdActions).Strings())) + } + + item, err := toProto(ctx, hs, hosts, outputOpts...) if err != nil { return nil, err } + + return &pbs.RemoveHostSetHostsResponse{Item: item}, nil +} + +func (s Service) getFromRepo(ctx context.Context, id string) (*static.HostSet, []*static.Host, error) { + repo, err := s.staticRepoFn() + if err != nil { + return nil, nil, err + } h, m, err := repo.LookupSet(ctx, id) if err != nil { - return nil, err + return nil, nil, err } if h == nil { - return nil, handlers.NotFoundErrorf("Host Set %q doesn't exist.", id) + return nil, nil, handlers.NotFoundErrorf("Host Set %q doesn't exist.", id) } - return toProto(h, m), nil + return h, m, nil } -func (s Service) createInRepo(ctx context.Context, scopeId, catalogId string, item *pb.HostSet) (*pb.HostSet, error) { +func (s Service) createInRepo(ctx context.Context, scopeId, catalogId string, item *pb.HostSet) (*static.HostSet, error) { const op = "host_sets.(Service).createInRepo" var opts []static.Option if item.GetName() != nil { @@ -270,10 +412,10 @@ func (s Service) createInRepo(ctx context.Context, scopeId, catalogId string, it if out == nil { return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create host set but no error returned from repository.") } - return toProto(out, nil), nil + return out, nil } -func (s Service) updateInRepo(ctx context.Context, scopeId, catalogId, id string, mask []string, item *pb.HostSet) (*pb.HostSet, error) { +func (s Service) updateInRepo(ctx context.Context, scopeId, catalogId, id string, mask []string, item *pb.HostSet) (*static.HostSet, []*static.Host, error) { const op = "host_sets.(Service).updateInRepo" var opts []static.Option if desc := item.GetDescription(); desc != nil { @@ -284,25 +426,25 @@ func (s Service) updateInRepo(ctx context.Context, scopeId, catalogId, id string } h, err := static.NewHostSet(catalogId, opts...) if err != nil { - return nil, errors.Wrap(err, op, errors.WithMsg("Unable to build host set for update")) + return nil, nil, errors.Wrap(err, op, errors.WithMsg("Unable to build host set for update")) } h.PublicId = id dbMask := maskManager.Translate(mask) if len(dbMask) == 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."}) + return nil, nil, handlers.InvalidArgumentErrorf("No valid fields included in the update mask.", map[string]string{"update_mask": "No valid fields provided in the update mask."}) } repo, err := s.staticRepoFn() if err != nil { - return nil, err + return nil, nil, err } out, m, rowsUpdated, err := repo.UpdateSet(ctx, scopeId, h, item.GetVersion(), dbMask) if err != nil { - return nil, errors.Wrap(err, op, errors.WithMsg("unable to update host set")) + return nil, nil, errors.Wrap(err, op, errors.WithMsg("unable to update host set")) } if rowsUpdated == 0 { - return nil, handlers.NotFoundErrorf("Host Set %q doesn't exist or incorrect version provided.", id) + return nil, nil, handlers.NotFoundErrorf("Host Set %q doesn't exist or incorrect version provided.", id) } - return toProto(out, m), nil + return out, m, nil } func (s Service) deleteFromRepo(ctx context.Context, scopeId, id string) (bool, error) { @@ -318,7 +460,7 @@ func (s Service) deleteFromRepo(ctx context.Context, scopeId, id string) (bool, return rows > 0, nil } -func (s Service) listFromRepo(ctx context.Context, catalogId string) ([]*pb.HostSet, error) { +func (s Service) listFromRepo(ctx context.Context, catalogId string) ([]*static.HostSet, error) { repo, err := s.staticRepoFn() if err != nil { return nil, err @@ -327,72 +469,68 @@ func (s Service) listFromRepo(ctx context.Context, catalogId string) ([]*pb.Host if err != nil { return nil, err } - var outH []*pb.HostSet - for _, h := range hl { - outH = append(outH, toProto(h, nil)) - } - return outH, nil + return hl, nil } -func (s Service) addInRepo(ctx context.Context, scopeId, setId string, hostIds []string, version uint32) (*pb.HostSet, error) { +func (s Service) addInRepo(ctx context.Context, scopeId, setId string, hostIds []string, version uint32) (*static.HostSet, []*static.Host, error) { const op = "host_sets.(Service).addInRepo" repo, err := s.staticRepoFn() if err != nil { - return nil, err + return nil, nil, err } _, err = repo.AddSetMembers(ctx, scopeId, setId, version, strutil.RemoveDuplicates(hostIds, false)) if err != nil { - return nil, errors.Wrap(err, op, errors.WithMsg("Unable to add hosts to host set")) + return nil, nil, errors.Wrap(err, op, errors.WithMsg("Unable to add hosts to host set")) } out, m, err := repo.LookupSet(ctx, setId) if err != nil { - return nil, errors.Wrap(err, op, errors.WithMsg("unable to look up host set after adding hosts")) + return nil, nil, errors.Wrap(err, op, errors.WithMsg("unable to look up host set after adding hosts")) } if out == nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup host set after adding hosts to it.") + return nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup host set after adding hosts to it.") } - return toProto(out, m), nil + return out, m, nil } -func (s Service) setInRepo(ctx context.Context, scopeId, setId string, hostIds []string, version uint32) (*pb.HostSet, error) { +func (s Service) setInRepo(ctx context.Context, scopeId, setId string, hostIds []string, version uint32) (*static.HostSet, []*static.Host, error) { const op = "host_sets.(Service).setInRepo" repo, err := s.staticRepoFn() if err != nil { - return nil, errors.Wrap(err, op) + return nil, nil, errors.Wrap(err, op) } _, _, err = repo.SetSetMembers(ctx, scopeId, setId, version, strutil.RemoveDuplicates(hostIds, false)) if err != nil { - return nil, errors.Wrap(err, op, errors.WithMsg("Unable to set hosts in host set")) + return nil, nil, errors.Wrap(err, op, errors.WithMsg("Unable to set hosts in host set")) } out, m, err := repo.LookupSet(ctx, setId) if err != nil { - return nil, errors.Wrap(err, op, errors.WithMsg("unable to look up host set after setting hosts")) + return nil, nil, errors.Wrap(err, op, errors.WithMsg("unable to look up host set after setting hosts")) } if out == nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup host set after setting hosts for it.") + return nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup host set after setting hosts for it.") } - return toProto(out, m), nil + return out, m, nil } -func (s Service) removeInRepo(ctx context.Context, scopeId, setId string, hostIds []string, version uint32) (*pb.HostSet, error) { +func (s Service) removeInRepo(ctx context.Context, scopeId, setId string, hostIds []string, version uint32) (*static.HostSet, []*static.Host, error) { const op = "host_sets.(Service).removeInRepo" repo, err := s.staticRepoFn() if err != nil { - return nil, errors.Wrap(err, op) + return nil, nil, errors.Wrap(err, op) } _, err = repo.DeleteSetMembers(ctx, scopeId, setId, version, strutil.RemoveDuplicates(hostIds, false)) if err != nil { - return nil, errors.Wrap(err, op, errors.WithMsg("Unable to remove hosts from host set")) + return nil, nil, errors.Wrap(err, op, errors.WithMsg("Unable to remove hosts from host set")) } out, m, err := repo.LookupSet(ctx, setId) if err != nil { - return nil, errors.Wrap(err, op, errors.WithMsg("unable to look up host set")) + return nil, nil, errors.Wrap(err, op, errors.WithMsg("unable to look up host set")) } if out == nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup host set after removing hosts from it.") + return nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup host set after removing hosts from it.") } - return toProto(out, m), nil + return out, m, nil } func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Type) (*static.HostCatalog, auth.VerifyResults) { @@ -435,25 +573,50 @@ func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Ty return cat, auth.Verify(ctx, opts...) } -func toProto(in *static.HostSet, hs []*static.Host) *pb.HostSet { - out := pb.HostSet{ - Id: in.GetPublicId(), - HostCatalogId: in.GetCatalogId(), - Type: host.StaticSubtype.String(), - CreatedTime: in.GetCreateTime().GetTimestamp(), - UpdatedTime: in.GetUpdateTime().GetTimestamp(), - Version: in.GetVersion(), +func toProto(ctx context.Context, in *static.HostSet, hosts []*static.Host, opt ...handlers.Option) (*pb.HostSet, error) { + opts := handlers.GetOpts(opt...) + if opts.WithOutputFields == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "output fields not found when building hostset proto") } - if in.GetDescription() != "" { - out.Description = wrapperspb.String(in.GetDescription()) + outputFields := *opts.WithOutputFields + + out := pb.HostSet{} + if outputFields.Has(globals.IdField) { + out.Id = in.GetPublicId() } - if in.GetName() != "" { - out.Name = wrapperspb.String(in.GetName()) + if outputFields.Has(globals.HostCatalogIdField) { + out.HostCatalogId = in.GetCatalogId() } - for _, h := range hs { - out.HostIds = append(out.HostIds, h.GetPublicId()) + if outputFields.Has(globals.TypeField) { + out.Type = host.StaticSubtype.String() } - return &out + if outputFields.Has(globals.DescriptionField) && in.GetDescription() != "" { + out.Description = &wrapperspb.StringValue{Value: in.GetDescription()} + } + if outputFields.Has(globals.NameField) && in.GetName() != "" { + out.Name = &wrapperspb.StringValue{Value: 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 + } + if outputFields.Has(globals.HostIdsField) { + for _, h := range hosts { + out.HostIds = append(out.HostIds, h.GetPublicId()) + } + } + return &out, nil } // A validateX method should exist for each method above. These methods do not make calls to any backing service but enforce diff --git a/internal/servers/controller/handlers/host_sets/host_set_service_test.go b/internal/servers/controller/handlers/host_sets/host_set_service_test.go index 42707ec89..36cb5bbda 100644 --- a/internal/servers/controller/handlers/host_sets/host_set_service_test.go +++ b/internal/servers/controller/handlers/host_sets/host_set_service_test.go @@ -194,14 +194,26 @@ func TestList(t *testing.T) { s, err := host_sets.NewService(repoFn) require.NoError(err, "Couldn't create new host set service.") + // Test with non-anon user got, gErr := s.ListHostSets(auth.DisabledAuthTestContext(iamRepoFn, proj.GetPublicId()), tc.req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "ListHostSets(%q) got error %v, wanted %v", tc.req, gErr, tc.err) - } else { - require.NoError(gErr) + return } + require.NoError(gErr) assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListHostSets(%q) got response %q, wanted %q", tc.req, got, tc.res) + + // Test with anon user + got, gErr = s.ListHostSets(auth.DisabledAuthTestContext(iamRepoFn, proj.GetPublicId(), auth.WithUserId(auth.AnonymousUserId)), tc.req) + require.NoError(gErr) + assert.Len(got.Items, len(tc.res.Items)) + for _, item := range got.GetItems() { + require.Nil(item.CreatedTime) + require.Nil(item.UpdatedTime) + require.Zero(item.Version) + require.Empty(item.HostIds) + } }) } } diff --git a/internal/servers/controller/handlers/hosts/host_service.go b/internal/servers/controller/handlers/hosts/host_service.go index cd1400dbb..ed7185635 100644 --- a/internal/servers/controller/handlers/hosts/host_service.go +++ b/internal/servers/controller/handlers/hosts/host_service.go @@ -6,6 +6,7 @@ import ( "net" "strings" + "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/auth" "github.com/hashicorp/boundary/internal/errors" pb "github.com/hashicorp/boundary/internal/gen/controller/api/resources/hosts" @@ -14,6 +15,7 @@ import ( "github.com/hashicorp/boundary/internal/host/static" "github.com/hashicorp/boundary/internal/host/static/store" "github.com/hashicorp/boundary/internal/perms" + "github.com/hashicorp/boundary/internal/requests" "github.com/hashicorp/boundary/internal/servers/controller/common" "github.com/hashicorp/boundary/internal/servers/controller/handlers" "github.com/hashicorp/boundary/internal/types/action" @@ -79,22 +81,43 @@ func (s Service) ListHosts(ctx context.Context, req *pbs.ListHostsRequest) (*pbs if err != nil { return nil, err } + if len(hl) == 0 { + return &pbs.ListHostsResponse{}, nil + } + filter, err := handlers.NewFilter(req.GetFilter()) if err != nil { return nil, err } finalItems := make([]*pb.Host, 0, len(hl)) - res := &perms.Resource{ + + res := perms.Resource{ ScopeId: authResults.Scope.Id, Type: resource.Host, Pin: req.GetHostCatalogId(), } for _, item := range hl { - item.Scope = authResults.Scope - item.AuthorizedActions = authResults.FetchActionSetForId(ctx, item.Id, IdActions, auth.WithResource(res)).Strings() - if len(item.AuthorizedActions) == 0 { + res.Id = item.GetPublicId() + authorizedActions := authResults.FetchActionSetForId(ctx, item.GetPublicId(), IdActions, auth.WithResource(&res)).Strings() + if len(authorizedActions) == 0 { continue } + + 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)) + } + + item, err := toProto(ctx, item, nil, outputOpts...) + if err != nil { + return nil, err + } + if filter.Match(item) { finalItems = append(finalItems, item) } @@ -104,6 +127,8 @@ func (s Service) ListHosts(ctx context.Context, req *pbs.ListHostsRequest) (*pbs // GetHost implements the interface pbs.HostServiceServer. func (s Service) GetHost(ctx context.Context, req *pbs.GetHostRequest) (*pbs.GetHostResponse, error) { + const op = "hosts.(Service).GetHost" + if err := validateGetRequest(req); err != nil { return nil, err } @@ -111,17 +136,37 @@ func (s Service) GetHost(ctx context.Context, req *pbs.GetHostRequest) (*pbs.Get if authResults.Error != nil { return nil, authResults.Error } - hc, err := s.getFromRepo(ctx, req.GetId()) + h, err := s.getFromRepo(ctx, req.GetId()) if err != nil { return nil, err } - hc.Scope = authResults.Scope - hc.AuthorizedActions = authResults.FetchActionSetForId(ctx, hc.Id, IdActions).Strings() - return &pbs.GetHostResponse{Item: hc}, nil + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, h.GetPublicId(), IdActions).Strings())) + } + + item, err := toProto(ctx, h, nil, outputOpts...) + if err != nil { + return nil, err + } + + return &pbs.GetHostResponse{Item: item}, nil } // CreateHost implements the interface pbs.HostServiceServer. func (s Service) CreateHost(ctx context.Context, req *pbs.CreateHostRequest) (*pbs.CreateHostResponse, error) { + const op = "hosts.(Service).CreateHost" + if err := validateCreateRequest(req); err != nil { return nil, err } @@ -133,16 +178,36 @@ func (s Service) CreateHost(ctx context.Context, req *pbs.CreateHostRequest) (*p if err != nil { return nil, err } - h.Scope = authResults.Scope - h.AuthorizedActions = authResults.FetchActionSetForId(ctx, h.Id, IdActions).Strings() + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, h.GetPublicId(), IdActions).Strings())) + } + + item, err := toProto(ctx, h, nil, outputOpts...) + if err != nil { + return nil, err + } + return &pbs.CreateHostResponse{ - Item: h, - Uri: fmt.Sprintf("hosts/%s", h.GetId()), + Item: item, + Uri: fmt.Sprintf("hosts/%s", item.GetId()), }, nil } // UpdateHost implements the interface pbs.HostServiceServer. func (s Service) UpdateHost(ctx context.Context, req *pbs.UpdateHostRequest) (*pbs.UpdateHostResponse, error) { + const op = "hosts.(Service).UpdateHost" + if err := validateUpdateRequest(req); err != nil { return nil, err } @@ -150,13 +215,31 @@ func (s Service) UpdateHost(ctx context.Context, req *pbs.UpdateHostRequest) (*p if authResults.Error != nil { return nil, authResults.Error } - hc, err := s.updateInRepo(ctx, authResults.Scope.GetId(), cat.GetPublicId(), req.GetId(), req.GetUpdateMask().GetPaths(), req.GetItem()) + h, err := s.updateInRepo(ctx, authResults.Scope.GetId(), cat.GetPublicId(), req.GetId(), req.GetUpdateMask().GetPaths(), req.GetItem()) if err != nil { return nil, err } - hc.Scope = authResults.Scope - hc.AuthorizedActions = authResults.FetchActionSetForId(ctx, hc.Id, IdActions).Strings() - return &pbs.UpdateHostResponse{Item: hc}, nil + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, h.GetPublicId(), IdActions).Strings())) + } + + item, err := toProto(ctx, h, nil, outputOpts...) + if err != nil { + return nil, err + } + + return &pbs.UpdateHostResponse{Item: item}, nil } // DeleteHost implements the interface pbs.HostServiceServer. @@ -175,7 +258,7 @@ func (s Service) DeleteHost(ctx context.Context, req *pbs.DeleteHostRequest) (*p return nil, nil } -func (s Service) getFromRepo(ctx context.Context, id string) (*pb.Host, error) { +func (s Service) getFromRepo(ctx context.Context, id string) (*static.Host, error) { repo, err := s.staticRepoFn() if err != nil { return nil, err @@ -187,10 +270,10 @@ func (s Service) getFromRepo(ctx context.Context, id string) (*pb.Host, error) { if h == nil { return nil, handlers.NotFoundErrorf("Host %q doesn't exist.", id) } - return toProto(h, nil) + return h, nil } -func (s Service) createInRepo(ctx context.Context, scopeId, catalogId string, item *pb.Host) (*pb.Host, error) { +func (s Service) createInRepo(ctx context.Context, scopeId, catalogId string, item *pb.Host) (*static.Host, error) { const op = "hosts.(Service).createInRepo" ha := &pb.StaticHostAttributes{} if err := handlers.StructToProto(item.GetAttributes(), ha); err != nil { @@ -222,10 +305,10 @@ func (s Service) createInRepo(ctx context.Context, scopeId, catalogId string, it if out == nil { return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create host but no error returned from repository.") } - return toProto(out, nil) + return out, nil } -func (s Service) updateInRepo(ctx context.Context, scopeId, catalogId, id string, mask []string, item *pb.Host) (*pb.Host, error) { +func (s Service) updateInRepo(ctx context.Context, scopeId, catalogId, id string, mask []string, item *pb.Host) (*static.Host, error) { const op = "hosts.(Service).updateInRepo" ha := &pb.StaticHostAttributes{} if err := handlers.StructToProto(item.GetAttributes(), ha); err != nil { @@ -261,7 +344,7 @@ func (s Service) updateInRepo(ctx context.Context, scopeId, catalogId, id string if rowsUpdated == 0 { return nil, handlers.NotFoundErrorf("Host %q doesn't exist or incorrect version provided.", id) } - return toProto(out, nil) + return out, nil } func (s Service) deleteFromRepo(ctx context.Context, scopeId, id string) (bool, error) { @@ -277,7 +360,7 @@ func (s Service) deleteFromRepo(ctx context.Context, scopeId, id string) (bool, return rows > 0, nil } -func (s Service) listFromRepo(ctx context.Context, catalogId string) ([]*pb.Host, error) { +func (s Service) listFromRepo(ctx context.Context, catalogId string) ([]*static.Host, error) { repo, err := s.staticRepoFn() if err != nil { return nil, err @@ -286,15 +369,7 @@ func (s Service) listFromRepo(ctx context.Context, catalogId string) ([]*pb.Host if err != nil { return nil, err } - var outHl []*pb.Host - for _, h := range hl { - p, err := toProto(h, nil) - if err != nil { - return nil, err - } - outHl = append(outHl, p) - } - return outHl, nil + return hl, nil } func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Type) (*static.HostCatalog, auth.VerifyResults) { @@ -337,29 +412,56 @@ func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Ty return cat, auth.Verify(ctx, opts...) } -func toProto(in *static.Host, members []*static.HostSet) (*pb.Host, error) { - out := pb.Host{ - Id: in.GetPublicId(), - HostCatalogId: in.GetCatalogId(), - Type: host.StaticSubtype.String(), - CreatedTime: in.GetCreateTime().GetTimestamp(), - UpdatedTime: in.GetUpdateTime().GetTimestamp(), - Version: in.GetVersion(), +func toProto(ctx context.Context, in *static.Host, hostSets []*static.HostSet, opt ...handlers.Option) (*pb.Host, error) { + opts := handlers.GetOpts(opt...) + if opts.WithOutputFields == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "output fields not found when building host proto") } - if in.GetDescription() != "" { - out.Description = wrapperspb.String(in.GetDescription()) + outputFields := *opts.WithOutputFields + + out := pb.Host{} + if outputFields.Has(globals.IdField) { + out.Id = in.GetPublicId() } - if in.GetName() != "" { - out.Name = wrapperspb.String(in.GetName()) + if outputFields.Has(globals.HostCatalogIdField) { + out.HostCatalogId = in.GetCatalogId() } - for _, m := range members { - out.HostSetIds = append(out.HostSetIds, m.GetPublicId()) + if outputFields.Has(globals.TypeField) { + out.Type = host.StaticSubtype.String() } - st, err := handlers.ProtoToStruct(&pb.StaticHostAttributes{Address: wrapperspb.String(in.GetAddress())}) - if err != nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to convert static attribute to struct: %s", err) + if outputFields.Has(globals.DescriptionField) && in.GetDescription() != "" { + out.Description = &wrapperspb.StringValue{Value: in.GetDescription()} + } + if outputFields.Has(globals.NameField) && in.GetName() != "" { + out.Name = &wrapperspb.StringValue{Value: 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 + } + if outputFields.Has(globals.HostSetIdsField) { + for _, hs := range hostSets { + out.HostSetIds = append(out.HostSetIds, hs.GetPublicId()) + } + } + if outputFields.Has(globals.AttributesField) { + st, err := handlers.ProtoToStruct(&pb.StaticHostAttributes{Address: wrapperspb.String(in.GetAddress())}) + if err != nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to convert static attribute to struct: %s", err) + } + out.Attributes = st } - out.Attributes = st return &out, nil } diff --git a/internal/servers/controller/handlers/hosts/host_service_test.go b/internal/servers/controller/handlers/hosts/host_service_test.go index 15a172489..71bf1ec8a 100644 --- a/internal/servers/controller/handlers/hosts/host_service_test.go +++ b/internal/servers/controller/handlers/hosts/host_service_test.go @@ -189,14 +189,25 @@ func TestList(t *testing.T) { s, err := hosts.NewService(repoFn) require.NoError(err, "Couldn't create new host set service.") + // Test non-anonymous listing got, gErr := s.ListHosts(auth.DisabledAuthTestContext(iamRepoFn, proj.GetPublicId()), tc.req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "ListHosts(%q) got error %v, wanted %v", tc.req, gErr, tc.err) - } else { - require.NoError(gErr) + return } + require.NoError(gErr) assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListHosts(%q) got response %q, wanted %q", tc.req, got, tc.res) + + // Test anonymous listing + got, gErr = s.ListHosts(auth.DisabledAuthTestContext(iamRepoFn, proj.GetPublicId(), auth.WithUserId(auth.AnonymousUserId)), tc.req) + require.NoError(gErr) + assert.Len(got.Items, len(tc.res.Items)) + for _, item := range got.GetItems() { + require.Nil(item.CreatedTime) + require.Nil(item.UpdatedTime) + require.Zero(item.Version) + } }) } } diff --git a/internal/servers/controller/handlers/option_test.go b/internal/servers/controller/handlers/option_test.go index ee40e0540..2e806db12 100644 --- a/internal/servers/controller/handlers/option_test.go +++ b/internal/servers/controller/handlers/option_test.go @@ -3,8 +3,10 @@ package handlers import ( "testing" + "github.com/hashicorp/boundary/internal/perms" "github.com/hashicorp/go-hclog" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // Test_GetOpts provides unit tests for GetOpts and all the options @@ -36,14 +38,26 @@ func Test_GetOpts(t *testing.T) { opts = GetOpts(WithLogger(hclog.New(nil))) assert.NotNil(opts.WithLogger) }) - - t.Run("WithAnonymousListing", func(t *testing.T) { + t.Run("WithUserIsAnonymous", func(t *testing.T) { assert := assert.New(t) opts := GetOpts() - assert.False(opts.WithAnonymousListing) + assert.False(opts.WithUserIsAnonymous) - opts = GetOpts(WithAnonymousListing(true)) - assert.True(opts.WithAnonymousListing) + opts = GetOpts(WithUserIsAnonymous(true)) + assert.True(opts.WithUserIsAnonymous) + }) + t.Run("WithOutputFields", func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + opts := GetOpts() + assert.Nil(opts.WithOutputFields) + + var out perms.OutputFieldsMap + + opts = GetOpts(WithOutputFields(&out)) + require.NotNil(opts.WithOutputFields) + assert.Nil(*opts.WithOutputFields) }) } diff --git a/internal/servers/controller/handlers/options.go b/internal/servers/controller/handlers/options.go index 383a4aaff..50265a352 100644 --- a/internal/servers/controller/handlers/options.go +++ b/internal/servers/controller/handlers/options.go @@ -1,6 +1,11 @@ package handlers -import "github.com/hashicorp/go-hclog" +import ( + "github.com/hashicorp/boundary/internal/gen/controller/api/resources/scopes" + "github.com/hashicorp/boundary/internal/perms" + "github.com/hashicorp/go-hclog" + "google.golang.org/protobuf/types/known/structpb" +) // GetOpts - iterate the inbound Options and return a struct func GetOpts(opt ...Option) options { @@ -18,9 +23,13 @@ type Option func(*options) // options = how options are represented type options struct { - withDiscardUnknownFields bool - WithLogger hclog.Logger - WithAnonymousListing bool + withDiscardUnknownFields bool + WithLogger hclog.Logger + WithUserIsAnonymous bool + WithOutputFields *perms.OutputFieldsMap + WithScope *scopes.ScopeInfo + WithAuthorizedActions []string + WithAuthorizedCollectionActions map[string]*structpb.ListValue } func getDefaultOptions() options { @@ -43,10 +52,42 @@ func WithLogger(logger hclog.Logger) Option { } } -// WithAnonymousListing provides an option when creating responses to only include those -// desired for listing to anonymous users. -func WithAnonymousListing(anonListing bool) Option { +// DEPRECATED: Superceded by WithOutputFields. Will be removed once all handlers +// have been migrated to that. +func WithUserIsAnonymous(anonListing bool) Option { return func(o *options) { - o.WithAnonymousListing = anonListing + o.WithUserIsAnonymous = anonListing + } +} + +// WithOutputFields provides an option when creating responses to only include +// specific fields +func WithOutputFields(fields *perms.OutputFieldsMap) Option { + return func(o *options) { + o.WithOutputFields = fields + } +} + +// WithScope provides an option when creating responses to include the given +// scope if allowed +func WithScope(scp *scopes.ScopeInfo) Option { + return func(o *options) { + o.WithScope = scp + } +} + +// WithAuthorizedActions provides an option when creating responses to include the given +// authorized actions if allowed +func WithAuthorizedActions(acts []string) Option { + return func(o *options) { + o.WithAuthorizedActions = acts + } +} + +// WithAuthorizedCollectionActions provides an option when creating responses to include the given +// authorized collection actions if allowed +func WithAuthorizedCollectionActions(colActs map[string]*structpb.ListValue) Option { + return func(o *options) { + o.WithAuthorizedCollectionActions = colActs } } diff --git a/internal/servers/controller/handlers/roles/role_service.go b/internal/servers/controller/handlers/roles/role_service.go index 305c3ea62..5d3a2bbc8 100644 --- a/internal/servers/controller/handlers/roles/role_service.go +++ b/internal/servers/controller/handlers/roles/role_service.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/auth" "github.com/hashicorp/boundary/internal/errors" pb "github.com/hashicorp/boundary/internal/gen/controller/api/resources/roles" @@ -11,6 +12,7 @@ import ( "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/iam/store" "github.com/hashicorp/boundary/internal/perms" + "github.com/hashicorp/boundary/internal/requests" "github.com/hashicorp/boundary/internal/servers/controller/common" "github.com/hashicorp/boundary/internal/servers/controller/common/scopeids" "github.com/hashicorp/boundary/internal/servers/controller/handlers" @@ -105,22 +107,41 @@ func (s Service) ListRoles(ctx context.Context, req *pbs.ListRolesRequest) (*pbs if err != nil { return nil, err } + if len(items) == 0 { + return &pbs.ListRolesResponse{}, nil + } filter, err := handlers.NewFilter(req.GetFilter()) if err != nil { return nil, err } finalItems := make([]*pb.Role, 0, len(items)) - res := &perms.Resource{ + res := perms.Resource{ Type: resource.Role, } for _, item := range items { - item.Scope = scopeInfoMap[item.GetScopeId()] - res.ScopeId = item.Scope.Id - item.AuthorizedActions = authResults.FetchActionSetForId(ctx, item.Id, IdActions, auth.WithResource(res)).Strings() - if len(item.AuthorizedActions) == 0 { + res.Id = item.GetPublicId() + res.ScopeId = item.GetScopeId() + authorizedActions := authResults.FetchActionSetForId(ctx, item.GetPublicId(), IdActions, auth.WithResource(&res)).Strings() + if len(authorizedActions) == 0 { continue } + + 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(scopeInfoMap[item.GetScopeId()])) + } + if outputFields.Has(globals.AuthorizedActionsField) { + outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authorizedActions)) + } + + item, err := toProto(ctx, item, nil, nil, outputOpts...) + if err != nil { + return nil, err + } + if filter.Match(item) { finalItems = append(finalItems, item) } @@ -130,6 +151,8 @@ func (s Service) ListRoles(ctx context.Context, req *pbs.ListRolesRequest) (*pbs // GetRoles implements the interface pbs.RoleServiceServer. func (s Service) GetRole(ctx context.Context, req *pbs.GetRoleRequest) (*pbs.GetRoleResponse, error) { + const op = "roles.(Service).GetRole" + if err := validateGetRequest(req); err != nil { return nil, err } @@ -137,17 +160,37 @@ func (s Service) GetRole(ctx context.Context, req *pbs.GetRoleRequest) (*pbs.Get if authResults.Error != nil { return nil, authResults.Error } - u, err := s.getFromRepo(ctx, req.GetId()) + r, prs, rgs, err := s.getFromRepo(ctx, req.GetId()) if err != nil { return nil, err } - u.Scope = authResults.Scope - u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions).Strings() - return &pbs.GetRoleResponse{Item: u}, nil + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, r.GetPublicId(), IdActions).Strings())) + } + + item, err := toProto(ctx, r, prs, rgs, outputOpts...) + if err != nil { + return nil, err + } + + return &pbs.GetRoleResponse{Item: item}, nil } // CreateRole implements the interface pbs.RoleServiceServer. func (s Service) CreateRole(ctx context.Context, req *pbs.CreateRoleRequest) (*pbs.CreateRoleResponse, error) { + const op = "roles.(Service).CreateRole" + if err := validateCreateRequest(req); err != nil { return nil, err } @@ -159,13 +202,33 @@ func (s Service) CreateRole(ctx context.Context, req *pbs.CreateRoleRequest) (*p if err != nil { return nil, err } - r.Scope = authResults.Scope - r.AuthorizedActions = authResults.FetchActionSetForId(ctx, r.Id, IdActions).Strings() - return &pbs.CreateRoleResponse{Item: r, Uri: fmt.Sprintf("roles/%s", r.GetId())}, nil + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, r.GetPublicId(), IdActions).Strings())) + } + + item, err := toProto(ctx, r, nil, nil, outputOpts...) + if err != nil { + return nil, err + } + + return &pbs.CreateRoleResponse{Item: item, Uri: fmt.Sprintf("roles/%s", item.GetId())}, nil } // UpdateRole implements the interface pbs.RoleServiceServer. func (s Service) UpdateRole(ctx context.Context, req *pbs.UpdateRoleRequest) (*pbs.UpdateRoleResponse, error) { + const op = "roles.(Service).UpdateRole" + if err := validateUpdateRequest(req); err != nil { return nil, err } @@ -173,13 +236,31 @@ func (s Service) UpdateRole(ctx context.Context, req *pbs.UpdateRoleRequest) (*p if authResults.Error != nil { return nil, authResults.Error } - u, err := s.updateInRepo(ctx, authResults.Scope.GetId(), req.GetId(), req.GetUpdateMask().GetPaths(), req.GetItem()) + r, prs, rgs, err := s.updateInRepo(ctx, authResults.Scope.GetId(), req.GetId(), req.GetUpdateMask().GetPaths(), req.GetItem()) if err != nil { return nil, err } - u.Scope = authResults.Scope - u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions).Strings() - return &pbs.UpdateRoleResponse{Item: u}, nil + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, r.GetPublicId(), IdActions).Strings())) + } + + item, err := toProto(ctx, r, prs, rgs, outputOpts...) + if err != nil { + return nil, err + } + + return &pbs.UpdateRoleResponse{Item: item}, nil } // DeleteRole implements the interface pbs.RoleServiceServer. @@ -200,6 +281,8 @@ func (s Service) DeleteRole(ctx context.Context, req *pbs.DeleteRoleRequest) (*p // AddRolePrincipals implements the interface pbs.RoleServiceServer. func (s Service) AddRolePrincipals(ctx context.Context, req *pbs.AddRolePrincipalsRequest) (*pbs.AddRolePrincipalsResponse, error) { + const op = "roles.(Service).AddRolePrincipals" + if err := validateAddRolePrincipalsRequest(req); err != nil { return nil, err } @@ -207,17 +290,37 @@ func (s Service) AddRolePrincipals(ctx context.Context, req *pbs.AddRolePrincipa if authResults.Error != nil { return nil, authResults.Error } - r, err := s.addPrinciplesInRepo(ctx, req.GetId(), req.GetPrincipalIds(), req.GetVersion()) + r, prs, rgs, err := s.addPrinciplesInRepo(ctx, req.GetId(), req.GetPrincipalIds(), req.GetVersion()) if err != nil { return nil, err } - r.Scope = authResults.Scope - r.AuthorizedActions = authResults.FetchActionSetForId(ctx, r.Id, IdActions).Strings() - return &pbs.AddRolePrincipalsResponse{Item: r}, nil + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, r.GetPublicId(), IdActions).Strings())) + } + + item, err := toProto(ctx, r, prs, rgs, outputOpts...) + if err != nil { + return nil, err + } + + return &pbs.AddRolePrincipalsResponse{Item: item}, nil } // SetRolePrincipals implements the interface pbs.RoleServiceServer. func (s Service) SetRolePrincipals(ctx context.Context, req *pbs.SetRolePrincipalsRequest) (*pbs.SetRolePrincipalsResponse, error) { + const op = "roles.(Service).SetRolePrincipals" + if err := validateSetRolePrincipalsRequest(req); err != nil { return nil, err } @@ -225,17 +328,37 @@ func (s Service) SetRolePrincipals(ctx context.Context, req *pbs.SetRolePrincipa if authResults.Error != nil { return nil, authResults.Error } - r, err := s.setPrinciplesInRepo(ctx, req.GetId(), req.GetPrincipalIds(), req.GetVersion()) + r, prs, rgs, err := s.setPrinciplesInRepo(ctx, req.GetId(), req.GetPrincipalIds(), req.GetVersion()) if err != nil { return nil, err } - r.Scope = authResults.Scope - r.AuthorizedActions = authResults.FetchActionSetForId(ctx, r.Id, IdActions).Strings() - return &pbs.SetRolePrincipalsResponse{Item: r}, nil + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, r.GetPublicId(), IdActions).Strings())) + } + + item, err := toProto(ctx, r, prs, rgs, outputOpts...) + if err != nil { + return nil, err + } + + return &pbs.SetRolePrincipalsResponse{Item: item}, nil } // RemoveRolePrincipals implements the interface pbs.RoleServiceServer. func (s Service) RemoveRolePrincipals(ctx context.Context, req *pbs.RemoveRolePrincipalsRequest) (*pbs.RemoveRolePrincipalsResponse, error) { + const op = "roles.(Service).RemoveRolePrincipals" + if err := validateRemoveRolePrincipalsRequest(req); err != nil { return nil, err } @@ -243,17 +366,37 @@ func (s Service) RemoveRolePrincipals(ctx context.Context, req *pbs.RemoveRolePr if authResults.Error != nil { return nil, authResults.Error } - r, err := s.removePrinciplesInRepo(ctx, req.GetId(), req.GetPrincipalIds(), req.GetVersion()) + r, prs, rgs, err := s.removePrinciplesInRepo(ctx, req.GetId(), req.GetPrincipalIds(), req.GetVersion()) if err != nil { return nil, err } - r.Scope = authResults.Scope - r.AuthorizedActions = authResults.FetchActionSetForId(ctx, r.Id, IdActions).Strings() - return &pbs.RemoveRolePrincipalsResponse{Item: r}, nil + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, r.GetPublicId(), IdActions).Strings())) + } + + item, err := toProto(ctx, r, prs, rgs, outputOpts...) + if err != nil { + return nil, err + } + + return &pbs.RemoveRolePrincipalsResponse{Item: item}, nil } // AddRoleGrants implements the interface pbs.RoleServiceServer. func (s Service) AddRoleGrants(ctx context.Context, req *pbs.AddRoleGrantsRequest) (*pbs.AddRoleGrantsResponse, error) { + const op = "roles.(Service).AddRoleGrants" + if err := validateAddRoleGrantsRequest(req); err != nil { return nil, err } @@ -261,17 +404,37 @@ func (s Service) AddRoleGrants(ctx context.Context, req *pbs.AddRoleGrantsReques if authResults.Error != nil { return nil, authResults.Error } - r, err := s.addGrantsInRepo(ctx, req.GetId(), req.GetGrantStrings(), req.GetVersion()) + r, prs, rgs, err := s.addGrantsInRepo(ctx, req.GetId(), req.GetGrantStrings(), req.GetVersion()) if err != nil { return nil, err } - r.Scope = authResults.Scope - r.AuthorizedActions = authResults.FetchActionSetForId(ctx, r.Id, IdActions).Strings() - return &pbs.AddRoleGrantsResponse{Item: r}, nil + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, r.GetPublicId(), IdActions).Strings())) + } + + item, err := toProto(ctx, r, prs, rgs, outputOpts...) + if err != nil { + return nil, err + } + + return &pbs.AddRoleGrantsResponse{Item: item}, nil } // SetRoleGrants implements the interface pbs.RoleServiceServer. func (s Service) SetRoleGrants(ctx context.Context, req *pbs.SetRoleGrantsRequest) (*pbs.SetRoleGrantsResponse, error) { + const op = "roles.(Service).SetRoleGrants" + if err := validateSetRoleGrantsRequest(req); err != nil { return nil, err } @@ -279,17 +442,37 @@ func (s Service) SetRoleGrants(ctx context.Context, req *pbs.SetRoleGrantsReques if authResults.Error != nil { return nil, authResults.Error } - r, err := s.setGrantsInRepo(ctx, req.GetId(), req.GetGrantStrings(), req.GetVersion()) + r, prs, rgs, err := s.setGrantsInRepo(ctx, req.GetId(), req.GetGrantStrings(), req.GetVersion()) if err != nil { return nil, err } - r.Scope = authResults.Scope - r.AuthorizedActions = authResults.FetchActionSetForId(ctx, r.Id, IdActions).Strings() - return &pbs.SetRoleGrantsResponse{Item: r}, nil + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, r.GetPublicId(), IdActions).Strings())) + } + + item, err := toProto(ctx, r, prs, rgs, outputOpts...) + if err != nil { + return nil, err + } + + return &pbs.SetRoleGrantsResponse{Item: item}, nil } // RemoveRoleGrants implements the interface pbs.RoleServiceServer. func (s Service) RemoveRoleGrants(ctx context.Context, req *pbs.RemoveRoleGrantsRequest) (*pbs.RemoveRoleGrantsResponse, error) { + const op = "roles.(Service).RemoveRoleGrants" + if err := validateRemoveRoleGrantsRequest(req); err != nil { return nil, err } @@ -297,34 +480,52 @@ func (s Service) RemoveRoleGrants(ctx context.Context, req *pbs.RemoveRoleGrants if authResults.Error != nil { return nil, authResults.Error } - r, err := s.removeGrantsInRepo(ctx, req.GetId(), req.GetGrantStrings(), req.GetVersion()) + r, prs, rgs, err := s.removeGrantsInRepo(ctx, req.GetId(), req.GetGrantStrings(), req.GetVersion()) if err != nil { return nil, err } - r.Scope = authResults.Scope - r.AuthorizedActions = authResults.FetchActionSetForId(ctx, r.Id, IdActions).Strings() - return &pbs.RemoveRoleGrantsResponse{Item: r}, nil -} -func (s Service) getFromRepo(ctx context.Context, id string) (*pb.Role, error) { - repo, err := s.repoFn() + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, r.GetPublicId(), IdActions).Strings())) + } + + item, err := toProto(ctx, r, prs, rgs, outputOpts...) if err != nil { return nil, err } + + return &pbs.RemoveRoleGrantsResponse{Item: item}, nil +} + +func (s Service) getFromRepo(ctx context.Context, id string) (*iam.Role, []iam.PrincipalRole, []*iam.RoleGrant, error) { + repo, err := s.repoFn() + if err != nil { + return nil, nil, nil, err + } out, pr, roleGrants, err := repo.LookupRole(ctx, id) if err != nil { if errors.IsNotFoundError(err) { - return nil, handlers.NotFoundErrorf("Role %q doesn't exist.", id) + return nil, nil, nil, handlers.NotFoundErrorf("Role %q doesn't exist.", id) } - return nil, err + return nil, nil, nil, err } if out == nil { - return nil, handlers.NotFoundErrorf("Role %q doesn't exist.", id) + return nil, nil, nil, handlers.NotFoundErrorf("Role %q doesn't exist.", id) } - return toProto(out, pr, roleGrants), nil + return out, pr, roleGrants, nil } -func (s Service) createInRepo(ctx context.Context, scopeId string, item *pb.Role) (*pb.Role, error) { +func (s Service) createInRepo(ctx context.Context, scopeId string, item *pb.Role) (*iam.Role, error) { const op = "roles.(Service).createInRepo" var opts []iam.Option if item.GetName() != nil { @@ -351,10 +552,10 @@ func (s Service) createInRepo(ctx context.Context, scopeId string, item *pb.Role if out == nil { return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create role but no error returned from repository.") } - return toProto(out, nil, nil), nil + return out, nil } -func (s Service) updateInRepo(ctx context.Context, scopeId, id string, mask []string, item *pb.Role) (*pb.Role, error) { +func (s Service) updateInRepo(ctx context.Context, scopeId, id string, mask []string, item *pb.Role) (*iam.Role, []iam.PrincipalRole, []*iam.RoleGrant, error) { const op = "roles.(Service).updateInRepo" var opts []iam.Option if desc := item.GetDescription(); desc != nil { @@ -370,25 +571,25 @@ func (s Service) updateInRepo(ctx context.Context, scopeId, id string, mask []st u, err := iam.NewRole(scopeId, opts...) if err != nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to build role for update: %v.", err) + return nil, nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to build role for update: %v.", err) } u.PublicId = id dbMask := maskManager.Translate(mask) if len(dbMask) == 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."}) + return nil, nil, nil, handlers.InvalidArgumentErrorf("No valid fields included in the update mask.", map[string]string{"update_mask": "No valid fields provided in the update mask."}) } repo, err := s.repoFn() if err != nil { - return nil, err + return nil, nil, nil, err } out, pr, gr, rowsUpdated, err := repo.UpdateRole(ctx, u, version, dbMask) if err != nil { - return nil, errors.Wrap(err, op) + return nil, nil, nil, errors.Wrap(err, op) } if rowsUpdated == 0 { - return nil, handlers.NotFoundErrorf("Role %q doesn't exist or incorrect version provided.", id) + return nil, nil, nil, handlers.NotFoundErrorf("Role %q doesn't exist or incorrect version provided.", id) } - return toProto(out, pr, gr), nil + return out, pr, gr, nil } func (s Service) deleteFromRepo(ctx context.Context, id string) (bool, error) { @@ -407,7 +608,7 @@ func (s Service) deleteFromRepo(ctx context.Context, id string) (bool, error) { return rows > 0, nil } -func (s Service) listFromRepo(ctx context.Context, scopeIds []string) ([]*pb.Role, error) { +func (s Service) listFromRepo(ctx context.Context, scopeIds []string) ([]*iam.Role, error) { repo, err := s.repoFn() if err != nil { return nil, err @@ -416,103 +617,98 @@ func (s Service) listFromRepo(ctx context.Context, scopeIds []string) ([]*pb.Rol if err != nil { return nil, err } - var outRl []*pb.Role - for _, g := range rl { - // TODO: Attach principals and grants to ListRoles response. - outRl = append(outRl, toProto(g, nil, nil)) - } - return outRl, nil + return rl, nil } -func (s Service) addPrinciplesInRepo(ctx context.Context, roleId string, principalIds []string, version uint32) (*pb.Role, error) { +func (s Service) addPrinciplesInRepo(ctx context.Context, roleId string, principalIds []string, version uint32) (*iam.Role, []iam.PrincipalRole, []*iam.RoleGrant, error) { const op = "roles.(Service).addPrincpleInRepo" repo, err := s.repoFn() if err != nil { - return nil, err + return nil, nil, nil, err } _, err = repo.AddPrincipalRoles(ctx, roleId, version, strutil.RemoveDuplicates(principalIds, false)) if err != nil { // TODO: Figure out a way to surface more helpful error info beyond the Internal error. - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to add principals to role: %v.", err) + return nil, nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to add principals to role: %v.", err) } out, pr, roleGrants, err := repo.LookupRole(ctx, roleId) if err != nil { - return nil, errors.Wrap(err, op, errors.WithMsg("unable to look up role after adding principals")) + return nil, nil, nil, errors.Wrap(err, op, errors.WithMsg("unable to look up role after adding principals")) } if out == nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup role after adding principals to it.") + return nil, nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup role after adding principals to it.") } - return toProto(out, pr, roleGrants), nil + return out, pr, roleGrants, nil } -func (s Service) setPrinciplesInRepo(ctx context.Context, roleId string, principalIds []string, version uint32) (*pb.Role, error) { +func (s Service) setPrinciplesInRepo(ctx context.Context, roleId string, principalIds []string, version uint32) (*iam.Role, []iam.PrincipalRole, []*iam.RoleGrant, error) { const op = "roles.(Service).setPrinciplesInRepo" repo, err := s.repoFn() if err != nil { - return nil, err + return nil, nil, nil, err } _, _, err = repo.SetPrincipalRoles(ctx, roleId, version, strutil.RemoveDuplicates(principalIds, false)) if err != nil { // TODO: Figure out a way to surface more helpful error info beyond the Internal error. - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to set principals on role: %v.", err) + return nil, nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to set principals on role: %v.", err) } out, pr, roleGrants, err := repo.LookupRole(ctx, roleId) if err != nil { - return nil, errors.Wrap(err, op, errors.WithMsg("unable to look up role after setting principals")) + return nil, nil, nil, errors.Wrap(err, op, errors.WithMsg("unable to look up role after setting principals")) } if out == nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup role after setting principals for it.") + return nil, nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup role after setting principals for it.") } - return toProto(out, pr, roleGrants), nil + return out, pr, roleGrants, nil } -func (s Service) removePrinciplesInRepo(ctx context.Context, roleId string, principalIds []string, version uint32) (*pb.Role, error) { +func (s Service) removePrinciplesInRepo(ctx context.Context, roleId string, principalIds []string, version uint32) (*iam.Role, []iam.PrincipalRole, []*iam.RoleGrant, error) { const op = "roles.(Service).removePrinciplesInRepo" repo, err := s.repoFn() if err != nil { - return nil, err + return nil, nil, nil, err } _, err = repo.DeletePrincipalRoles(ctx, roleId, version, strutil.RemoveDuplicates(principalIds, false)) if err != nil { // TODO: Figure out a way to surface more helpful error info beyond the Internal error. - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to remove principals from role: %v.", err) + return nil, nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to remove principals from role: %v.", err) } out, pr, roleGrants, err := repo.LookupRole(ctx, roleId) if err != nil { - return nil, errors.Wrap(err, op, errors.WithMsg("unable to look up role after removing principals")) + return nil, nil, nil, errors.Wrap(err, op, errors.WithMsg("unable to look up role after removing principals")) } if out == nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup role after removing principals from it.") + return nil, nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup role after removing principals from it.") } - return toProto(out, pr, roleGrants), nil + return out, pr, roleGrants, nil } -func (s Service) addGrantsInRepo(ctx context.Context, roleId string, grants []string, version uint32) (*pb.Role, error) { +func (s Service) addGrantsInRepo(ctx context.Context, roleId string, grants []string, version uint32) (*iam.Role, []iam.PrincipalRole, []*iam.RoleGrant, error) { const op = "service.(Service).addGrantsInRepo" repo, err := s.repoFn() if err != nil { - return nil, err + return nil, nil, nil, err } _, err = repo.AddRoleGrants(ctx, roleId, version, strutil.RemoveDuplicates(grants, false)) if err != nil { // TODO: Figure out a way to surface more helpful error info beyond the Internal error. - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to add grants to role: %v.", err) + return nil, nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to add grants to role: %v.", err) } out, pr, roleGrants, err := repo.LookupRole(ctx, roleId) if err != nil { - return nil, errors.Wrap(err, op, errors.WithMsg("unable to look up role after adding grants")) + return nil, nil, nil, errors.Wrap(err, op, errors.WithMsg("unable to look up role after adding grants")) } if out == nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup role after adding grants to it.") + return nil, nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup role after adding grants to it.") } - return toProto(out, pr, roleGrants), nil + return out, pr, roleGrants, nil } -func (s Service) setGrantsInRepo(ctx context.Context, roleId string, grants []string, version uint32) (*pb.Role, error) { +func (s Service) setGrantsInRepo(ctx context.Context, roleId string, grants []string, version uint32) (*iam.Role, []iam.PrincipalRole, []*iam.RoleGrant, error) { const op = "roles.(Service).setGrantsInRepo" repo, err := s.repoFn() if err != nil { - return nil, err + return nil, nil, nil, err } // If no grant was provided, we clear the grants. if grants == nil { @@ -521,37 +717,37 @@ func (s Service) setGrantsInRepo(ctx context.Context, roleId string, grants []st _, _, err = repo.SetRoleGrants(ctx, roleId, version, strutil.RemoveDuplicates(grants, false)) if err != nil { // TODO: Figure out a way to surface more helpful error info beyond the Internal error. - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to set grants on role: %v.", err) + return nil, nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to set grants on role: %v.", err) } out, pr, roleGrants, err := repo.LookupRole(ctx, roleId) if err != nil { - return nil, errors.Wrap(err, op, errors.WithMsg("unable to look up role after setting grants")) + return nil, nil, nil, errors.Wrap(err, op, errors.WithMsg("unable to look up role after setting grants")) } if out == nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup role after setting grants on it.") + return nil, nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup role after setting grants on it.") } - return toProto(out, pr, roleGrants), nil + return out, pr, roleGrants, nil } -func (s Service) removeGrantsInRepo(ctx context.Context, roleId string, grants []string, version uint32) (*pb.Role, error) { +func (s Service) removeGrantsInRepo(ctx context.Context, roleId string, grants []string, version uint32) (*iam.Role, []iam.PrincipalRole, []*iam.RoleGrant, error) { const op = "roles.(Service).removeGrantsInRepo" repo, err := s.repoFn() if err != nil { - return nil, err + return nil, nil, nil, err } _, err = repo.DeleteRoleGrants(ctx, roleId, version, strutil.RemoveDuplicates(grants, false)) if err != nil { // TODO: Figure out a way to surface more helpful error info beyond the Internal error. - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to remove grants from role: %v", err) + return nil, nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to remove grants from role: %v", err) } out, pr, roleGrants, err := repo.LookupRole(ctx, roleId) if err != nil { - return nil, errors.Wrap(err, op, errors.WithMsg("uable to look up role after removing grant")) + return nil, nil, nil, errors.Wrap(err, op, errors.WithMsg("uable to look up role after removing grant")) } if out == nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup role after removing grants from it.") + return nil, nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup role after removing grants from it.") } - return toProto(out, pr, roleGrants), nil + return out, pr, roleGrants, nil } func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.VerifyResults { @@ -593,57 +789,91 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type) auth. return auth.Verify(ctx, opts...) } -func toProto(in *iam.Role, principals []iam.PrincipalRole, grants []*iam.RoleGrant) *pb.Role { - out := pb.Role{ - Id: in.GetPublicId(), - ScopeId: in.GetScopeId(), - CreatedTime: in.GetCreateTime().GetTimestamp(), - UpdatedTime: in.GetUpdateTime().GetTimestamp(), - Version: in.GetVersion(), +func toProto(ctx context.Context, in *iam.Role, principals []iam.PrincipalRole, grants []*iam.RoleGrant, opt ...handlers.Option) (*pb.Role, error) { + opts := handlers.GetOpts(opt...) + if opts.WithOutputFields == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "output fields not found when building role proto") } - if in.GetDescription() != "" { - out.Description = &wrapperspb.StringValue{Value: in.GetDescription()} + outputFields := *opts.WithOutputFields + + out := pb.Role{} + if outputFields.Has(globals.IdField) { + out.Id = in.GetPublicId() } - if in.GetName() != "" { - out.Name = &wrapperspb.StringValue{Value: in.GetName()} + if outputFields.Has(globals.ScopeIdField) { + out.ScopeId = in.GetScopeId() } - for _, p := range principals { - principal := &pb.Principal{ - Id: p.GetPrincipalId(), - Type: p.GetType(), - ScopeId: p.GetPrincipalScopeId(), - } - out.Principals = append(out.Principals, principal) - out.PrincipalIds = append(out.PrincipalIds, p.GetPrincipalId()) + if outputFields.Has(globals.DescriptionField) && in.GetDescription() != "" { + out.Description = wrapperspb.String(in.GetDescription()) } - for _, g := range grants { - out.GrantStrings = append(out.GrantStrings, g.GetRawGrant()) - parsed, err := perms.Parse(in.GetGrantScopeId(), g.GetRawGrant()) - if err != nil { - // This should never happen as we validate on the way in, but let's - // return what we can since we are still returning the raw grant - out.Grants = append(out.Grants, &pb.Grant{ - Raw: g.GetRawGrant(), - Canonical: "", - Json: nil, - }) - } else { - _, actions := parsed.Actions() - out.Grants = append(out.Grants, &pb.Grant{ - Raw: g.GetRawGrant(), - Canonical: g.GetCanonicalGrant(), - Json: &pb.GrantJson{ - Id: parsed.Id(), - Type: parsed.Type().String(), - Actions: actions, - }, - }) - } + if outputFields.Has(globals.NameField) && in.GetName() != "" { + out.Name = wrapperspb.String(in.GetName()) } - if in.GetGrantScopeId() != "" { + 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 + } + if outputFields.Has(globals.GrantScopeIdField) && in.GetGrantScopeId() != "" { out.GrantScopeId = &wrapperspb.StringValue{Value: in.GetGrantScopeId()} } - return &out + if outputFields.Has(globals.PrincipalIdsField) { + for _, p := range principals { + out.PrincipalIds = append(out.PrincipalIds, p.GetPrincipalId()) + } + } + if outputFields.Has(globals.PrincipalsField) { + for _, p := range principals { + principal := &pb.Principal{ + Id: p.GetPrincipalId(), + Type: p.GetType(), + ScopeId: p.GetPrincipalScopeId(), + } + out.Principals = append(out.Principals, principal) + } + } + if outputFields.Has(globals.GrantStringsField) { + for _, g := range grants { + out.GrantStrings = append(out.GrantStrings, g.GetRawGrant()) + } + } + if outputFields.Has(globals.GrantsField) { + for _, g := range grants { + parsed, err := perms.Parse(in.GetGrantScopeId(), g.GetRawGrant()) + if err != nil { + // This should never happen as we validate on the way in, but let's + // return what we can since we are still returning the raw grant + out.Grants = append(out.Grants, &pb.Grant{ + Raw: g.GetRawGrant(), + Canonical: "", + Json: nil, + }) + } else { + _, actions := parsed.Actions() + out.Grants = append(out.Grants, &pb.Grant{ + Raw: g.GetRawGrant(), + Canonical: g.GetCanonicalGrant(), + Json: &pb.GrantJson{ + Id: parsed.Id(), + Type: parsed.Type().String(), + Actions: actions, + }, + }) + } + } + } + + return &out, nil } // A validateX method should exist for each method above. These methods do not make calls to any backing service but enforce diff --git a/internal/servers/controller/handlers/roles/role_service_test.go b/internal/servers/controller/handlers/roles/role_service_test.go index 2408a7a96..c339e6b10 100644 --- a/internal/servers/controller/handlers/roles/role_service_test.go +++ b/internal/servers/controller/handlers/roles/role_service_test.go @@ -294,14 +294,29 @@ func TestList(t *testing.T) { s, err := roles.NewService(repoFn) require.NoError(err, "Couldn't create new role service.") + // Test the non-anon case got, gErr := s.ListRoles(auth.DisabledAuthTestContext(repoFn, tc.req.GetScopeId()), tc.req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err)) - } else { - require.NoError(gErr) + return } + require.NoError(gErr) assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListRoles(%q) got response %q, wanted %q", tc.req, got, tc.res) + + // Test the anon case + got, gErr = s.ListRoles(auth.DisabledAuthTestContext(repoFn, tc.req.GetScopeId(), auth.WithUserId(auth.AnonymousUserId)), tc.req) + require.NoError(gErr) + assert.Len(got.Items, len(tc.res.Items)) + for _, item := range got.GetItems() { + require.Nil(item.CreatedTime) + require.Nil(item.PrincipalIds) + require.Nil(item.Principals) + require.Nil(item.Grants) + require.Nil(item.GrantStrings) + require.Nil(item.UpdatedTime) + require.Zero(item.Version) + } }) } } diff --git a/internal/servers/controller/handlers/scopes/scope_service.go b/internal/servers/controller/handlers/scopes/scope_service.go index c4c6734b6..3cc5f66ed 100644 --- a/internal/servers/controller/handlers/scopes/scope_service.go +++ b/internal/servers/controller/handlers/scopes/scope_service.go @@ -6,6 +6,7 @@ import ( "sort" "strings" + "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/auth" "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/auth/password" @@ -15,6 +16,7 @@ import ( "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/iam/store" "github.com/hashicorp/boundary/internal/perms" + "github.com/hashicorp/boundary/internal/requests" "github.com/hashicorp/boundary/internal/servers/controller/common" "github.com/hashicorp/boundary/internal/servers/controller/common/scopeids" "github.com/hashicorp/boundary/internal/servers/controller/handlers" @@ -29,9 +31,7 @@ import ( "github.com/hashicorp/boundary/internal/types/action" "github.com/hashicorp/boundary/internal/types/resource" "github.com/hashicorp/boundary/internal/types/scope" - "github.com/hashicorp/boundary/sdk/strutil" "google.golang.org/grpc/codes" - "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/wrapperspb" ) @@ -54,21 +54,33 @@ var ( action.List, } - orgCollectionTypeMap = map[resource.Type]action.ActionSet{ - resource.AuthMethod: authmethods.CollectionActions, - resource.AuthToken: authtokens.CollectionActions, - resource.Group: groups.CollectionActions, - resource.Role: roles.CollectionActions, - resource.Scope: CollectionActions, - resource.Session: sessions.CollectionActions, - resource.User: users.CollectionActions, - } + scopeCollectionTypeMapMap = map[string]map[resource.Type]action.ActionSet{ + scope.Global.String(): { + resource.AuthMethod: authmethods.CollectionActions, + resource.AuthToken: authtokens.CollectionActions, + resource.Group: groups.CollectionActions, + resource.Role: roles.CollectionActions, + resource.Scope: CollectionActions, + resource.Session: sessions.CollectionActions, + resource.User: users.CollectionActions, + }, - projectCollectionTypeMap = map[resource.Type]action.ActionSet{ - resource.Group: groups.CollectionActions, - resource.HostCatalog: host_catalogs.CollectionActions, - resource.Role: roles.CollectionActions, - resource.Target: targets.CollectionActions, + scope.Org.String(): { + resource.AuthMethod: authmethods.CollectionActions, + resource.AuthToken: authtokens.CollectionActions, + resource.Group: groups.CollectionActions, + resource.Role: roles.CollectionActions, + resource.Scope: CollectionActions, + resource.Session: sessions.CollectionActions, + resource.User: users.CollectionActions, + }, + + scope.Project.String(): { + resource.Group: groups.CollectionActions, + resource.HostCatalog: host_catalogs.CollectionActions, + resource.Role: roles.CollectionActions, + resource.Target: targets.CollectionActions, + }, } ) @@ -97,36 +109,6 @@ func NewService(repo common.IamRepoFactory) (Service, error) { var _ pbs.ScopeServiceServer = Service{} -func populateCollectionAuthorizedActions(ctx context.Context, - authResults auth.VerifyResults, - item *pb.Scope) error { - res := &perms.Resource{ - ScopeId: item.Id, - } - mapToRange := orgCollectionTypeMap - if item.Type == "project" { - mapToRange = projectCollectionTypeMap - } - // Range over the defined collections and check permissions against those - // collections. We use the ID of this scope being returned, not its parent, - // hence passing in a resource here. - for k, v := range mapToRange { - res.Type = k - acts := authResults.FetchActionSetForType(ctx, k, v, auth.WithResource(res)).Strings() - if len(acts) > 0 { - if item.AuthorizedCollectionActions == nil { - item.AuthorizedCollectionActions = make(map[string]*structpb.ListValue) - } - lv, err := structpb.NewList(strutil.StringListToInterfaceList(acts)) - if err != nil { - return err - } - item.AuthorizedCollectionActions[k.String()+"s"] = lv - } - } - return nil -} - // ListScopes implements the interface pbs.ScopeServiceServer. func (s Service) ListScopes(ctx context.Context, req *pbs.ListScopesRequest) (*pbs.ListScopesResponse, error) { if req.GetScopeId() == "" { @@ -158,39 +140,65 @@ func (s Service) ListScopes(ctx context.Context, req *pbs.ListScopesRequest) (*p return &pbs.ListScopesResponse{}, nil } - pl, err := s.listFromRepo(ctx, scopeIds, authResults.UserId == auth.AnonymousUserId) + pl, err := s.listFromRepo(ctx, scopeIds) if err != nil { return nil, err } + if len(pl) == 0 { + return &pbs.ListScopesResponse{}, nil + } filter, err := handlers.NewFilter(req.GetFilter()) if err != nil { return nil, err } finalItems := make([]*pb.Scope, 0, len(pl)) - res := &perms.Resource{ + res := perms.Resource{ Type: resource.Scope, } for _, item := range pl { - item.Scope = scopeInfoMap[item.GetScopeId()] - res.ScopeId = item.Scope.Id - item.AuthorizedActions = authResults.FetchActionSetForId(ctx, item.Id, IdActions, auth.WithResource(res)).Strings() - if len(item.AuthorizedActions) == 0 { + res.Id = item.GetPublicId() + res.ScopeId = item.GetParentId() + + authorizedActions := authResults.FetchActionSetForId(ctx, item.GetPublicId(), IdActions, auth.WithResource(&res)).Strings() + if len(authorizedActions) == 0 { continue } - if filter.Match(item) { - finalItems = append(finalItems, item) - if err := populateCollectionAuthorizedActions(ctx, authResults, item); err != nil { + + 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(scopeInfoMap[item.GetParentId()])) + } + if outputFields.Has(globals.AuthorizedActionsField) { + outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authorizedActions)) + } + if outputFields.Has(globals.AuthorizedCollectionActionsField) { + collectionActions, err := auth.CalculateAuthorizedCollectionActions(ctx, authResults, scopeCollectionTypeMapMap[item.Type], item.GetPublicId(), "") + if err != nil { return nil, err } + outputOpts = append(outputOpts, handlers.WithAuthorizedCollectionActions(collectionActions)) + } + + item, err := ToProto(ctx, item, outputOpts...) + if err != nil { + return nil, err + } + + if filter.Match(item) { + finalItems = append(finalItems, item) } } - + SortScopes(finalItems) return &pbs.ListScopesResponse{Items: finalItems}, nil } // GetScopes implements the interface pbs.ScopeServiceServer. func (s Service) GetScope(ctx context.Context, req *pbs.GetScopeRequest) (*pbs.GetScopeResponse, error) { + const op = "scopes.(Service).GetScope" + if err := validateGetRequest(req); err != nil { return nil, err } @@ -202,21 +210,45 @@ func (s Service) GetScope(ctx context.Context, req *pbs.GetScopeRequest) (*pbs.G if err != nil { return nil, err } - p.Scope = authResults.Scope + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + outputOpts := make([]handlers.Option, 0, 3) + outputOpts = append(outputOpts, handlers.WithOutputFields(&outputFields)) + if outputFields.Has(globals.ScopeField) { + outputOpts = append(outputOpts, handlers.WithScope(authResults.Scope)) + } act := IdActions // Can't delete global so elide it - if p.Id == "global" { - act = act[0:2] + if p.GetPublicId() == "global" { + act = act[0:3] } - p.AuthorizedActions = authResults.FetchActionSetForId(ctx, p.Id, act).Strings() - if err := populateCollectionAuthorizedActions(ctx, authResults, p); err != nil { + if outputFields.Has(globals.AuthorizedActionsField) { + outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authResults.FetchActionSetForId(ctx, p.GetPublicId(), act).Strings())) + } + if outputFields.Has(globals.AuthorizedCollectionActionsField) { + collectionActions, err := auth.CalculateAuthorizedCollectionActions(ctx, authResults, scopeCollectionTypeMapMap[p.Type], p.GetPublicId(), "") + if err != nil { + return nil, err + } + outputOpts = append(outputOpts, handlers.WithAuthorizedCollectionActions(collectionActions)) + } + + item, err := ToProto(ctx, p, outputOpts...) + if err != nil { return nil, err } - return &pbs.GetScopeResponse{Item: p}, nil + + return &pbs.GetScopeResponse{Item: item}, nil } // CreateScope implements the interface pbs.ScopeServiceServer. func (s Service) CreateScope(ctx context.Context, req *pbs.CreateScopeRequest) (*pbs.CreateScopeResponse, error) { + const op = "scopes.(Service).CreateScope" + if err := validateCreateRequest(req); err != nil { return nil, err } @@ -228,16 +260,40 @@ func (s Service) CreateScope(ctx context.Context, req *pbs.CreateScopeRequest) ( if err != nil { return nil, err } - p.Scope = authResults.Scope - p.AuthorizedActions = authResults.FetchActionSetForId(ctx, p.Id, IdActions).Strings() - if err := populateCollectionAuthorizedActions(ctx, authResults, p); err != nil { + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, p.GetPublicId(), IdActions).Strings())) + } + if outputFields.Has(globals.AuthorizedCollectionActionsField) { + collectionActions, err := auth.CalculateAuthorizedCollectionActions(ctx, authResults, scopeCollectionTypeMapMap[p.Type], p.GetPublicId(), "") + if err != nil { + return nil, err + } + outputOpts = append(outputOpts, handlers.WithAuthorizedCollectionActions(collectionActions)) + } + + item, err := ToProto(ctx, p, outputOpts...) + if err != nil { return nil, err } - return &pbs.CreateScopeResponse{Item: p, Uri: fmt.Sprintf("scopes/%s", p.GetId())}, nil + + return &pbs.CreateScopeResponse{Item: item, Uri: fmt.Sprintf("scopes/%s", item.GetId())}, nil } // UpdateScope implements the interface pbs.ScopeServiceServer. func (s Service) UpdateScope(ctx context.Context, req *pbs.UpdateScopeRequest) (*pbs.UpdateScopeResponse, error) { + const op = "scopes.(Service).UpdateScope" + if err := validateUpdateRequest(req); err != nil { return nil, err } @@ -249,12 +305,34 @@ func (s Service) UpdateScope(ctx context.Context, req *pbs.UpdateScopeRequest) ( if err != nil { return nil, err } - p.Scope = authResults.Scope - p.AuthorizedActions = authResults.FetchActionSetForId(ctx, p.Id, IdActions).Strings() - if err := populateCollectionAuthorizedActions(ctx, authResults, p); err != nil { + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, p.GetPublicId(), IdActions).Strings())) + } + if outputFields.Has(globals.AuthorizedCollectionActionsField) { + collectionActions, err := auth.CalculateAuthorizedCollectionActions(ctx, authResults, scopeCollectionTypeMapMap[p.Type], p.GetPublicId(), "") + if err != nil { + return nil, err + } + outputOpts = append(outputOpts, handlers.WithAuthorizedCollectionActions(collectionActions)) + } + + item, err := ToProto(ctx, p, outputOpts...) + if err != nil { return nil, err } - return &pbs.UpdateScopeResponse{Item: p}, nil + + return &pbs.UpdateScopeResponse{Item: item}, nil } // DeleteScope implements the interface pbs.ScopeServiceServer. @@ -273,22 +351,22 @@ func (s Service) DeleteScope(ctx context.Context, req *pbs.DeleteScopeRequest) ( return nil, nil } -func (s Service) getFromRepo(ctx context.Context, id string) (*pb.Scope, error) { +func (s Service) getFromRepo(ctx context.Context, id string) (*iam.Scope, error) { repo, err := s.repoFn() if err != nil { return nil, err } - p, err := repo.LookupScope(ctx, id) + out, err := repo.LookupScope(ctx, id) if err != nil { return nil, err } - if p == nil { + if out == nil { return nil, handlers.NotFoundErrorf("Scope %q doesn't exist.", id) } - return ToProto(p), nil + return out, nil } -func (s Service) createInRepo(ctx context.Context, authResults auth.VerifyResults, req *pbs.CreateScopeRequest) (*pb.Scope, error) { +func (s Service) createInRepo(ctx context.Context, authResults auth.VerifyResults, req *pbs.CreateScopeRequest) (*iam.Scope, error) { const op = "scopes.(Service).createInRepo" item := req.GetItem() var opts []iam.Option @@ -324,10 +402,10 @@ func (s Service) createInRepo(ctx context.Context, authResults auth.VerifyResult if out == nil { return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create scope but no error returned from repository.") } - return ToProto(out), nil + return out, nil } -func (s Service) updateInRepo(ctx context.Context, parentScope *pb.ScopeInfo, scopeId string, mask []string, item *pb.Scope) (*pb.Scope, error) { +func (s Service) updateInRepo(ctx context.Context, parentScope *pb.ScopeInfo, scopeId string, mask []string, item *pb.Scope) (*iam.Scope, error) { const op = "scope.(Service).updateInRepo" var opts []iam.Option var scopeDesc, scopeName, scopePrimaryAuthMethodId string @@ -384,7 +462,7 @@ func (s Service) updateInRepo(ctx context.Context, parentScope *pb.ScopeInfo, sc if rowsUpdated == 0 { return nil, handlers.NotFoundErrorf("Scope %q doesn't exist or incorrect version provided.", scopeId) } - return ToProto(out), nil + return out, nil } func (s Service) deleteFromRepo(ctx context.Context, scopeId string) (bool, error) { @@ -408,7 +486,7 @@ func SortScopes(scps []*pb.Scope) { }) } -func (s Service) listFromRepo(ctx context.Context, scopeIds []string, anonUser bool) ([]*pb.Scope, error) { +func (s Service) listFromRepo(ctx context.Context, scopeIds []string) ([]*iam.Scope, error) { repo, err := s.repoFn() if err != nil { return nil, err @@ -417,13 +495,7 @@ func (s Service) listFromRepo(ctx context.Context, scopeIds []string, anonUser b if err != nil { return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to list scopes: %v", err) } - - var outPl []*pb.Scope - for _, scp := range scps { - outPl = append(outPl, ToProto(scp, handlers.WithAnonymousListing(anonUser))) - } - SortScopes(outPl) - return outPl, nil + return scps, nil } func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.VerifyResults { @@ -465,29 +537,52 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type) auth. return auth.Verify(ctx, opts...) } -func ToProto(in *iam.Scope, opt ...handlers.Option) *pb.Scope { - anonListing := handlers.GetOpts(opt...).WithAnonymousListing - out := pb.Scope{ - Id: in.GetPublicId(), - ScopeId: in.GetParentId(), - Type: in.GetType(), +func ToProto(ctx context.Context, in *iam.Scope, opt ...handlers.Option) (*pb.Scope, error) { + opts := handlers.GetOpts(opt...) + if opts.WithOutputFields == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "output fields not found when building scope proto") } - if in.GetDescription() != "" { - out.Description = &wrapperspb.StringValue{Value: in.GetDescription()} + outputFields := *opts.WithOutputFields + + out := pb.Scope{} + if outputFields.Has(globals.IdField) { + out.Id = in.GetPublicId() } - if in.GetName() != "" { - out.Name = &wrapperspb.StringValue{Value: in.GetName()} + if outputFields.Has(globals.ScopeIdField) { + out.ScopeId = in.GetParentId() } - if in.GetPrimaryAuthMethodId() != "" { - out.PrimaryAuthMethodId = &wrapperspb.StringValue{Value: in.GetPrimaryAuthMethodId()} + if outputFields.Has(globals.TypeField) { + out.Type = in.GetType() } - if !anonListing { + 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 + } + if outputFields.Has(globals.AuthorizedCollectionActionsField) { + out.AuthorizedCollectionActions = opts.WithAuthorizedCollectionActions + } + if outputFields.Has(globals.PrimaryAuthMethodId) && in.GetPrimaryAuthMethodId() != "" { + out.PrimaryAuthMethodId = &wrapperspb.StringValue{Value: in.GetPrimaryAuthMethodId()} + } - return &out + return &out, nil } // A validateX method should exist for each method above. These methods do not make calls to any backing service but enforce diff --git a/internal/servers/controller/handlers/scopes/scope_service_test.go b/internal/servers/controller/handlers/scopes/scope_service_test.go index 475e44e1d..09ac1495f 100644 --- a/internal/servers/controller/handlers/scopes/scope_service_test.go +++ b/internal/servers/controller/handlers/scopes/scope_service_test.go @@ -14,6 +14,7 @@ import ( pb "github.com/hashicorp/boundary/internal/gen/controller/api/resources/scopes" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/perms" "github.com/hashicorp/boundary/internal/servers/controller/handlers" "github.com/hashicorp/boundary/internal/servers/controller/handlers/scopes" "github.com/hashicorp/boundary/internal/types/scope" @@ -290,13 +291,16 @@ func TestList(t *testing.T) { _, err = repo.DeleteScope(context.Background(), p2.GetPublicId()) require.NoError(t, err) + outputFields := perms.OutputFieldsMap(nil).SelfOrDefaults("u_auth") var initialOrgs []*pb.Scope globalScope := &pb.ScopeInfo{Id: "global", Type: scope.Global.String(), Name: scope.Global.String(), Description: "Global Scope"} - oNoProjectsProto := scopes.ToProto(oNoProjects) + oNoProjectsProto, err := scopes.ToProto(context.Background(), oNoProjects, handlers.WithOutputFields(&outputFields)) + require.NoError(t, err) oNoProjectsProto.Scope = globalScope oNoProjectsProto.AuthorizedActions = testAuthorizedActions oNoProjectsProto.AuthorizedCollectionActions = orgAuthorizedCollectionActions - oWithProjectsProto := scopes.ToProto(oWithProjects) + oWithProjectsProto, err := scopes.ToProto(context.Background(), oWithProjects, handlers.WithOutputFields(&outputFields)) + require.NoError(t, err) oWithProjectsProto.Scope = globalScope oWithProjectsProto.AuthorizedActions = testAuthorizedActions oWithProjectsProto.AuthorizedCollectionActions = orgAuthorizedCollectionActions @@ -342,7 +346,7 @@ func TestList(t *testing.T) { require.NoError(err, "Couldn't create new role service.") // Test with non-anonymous listing first - got, gErr := s.ListScopes(auth.DisabledAuthTestContext(repoFn, tc.scopeId, auth.WithUserId("u_auth")), tc.req) + got, gErr := s.ListScopes(auth.DisabledAuthTestContext(repoFn, tc.scopeId), tc.req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "ListScopes(%+v) got error\n%v, wanted\n%v", tc.req, gErr, tc.err) @@ -351,8 +355,9 @@ func TestList(t *testing.T) { assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListScopes(%q) got response\n%q\nwanted\n%q", tc.req, got, tc.res) // Now test with anonymous listing - got, gErr = s.ListScopes(auth.DisabledAuthTestContext(repoFn, tc.scopeId), tc.req) + got, gErr = s.ListScopes(auth.DisabledAuthTestContext(repoFn, tc.scopeId, auth.WithUserId(auth.AnonymousUserId)), tc.req) require.NoError(gErr) + assert.Len(got.Items, len(tc.res.Items)) for _, item := range got.GetItems() { assert.Nil(item.CreatedTime) assert.Nil(item.UpdatedTime) @@ -455,7 +460,7 @@ func TestList(t *testing.T) { require.NoError(err, "Couldn't create new role service.") // Test with non-anonymous listing first - got, gErr := s.ListScopes(auth.DisabledAuthTestContext(repoFn, tc.scopeId, auth.WithUserId("u_auth")), tc.req) + got, gErr := s.ListScopes(auth.DisabledAuthTestContext(repoFn, tc.scopeId), tc.req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "ListScopes(%+v) got error\n%v, wanted\n%v", tc.req, gErr, tc.err) @@ -465,8 +470,9 @@ func TestList(t *testing.T) { assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListScopes(%q) got response\n%q, wanted\n%q", tc.req, got, tc.res) // Now test with anonymous listing - got, gErr = s.ListScopes(auth.DisabledAuthTestContext(repoFn, tc.scopeId), tc.req) + got, gErr = s.ListScopes(auth.DisabledAuthTestContext(repoFn, tc.scopeId, auth.WithUserId(auth.AnonymousUserId)), tc.req) require.NoError(gErr) + assert.Len(got.Items, len(tc.res.Items)) for _, item := range got.GetItems() { assert.Nil(item.CreatedTime) assert.Nil(item.UpdatedTime) diff --git a/internal/servers/controller/handlers/sessions/session_service.go b/internal/servers/controller/handlers/sessions/session_service.go index b8dcd55a5..7dd58fe45 100644 --- a/internal/servers/controller/handlers/sessions/session_service.go +++ b/internal/servers/controller/handlers/sessions/session_service.go @@ -5,11 +5,13 @@ import ( stderrors "errors" "fmt" + "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/auth" "github.com/hashicorp/boundary/internal/errors" pb "github.com/hashicorp/boundary/internal/gen/controller/api/resources/sessions" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" "github.com/hashicorp/boundary/internal/perms" + "github.com/hashicorp/boundary/internal/requests" "github.com/hashicorp/boundary/internal/servers/controller/common" "github.com/hashicorp/boundary/internal/servers/controller/common/scopeids" "github.com/hashicorp/boundary/internal/servers/controller/handlers" @@ -18,6 +20,7 @@ import ( "github.com/hashicorp/boundary/internal/types/action" "github.com/hashicorp/boundary/internal/types/resource" "github.com/hashicorp/boundary/internal/types/scope" + "google.golang.org/grpc/codes" ) var ( @@ -62,6 +65,8 @@ var _ pbs.SessionServiceServer = Service{} // GetSessions implements the interface pbs.SessionServiceServer. func (s Service) GetSession(ctx context.Context, req *pbs.GetSessionRequest) (*pbs.GetSessionResponse, error) { + const op = "sessions.(Service).GetSession" + if err := validateGetRequest(req); err != nil { return nil, err } @@ -74,17 +79,42 @@ func (s Service) GetSession(ctx context.Context, req *pbs.GetSessionRequest) (*p return nil, err } - authzdActions := authResults.FetchActionSetForId(ctx, ses.Id, IdActions) + var outputFields perms.OutputFieldsMap + authorizedActions := authResults.FetchActionSetForId(ctx, ses.GetPublicId(), IdActions) + // Check to see if we need to verify Read vs. just ReadSelf - if ses.GetUserId() != authResults.UserId { - if !authzdActions.HasAction(action.Read) { + if ses.UserId != authResults.UserId { + if !authorizedActions.HasAction(action.Read) { return nil, handlers.ForbiddenError() } + outputFields = authResults.FetchOutputFields(perms.Resource{ + Id: ses.GetPublicId(), + ScopeId: ses.ScopeId, + Type: resource.Session, + }, action.Read).SelfOrDefaults(authResults.UserId) + } else { + var ok bool + outputFields, ok = requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } } - ses.Scope = authResults.Scope - ses.AuthorizedActions = authzdActions.Strings() - return &pbs.GetSessionResponse{Item: ses}, nil + 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.Strings())) + } + + item, err := toProto(ctx, ses, outputOpts...) + if err != nil { + return nil, err + } + + return &pbs.GetSessionResponse{Item: item}, nil } // ListSessions implements the interface pbs.SessionServiceServer. @@ -116,32 +146,48 @@ func (s Service) ListSessions(ctx context.Context, req *pbs.ListSessionsRequest) return &pbs.ListSessionsResponse{}, nil } - seslist, err := s.listFromRepo(ctx, session.WithScopeIds(scopeIds)) + sesList, err := s.listFromRepo(ctx, scopeIds) if err != nil { return nil, err } + if len(sesList) == 0 { + return &pbs.ListSessionsResponse{}, nil + } filter, err := handlers.NewFilter(req.GetFilter()) if err != nil { return nil, err } - finalItems := make([]*pb.Session, 0, len(seslist)) - res := &perms.Resource{ + finalItems := make([]*pb.Session, 0, len(sesList)) + res := perms.Resource{ Type: resource.Session, } - for _, item := range seslist { - item.Scope = scopeInfoMap[item.GetScopeId()] - res.ScopeId = item.Scope.Id - authorizedActions := authResults.FetchActionSetForId(ctx, item.Id, IdActions, auth.WithResource(res)) + for _, item := range sesList { + res.Id = item.GetPublicId() + res.ScopeId = item.ScopeId + authorizedActions := authResults.FetchActionSetForId(ctx, item.GetPublicId(), IdActions, auth.WithResource(&res)) if len(authorizedActions) == 0 { continue } - if authorizedActions.OnlySelf() && item.GetUserId() != authResults.UserId { + if authorizedActions.OnlySelf() && item.UserId != authResults.UserId { continue } - item.AuthorizedActions = authorizedActions.Strings() + 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(scopeInfoMap[item.ScopeId])) + } + if outputFields.Has(globals.AuthorizedActionsField) { + outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authorizedActions.Strings())) + } + + item, err := toProto(ctx, item, outputOpts...) + if err != nil { + return nil, err + } if filter.Match(item) { finalItems = append(finalItems, item) @@ -153,6 +199,8 @@ func (s Service) ListSessions(ctx context.Context, req *pbs.ListSessionsRequest) // CancelSession implements the interface pbs.SessionServiceServer. func (s Service) CancelSession(ctx context.Context, req *pbs.CancelSessionRequest) (*pbs.CancelSessionResponse, error) { + const op = "sessions.(Service).CancelSession" + if err := validateCancelRequest(req); err != nil { return nil, err } @@ -165,24 +213,50 @@ func (s Service) CancelSession(ctx context.Context, req *pbs.CancelSessionReques if err != nil { return nil, err } - authzdActions := authResults.FetchActionSetForId(ctx, ses.Id, IdActions) - // Check to see if we need to verify Cancel vs. just CancelSelf - if ses.GetUserId() != authResults.UserId { - if !authzdActions.HasAction(action.Cancel) { + + var outputFields perms.OutputFieldsMap + authorizedActions := authResults.FetchActionSetForId(ctx, ses.GetPublicId(), IdActions) + + // Check to see if we need to verify Read vs. just ReadSelf + if ses.UserId != authResults.UserId { + if !authorizedActions.HasAction(action.Cancel) { return nil, handlers.ForbiddenError() } + outputFields = authResults.FetchOutputFields(perms.Resource{ + Id: ses.GetPublicId(), + ScopeId: ses.ScopeId, + Type: resource.Session, + }, action.Cancel).SelfOrDefaults(authResults.UserId) + } else { + var ok bool + outputFields, ok = requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } } ses, err = s.cancelInRepo(ctx, req.GetId(), req.GetVersion()) if err != nil { return nil, err } - ses.Scope = authResults.Scope - ses.AuthorizedActions = authzdActions.Strings() - return &pbs.CancelSessionResponse{Item: ses}, nil + + 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.Strings())) + } + + item, err := toProto(ctx, ses, outputOpts...) + if err != nil { + return nil, err + } + return &pbs.CancelSessionResponse{Item: item}, nil } -func (s Service) getFromRepo(ctx context.Context, id string) (*pb.Session, error) { +func (s Service) getFromRepo(ctx context.Context, id string) (*session.Session, error) { repo, err := s.repoFn() if err != nil { return nil, err @@ -197,26 +271,22 @@ func (s Service) getFromRepo(ctx context.Context, id string) (*pb.Session, error if sess == nil { return nil, handlers.NotFoundErrorf("Session %q doesn't exist.", id) } - return toProto(sess), nil + return sess, nil } -func (s Service) listFromRepo(ctx context.Context, opts ...session.Option) ([]*pb.Session, error) { +func (s Service) listFromRepo(ctx context.Context, scopeIds []string) ([]*session.Session, error) { repo, err := s.repoFn() if err != nil { return nil, err } - seslist, err := repo.ListSessions(ctx, opts...) + sesList, err := repo.ListSessions(ctx, session.WithScopeIds(scopeIds)) if err != nil { return nil, err } - var outSl []*pb.Session - for _, ses := range seslist { - outSl = append(outSl, toProto(ses)) - } - return outSl, nil + return sesList, nil } -func (s Service) cancelInRepo(ctx context.Context, id string, version uint32) (*pb.Session, error) { +func (s Service) cancelInRepo(ctx context.Context, id string, version uint32) (*session.Session, error) { const op = "sessions.(Service).cancelInRepo" repo, err := s.repoFn() if err != nil { @@ -226,7 +296,7 @@ func (s Service) cancelInRepo(ctx context.Context, id string, version uint32) (* if err != nil { return nil, errors.Wrap(err, op, errors.WithMsg("unable to update session")) } - return toProto(out), nil + return out, nil } func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.VerifyResults { @@ -276,42 +346,89 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type) auth. return auth.Verify(ctx, opts...) } -func toProto(in *session.Session) *pb.Session { - out := pb.Session{ - Id: in.GetPublicId(), - ScopeId: in.ScopeId, - TargetId: in.TargetId, - Version: in.Version, - UserId: in.UserId, - HostId: in.HostId, - HostSetId: in.HostSetId, - AuthTokenId: in.AuthTokenId, - Endpoint: in.Endpoint, - Type: target.SubtypeFromId(in.TargetId).String(), - // TODO: Provide the ServerType and the ServerId when that information becomes relevant in the API. +func toProto(ctx context.Context, in *session.Session, opt ...handlers.Option) (*pb.Session, error) { + opts := handlers.GetOpts(opt...) + if opts.WithOutputFields == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "output fields not found when building auth token proto") + } + outputFields := *opts.WithOutputFields - CreatedTime: in.CreateTime.GetTimestamp(), - UpdatedTime: in.UpdateTime.GetTimestamp(), - ExpirationTime: in.ExpirationTime.GetTimestamp(), - Certificate: in.Certificate, - TerminationReason: in.TerminationReason, + out := pb.Session{} + if outputFields.Has(globals.IdField) { + out.Id = in.GetPublicId() } + if outputFields.Has(globals.ScopeIdField) { + out.ScopeId = in.ScopeId + } + if outputFields.Has(globals.TargetIdField) { + out.TargetId = in.TargetId + } + if outputFields.Has(globals.TypeField) { + out.Type = target.SubtypeFromId(in.TargetId).String() + } + if outputFields.Has(globals.CreatedTimeField) { + out.CreatedTime = in.CreateTime.GetTimestamp() + } + if outputFields.Has(globals.UpdatedTimeField) { + out.UpdatedTime = in.UpdateTime.GetTimestamp() + } + if outputFields.Has(globals.VersionField) { + out.Version = in.Version + } + if outputFields.Has(globals.UserIdField) { + out.UserId = in.UserId + } + if outputFields.Has(globals.HostIdField) { + out.HostId = in.HostId + } + if outputFields.Has(globals.HostSetIdField) { + out.HostSetId = in.HostSetId + } + if outputFields.Has(globals.AuthTokenIdField) { + out.AuthTokenId = in.AuthTokenId + } + if outputFields.Has(globals.EndpointField) { + out.Endpoint = in.Endpoint + } + if outputFields.Has(globals.HostIdField) { + out.HostId = in.HostId + } + if outputFields.Has(globals.ExpirationTimeField) { + out.ExpirationTime = in.ExpirationTime.GetTimestamp() + } + if outputFields.Has(globals.CertificateField) { + out.Certificate = in.Certificate + } + if outputFields.Has(globals.TerminationReasonField) { + out.TerminationReason = in.TerminationReason + } + if outputFields.Has(globals.ScopeField) { + out.Scope = opts.WithScope + } + if outputFields.Has(globals.AuthorizedActionsField) { + out.AuthorizedActions = opts.WithAuthorizedActions + } + // TODO: Provide the ServerType and the ServerId when that information becomes relevant in the API. if len(in.States) > 0 { - out.Status = in.States[0].Status.String() + if outputFields.Has(globals.StatusField) { + out.Status = in.States[0].Status.String() + } + if outputFields.Has(globals.StatesField) { + for _, s := range in.States { + sessState := &pb.SessionState{ + Status: s.Status.String(), + } + if s.StartTime != nil { + sessState.StartTime = s.StartTime.GetTimestamp() + } + if s.EndTime != nil { + sessState.EndTime = s.EndTime.GetTimestamp() + } + out.States = append(out.States, sessState) + } + } } - for _, s := range in.States { - sessState := &pb.SessionState{ - Status: s.Status.String(), - } - if s.StartTime != nil { - sessState.StartTime = s.StartTime.GetTimestamp() - } - if s.EndTime != nil { - sessState.EndTime = s.EndTime.GetTimestamp() - } - out.States = append(out.States, sessState) - } - return &out + return &out, nil } // A validateX method should exist for each method above. These methods do not make calls to any backing service but enforce diff --git a/internal/servers/controller/handlers/sessions/session_service_test.go b/internal/servers/controller/handlers/sessions/session_service_test.go index d4c330a3d..35cd45cbf 100644 --- a/internal/servers/controller/handlers/sessions/session_service_test.go +++ b/internal/servers/controller/handlers/sessions/session_service_test.go @@ -387,24 +387,44 @@ func TestList(t *testing.T) { } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { + require, assert := require.New(t), assert.New(t) s, err := sessions.NewService(sessRepoFn, iamRepoFn) - require.NoError(t, err, "Couldn't create new session service.") + require.NoError(err, "Couldn't create new session service.") + // Test without anon user got, gErr := s.ListSessions(auth.DisabledAuthTestContext(iamRepoFn, tc.req.GetScopeId()), tc.req) if tc.err != nil { - require.Error(t, gErr) - assert.True(t, errors.Is(gErr, tc.err), "ListSessions(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) - } else { - require.NoError(t, gErr) + require.Error(gErr) + assert.True(errors.Is(gErr, tc.err), "ListSessions(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) + return } + require.NoError(gErr) if tc.res != nil { - require.Equal(t, len(tc.res.GetItems()), len(got.GetItems()), "Didn't get expected number of sessions: %v", got.GetItems()) + require.Equal(len(tc.res.GetItems()), len(got.GetItems()), "Didn't get expected number of sessions: %v", got.GetItems()) for i, wantSess := range tc.res.GetItems() { - assert.True(t, got.GetItems()[i].GetExpirationTime().AsTime().Sub(wantSess.GetExpirationTime().AsTime()) < 10*time.Millisecond) + assert.True(got.GetItems()[i].GetExpirationTime().AsTime().Sub(wantSess.GetExpirationTime().AsTime()) < 10*time.Millisecond) wantSess.ExpirationTime = got.GetItems()[i].GetExpirationTime() } } - assert.Empty(t, cmp.Diff(got, tc.res, protocmp.Transform()), "ListSessions(%q) got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListSessions(%q) got response %q, wanted %q", tc.req, got, tc.res) + + // Test with anon user + got, gErr = s.ListSessions(auth.DisabledAuthTestContext(iamRepoFn, tc.req.GetScopeId(), auth.WithUserId(auth.AnonymousUserId)), tc.req) + require.NoError(gErr) + assert.Len(got.Items, len(tc.res.Items)) + for _, item := range got.GetItems() { + require.Empty(item.Version) + require.Empty(item.UserId) + require.Empty(item.HostId) + require.Empty(item.HostSetId) + require.Empty(item.AuthTokenId) + require.Empty(item.Endpoint) + require.Nil(item.CreatedTime) + require.Nil(item.ExpirationTime) + require.Nil(item.UpdatedTime) + require.Empty(item.Certificate) + require.Empty(item.TerminationReason) + } }) } } diff --git a/internal/servers/controller/handlers/targets/target_service.go b/internal/servers/controller/handlers/targets/target_service.go index dc7ce6fcd..f5f47a535 100644 --- a/internal/servers/controller/handlers/targets/target_service.go +++ b/internal/servers/controller/handlers/targets/target_service.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/golang/protobuf/ptypes/wrappers" + "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/auth" "github.com/hashicorp/boundary/internal/db/timestamp" "github.com/hashicorp/boundary/internal/errors" @@ -18,6 +19,7 @@ import ( "github.com/hashicorp/boundary/internal/host/static" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/perms" + "github.com/hashicorp/boundary/internal/requests" "github.com/hashicorp/boundary/internal/servers" "github.com/hashicorp/boundary/internal/servers/controller/common" "github.com/hashicorp/boundary/internal/servers/controller/common/scopeids" @@ -145,26 +147,45 @@ func (s Service) ListTargets(ctx context.Context, req *pbs.ListTargetsRequest) ( return &pbs.ListTargetsResponse{}, nil } - ul, err := s.listFromRepo(ctx, scopeIds) + tl, err := s.listFromRepo(ctx, scopeIds) if err != nil { return nil, err } + if len(tl) == 0 { + return &pbs.ListTargetsResponse{}, nil + } filter, err := handlers.NewFilter(req.GetFilter()) if err != nil { return nil, err } - finalItems := make([]*pb.Target, 0, len(ul)) - res := &perms.Resource{ + finalItems := make([]*pb.Target, 0, len(tl)) + res := perms.Resource{ Type: resource.Target, } - for _, item := range ul { - item.Scope = scopeInfoMap[item.GetScopeId()] - res.ScopeId = item.Scope.Id - item.AuthorizedActions = authResults.FetchActionSetForId(ctx, item.Id, IdActions, auth.WithResource(res)).Strings() - if len(item.AuthorizedActions) == 0 { + for _, item := range tl { + res.Id = item.GetPublicId() + res.ScopeId = item.GetScopeId() + authorizedActions := authResults.FetchActionSetForId(ctx, item.GetPublicId(), IdActions, auth.WithResource(&res)).Strings() + if len(authorizedActions) == 0 { continue } + + 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(scopeInfoMap[item.GetScopeId()])) + } + if outputFields.Has(globals.AuthorizedActionsField) { + outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authorizedActions)) + } + + item, err := toProto(ctx, item, nil, outputOpts...) + if err != nil { + return nil, err + } + if filter.Match(item) { finalItems = append(finalItems, item) } @@ -174,6 +195,8 @@ func (s Service) ListTargets(ctx context.Context, req *pbs.ListTargetsRequest) ( // GetTargets implements the interface pbs.TargetServiceServer. func (s Service) GetTarget(ctx context.Context, req *pbs.GetTargetRequest) (*pbs.GetTargetResponse, error) { + const op = "targets.(Service).GetTarget" + if err := validateGetRequest(req); err != nil { return nil, err } @@ -181,17 +204,37 @@ func (s Service) GetTarget(ctx context.Context, req *pbs.GetTargetRequest) (*pbs if authResults.Error != nil { return nil, authResults.Error } - u, err := s.getFromRepo(ctx, req.GetId()) + t, ts, err := s.getFromRepo(ctx, req.GetId()) if err != nil { return nil, err } - u.Scope = authResults.Scope - u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions).Strings() - return &pbs.GetTargetResponse{Item: u}, nil + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, t.GetPublicId(), IdActions).Strings())) + } + + item, err := toProto(ctx, t, ts, outputOpts...) + if err != nil { + return nil, err + } + + return &pbs.GetTargetResponse{Item: item}, nil } // CreateTarget implements the interface pbs.TargetServiceServer. func (s Service) CreateTarget(ctx context.Context, req *pbs.CreateTargetRequest) (*pbs.CreateTargetResponse, error) { + const op = "targets.(Service).CreateTarget" + if err := validateCreateRequest(req); err != nil { return nil, err } @@ -199,17 +242,37 @@ func (s Service) CreateTarget(ctx context.Context, req *pbs.CreateTargetRequest) if authResults.Error != nil { return nil, authResults.Error } - u, err := s.createInRepo(ctx, req.GetItem()) + t, ts, err := s.createInRepo(ctx, req.GetItem()) if err != nil { return nil, err } - u.Scope = authResults.Scope - u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions).Strings() - return &pbs.CreateTargetResponse{Item: u, Uri: fmt.Sprintf("targets/%s", u.GetId())}, nil + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, t.GetPublicId(), IdActions).Strings())) + } + + item, err := toProto(ctx, t, ts, outputOpts...) + if err != nil { + return nil, err + } + + return &pbs.CreateTargetResponse{Item: item, Uri: fmt.Sprintf("targets/%s", item.GetId())}, nil } // UpdateTarget implements the interface pbs.TargetServiceServer. func (s Service) UpdateTarget(ctx context.Context, req *pbs.UpdateTargetRequest) (*pbs.UpdateTargetResponse, error) { + const op = "targets.(Service).UpdateTarget" + if err := validateUpdateRequest(req); err != nil { return nil, err } @@ -217,13 +280,31 @@ func (s Service) UpdateTarget(ctx context.Context, req *pbs.UpdateTargetRequest) if authResults.Error != nil { return nil, authResults.Error } - u, err := s.updateInRepo(ctx, authResults.Scope.GetId(), req.GetId(), req.GetUpdateMask().GetPaths(), req.GetItem()) + t, ts, err := s.updateInRepo(ctx, authResults.Scope.GetId(), req.GetId(), req.GetUpdateMask().GetPaths(), req.GetItem()) if err != nil { return nil, err } - u.Scope = authResults.Scope - u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions).Strings() - return &pbs.UpdateTargetResponse{Item: u}, nil + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, t.GetPublicId(), IdActions).Strings())) + } + + item, err := toProto(ctx, t, ts, outputOpts...) + if err != nil { + return nil, err + } + + return &pbs.UpdateTargetResponse{Item: item}, nil } // DeleteTarget implements the interface pbs.TargetServiceServer. @@ -244,6 +325,8 @@ func (s Service) DeleteTarget(ctx context.Context, req *pbs.DeleteTargetRequest) // AddTargetHostSets implements the interface pbs.TargetServiceServer. func (s Service) AddTargetHostSets(ctx context.Context, req *pbs.AddTargetHostSetsRequest) (*pbs.AddTargetHostSetsResponse, error) { + const op = "targets.(Service).AddTargetHostSets" + if err := validateAddRequest(req); err != nil { return nil, err } @@ -251,17 +334,37 @@ func (s Service) AddTargetHostSets(ctx context.Context, req *pbs.AddTargetHostSe if authResults.Error != nil { return nil, authResults.Error } - u, err := s.addInRepo(ctx, req.GetId(), req.GetHostSetIds(), req.GetVersion()) + t, ts, err := s.addInRepo(ctx, req.GetId(), req.GetHostSetIds(), req.GetVersion()) if err != nil { return nil, err } - u.Scope = authResults.Scope - u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions).Strings() - return &pbs.AddTargetHostSetsResponse{Item: u}, nil + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, t.GetPublicId(), IdActions).Strings())) + } + + item, err := toProto(ctx, t, ts, outputOpts...) + if err != nil { + return nil, err + } + + return &pbs.AddTargetHostSetsResponse{Item: item}, nil } // SetTargetHostSets implements the interface pbs.TargetServiceServer. func (s Service) SetTargetHostSets(ctx context.Context, req *pbs.SetTargetHostSetsRequest) (*pbs.SetTargetHostSetsResponse, error) { + const op = "targets.(Service).SetTargetHostSets" + if err := validateSetRequest(req); err != nil { return nil, err } @@ -269,17 +372,37 @@ func (s Service) SetTargetHostSets(ctx context.Context, req *pbs.SetTargetHostSe if authResults.Error != nil { return nil, authResults.Error } - u, err := s.setInRepo(ctx, req.GetId(), req.GetHostSetIds(), req.GetVersion()) + t, ts, err := s.setInRepo(ctx, req.GetId(), req.GetHostSetIds(), req.GetVersion()) if err != nil { return nil, err } - u.Scope = authResults.Scope - u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions).Strings() - return &pbs.SetTargetHostSetsResponse{Item: u}, nil + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, t.GetPublicId(), IdActions).Strings())) + } + + item, err := toProto(ctx, t, ts, outputOpts...) + if err != nil { + return nil, err + } + + return &pbs.SetTargetHostSetsResponse{Item: item}, nil } // RemoveTargetHostSets implements the interface pbs.TargetServiceServer. func (s Service) RemoveTargetHostSets(ctx context.Context, req *pbs.RemoveTargetHostSetsRequest) (*pbs.RemoveTargetHostSetsResponse, error) { + const op = "targets.(Service).RemoveTargetHostSets" + if err := validateRemoveRequest(req); err != nil { return nil, err } @@ -287,13 +410,31 @@ func (s Service) RemoveTargetHostSets(ctx context.Context, req *pbs.RemoveTarget if authResults.Error != nil { return nil, authResults.Error } - u, err := s.removeInRepo(ctx, req.GetId(), req.GetHostSetIds(), req.GetVersion()) + t, ts, err := s.removeInRepo(ctx, req.GetId(), req.GetHostSetIds(), req.GetVersion()) if err != nil { return nil, err } - u.Scope = authResults.Scope - u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions).Strings() - return &pbs.RemoveTargetHostSetsResponse{Item: u}, nil + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, t.GetPublicId(), IdActions).Strings())) + } + + item, err := toProto(ctx, t, ts, outputOpts...) + if err != nil { + return nil, err + } + + return &pbs.RemoveTargetHostSetsResponse{Item: item}, nil } func (s Service) AuthorizeSession(ctx context.Context, req *pbs.AuthorizeSessionRequest) (*pbs.AuthorizeSessionResponse, error) { @@ -574,25 +715,25 @@ HostSetIterationLoop: return &pbs.AuthorizeSessionResponse{Item: ret}, nil } -func (s Service) getFromRepo(ctx context.Context, id string) (*pb.Target, error) { +func (s Service) getFromRepo(ctx context.Context, id string) (target.Target, []*target.TargetSet, error) { repo, err := s.repoFn() if err != nil { - return nil, err + return nil, nil, err } u, m, err := repo.LookupTarget(ctx, id) if err != nil { if errors.IsNotFoundError(err) { - return nil, handlers.NotFoundErrorf("Target %q doesn't exist.", id) + return nil, nil, handlers.NotFoundErrorf("Target %q doesn't exist.", id) } - return nil, err + return nil, nil, err } if u == nil { - return nil, handlers.NotFoundErrorf("Target %q doesn't exist.", id) + return nil, nil, handlers.NotFoundErrorf("Target %q doesn't exist.", id) } - return toProto(u, m) + return u, m, nil } -func (s Service) createInRepo(ctx context.Context, item *pb.Target) (*pb.Target, error) { +func (s Service) createInRepo(ctx context.Context, item *pb.Target) (target.Target, []*target.TargetSet, error) { const op = "targets.(Service).createInRepo" opts := []target.Option{target.WithName(item.GetName().GetValue())} if item.GetDescription() != nil { @@ -609,30 +750,30 @@ func (s Service) createInRepo(ctx context.Context, item *pb.Target) (*pb.Target, } tcpAttrs := &pb.TcpTargetAttributes{} if err := handlers.StructToProto(item.GetAttributes(), tcpAttrs); err != nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, "Provided attributes don't match expected format.") + return nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, "Provided attributes don't match expected format.") } if tcpAttrs.GetDefaultPort().GetValue() != 0 { opts = append(opts, target.WithDefaultPort(tcpAttrs.GetDefaultPort().GetValue())) } u, err := target.NewTcpTarget(item.GetScopeId(), opts...) if err != nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to build target for creation: %v.", err) + return nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to build target for creation: %v.", err) } repo, err := s.repoFn() if err != nil { - return nil, err + return nil, nil, err } out, m, err := repo.CreateTcpTarget(ctx, u) if err != nil { - return nil, errors.Wrap(err, op, errors.WithMsg("unable to create target")) + return nil, nil, errors.Wrap(err, op, errors.WithMsg("unable to create target")) } if out == nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create target but no error returned from repository.") + return nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create target but no error returned from repository.") } - return toProto(out, m) + return out, m, nil } -func (s Service) updateInRepo(ctx context.Context, scopeId, id string, mask []string, item *pb.Target) (*pb.Target, error) { +func (s Service) updateInRepo(ctx context.Context, scopeId, id string, mask []string, item *pb.Target) (target.Target, []*target.TargetSet, error) { const op = "targets.(Service).updateInRepo" var opts []target.Option if desc := item.GetDescription(); desc != nil { @@ -652,7 +793,7 @@ func (s Service) updateInRepo(ctx context.Context, scopeId, id string, mask []st } tcpAttrs := &pb.TcpTargetAttributes{} if err := handlers.StructToProto(item.GetAttributes(), tcpAttrs); err != nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, "Provided attributes don't match expected format.") + return nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, "Provided attributes don't match expected format.") } if tcpAttrs.GetDefaultPort().GetValue() != 0 { opts = append(opts, target.WithDefaultPort(tcpAttrs.GetDefaultPort().GetValue())) @@ -660,25 +801,25 @@ func (s Service) updateInRepo(ctx context.Context, scopeId, id string, mask []st version := item.GetVersion() u, err := target.NewTcpTarget(scopeId, opts...) if err != nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to build target for update: %v.", err) + return nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to build target for update: %v.", err) } u.PublicId = id dbMask := maskManager.Translate(mask) if len(dbMask) == 0 { - return nil, handlers.InvalidArgumentErrorf("No valid fields included in the update mask.", map[string]string{"update_mask": "No valid paths provided in the update mask."}) + return nil, nil, handlers.InvalidArgumentErrorf("No valid fields included in the update mask.", map[string]string{"update_mask": "No valid paths provided in the update mask."}) } repo, err := s.repoFn() if err != nil { - return nil, err + return nil, nil, err } out, m, rowsUpdated, err := repo.UpdateTcpTarget(ctx, u, version, dbMask) if err != nil { - return nil, errors.Wrap(err, op, errors.WithMsg("unable to update target")) + return nil, nil, errors.Wrap(err, op, errors.WithMsg("unable to update target")) } if rowsUpdated == 0 { - return nil, handlers.NotFoundErrorf("Target %q not found or incorrect version provided.", id) + return nil, nil, handlers.NotFoundErrorf("Target %q not found or incorrect version provided.", id) } - return toProto(out, m) + return out, m, nil } func (s Service) deleteFromRepo(ctx context.Context, id string) (bool, error) { @@ -697,7 +838,7 @@ func (s Service) deleteFromRepo(ctx context.Context, id string) (bool, error) { return rows > 0, nil } -func (s Service) listFromRepo(ctx context.Context, scopeIds []string) ([]*pb.Target, error) { +func (s Service) listFromRepo(ctx context.Context, scopeIds []string) ([]target.Target, error) { repo, err := s.repoFn() if err != nil { return nil, err @@ -706,74 +847,66 @@ func (s Service) listFromRepo(ctx context.Context, scopeIds []string) ([]*pb.Tar if err != nil { return nil, err } - var outUl []*pb.Target - for _, u := range ul { - o, err := toProto(u, nil) - if err != nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to convert value to proto: %v.", err) - } - outUl = append(outUl, o) - } - return outUl, nil + return ul, nil } -func (s Service) addInRepo(ctx context.Context, targetId string, hostSetId []string, version uint32) (*pb.Target, error) { +func (s Service) addInRepo(ctx context.Context, targetId string, hostSetId []string, version uint32) (target.Target, []*target.TargetSet, error) { repo, err := s.repoFn() if err != nil { - return nil, err + return nil, nil, err } out, m, err := repo.AddTargetHostSets(ctx, targetId, version, strutil.RemoveDuplicates(hostSetId, false)) if err != nil { // TODO: Figure out a way to surface more helpful error info beyond the Internal error. - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to add host sets to target: %v.", err) + return nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to add host sets to target: %v.", err) } if out == nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup target after adding host sets to it.") + return nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup target after adding host sets to it.") } - return toProto(out, m) + return out, m, nil } -func (s Service) setInRepo(ctx context.Context, targetId string, hostSetIds []string, version uint32) (*pb.Target, error) { +func (s Service) setInRepo(ctx context.Context, targetId string, hostSetIds []string, version uint32) (target.Target, []*target.TargetSet, error) { const op = "targets.(Service).setInRepo" repo, err := s.repoFn() if err != nil { - return nil, err + return nil, nil, err } _, _, err = repo.SetTargetHostSets(ctx, targetId, version, strutil.RemoveDuplicates(hostSetIds, false)) if err != nil { // TODO: Figure out a way to surface more helpful error info beyond the Internal error. - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to set host sets in target: %v.", err) + return nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to set host sets in target: %v.", err) } out, m, err := repo.LookupTarget(ctx, targetId) if err != nil { - return nil, errors.Wrap(err, op, errors.WithMsg("unable to look up target after setting host sets")) + return nil, nil, errors.Wrap(err, op, errors.WithMsg("unable to look up target after setting host sets")) } if out == nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup target after setting host sets for it.") + return nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup target after setting host sets for it.") } - return toProto(out, m) + return out, m, nil } -func (s Service) removeInRepo(ctx context.Context, targetId string, hostSetIds []string, version uint32) (*pb.Target, error) { +func (s Service) removeInRepo(ctx context.Context, targetId string, hostSetIds []string, version uint32) (target.Target, []*target.TargetSet, error) { const op = "targets.(Service).removeInRepo" repo, err := s.repoFn() if err != nil { - return nil, err + return nil, nil, err } _, err = repo.DeleteTargeHostSets(ctx, targetId, version, strutil.RemoveDuplicates(hostSetIds, false)) if err != nil { // TODO: Figure out a way to surface more helpful error info beyond the Internal error. - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to remove host sets from target: %v.", err) + return nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to remove host sets from target: %v.", err) } out, m, err := repo.LookupTarget(ctx, targetId) if err != nil { - return nil, errors.Wrap(err, op, errors.WithMsg("unable to look up target after removing host sets")) + return nil, nil, errors.Wrap(err, op, errors.WithMsg("unable to look up target after removing host sets")) } if out == nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup target after removing host sets from it.") + return nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup target after removing host sets from it.") } - return toProto(out, m) + return out, m, nil } func (s Service) authResult(ctx context.Context, id string, a action.Type, lookupOpt ...target.Option) auth.VerifyResults { @@ -829,41 +962,76 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type, looku return ret } -func toProto(in target.Target, m []*target.TargetSet) (*pb.Target, error) { - out := pb.Target{ - Id: in.GetPublicId(), - ScopeId: in.GetScopeId(), - CreatedTime: in.GetCreateTime().GetTimestamp(), - UpdatedTime: in.GetUpdateTime().GetTimestamp(), - Version: in.GetVersion(), - Type: target.TcpTargetType.String(), - SessionMaxSeconds: wrapperspb.UInt32(in.GetSessionMaxSeconds()), - SessionConnectionLimit: wrapperspb.Int32(in.GetSessionConnectionLimit()), +func toProto(ctx context.Context, in target.Target, m []*target.TargetSet, opt ...handlers.Option) (*pb.Target, error) { + opts := handlers.GetOpts(opt...) + if opts.WithOutputFields == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "output fields not found when building target proto") } - if in.GetDescription() != "" { + outputFields := *opts.WithOutputFields + + out := pb.Target{} + if outputFields.Has(globals.IdField) { + out.Id = in.GetPublicId() + } + if outputFields.Has(globals.ScopeIdField) { + out.ScopeId = in.GetScopeId() + } + if outputFields.Has(globals.TypeField) { + out.Type = target.TcpTargetType.String() + } + if outputFields.Has(globals.DescriptionField) && in.GetDescription() != "" { out.Description = wrapperspb.String(in.GetDescription()) } - if in.GetName() != "" { + if outputFields.Has(globals.NameField) && in.GetName() != "" { out.Name = wrapperspb.String(in.GetName()) } - if in.GetWorkerFilter() != "" { + 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.SessionMaxSecondsField) { + out.SessionMaxSeconds = wrapperspb.UInt32(in.GetSessionMaxSeconds()) + } + if outputFields.Has(globals.SessionConnectionLimitField) { + out.SessionConnectionLimit = wrapperspb.Int32(in.GetSessionConnectionLimit()) + } + if outputFields.Has(globals.WorkerFilterField) && in.GetWorkerFilter() != "" { out.WorkerFilter = wrapperspb.String(in.GetWorkerFilter()) } - attrs := &pb.TcpTargetAttributes{} - if in.GetDefaultPort() > 0 { - attrs.DefaultPort = &wrappers.UInt32Value{Value: in.GetDefaultPort()} + if outputFields.Has(globals.ScopeField) { + out.Scope = opts.WithScope } - st, err := handlers.ProtoToStruct(attrs) - if err != nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "failed building password attribute struct: %v", err) + if outputFields.Has(globals.AuthorizedActionsField) { + out.AuthorizedActions = opts.WithAuthorizedActions } - out.Attributes = st - for _, hs := range m { - out.HostSetIds = append(out.HostSetIds, hs.GetPublicId()) - out.HostSets = append(out.HostSets, &pb.HostSet{ - Id: hs.GetPublicId(), - HostCatalogId: hs.GetCatalogId(), - }) + if outputFields.Has(globals.HostSetIdsField) { + for _, hs := range m { + out.HostSetIds = append(out.HostSetIds, hs.GetPublicId()) + } + } + if outputFields.Has(globals.HostSetsField) { + for _, hs := range m { + out.HostSets = append(out.HostSets, &pb.HostSet{ + Id: hs.GetPublicId(), + HostCatalogId: hs.GetCatalogId(), + }) + } + } + if outputFields.Has(globals.AttributesField) { + attrs := &pb.TcpTargetAttributes{} + if in.GetDefaultPort() > 0 { + attrs.DefaultPort = &wrappers.UInt32Value{Value: in.GetDefaultPort()} + } + st, err := handlers.ProtoToStruct(attrs) + if err != nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "failed building password attribute struct: %v", err) + } + out.Attributes = st } return &out, nil } diff --git a/internal/servers/controller/handlers/targets/target_service_test.go b/internal/servers/controller/handlers/targets/target_service_test.go index f45dec070..8123d118d 100644 --- a/internal/servers/controller/handlers/targets/target_service_test.go +++ b/internal/servers/controller/handlers/targets/target_service_test.go @@ -243,14 +243,29 @@ func TestList(t *testing.T) { s, err := testService(t, conn, kms, wrapper) require.NoError(err, "Couldn't create new host set service.") + // Test with non-anon user got, gErr := s.ListTargets(auth.DisabledAuthTestContext(iamRepoFn, tc.req.GetScopeId()), tc.req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "ListTargets(%q) got error %v, wanted %v", tc.req.GetScopeId(), gErr, tc.err) - } else { - require.NoError(gErr) + return } + require.NoError(gErr) assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListTargets(%q) scope %q, got response %q, wanted %q", tc.name, tc.req.GetScopeId(), got, tc.res) + + // Test with anon user + got, gErr = s.ListTargets(auth.DisabledAuthTestContext(iamRepoFn, tc.req.GetScopeId(), auth.WithUserId(auth.AnonymousUserId)), tc.req) + require.NoError(gErr) + assert.Len(got.Items, len(tc.res.Items)) + for _, item := range got.GetItems() { + require.Empty(item.Version) + require.Nil(item.CreatedTime) + require.Nil(item.UpdatedTime) + require.Nil(item.SessionMaxSeconds) + require.Nil(item.SessionConnectionLimit) + require.Empty(item.WorkerFilter) + require.Nil(item.Attributes) + } }) } } diff --git a/internal/servers/controller/handlers/users/user_service.go b/internal/servers/controller/handlers/users/user_service.go index 9b4d81623..5e29132d5 100644 --- a/internal/servers/controller/handlers/users/user_service.go +++ b/internal/servers/controller/handlers/users/user_service.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/auth" "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/auth/password" @@ -13,6 +14,7 @@ import ( "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/iam/store" "github.com/hashicorp/boundary/internal/perms" + "github.com/hashicorp/boundary/internal/requests" "github.com/hashicorp/boundary/internal/servers/controller/common" "github.com/hashicorp/boundary/internal/servers/controller/common/scopeids" "github.com/hashicorp/boundary/internal/servers/controller/handlers" @@ -104,21 +106,41 @@ func (s Service) ListUsers(ctx context.Context, req *pbs.ListUsersRequest) (*pbs if err != nil { return nil, err } + if len(ul) == 0 { + return &pbs.ListUsersResponse{}, nil + } + filter, err := handlers.NewFilter(req.GetFilter()) if err != nil { return nil, err } finalItems := make([]*pb.User, 0, len(ul)) - res := &perms.Resource{ + res := perms.Resource{ Type: resource.User, } for _, item := range ul { - item.Scope = scopeInfoMap[item.GetScopeId()] - res.ScopeId = item.Scope.Id - item.AuthorizedActions = authResults.FetchActionSetForId(ctx, item.Id, IdActions, auth.WithResource(res)).Strings() - if len(item.AuthorizedActions) == 0 { + res.Id = item.GetPublicId() + res.ScopeId = item.GetScopeId() + authorizedActions := authResults.FetchActionSetForId(ctx, item.GetPublicId(), IdActions, auth.WithResource(&res)).Strings() + if len(authorizedActions) == 0 { continue } + + 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(scopeInfoMap[item.GetScopeId()])) + } + if outputFields.Has(globals.AuthorizedActionsField) { + outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authorizedActions)) + } + + item, err := toProto(ctx, item, nil, outputOpts...) + if err != nil { + return nil, err + } + if filter.Match(item) { finalItems = append(finalItems, item) } @@ -128,6 +150,8 @@ func (s Service) ListUsers(ctx context.Context, req *pbs.ListUsersRequest) (*pbs // GetUsers implements the interface pbs.UserServiceServer. func (s Service) GetUser(ctx context.Context, req *pbs.GetUserRequest) (*pbs.GetUserResponse, error) { + const op = "users.(Service).GetUser" + if err := validateGetRequest(req); err != nil { return nil, err } @@ -135,17 +159,37 @@ func (s Service) GetUser(ctx context.Context, req *pbs.GetUserRequest) (*pbs.Get if authResults.Error != nil { return nil, authResults.Error } - u, err := s.getFromRepo(ctx, req.GetId()) + u, accts, err := s.getFromRepo(ctx, req.GetId()) if err != nil { return nil, err } - u.Scope = authResults.Scope - u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions).Strings() - return &pbs.GetUserResponse{Item: u}, nil + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, u.GetPublicId(), IdActions).Strings())) + } + + item, err := toProto(ctx, u, accts, outputOpts...) + if err != nil { + return nil, err + } + + return &pbs.GetUserResponse{Item: item}, nil } // CreateUser implements the interface pbs.UserServiceServer. func (s Service) CreateUser(ctx context.Context, req *pbs.CreateUserRequest) (*pbs.CreateUserResponse, error) { + const op = "users.(Service).CreateUser" + if err := validateCreateRequest(req); err != nil { return nil, err } @@ -157,13 +201,32 @@ func (s Service) CreateUser(ctx context.Context, req *pbs.CreateUserRequest) (*p if err != nil { return nil, err } - u.Scope = authResults.Scope - u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions).Strings() - return &pbs.CreateUserResponse{Item: u, Uri: fmt.Sprintf("users/%s", u.GetId())}, nil + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, u.GetPublicId(), IdActions).Strings())) + } + + item, err := toProto(ctx, u, nil, outputOpts...) + if err != nil { + return nil, err + } + return &pbs.CreateUserResponse{Item: item, Uri: fmt.Sprintf("users/%s", item.GetId())}, nil } // UpdateUser implements the interface pbs.UserServiceServer. func (s Service) UpdateUser(ctx context.Context, req *pbs.UpdateUserRequest) (*pbs.UpdateUserResponse, error) { + const op = "users.(Service).UpdateUser" + if err := validateUpdateRequest(req); err != nil { return nil, err } @@ -171,13 +234,31 @@ func (s Service) UpdateUser(ctx context.Context, req *pbs.UpdateUserRequest) (*p if authResults.Error != nil { return nil, authResults.Error } - u, err := s.updateInRepo(ctx, authResults.Scope.GetId(), req.GetId(), req.GetUpdateMask().GetPaths(), req.GetItem()) + u, accts, err := s.updateInRepo(ctx, authResults.Scope.GetId(), req.GetId(), req.GetUpdateMask().GetPaths(), req.GetItem()) if err != nil { return nil, err } - u.Scope = authResults.Scope - u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions).Strings() - return &pbs.UpdateUserResponse{Item: u}, nil + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, u.GetPublicId(), IdActions).Strings())) + } + + item, err := toProto(ctx, u, accts, outputOpts...) + if err != nil { + return nil, err + } + + return &pbs.UpdateUserResponse{Item: item}, nil } // DeleteUser implements the interface pbs.UserServiceServer. @@ -198,6 +279,8 @@ func (s Service) DeleteUser(ctx context.Context, req *pbs.DeleteUserRequest) (*p // AddUserAccounts implements the interface pbs.GroupServiceServer. func (s Service) AddUserAccounts(ctx context.Context, req *pbs.AddUserAccountsRequest) (*pbs.AddUserAccountsResponse, error) { + const op = "users.(Service).AddUserAccounts" + if err := validateAddUserAccountsRequest(req); err != nil { return nil, err } @@ -205,17 +288,37 @@ func (s Service) AddUserAccounts(ctx context.Context, req *pbs.AddUserAccountsRe if authResults.Error != nil { return nil, authResults.Error } - u, err := s.addInRepo(ctx, req.GetId(), req.GetAccountIds(), req.GetVersion()) + u, accts, err := s.addInRepo(ctx, req.GetId(), req.GetAccountIds(), req.GetVersion()) if err != nil { return nil, err } - u.Scope = authResults.Scope - u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions).Strings() - return &pbs.AddUserAccountsResponse{Item: u}, nil + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, u.GetPublicId(), IdActions).Strings())) + } + + item, err := toProto(ctx, u, accts, outputOpts...) + if err != nil { + return nil, err + } + + return &pbs.AddUserAccountsResponse{Item: item}, nil } // SetUserAccounts implements the interface pbs.GroupServiceServer. func (s Service) SetUserAccounts(ctx context.Context, req *pbs.SetUserAccountsRequest) (*pbs.SetUserAccountsResponse, error) { + const op = "users.(Service).SetUserAccounts" + if err := validateSetUserAccountsRequest(req); err != nil { return nil, err } @@ -223,17 +326,37 @@ func (s Service) SetUserAccounts(ctx context.Context, req *pbs.SetUserAccountsRe if authResults.Error != nil { return nil, authResults.Error } - u, err := s.setInRepo(ctx, req.GetId(), req.GetAccountIds(), req.GetVersion()) + u, accts, err := s.setInRepo(ctx, req.GetId(), req.GetAccountIds(), req.GetVersion()) if err != nil { return nil, err } - u.Scope = authResults.Scope - u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions).Strings() - return &pbs.SetUserAccountsResponse{Item: u}, nil + + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, u.GetPublicId(), IdActions).Strings())) + } + + item, err := toProto(ctx, u, accts, outputOpts...) + if err != nil { + return nil, err + } + + return &pbs.SetUserAccountsResponse{Item: item}, nil } // RemoveUserAccounts implements the interface pbs.GroupServiceServer. func (s Service) RemoveUserAccounts(ctx context.Context, req *pbs.RemoveUserAccountsRequest) (*pbs.RemoveUserAccountsResponse, error) { + const op = "users.(Service).RemoveUserAccounts" + if err := validateRemoveUserAccountsRequest(req); err != nil { return nil, err } @@ -241,34 +364,52 @@ func (s Service) RemoveUserAccounts(ctx context.Context, req *pbs.RemoveUserAcco if authResults.Error != nil { return nil, authResults.Error } - u, err := s.removeInRepo(ctx, req.GetId(), req.GetAccountIds(), req.GetVersion()) + u, accts, err := s.removeInRepo(ctx, req.GetId(), req.GetAccountIds(), req.GetVersion()) if err != nil { return nil, err } - u.Scope = authResults.Scope - u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions).Strings() - return &pbs.RemoveUserAccountsResponse{Item: u}, nil -} -func (s Service) getFromRepo(ctx context.Context, id string) (*pb.User, error) { - repo, err := s.repoFn() + outputFields, ok := requests.OutputFields(ctx) + if !ok { + return nil, errors.New(errors.Internal, op, "no request context found") + } + + 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(authResults.FetchActionSetForId(ctx, u.GetPublicId(), IdActions).Strings())) + } + + item, err := toProto(ctx, u, accts, outputOpts...) if err != nil { return nil, err } + + return &pbs.RemoveUserAccountsResponse{Item: item}, nil +} + +func (s Service) getFromRepo(ctx context.Context, id string) (*iam.User, []string, error) { + repo, err := s.repoFn() + if err != nil { + return nil, nil, err + } u, accts, err := repo.LookupUser(ctx, id) if err != nil { if errors.IsNotFoundError(err) { - return nil, handlers.NotFoundErrorf("User %q doesn't exist.", id) + return nil, nil, handlers.NotFoundErrorf("User %q doesn't exist.", id) } - return nil, err + return nil, nil, err } if u == nil { - return nil, handlers.NotFoundErrorf("User %q doesn't exist.", id) + return nil, nil, handlers.NotFoundErrorf("User %q doesn't exist.", id) } - return toProto(u, accts), nil + return u, accts, nil } -func (s Service) createInRepo(ctx context.Context, orgId string, item *pb.User) (*pb.User, error) { +func (s Service) createInRepo(ctx context.Context, orgId string, item *pb.User) (*iam.User, error) { const op = "users.(Service).createInRepo" var opts []iam.Option if item.GetName() != nil { @@ -292,10 +433,10 @@ func (s Service) createInRepo(ctx context.Context, orgId string, item *pb.User) if out == nil { return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create user but no error returned from repository.") } - return toProto(out, nil), nil + return out, nil } -func (s Service) updateInRepo(ctx context.Context, orgId, id string, mask []string, item *pb.User) (*pb.User, error) { +func (s Service) updateInRepo(ctx context.Context, orgId, id string, mask []string, item *pb.User) (*iam.User, []string, error) { const op = "users.(Service).updateInRepo" var opts []iam.Option if desc := item.GetDescription(); desc != nil { @@ -307,25 +448,25 @@ func (s Service) updateInRepo(ctx context.Context, orgId, id string, mask []stri version := item.GetVersion() u, err := iam.NewUser(orgId, opts...) if err != nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to build user for update: %v.", err) + return nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to build user for update: %v.", err) } u.PublicId = id dbMask := maskManager.Translate(mask) if len(dbMask) == 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."}) + return nil, nil, handlers.InvalidArgumentErrorf("No valid fields included in the update mask.", map[string]string{"update_mask": "No valid fields provided in the update mask."}) } repo, err := s.repoFn() if err != nil { - return nil, err + return nil, nil, err } out, accts, rowsUpdated, err := repo.UpdateUser(ctx, u, version, dbMask) if err != nil { - return nil, errors.Wrap(err, op, errors.WithMsg("unable to update user")) + return nil, nil, errors.Wrap(err, op, errors.WithMsg("unable to update user")) } if rowsUpdated == 0 { - return nil, handlers.NotFoundErrorf("User %q doesn't exist or incorrect version provided.", id) + return nil, nil, handlers.NotFoundErrorf("User %q doesn't exist or incorrect version provided.", id) } - return toProto(out, accts), nil + return out, accts, nil } func (s Service) deleteFromRepo(ctx context.Context, id string) (bool, error) { @@ -344,7 +485,7 @@ func (s Service) deleteFromRepo(ctx context.Context, id string) (bool, error) { return rows > 0, nil } -func (s Service) listFromRepo(ctx context.Context, scopeIds []string) ([]*pb.User, error) { +func (s Service) listFromRepo(ctx context.Context, scopeIds []string) ([]*iam.User, error) { repo, err := s.repoFn() if err != nil { return nil, err @@ -353,71 +494,67 @@ func (s Service) listFromRepo(ctx context.Context, scopeIds []string) ([]*pb.Use if err != nil { return nil, err } - var outUl []*pb.User - for _, u := range ul { - outUl = append(outUl, toProto(u, nil)) - } - return outUl, nil + return ul, nil } -func (s Service) addInRepo(ctx context.Context, userId string, accountIds []string, version uint32) (*pb.User, error) { +func (s Service) addInRepo(ctx context.Context, userId string, accountIds []string, version uint32) (*iam.User, []string, error) { const op = "users.(Service).addInRepo" repo, err := s.repoFn() if err != nil { - return nil, err + return nil, nil, err } _, err = repo.AddUserAccounts(ctx, userId, version, strutil.RemoveDuplicates(accountIds, false)) if err != nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to add accounts to user: %v.", err) + return nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to add accounts to user: %v.", err) } out, accts, err := repo.LookupUser(ctx, userId) if err != nil { - return nil, errors.Wrap(err, op, errors.WithMsg("unable to look up user after adding accounts")) + return nil, nil, errors.Wrap(err, op, errors.WithMsg("unable to look up user after adding accounts")) } if out == nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup user after adding accounts to it.") + return nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup user after adding accounts to it.") } - return toProto(out, accts), nil + return out, accts, nil } -func (s Service) setInRepo(ctx context.Context, userId string, accountIds []string, version uint32) (*pb.User, error) { +func (s Service) setInRepo(ctx context.Context, userId string, accountIds []string, version uint32) (*iam.User, []string, error) { const op = "users.(Service).setInRepo" repo, err := s.repoFn() if err != nil { - return nil, err + return nil, nil, err } _, err = repo.SetUserAccounts(ctx, userId, version, strutil.RemoveDuplicates(accountIds, false)) if err != nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to set accounts for the user: %v.", err) + return nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to set accounts for the user: %v.", err) } out, accts, err := repo.LookupUser(ctx, userId) if err != nil { - return nil, errors.Wrap(err, op, errors.WithMsg("unable to look up user after setting accounts")) + return nil, nil, errors.Wrap(err, op, errors.WithMsg("unable to look up user after setting accounts")) } if out == nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup user after setting accounts for it.") + return nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup user after setting accounts for it.") } - return toProto(out, accts), nil + return out, accts, nil } -func (s Service) removeInRepo(ctx context.Context, userId string, accountIds []string, version uint32) (*pb.User, error) { +func (s Service) removeInRepo(ctx context.Context, userId string, accountIds []string, version uint32) (*iam.User, []string, error) { const op = "users.(Service).removeInRepo" repo, err := s.repoFn() if err != nil { - return nil, err + return nil, nil, err } _, err = repo.DeleteUserAccounts(ctx, userId, version, strutil.RemoveDuplicates(accountIds, false)) if err != nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to remove accounts from user: %v.", err) + return nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to remove accounts from user: %v.", err) } out, accts, err := repo.LookupUser(ctx, userId) if err != nil { - return nil, errors.Wrap(err, op, errors.WithMsg("unable to look up user after removing accounts")) + return nil, nil, errors.Wrap(err, op, errors.WithMsg("unable to look up user after removing accounts")) } if out == nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup user after removing accounts from it.") + return nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup user after removing accounts from it.") } - return toProto(out, accts), nil + return out, accts, nil } func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.VerifyResults { @@ -459,41 +596,66 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type) auth. return auth.Verify(ctx, opts...) } -func toProto(in *iam.User, accts []string) *pb.User { - out := pb.User{ - Id: in.GetPublicId(), - ScopeId: in.GetScopeId(), - CreatedTime: in.GetCreateTime().GetTimestamp(), - UpdatedTime: in.GetUpdateTime().GetTimestamp(), - Version: in.GetVersion(), - AccountIds: accts, +func toProto(ctx context.Context, in *iam.User, accts []string, opt ...handlers.Option) (*pb.User, error) { + opts := handlers.GetOpts(opt...) + if opts.WithOutputFields == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "output fields not found when building user proto") } - if in.GetDescription() != "" { - out.Description = &wrapperspb.StringValue{Value: in.GetDescription()} + outputFields := *opts.WithOutputFields + + out := pb.User{} + if outputFields.Has(globals.IdField) { + out.Id = in.GetPublicId() } - if in.GetName() != "" { - out.Name = &wrapperspb.StringValue{Value: in.GetName()} + if outputFields.Has(globals.ScopeIdField) { + out.ScopeId = in.GetScopeId() } - if in.GetPrimaryAccountId() != "" { + 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.AccountIdsField) { + out.AccountIds = accts + } + if outputFields.Has(globals.PrimaryAccountIdField) { out.PrimaryAccountId = in.GetPrimaryAccountId() } - if in.GetLoginName() != "" { + if outputFields.Has(globals.LoginNameField) { out.LoginName = in.GetLoginName() } - if in.GetFullName() != "" { + if outputFields.Has(globals.FullNameField) { out.FullName = in.GetFullName() } - if in.GetEmail() != "" { + if outputFields.Has(globals.EmailField) { out.Email = in.GetEmail() } - for _, a := range accts { - out.Accounts = append(out.Accounts, &pb.Account{ - Id: a, - // TODO: Update this when an account can be associated with a user from a different scope. - ScopeId: in.GetScopeId(), - }) + if outputFields.Has(globals.ScopeField) { + out.Scope = opts.WithScope } - return &out + if outputFields.Has(globals.AuthorizedActionsField) { + out.AuthorizedActions = opts.WithAuthorizedActions + } + if outputFields.Has(globals.AccountsField) { + for _, a := range accts { + out.Accounts = append(out.Accounts, &pb.Account{ + Id: a, + // TODO: Update this when an account can be associated with a user from a different scope. + ScopeId: in.GetScopeId(), + }) + } + } + return &out, nil } // A validateX method should exist for each method above. These methods do not make calls to any backing service but enforce diff --git a/internal/servers/controller/handlers/users/user_service_test.go b/internal/servers/controller/handlers/users/user_service_test.go index fd07293a4..9be878b2a 100644 --- a/internal/servers/controller/handlers/users/user_service_test.go +++ b/internal/servers/controller/handlers/users/user_service_test.go @@ -227,7 +227,7 @@ func TestList(t *testing.T) { } // Populate these users into the total - ctx = auth.DisabledAuthTestContext(repoFn, oWithUsers.GetPublicId()) + ctx = auth.DisabledAuthTestContext(repoFn, oWithUsers.GetPublicId(), auth.WithUserId("u_auth")) usersInOrg, err := s.ListUsers(ctx, &pbs.ListUsersRequest{ScopeId: oWithUsers.GetPublicId()}) require.NoError(t, err) totalUsers = append(totalUsers, usersInOrg.GetItems()...) @@ -278,10 +278,12 @@ func TestList(t *testing.T) { assert, require := assert.New(t), require.New(t) require.NoError(err, "Couldn't create new user service.") + // Test with non-anon user got, gErr := s.ListUsers(auth.DisabledAuthTestContext(repoFn, tc.req.GetScopeId()), tc.req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "ListUsers(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) + return } gotUsers := sortableUsers{ users: got.GetItems(), @@ -298,6 +300,18 @@ func TestList(t *testing.T) { tc.res.Items = resUsers.users } assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListUsers(%q) got response %q, wanted %q", tc.req, got, tc.res) + // Test with anon user + + got, gErr = s.ListUsers(auth.DisabledAuthTestContext(repoFn, tc.req.GetScopeId(), auth.WithUserId(auth.AnonymousUserId)), tc.req) + require.NoError(gErr) + assert.Len(got.Items, len(tc.res.Items)) + for _, item := range got.GetItems() { + require.Empty(item.Version) + require.Nil(item.CreatedTime) + require.Nil(item.UpdatedTime) + require.Nil(item.Accounts) + require.Nil(item.AccountIds) + } }) } } diff --git a/website/content/docs/concepts/security/permissions.mdx b/website/content/docs/concepts/security/permissions.mdx index 6fab029d3..d8d866a1e 100644 --- a/website/content/docs/concepts/security/permissions.mdx +++ b/website/content/docs/concepts/security/permissions.mdx @@ -7,27 +7,107 @@ description: |- # Permissions in Boundary +## Overview + Boundary's permissions model is a composable, RBAC, allow-only model that attempts to marry flexibility with usability. This page discusses the permission model's fundamental concepts, provides examples of the specific forms of allowed grants, and contains a table that acts as an easy cheat sheet to help those new to its grant syntax with crafting roles. -Each grant is a mapping that describes a resource or set of resources and the -actions that should be allowed on them. Thus, each grant contains one or more -of: +Boundary's [domain model](/docs/concepts/domain-model) is based on resource +types. These can be implemented directly, such as with targets, or they can be +abstract types that are implemented by concrete types within the system. As an +example of the latter, a host catalog is an abstract type and a Static host +catalog is a concrete type. + +From a permissions standpoint, all actions take place against directly +implemented or abstract types. There may be actions that are only implemented by +some concrete types (e.g., not all `auth-method` implementations will support a +`change-password` action), but the permissions model still defines these +capabilities at the abstract type level. This helps keep the overall system +relatively simple and predictable. + +At a very high level, permissions within Boundary are declared via grant strings +and mapped to users via roles. + +### Grant Strings + +A grant string has a form similar to: + +`id=;type=;actions=;output_fields=` + +Each grant string is a mapping that describes a resource or set of resources and +the permissions that should be granted on them. + +There are currently two types of selectors: - An `id` field that indicates a specific resource or a wildcard to match all -- A `type` field that indicates a specific resource type or a wildcard to match all -- An `actions` field indicating which actions to allow the client to perform on the resources matched by `id` and `type` + +- A `type` field that indicates a specific resource type or a wildcard to match + all; this might also be used to grant permissions on collections of resources + +Selectors are used to indicate which resources on which the grant should apply, +using specific IDs or wildcard IDs and type selectors. (The acceptable grant +string formats are detailed later on this page.) + +Additionally, there are two types of assigned permissions: + +- An `actions` field indicating which actions to allow the client to perform on + the resources matched by `id` and `type` + +- An `output_fields` field indicating which top-level fields to return in the + response (0.2.1+) Grant strings can be supplied via a human-friendly string syntax or via JSON. -Roles are composable; a user's final set of grants will be composed of various -roles that each contribute grants to a set of principals that include that user -or groups of which that user is a member. +### Roles -## Subactions +Roles map grant strings to _principals_, currently users and groups. Every role +assigns grants within a specific scope: either the scope in which the role +exists, or a scope that is a child of the scope in which the role exists, +controlled by the role's "grant scope ID" value + +When a request is made, the scope in which to discover grants is either provided +by the client (if against a resource collection itself) or is looked up using +the resource's ID. This scope ID, along with the user's ID and the IDs of the +groups the user belongs to, controls which roles are fetched to provide grants +for the request. + +A role provides grants for a request if the grant scope ID matches the request's +scope ID and one or more of the following are true: + +- The user's ID is contained in the principal IDs set on the role + +- A group the user belongs to is contained in the principal IDs set on the role + +- The user is logged in and the `u_auth` user is contained in the principal IDs + set on the role + +- The role contains the `u_anon` user in the in the principal IDs set on the + role + +Roles are composable; a user's final set of grants will be composed of grants +that originate from all matching roles.a + +## Assignable Permissions + +Resources identified by the ID/Type selectors have permissions granted to the +user in the form of actions and visibility (via `output_fields`). Each of these +is detailed in the subsections below. + +### Actions + +Actions convey the ability to perform some action against a resource or +collection. Many of these are common CRUD actions (`create`, `read`, `update`, +`delete`) but many resources also specify actions specific to their type; for +instance, a `target` has an `authorize-session` action to allow you to request +making a connection to that target, and `auth-method` resources have an +`authenticate` action to allow you to authenticate to Boundary. For the most +part these are straightforward, however there are a couple of special cases to +know. + +#### Subactions Starting in Boundary 0.1.6, some subactions are supported. These actions have a format `top_level_action:subaction`, such as `read:self`. Being granted the top @@ -36,61 +116,120 @@ level action infers being granted all subactions. Thus, if a grant conveys grant conveys `read:self`, it will match the API action `read:self` but will not match `read`. -## The `no-op` Action +#### The `no-op` Action Starting in Boundary 0.2.1 there is an action that can be granted called `no-op`. As might be apparent, `no-op` is not used for any real action in Boundary; the purpose of this action is for listing visibility. Boundary only shows resources in the output of a `list` action for which a user has at least one granted action. Thus, without `no-op`, in order for a resource to be visible -in a list, a different action such as `read` would have to be granted to a user. -This could result in exposing more information than desired, especially in the -case of listing scopes and authentication methods so that users can perform -initial authentication to Boundary (that is, granting access to `u_anon`). +in a `list` action, a different action such as `read` would have to be granted +to a user. This could result in exposing more information than desired, +especially in the case of listing scopes and authentication methods so that +users can perform initial authentication to Boundary (that is, granting access +to `u_anon`). By granting the `no-op` action to users, they can see the resources in the output of a list command without needing other capability grants as well. -## Applicable Resource Types +### Output Fields -Boundary's [domain model](/docs/concepts/domain-model) is based on resource -types. These can be implemented directly, such as with targets, or they can be -abstract types that are implemented by concrete types within the system. As an -example of the latter, a host catalog is an abstract type and a Static host -catalog is a concrete type. +Starting in Boundary 0.2.1, grant strings can contain an `output_fields` field. +This allows fine-grained control over visibility of data returned to users. -From a permissions standpoint, however, all actions take place against directly -implemented or abstract types. There may be actions that are only implemented by -some concrete types (e.g., not all auth methods will support a `change-password` -action), but the permissions model still defines these at the abstract level. -This helps keep the overall system relatively simple and predictable. +In many cases, `output_fields` will not need to be set directly. However, an +example in the form of some history helps provide some context as to when this +might be useful. -## Scopes are Permission Boundaries +In Boundary 0.2.0, we restricted the set of fields returned for some `list` +calls (those on the `scopes` and `auth-methods` collections) to a small set if +the user was the anonymous user `u_anon`. Although default behavior in Boundary +was to display all resource fields when listing, because the default was also to +allow anonymous access to perform `list` on scopes and auth methods (in order to +discover auth methods and then authenticate to them), returning all +configuration information for all scopes and auth methods felt like it was +publicly disclosing more information to users than might be desired. -Every role assigns grants within a specific scope: either the scope in which the -role exists, or a scope that is a child of the scope in which the role exists. -This is controlled by the role's "grant scope ID". +At the same time, this was not an ideal solution for two reasons: -When a request is made, the scope in which to discover grants is either provided -by the client (if against specific collection types) or is looked up using the -resource's ID. This scope ID, along with the user's ID and the IDs of the groups -the user belongs to, controls which roles are fetched to provide grants for the -request. +1. There is no one-size-fits-all security policy, and what we thought were + reasonable defaults may not work in all situations -A role provides grants for a request if the grant scope ID matches the request's -scope ID and one or more of the following are true: +2. It made the scopes and auth methods listing behavior work differently from + any other `list` action, which is not ideal -- The user's ID is contained in the principal IDs set on the role -- A group the user belongs to is contained in the principal IDs set on the role -- The user is logged in and the `u_auth` user is contained in the principal IDs set on the role -- The role contains the `u_anon` user in the in the principal IDs set on the role +As a result, we decided to approach this problem the same way we normally try to +within Boundary: set resonable defaults, but give the administrator ultimate +control. + +The resulting behavior is as follows: `output_fields` references the field names +of the JSON object response, composes just like other parts of the RBAC model, +and are action-specific. That is, a grant like: + +`id=*;type=auth-methods;actions=list,no-op;output_fields=scope_id,name,description` + +will, if all by itself, result in those three identified output fields applying +to `list` (and `no-op`) actions for all auth methods in the scope. Thus when +performing a `list`, each item returned will have only those three fields +populated. Any other actions (like `read` or `update`) are unaffected and will +use defaults. + +A grant like: + +`id=*;type=auth-methods;output_fields=id` + +will, if all by itself, result in _any_ action against auth methods in the scope +having only `id` returned. + +However, if both of the above grants are included, since grants are composable +the final set of output fields will be `id,scope_id,name,description` for `list` +(and `no-op`) actions, and `id` for all other actions. + +If, after the grants are composed for a given request, none of the grants +applicable to the resource/action contain `output_fields` definitions, the +defaults are used. These defaults are implemented using internal values for +`output_fields` and vary based on whether the user is the anonymous user: + +- If the user is the anonymous user, a useful but restricted set of fields is + returned. This includes `id`, `scope_id`, `scope`, `name`, `description`, and a + few more. + +- If the user is any authenticated user, the full set of fields is returned + +To think about it a different way, empty `output_fields`, after grant +composition, is equivalent to using Boundary's default; however the moment that +grants start specifying output fields, it is composed from an empty set and thus +nothing is contained unless explicitly specified. (An actual empty set is not +currently supported, as we don't perform validation on the values given. +However, this means setting `output_fields=none` is functionally equivalent!) + +#### Output Fields and Monitoring + +As another example, it's possible to get creative and use `output_fields` to +allow services to keep tabs on various aspects of the system for monitoring +purposes without needing to provision Boundary tokens to those services. For +instance, by allowing access to list users to `u_anon` but restrict the set of +fields to `id` only, a monitoring system could watch and alert if more users +than expected are showing up in the system, while the IDs themselves are not +really meaningful to any other caller that accesses the same endpoint. + +This will be especially useful once we introduce the ability to have CIDR +restrictions against roles and/or grants, which are planned but not currently +scheduled, as you can only have these grants apply to specific internal +services, along with restricting the data that is returned for those services +that do match. ## Permission Grant Formats Because of the aforementioned properties of the permissions model, grants are relatively simple. All grants take one of four forms. These examples use the canonical string syntax; the JSON equivalents are simply an object with a string -`id` value, a string `type` value, and a string array `actions` value. +`id` value, a string `type` value, a string array `actions` value, and a string +array `output_fields` value. + +~> `output_fields` is omitted in most example below for brevity but are valid +in all of them. It is also valid in each case to omit `actions` and specify +_only_ `output_fields`. ### ID Only @@ -202,6 +341,9 @@ The following table works as a quick cheat-sheet to help you manage your permissions. Note that it's not exhaustive; for brevity it does _not_ show wildcard or templated grant strings. +Additionally, this does not include available output fields; see the [service +documentation](https://www.boundaryproject.io/api-docs) for guidance. + @@ -229,11 +371,11 @@ wildcard or templated grant strings. @@ -260,23 +406,23 @@ wildcard or templated grant strings. @@ -333,11 +503,11 @@ wildcard or templated grant strings. @@ -364,17 +538,17 @@ wildcard or templated grant strings. @@ -420,11 +602,11 @@ wildcard or templated grant strings. @@ -445,17 +629,17 @@ wildcard or templated grant strings. @@ -490,11 +678,11 @@ wildcard or templated grant strings. @@ -521,17 +713,17 @@ wildcard or templated grant strings. @@ -588,11 +792,11 @@ wildcard or templated grant strings. @@ -619,23 +827,23 @@ wildcard or templated grant strings. @@ -677,11 +897,11 @@ wildcard or templated grant strings. @@ -708,17 +932,17 @@ wildcard or templated grant strings. @@ -757,11 +987,11 @@ wildcard or templated grant strings. @@ -788,23 +1022,23 @@ wildcard or templated grant strings. @@ -869,11 +1127,11 @@ wildcard or templated grant strings. @@ -900,17 +1162,17 @@ wildcard or templated grant strings. @@ -986,11 +1267,11 @@ wildcard or templated grant strings. @@ -1017,17 +1302,17 @@ wildcard or templated grant strings. @@ -1066,11 +1357,11 @@ wildcard or templated grant strings. @@ -1091,17 +1384,17 @@ wildcard or templated grant strings. @@ -1146,11 +1449,11 @@ wildcard or templated grant strings. @@ -1177,17 +1484,17 @@ wildcard or templated grant strings. @@ -1251,11 +1573,11 @@ wildcard or templated grant strings. @@ -1282,17 +1608,17 @@ wildcard or templated grant strings.
  • Type
  • -
      -
    • - account -
    • -
    +
      +
    • + account +
    • +
@@ -241,15 +383,19 @@ wildcard or templated grant strings.
  • create: Create an account
  • -
      -
    • type=<type>;actions=create
    • -
    +
      +
    • + type=<type>;actions=create +
    • +
  • list: List accounts
  • -
      -
    • type=<type>;actions=list
    • -
    +
      +
    • + type=<type>;actions=list +
    • +
    • ID
    • -
        -
      • - <id> -
      • -
      +
        +
      • + <id> +
      • +
    • Pin
    • -
        -
      • - <auth-method-id> -
      • -
      +
        +
      • + <auth-method-id> +
      • +
    • Type
    • -
        -
      • - account -
      • -
      +
        +
      • + account +
      • +
    @@ -284,38 +430,62 @@ wildcard or templated grant strings.
  • read: Read an account
  • -
      -
    • id=<id>;actions=read
    • -
    • id=<pin>;type=<type>;actions=read
    • -
    +
      +
    • + id=<id>;actions=read +
    • +
    • + id=<pin>;type=<type>;actions=read +
    • +
  • update: Update an account
  • -
      -
    • id=<id>;actions=update
    • -
    • id=<pin>;type=<type>;actions=update
    • -
    +
      +
    • + id=<id>;actions=update +
    • +
    • + id=<pin>;type=<type>;actions=update +
    • +
  • delete: Delete an account
  • -
      -
    • id=<id>;actions=delete
    • -
    • id=<pin>;type=<type>;actions=delete
    • -
    +
      +
    • + id=<id>;actions=delete +
    • +
    • + id=<pin>;type=<type>;actions=delete +
    • +
  • - set-password: Set a password on an account, without requiring the current password + set-password: Set a password on an account, without + requiring the current password
  • -
      -
    • id=<id>;actions=set-password
    • -
    • id=<pin>;type=<type>;actions=set-password
    • -
    +
      +
    • + id=<id>;actions=set-password +
    • +
    • + id=<pin>;type=<type>;actions=set-password +
    • +
  • - change-password: Change a password on an account given the current password + change-password: Change a password on an account given + the current password
  • -
      -
    • id=<id>;actions=change-password
    • -
    • id=<pin>;type=<type>;actions=change-password
    • -
    +
      +
    • + id=<id>;actions=change-password +
    • +
    • + + id=<pin>;type=<type>;actions=change-password + +
    • +
    • Type
    • -
        -
      • - auth-method -
      • -
      +
        +
      • + auth-method +
      • +
    @@ -345,15 +515,19 @@ wildcard or templated grant strings.
  • create: Create an auth method
  • -
      -
    • type=<type>;actions=create
    • -
    +
      +
    • + type=<type>;actions=create +
    • +
  • list: List auth methods
  • -
      -
    • type=<type>;actions=list
    • -
    +
      +
    • + type=<type>;actions=list +
    • +
    • ID
    • -
        -
      • - <id> -
      • -
      +
        +
      • + <id> +
      • +
    • Type
    • -
        -
      • - auth-method -
      • -
      +
        +
      • + auth-method +
      • +
    @@ -382,27 +556,35 @@ wildcard or templated grant strings.
  • read: Read an auth method
  • -
      -
    • id=<id>;actions=read
    • -
    +
      +
    • + id=<id>;actions=read +
    • +
  • update: Update an auth method
  • -
      -
    • id=<id>;actions=update
    • -
    +
      +
    • + id=<id>;actions=update +
    • +
  • delete: Delete an auth method
  • -
      -
    • id=<id>;actions=delete
    • -
    +
      +
    • + id=<id>;actions=delete +
    • +
  • authenticate: Authenticate to an auth method
  • -
      -
    • id=<id>;actions=authenticate
    • -
    +
      +
    • + id=<id>;actions=authenticate +
    • +
    • Type
    • -
        -
      • - auth-token -
      • -
      +
        +
      • + auth-token +
      • +
    @@ -432,9 +614,11 @@ wildcard or templated grant strings.
  • list: List auth tokens
  • -
      -
    • type=<type>;actions=list
    • -
    +
      +
    • + type=<type>;actions=list +
    • +
    • ID
    • -
        -
      • - <id> -
      • -
      +
        +
      • + <id> +
      • +
    • Type
    • -
        -
      • - auth-token -
      • -
      +
        +
      • + auth-token +
      • +
    @@ -463,15 +647,19 @@ wildcard or templated grant strings.
  • read: Read an auth token
  • -
      -
    • id=<id>;actions=read
    • -
    +
      +
    • + id=<id>;actions=read +
    • +
  • delete: Delete an auth token
  • -
      -
    • id=<id>;actions=delete
    • -
    +
      +
    • + id=<id>;actions=delete +
    • +
    • Type
    • -
        -
      • - group -
      • -
      +
        +
      • + group +
      • +
    @@ -502,15 +690,19 @@ wildcard or templated grant strings.
  • create: Create a group
  • -
      -
    • type=<type>;actions=create
    • -
    +
      +
    • + type=<type>;actions=create +
    • +
  • list: List groups
  • -
      -
    • type=<type>;actions=list
    • -
    +
      +
    • + type=<type>;actions=list +
    • +
    • ID
    • -
        -
      • - <id> -
      • -
      +
        +
      • + <id> +
      • +
    • Type
    • -
        -
      • - group -
      • -
      +
        +
      • + group +
      • +
    @@ -539,39 +731,51 @@ wildcard or templated grant strings.
  • read: Read a group
  • -
      -
    • id=<id>;actions=read
    • -
    +
      +
    • + id=<id>;actions=read +
    • +
  • update: Update a group
  • -
      -
    • id=<id>;actions=update
    • -
    +
      +
    • + id=<id>;actions=update +
    • +
  • delete: Delete a group
  • -
      -
    • id=<id>;actions=delete
    • -
    +
      +
    • + id=<id>;actions=delete +
    • +
  • add-members: Add members to a group
  • -
      -
    • id=<id>;actions=add-members
    • -
    +
      +
    • + id=<id>;actions=add-members +
    • +
  • set-members: Set the full set of members on a group
  • -
      -
    • id=<id>;actions=set-members
    • -
    +
      +
    • + id=<id>;actions=set-members +
    • +
  • remove-members: Remove members from a group
  • -
      -
    • id=<id>;actions=remove-members
    • -
    +
      +
    • + id=<id>;actions=remove-members +
    • +
    • Type
    • -
        -
      • - host -
      • -
      +
        +
      • + host +
      • +
    @@ -600,15 +804,19 @@ wildcard or templated grant strings.
  • create: Create a host
  • -
      -
    • type=<type>;actions=create
    • -
    +
      +
    • + type=<type>;actions=create +
    • +
  • list: List hosts
  • -
      -
    • type=<type>;actions=list
    • -
    +
      +
    • + type=<type>;actions=list +
    • +
    • ID
    • -
        -
      • - <id> -
      • -
      +
        +
      • + <id> +
      • +
    • Pin
    • -
        -
      • - <host-catalog-id> -
      • -
      +
        +
      • + <host-catalog-id> +
      • +
    • Type
    • -
        -
      • - host -
      • -
      +
        +
      • + host +
      • +
    @@ -643,24 +851,36 @@ wildcard or templated grant strings.
  • read: Read a host
  • -
      -
    • id=<id>;actions=read
    • -
    • id=<pin>;type=<type>;actions=read
    • -
    +
      +
    • + id=<id>;actions=read +
    • +
    • + id=<pin>;type=<type>;actions=read +
    • +
  • update: Update a host
  • -
      -
    • id=<id>;actions=update
    • -
    • id=<pin>;type=<type>;actions=update
    • -
    +
      +
    • + id=<id>;actions=update +
    • +
    • + id=<pin>;type=<type>;actions=update +
    • +
  • delete: Delete a host
  • -
      -
    • id=<id>;actions=delete
    • -
    • id=<pin>;type=<type>;actions=delete
    • -
    +
      +
    • + id=<id>;actions=delete +
    • +
    • + id=<pin>;type=<type>;actions=delete +
    • +
    • Type
    • -
        -
      • - host-catalog -
      • -
      +
        +
      • + host-catalog +
      • +
    @@ -689,15 +909,19 @@ wildcard or templated grant strings.
  • create: Create a host catalog
  • -
      -
    • type=<type>;actions=create
    • -
    +
      +
    • + type=<type>;actions=create +
    • +
  • list: List host catalogs
  • -
      -
    • type=<type>;actions=list
    • -
    +
      +
    • + type=<type>;actions=list +
    • +
    • ID
    • -
        -
      • - <id> -
      • -
      +
        +
      • + <id> +
      • +
    • Type
    • -
        -
      • - host-catalog -
      • -
      +
        +
      • + host-catalog +
      • +
    @@ -726,21 +950,27 @@ wildcard or templated grant strings.
  • read: Read a host catalog
  • -
      -
    • id=<id>;actions=read
    • -
    +
      +
    • + id=<id>;actions=read +
    • +
  • update: Update a host catalog
  • -
      -
    • id=<id>;actions=update
    • -
    +
      +
    • + id=<id>;actions=update +
    • +
  • delete: Delete a host catalog
  • -
      -
    • id=<id>;actions=delete
    • -
    +
      +
    • + id=<id>;actions=delete +
    • +
    • Type
    • -
        -
      • - host-set -
      • -
      +
        +
      • + host-set +
      • +
    @@ -769,15 +999,19 @@ wildcard or templated grant strings.
  • create: Create a host set
  • -
      -
    • type=<type>;actions=create
    • -
    +
      +
    • + type=<type>;actions=create +
    • +
  • list: List host sets
  • -
      -
    • type=<type>;actions=list
    • -
    +
      +
    • + type=<type>;actions=list +
    • +
    • ID
    • -
        -
      • - <id> -
      • -
      +
        +
      • + <id> +
      • +
    • Pin
    • -
        -
      • - <host-catalog-id> -
      • -
      +
        +
      • + <host-catalog-id> +
      • +
    • Type
    • -
        -
      • - host-set -
      • -
      +
        +
      • + host-set +
      • +
    @@ -812,45 +1046,69 @@ wildcard or templated grant strings.
  • read: Read a host set
  • -
      -
    • id=<id>;actions=read
    • -
    • id=<pin>;type=<type>;actions=read
    • -
    +
      +
    • + id=<id>;actions=read +
    • +
    • + id=<pin>;type=<type>;actions=read +
    • +
  • update: Update a host set
  • -
      -
    • id=<id>;actions=update
    • -
    • id=<pin>;type=<type>;actions=update
    • -
    +
      +
    • + id=<id>;actions=update +
    • +
    • + id=<pin>;type=<type>;actions=update +
    • +
  • delete: Delete a host set
  • -
      -
    • id=<id>;actions=delete
    • -
    • id=<pin>;type=<type>;actions=delete
    • -
    +
      +
    • + id=<id>;actions=delete +
    • +
    • + id=<pin>;type=<type>;actions=delete +
    • +
  • add-hosts: Add hosts to a host-set
  • -
      -
    • id=<id>;actions=add-hosts
    • -
    • id=<pin>;type=<type>;actions=add-hosts
    • -
    +
      +
    • + id=<id>;actions=add-hosts +
    • +
    • + id=<pin>;type=<type>;actions=add-hosts +
    • +
  • set-hosts: Set the full set of hosts on a host set
  • -
      -
    • id=<id>;actions=set-hosts
    • -
    • id=<pin>;type=<type>;actions=set-hosts
    • -
    +
      +
    • + id=<id>;actions=set-hosts +
    • +
    • + id=<pin>;type=<type>;actions=set-hosts +
    • +
  • remove-hosts: Remove hosts from a host set
  • -
      -
    • id=<id>;actions=remove-hosts
    • -
    • id=<pin>;type=<type>;actions=remove-hosts
    • -
    +
      +
    • + id=<id>;actions=remove-hosts +
    • +
    • + id=<pin>;type=<type>;actions=remove-hosts +
    • +
    • Type
    • -
        -
      • - role -
      • -
      +
        +
      • + role +
      • +
    @@ -881,15 +1139,19 @@ wildcard or templated grant strings.
  • create: Create a role
  • -
      -
    • type=<type>;actions=create
    • -
    +
      +
    • + type=<type>;actions=create +
    • +
  • list: List roles
  • -
      -
    • type=<type>;actions=list
    • -
    +
      +
    • + type=<type>;actions=list +
    • +
    • ID
    • -
        -
      • - <id> -
      • -
      +
        +
      • + <id> +
      • +
    • Type
    • -
        -
      • - role -
      • -
      +
        +
      • + role +
      • +
    @@ -918,57 +1180,76 @@ wildcard or templated grant strings.
  • read: Read a role
  • -
      -
    • id=<id>;actions=read
    • -
    +
      +
    • + id=<id>;actions=read +
    • +
  • update: Update a role
  • -
      -
    • id=<id>;actions=update
    • -
    +
      +
    • + id=<id>;actions=update +
    • +
  • delete: Delete a role
  • -
      -
    • id=<id>;actions=delete
    • -
    +
      +
    • + id=<id>;actions=delete +
    • +
  • add-principals: Add principals to a role
  • -
      -
    • id=<id>;actions=add-principals
    • -
    +
      +
    • + id=<id>;actions=add-principals +
    • +
  • - set-principals: Set the full set of principals on a role + set-principals: Set the full set of principals on a + role
  • -
      -
    • id=<id>;actions=set-principals
    • -
    +
      +
    • + id=<id>;actions=set-principals +
    • +
  • remove-principals: Remove principals from a role
  • -
      -
    • id=<id>;actions=remove-principals
    • -
    +
      +
    • + id=<id>;actions=remove-principals +
    • +
  • add-grants: Add grants to a role
  • -
      -
    • id=<id>;actions=add-grants
    • -
    +
      +
    • + id=<id>;actions=add-grants +
    • +
  • set-grants: Set the full set of grants on a role
  • -
      -
    • id=<id>;actions=set-grants
    • -
    +
      +
    • + id=<id>;actions=set-grants +
    • +
  • remove-grants: Remove grants from a role
  • -
      -
    • id=<id>;actions=remove-grants
    • -
    +
      +
    • + id=<id>;actions=remove-grants +
    • +
    • Type
    • -
        -
      • - scope -
      • -
      +
        +
      • + scope +
      • +
    @@ -998,15 +1279,19 @@ wildcard or templated grant strings.
  • create: Create a scope
  • -
      -
    • type=<type>;actions=create
    • -
    +
      +
    • + type=<type>;actions=create +
    • +
  • list: List scopes
  • -
      -
    • type=<type>;actions=list
    • -
    +
      +
    • + type=<type>;actions=list +
    • +
    • ID
    • -
        -
      • - <id> -
      • -
      +
        +
      • + <id> +
      • +
    • Type
    • -
        -
      • - scope -
      • -
      +
        +
      • + scope +
      • +
    @@ -1035,21 +1320,27 @@ wildcard or templated grant strings.
  • read: Read a scope
  • -
      -
    • id=<id>;actions=read
    • -
    +
      +
    • + id=<id>;actions=read +
    • +
  • update: Update a scope
  • -
      -
    • id=<id>;actions=update
    • -
    +
      +
    • + id=<id>;actions=update +
    • +
  • delete: Delete a scope
  • -
      -
    • id=<id>;actions=delete
    • -
    +
      +
    • + id=<id>;actions=delete +
    • +
    • Type
    • -
        -
      • - session -
      • -
      +
        +
      • + session +
      • +
    @@ -1078,9 +1369,11 @@ wildcard or templated grant strings.
  • list: List sessions
  • -
      -
    • type=<type>;actions=list
    • -
    +
      +
    • + type=<type>;actions=list +
    • +
    • ID
    • -
        -
      • - <id> -
      • -
      +
        +
      • + <id> +
      • +
    • Type
    • -
        -
      • - session -
      • -
      +
        +
      • + session +
      • +
    @@ -1109,27 +1402,37 @@ wildcard or templated grant strings.
  • read: Read a session
  • -
      -
    • id=<id>;actions=read
    • -
    +
      +
    • + id=<id>;actions=read +
    • +
  • cancel: Cancel a session
  • -
      -
    • id=<id>;actions=cancel
    • -
    +
      +
    • + id=<id>;actions=cancel +
    • +
  • - read:self: Read a session, which must be associated with the calling user + read:self: Read a session, which must be associated + with the calling user
  • -
      -
    • id=*;type=session;actions=read:self
    • -
    +
      +
    • + id=*;type=session;actions=read:self +
    • +
  • - cancel:self: Cancel a session, which must be associated with the calling user + cancel:self: Cancel a session, which must be associated + with the calling user
  • -
      -
    • id=*;type=session;actions=cancel:self
    • -
    +
      +
    • + id=*;type=session;actions=cancel:self +
    • +
    • Type
    • -
        -
      • - target -
      • -
      +
        +
      • + target +
      • +
    @@ -1158,15 +1461,19 @@ wildcard or templated grant strings.
  • create: Create a target
  • -
      -
    • type=<type>;actions=create
    • -
    +
      +
    • + type=<type>;actions=create +
    • +
  • list: List targets
  • -
      -
    • type=<type>;actions=list
    • -
    +
      +
    • + type=<type>;actions=list +
    • +
    • ID
    • -
        -
      • - <id> -
      • -
      +
        +
      • + <id> +
      • +
    • Type
    • -
        -
      • - target -
      • -
      +
        +
      • + target +
      • +
    @@ -1195,45 +1502,60 @@ wildcard or templated grant strings.
  • read: Read a target
  • -
      -
    • id=<id>;actions=read
    • -
    +
      +
    • + id=<id>;actions=read +
    • +
  • update: Update a target
  • -
      -
    • id=<id>;actions=update
    • -
    +
      +
    • + id=<id>;actions=update +
    • +
  • delete: Delete a target
  • -
      -
    • id=<id>;actions=delete
    • -
    +
      +
    • + id=<id>;actions=delete +
    • +
  • add-host-sets: Add host sets to a target
  • -
      -
    • id=<id>;actions=add-host-sets
    • -
    +
      +
    • + id=<id>;actions=add-host-sets +
    • +
  • - set-host-sets: Set the full set of host sets on a target + set-host-sets: Set the full set of host sets on a + target
  • -
      -
    • id=<id>;actions=set-host-sets
    • -
    +
      +
    • + id=<id>;actions=set-host-sets +
    • +
  • remove-host-sets: Remove host sets from a target
  • -
      -
    • id=<id>;actions=remove-host-sets
    • -
    +
      +
    • + id=<id>;actions=remove-host-sets +
    • +
  • authorize-session: Authorize a session via the target
  • -
      -
    • id=<id>;actions=authorize-session
    • -
    +
      +
    • + id=<id>;actions=authorize-session +
    • +
    • Type
    • -
        -
      • - user -
      • -
      +
        +
      • + user +
      • +
    @@ -1263,15 +1585,19 @@ wildcard or templated grant strings.
  • create: Create a user
  • -
      -
    • type=<type>;actions=create
    • -
    +
      +
    • + type=<type>;actions=create +
    • +
  • list: List users
  • -
      -
    • type=<type>;actions=list
    • -
    +
      +
    • + type=<type>;actions=list +
    • +
    • ID
    • -
        -
      • - <id> -
      • -
      +
        +
      • + <id> +
      • +
    • Type
    • -
        -
      • - user -
      • -
      +
        +
      • + user +
      • +
    @@ -1300,39 +1626,51 @@ wildcard or templated grant strings.
  • read: Read a user
  • -
      -
    • id=<id>;actions=read
    • -
    +
      +
    • + id=<id>;actions=read +
    • +
  • update: Update a user
  • -
      -
    • id=<id>;actions=update
    • -
    +
      +
    • + id=<id>;actions=update +
    • +
  • delete: Delete a user
  • -
      -
    • id=<id>;actions=delete
    • -
    +
      +
    • + id=<id>;actions=delete +
    • +
  • add-accounts: Add accounts to a user
  • -
      -
    • id=<id>;actions=add-accounts
    • -
    +
      +
    • + id=<id>;actions=add-accounts +
    • +
  • set-accounts: Set the full set of accounts on a user
  • -
      -
    • id=<id>;actions=set-accounts
    • -
    +
      +
    • + id=<id>;actions=set-accounts +
    • +
  • remove-accounts: Remove accounts from a user
  • -
      -
    • id=<id>;actions=remove-accounts
    • -
    +
      +
    • + id=<id>;actions=remove-accounts +
    • +