Add Output Fields support (#1192)

This commit is contained in:
Jeff Mitchell authored and GitHub committed 2021-05-04 20:57:35 -04:00
1 parent 1948984ca2
commit 22d5c88a0b
84 files changed
+5688 -2122

No files matched your search

+37 -23
View File
@@ -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
+56
View File
@@ -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"
)
+2 -2
View File
@@ -15,8 +15,8 @@ const (
)
type (
ContextMaxRequestSizeType int
ContextOriginalRequestPathType int
ContextMaxRequestSizeType struct{}
ContextOriginalRequestPathType struct{}
)
var (
@@ -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)
})
}
}
+36 -3
View File
@@ -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.
+45
View File
@@ -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
}
+1 -3
View File
@@ -33,9 +33,7 @@ type options struct {
}
func getDefaultOptions() options {
return options{
withUserId: AnonymousUserId,
}
return options{}
}
func WithScopeId(id string) Option {
+1 -5
View File
@@ -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"),
+35 -27
View File
@@ -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
}
+9 -2
View File
@@ -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)
}
+3
View File
@@ -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 " <not included in response>"
}
vals := map[string]interface{}{
"ID": scp.Id,
"Type": scp.Type,
@@ -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 {
+51 -23
View File
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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 {
+72 -44
View File
@@ -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),
)
}
@@ -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 {
@@ -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 {
@@ -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 {
+37 -15
View File
@@ -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),
)
}
+39 -23
View File
@@ -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 {
@@ -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 {
+55 -32
View File
@@ -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),
)
}
@@ -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 {
@@ -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 {
+55 -26
View File
@@ -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),
)
}
+1 -2
View File
@@ -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 {
@@ -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 {
+64 -36
View File
@@ -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),
)
}
@@ -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 {
@@ -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 {
+44 -28
View File
@@ -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),
)
+1 -2
View File
@@ -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 {
+36 -23
View File
@@ -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]),
)
}
}
@@ -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 {
+89 -34
View File
@@ -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", ""),
)
@@ -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 {
+60 -29
View File
@@ -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)
}
@@ -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 {
@@ -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 {
+65 -49
View File
@@ -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:",
+1 -2
View File
@@ -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 {
+1 -2
View File
@@ -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 {
+45 -14
View File
@@ -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=<resource.id>;actions=<action> where ID cannot be a wildcard
// id=<resource.id>;actions=<action> where ID cannot be a wildcard; or
// id=<resource.id>;output_fields=<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=<resource.type>;actions=<action> when action is list(:self) or
// create. Must be a top level collection, otherwise must be one of the
// two formats specified below.
// type=<resource.type>;actions=<action> 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=<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=<resource.type>;actions=<action> 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=<resource.type>;output_fields=<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=<pin>;type=<resource.type>;actions=<action> 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
+26 -21
View File
@@ -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)
}
})
}
+70 -18
View File
@@ -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")
}
+123 -2
View File
@@ -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`,
+96
View File
@@ -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]
}
+267
View File
@@ -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))
})
}
}
+31
View File
@@ -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
}
}
+73
View File
@@ -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
}
+47
View File
@@ -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)
})
}
}
+13
View File
@@ -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)
@@ -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(),
@@ -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(),
@@ -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
@@ -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)
@@ -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,
)
@@ -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,
@@ -127,7 +127,7 @@ func (s Service) authenticateWithPwRepo(ctx context.Context, scopeId, authMethod
return nil, err
}
return s.convertInternalAuthTokenToApiAuthToken(
return s.ConvertInternalAuthTokenToApiAuthToken(
ctx,
tok,
)
@@ -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
@@ -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)
})
}
}
@@ -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
@@ -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)
}
})
}
}
@@ -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
@@ -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)
}
})
}
}
@@ -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
@@ -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)
}
})
}
}
@@ -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
}
@@ -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)
}
})
}
}
@@ -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)
})
}
@@ -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
}
}
@@ -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: "<parse_error>",
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: "<parse_error>",
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
@@ -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)
}
})
}
}
@@ -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
@@ -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)
@@ -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
@@ -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)
}
})
}
}
@@ -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
}
@@ -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)
}
})
}
}
@@ -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
@@ -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)
}
})
}
}
File diff suppressed because it is too large. Load diff