Move actions, resource (types), and scopes to packages. (#149)

This commit is contained in:
Jeff Mitchell authored and GitHub committed 2020-06-26 11:00:14 -04:00
1 parent 9d1d0ba08e
commit d8f11af4d7
37 files changed
+721 -460

No files matched your search

+14 -55
View File
@@ -1,65 +1,24 @@
package iam
// Action defines a type for the Actions of Resources
// actions are also stored as a lookup db table named iam_action
type Action int
// not using iota intentionally, since the values are stored in the db as well.
const (
ActionUnknown Action = 0
ActionList Action = 1
ActionCreate Action = 2
ActionUpdate Action = 3
ActionRead Action = 4
ActionDelete Action = 5
ActionAuthen Action = 6
ActionAll Action = 7
ActionConnect Action = 8
)
var ActionMap = map[string]Action{
"unknown": ActionUnknown,
"list": ActionList,
"create": ActionCreate,
"update": ActionUpdate,
"read": ActionRead,
"delete": ActionDelete,
"authen": ActionAuthen,
"*": ActionAll,
"connect": ActionConnect,
}
func (a Action) String() string {
return [...]string{
"unknown",
"list",
"create",
"update",
"read",
"delete",
"authen",
"*",
"connect",
}[a]
}
import "github.com/hashicorp/watchtower/internal/types/action"
// CrudActions returns a standard set of actions for resources that support a CRUD API
func CrudActions() map[string]Action {
return map[string]Action{
ActionCreate.String(): ActionCreate,
ActionUpdate.String(): ActionUpdate,
ActionRead.String(): ActionRead,
ActionDelete.String(): ActionDelete,
func CrudActions() map[string]action.Type {
return map[string]action.Type{
action.Create.String(): action.Create,
action.Update.String(): action.Update,
action.Read.String(): action.Read,
action.Delete.String(): action.Delete,
}
}
// CrudlActions adds list to the standard set of actions for resources that support a CRUD API
func CrudlActions() map[string]Action {
return map[string]Action{
ActionCreate.String(): ActionCreate,
ActionUpdate.String(): ActionUpdate,
ActionRead.String(): ActionRead,
ActionDelete.String(): ActionDelete,
ActionList.String(): ActionList,
func CrudlActions() map[string]action.Type {
return map[string]action.Type{
action.Create.String(): action.Create,
action.Update.String(): action.Update,
action.Read.String(): action.Read,
action.Delete.String(): action.Delete,
action.List.String(): action.List,
}
}
+14 -13
View File
@@ -3,6 +3,7 @@ package iam
import (
"testing"
"github.com/hashicorp/watchtower/internal/types/action"
"github.com/stretchr/testify/assert"
)
@@ -12,17 +13,17 @@ func Test_CrudActions(t *testing.T) {
t.Run("valid", func(t *testing.T) {
assert := assert.New(t)
a := CrudlActions()
assert.Equal(a[ActionCreate.String()], ActionCreate)
assert.Equal(a[ActionUpdate.String()], ActionUpdate)
assert.Equal(a[ActionRead.String()], ActionRead)
assert.Equal(a[ActionDelete.String()], ActionDelete)
assert.Equal(a[action.Create.String()], action.Create)
assert.Equal(a[action.Update.String()], action.Update)
assert.Equal(a[action.Read.String()], action.Read)
assert.Equal(a[action.Delete.String()], action.Delete)
})
t.Run("invalid", func(t *testing.T) {
assert := assert.New(t)
a := CrudlActions()
action, ok := a["invalid"]
aType, ok := a["invalid"]
assert.Equal(ok, false)
assert.Equal(action, ActionUnknown)
assert.Equal(aType, action.Unknown)
})
}
@@ -32,17 +33,17 @@ func Test_CrudlActions(t *testing.T) {
t.Run("valid", func(t *testing.T) {
assert := assert.New(t)
a := CrudlActions()
assert.Equal(a[ActionList.String()], ActionList)
assert.Equal(a[ActionCreate.String()], ActionCreate)
assert.Equal(a[ActionUpdate.String()], ActionUpdate)
assert.Equal(a[ActionRead.String()], ActionRead)
assert.Equal(a[ActionDelete.String()], ActionDelete)
assert.Equal(a[action.List.String()], action.List)
assert.Equal(a[action.Create.String()], action.Create)
assert.Equal(a[action.Update.String()], action.Update)
assert.Equal(a[action.Read.String()], action.Read)
assert.Equal(a[action.Delete.String()], action.Delete)
})
t.Run("invalid", func(t *testing.T) {
assert := assert.New(t)
a := CrudlActions()
action, ok := a["invalid"]
aType, ok := a["invalid"]
assert.Equal(ok, false)
assert.Equal(action, ActionUnknown)
assert.Equal(aType, action.Unknown)
})
}
+7 -4
View File
@@ -6,6 +6,9 @@ import (
"github.com/hashicorp/watchtower/internal/db"
"github.com/hashicorp/watchtower/internal/iam/store"
"github.com/hashicorp/watchtower/internal/types/action"
"github.com/hashicorp/watchtower/internal/types/resource"
"github.com/hashicorp/watchtower/internal/types/scope"
"google.golang.org/protobuf/proto"
)
@@ -63,8 +66,8 @@ func (g *Group) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType,
return nil
}
func (u *Group) validScopeTypes() []ScopeType {
return []ScopeType{OrganizationScope, ProjectScope}
func (u *Group) validScopeTypes() []scope.Type {
return []scope.Type{scope.Organization, scope.Project}
}
// GetScope returns the scope for the Group.
@@ -73,10 +76,10 @@ func (g *Group) GetScope(ctx context.Context, r db.Reader) (*Scope, error) {
}
// ResourceType returns the type of the Group.
func (*Group) ResourceType() ResourceType { return ResourceTypeGroup }
func (*Group) ResourceType() resource.Type { return resource.StaticGroup }
// Actions returns the available actions for Group
func (*Group) Actions() map[string]Action {
func (*Group) Actions() map[string]action.Type {
return CrudActions()
}
+7 -5
View File
@@ -9,6 +9,8 @@ import (
"github.com/hashicorp/watchtower/internal/db"
dbassert "github.com/hashicorp/watchtower/internal/db/assert"
"github.com/hashicorp/watchtower/internal/oplog"
"github.com/hashicorp/watchtower/internal/types/action"
"github.com/hashicorp/watchtower/internal/types/resource"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
@@ -450,17 +452,17 @@ func TestGroup_Actions(t *testing.T) {
assert := assert.New(t)
r := &Group{}
a := r.Actions()
assert.Equal(a[ActionCreate.String()], ActionCreate)
assert.Equal(a[ActionUpdate.String()], ActionUpdate)
assert.Equal(a[ActionRead.String()], ActionRead)
assert.Equal(a[ActionDelete.String()], ActionDelete)
assert.Equal(a[action.Create.String()], action.Create)
assert.Equal(a[action.Update.String()], action.Update)
assert.Equal(a[action.Read.String()], action.Read)
assert.Equal(a[action.Delete.String()], action.Delete)
}
func TestGroup_ResourceType(t *testing.T) {
assert := assert.New(t)
r := &Group{}
ty := r.ResourceType()
assert.Equal(ty, ResourceTypeGroup)
assert.Equal(ty, resource.StaticGroup)
}
func TestGroup_GetScope(t *testing.T) {
+3 -2
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"github.com/hashicorp/watchtower/internal/db"
"github.com/hashicorp/watchtower/internal/types/scope"
)
const (
@@ -36,8 +37,8 @@ func newGroupId() (string, error) {
return id, nil
}
func newScopeId(scopeType ScopeType) (string, error) {
if scopeType == UnknownScope {
func newScopeId(scopeType scope.Type) (string, error) {
if scopeType == scope.Unknown {
return "", fmt.Errorf("new scope id: unknown is not supported %w", db.ErrInvalidParameter)
}
id, err := db.NewPublicId(scopeType.Prefix())
+6 -5
View File
@@ -7,6 +7,7 @@ import (
"errors"
"github.com/hashicorp/watchtower/internal/db"
"github.com/hashicorp/watchtower/internal/types/scope"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -28,15 +29,15 @@ func Test_PublicIds(t *testing.T) {
assert.True(t, strings.HasPrefix(id, GroupPrefix+"_"))
})
t.Run("scopes", func(t *testing.T) {
id, err := newScopeId(OrganizationScope)
id, err := newScopeId(scope.Organization)
require.NoError(t, err)
assert.True(t, strings.HasPrefix(id, OrganizationScope.Prefix()))
assert.True(t, strings.HasPrefix(id, scope.Organization.Prefix()))
id, err = newScopeId(ProjectScope)
id, err = newScopeId(scope.Project)
require.NoError(t, err)
assert.True(t, strings.HasPrefix(id, ProjectScope.Prefix()))
assert.True(t, strings.HasPrefix(id, scope.Project.Prefix()))
id, err = newScopeId(UnknownScope)
id, err = newScopeId(scope.Unknown)
require.Error(t, err)
assert.Empty(t, id)
assert.True(t, errors.Is(err, db.ErrInvalidParameter))
+13 -12
View File
@@ -10,6 +10,7 @@ import (
wrapping "github.com/hashicorp/go-kms-wrapping"
"github.com/hashicorp/watchtower/internal/db"
"github.com/hashicorp/watchtower/internal/oplog"
"github.com/hashicorp/watchtower/internal/types/scope"
)
var (
@@ -177,27 +178,27 @@ func (r *Repository) delete(ctx context.Context, resource Resource, opt ...Optio
func (r *Repository) stdMetadata(ctx context.Context, resource Resource) (oplog.Metadata, error) {
if s, ok := resource.(*Scope); ok {
scope := allocScope()
scope.PublicId = s.PublicId
scope.Type = s.Type
if scope.Type == "" {
if err := r.reader.LookupByPublicId(ctx, &scope); err != nil {
newScope := allocScope()
newScope.PublicId = s.PublicId
newScope.Type = s.Type
if newScope.Type == "" {
if err := r.reader.LookupByPublicId(ctx, &newScope); err != nil {
return nil, ErrMetadataScopeNotFound
}
}
switch scope.Type {
case OrganizationScope.String():
switch newScope.Type {
case scope.Organization.String():
return oplog.Metadata{
"resource-public-id": []string{resource.GetPublicId()},
"scope-id": []string{scope.PublicId},
"scope-type": []string{scope.Type},
"scope-id": []string{newScope.PublicId},
"scope-type": []string{newScope.Type},
"resource-type": []string{resource.ResourceType().String()},
}, nil
case ProjectScope.String():
case scope.Project.String():
return oplog.Metadata{
"resource-public-id": []string{resource.GetPublicId()},
"scope-id": []string{scope.ParentId},
"scope-type": []string{scope.Type},
"scope-id": []string{newScope.ParentId},
"scope-type": []string{newScope.Type},
"resource-type": []string{resource.ResourceType().String()},
}, nil
default:
+13 -12
View File
@@ -7,23 +7,24 @@ import (
"strings"
"github.com/hashicorp/watchtower/internal/db"
"github.com/hashicorp/watchtower/internal/types/scope"
)
// CreateScope will create a scope in the repository and return the written
// scope. Supported options include: WithPublicId.
func (r *Repository) CreateScope(ctx context.Context, scope *Scope, opt ...Option) (*Scope, error) {
if scope == nil {
func (r *Repository) CreateScope(ctx context.Context, s *Scope, opt ...Option) (*Scope, error) {
if s == nil {
return nil, fmt.Errorf("create scope: missing scope %w", db.ErrNilParameter)
}
if scope.Scope == nil {
if s.Scope == nil {
return nil, fmt.Errorf("create scope: missing scope store %w", db.ErrNilParameter)
}
if scope.PublicId != "" {
if s.PublicId != "" {
return nil, fmt.Errorf("create scope: public id not empty: %w", db.ErrInvalidParameter)
}
opts := getOpts(opt...)
var publicId string
t := stringToScopeType(scope.Type)
t := scope.StringToScopeType(s.Type)
if opts.withPublicId != "" {
if !strings.HasPrefix(opts.withPublicId, t.Prefix()+"_") {
return nil, fmt.Errorf("create scope: passed-in public ID %q has wrong prefix for type %q which uses prefix %q", opts.withPublicId, t.String(), t.Prefix())
@@ -36,14 +37,14 @@ func (r *Repository) CreateScope(ctx context.Context, scope *Scope, opt ...Optio
return nil, fmt.Errorf("create scope: error generating public id %w for new scope", err)
}
}
s := scope.Clone().(*Scope)
s.PublicId = publicId
resource, err := r.create(ctx, s)
sc := s.Clone().(*Scope)
sc.PublicId = publicId
resource, err := r.create(ctx, sc)
if err != nil {
if db.IsUniqueError(err) {
return nil, fmt.Errorf("create scope: scope %s/%s already exists: %w", s.PublicId, s.Name, db.ErrNotUnique)
return nil, fmt.Errorf("create scope: scope %s/%s already exists: %w", sc.PublicId, sc.Name, db.ErrNotUnique)
}
return nil, fmt.Errorf("create scope: %w for %s", err, s.PublicId)
return nil, fmt.Errorf("create scope: %w for %s", err, sc.PublicId)
}
return resource.(*Scope), nil
}
@@ -127,7 +128,7 @@ func (r *Repository) ListProjects(ctx context.Context, withOrganizationId string
return nil, fmt.Errorf("list projects: missing organization id %w", db.ErrInvalidParameter)
}
var projects []*Scope
err := r.list(ctx, &projects, "parent_id = ? and type = ?", []interface{}{withOrganizationId, ProjectScope.String()}, opt...)
err := r.list(ctx, &projects, "parent_id = ? and type = ?", []interface{}{withOrganizationId, scope.Project.String()}, opt...)
if err != nil {
return nil, fmt.Errorf("list projects: %w", err)
}
@@ -137,7 +138,7 @@ func (r *Repository) ListProjects(ctx context.Context, withOrganizationId string
// ListOrganizations and supports the WithLimit option.
func (r *Repository) ListOrganizations(ctx context.Context, opt ...Option) ([]*Scope, error) {
var organizations []*Scope
err := r.list(ctx, &organizations, "type = ?", []interface{}{OrganizationScope.String()}, opt...)
err := r.list(ctx, &organizations, "type = ?", []interface{}{scope.Organization.String()}, opt...)
if err != nil {
return nil, fmt.Errorf("list organizations: %w", err)
}
+3 -2
View File
@@ -11,6 +11,7 @@ import (
"github.com/hashicorp/watchtower/internal/db/timestamp"
iam_store "github.com/hashicorp/watchtower/internal/iam/store"
"github.com/hashicorp/watchtower/internal/oplog"
"github.com/hashicorp/watchtower/internal/types/scope"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
@@ -55,7 +56,7 @@ func Test_Repository_CreateScope(t *testing.T) {
wrapper := db.TestWrapper(t)
repo, err := NewRepository(rw, rw, wrapper)
require.NoError(err)
publicId, err := newScopeId(OrganizationScope)
publicId, err := newScopeId(scope.Organization)
require.NoError(err)
s, err := NewOrganization()
require.NoError(err)
@@ -421,7 +422,7 @@ func TestRepository_UpdateScope(t *testing.T) {
Scope: &iam_store.Scope{
Name: "type" + id,
Description: "",
Type: ProjectScope.String(),
Type: scope.Project.String(),
CreateTime: now,
UpdateTime: now,
PublicId: publicId,
+2 -1
View File
@@ -13,6 +13,7 @@ import (
iam_store "github.com/hashicorp/watchtower/internal/iam/store"
"github.com/hashicorp/watchtower/internal/oplog"
"github.com/hashicorp/watchtower/internal/oplog/store"
"github.com/hashicorp/watchtower/internal/types/scope"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
@@ -123,7 +124,7 @@ func Test_Repository_create(t *testing.T) {
s, err := NewOrganization(WithName("fname-" + id))
assert.NoError(err)
s.PublicId, err = newScopeId(OrganizationScope)
s.PublicId, err = newScopeId(scope.Organization)
require.NoError(err)
retScope, err := repo.create(context.Background(), s)
require.NoError(err)
+6 -42
View File
@@ -6,6 +6,9 @@ import (
"fmt"
"github.com/hashicorp/watchtower/internal/db"
"github.com/hashicorp/watchtower/internal/types/action"
"github.com/hashicorp/watchtower/internal/types/resource"
"github.com/hashicorp/watchtower/internal/types/scope"
)
// Resource declares the shared behavior of IAM Resources
@@ -24,51 +27,12 @@ type Resource interface {
GetScope(ctx context.Context, r db.Reader) (*Scope, error)
// Type of Resource (Target, Policy, User, Group, etc)
ResourceType() ResourceType
ResourceType() resource.Type
// Actions that can be assigned permissions for
// the Resource in Policies. Action String() is key for
// the map of Actions returned.
Actions() map[string]Action
}
// ResourceType defines the types of resources in the system
type ResourceType int
const (
ResourceTypeUnknown ResourceType = 0
ResourceTypeScope ResourceType = 1
ResourceTypeUser ResourceType = 2
ResourceTypeGroup ResourceType = 3
ResourceTypeRole ResourceType = 4
ResourceTypeOrganization ResourceType = 5
ResourceTypeGroupMember ResourceType = 6
ResourceTypeGroupUserMember ResourceType = 7
ResourceTypeAssignedRole ResourceType = 8
ResourceTypeAssignedUserRole ResourceType = 9
ResourceTypeAssignedGroupRole ResourceType = 10
ResourceTypeRoleGrant ResourceType = 11
ResourceTypeAuthMethod ResourceType = 12
ResourceTypeProject ResourceType = 13
)
func (r ResourceType) String() string {
return [...]string{
"unknown",
"scope",
"user",
"group",
"role",
"organization",
"group member",
"group user member",
"assigned role",
"assigned user role",
"assigned group role",
"role grant",
"auth method",
"project",
}[r]
Actions() map[string]action.Type
}
type Clonable interface {
@@ -79,7 +43,7 @@ type Clonable interface {
type ResourceWithScope interface {
GetPublicId() string
GetScopeId() string
validScopeTypes() []ScopeType
validScopeTypes() []scope.Type
}
// LookupScope looks up the resource's scope
+7 -4
View File
@@ -7,6 +7,9 @@ import (
"github.com/hashicorp/watchtower/internal/db"
"github.com/hashicorp/watchtower/internal/iam/store"
"github.com/hashicorp/watchtower/internal/types/action"
"github.com/hashicorp/watchtower/internal/types/resource"
"github.com/hashicorp/watchtower/internal/types/scope"
"google.golang.org/protobuf/proto"
)
@@ -63,8 +66,8 @@ func (role *Role) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType
return nil
}
func (u *Role) validScopeTypes() []ScopeType {
return []ScopeType{OrganizationScope, ProjectScope}
func (u *Role) validScopeTypes() []scope.Type {
return []scope.Type{scope.Organization, scope.Project}
}
// Getscope returns the scope for the Role.
@@ -73,10 +76,10 @@ func (role *Role) GetScope(ctx context.Context, r db.Reader) (*Scope, error) {
}
// ResourceType returns the type of the Role.
func (*Role) ResourceType() ResourceType { return ResourceTypeRole }
func (*Role) ResourceType() resource.Type { return resource.Role }
// Actions returns the available actions for Role.
func (*Role) Actions() map[string]Action {
func (*Role) Actions() map[string]action.Type {
return CrudActions()
}
+7 -5
View File
@@ -9,6 +9,8 @@ import (
"github.com/hashicorp/watchtower/internal/db"
dbassert "github.com/hashicorp/watchtower/internal/db/assert"
"github.com/hashicorp/watchtower/internal/oplog"
"github.com/hashicorp/watchtower/internal/types/action"
"github.com/hashicorp/watchtower/internal/types/resource"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
@@ -448,17 +450,17 @@ func TestRole_Actions(t *testing.T) {
assert := assert.New(t)
r := &Role{}
a := r.Actions()
assert.Equal(a[ActionCreate.String()], ActionCreate)
assert.Equal(a[ActionUpdate.String()], ActionUpdate)
assert.Equal(a[ActionRead.String()], ActionRead)
assert.Equal(a[ActionDelete.String()], ActionDelete)
assert.Equal(a[action.Create.String()], action.Create)
assert.Equal(a[action.Update.String()], action.Update)
assert.Equal(a[action.Read.String()], action.Read)
assert.Equal(a[action.Delete.String()], action.Delete)
}
func TestRole_ResourceType(t *testing.T) {
assert := assert.New(t)
r := &Role{}
ty := r.ResourceType()
assert.Equal(ty, ResourceTypeRole)
assert.Equal(ty, resource.Role)
}
func TestRole_GetScope(t *testing.T) {
+21 -54
View File
@@ -7,45 +7,12 @@ import (
"github.com/hashicorp/watchtower/internal/db"
"github.com/hashicorp/watchtower/internal/iam/store"
"github.com/hashicorp/watchtower/internal/types/action"
"github.com/hashicorp/watchtower/internal/types/resource"
"github.com/hashicorp/watchtower/internal/types/scope"
"google.golang.org/protobuf/proto"
)
// ScopeType defines the possible types for Scopes
type ScopeType uint32
const (
UnknownScope ScopeType = 0
OrganizationScope ScopeType = 1
ProjectScope ScopeType = 2
)
func (s ScopeType) String() string {
return [...]string{
"unknown",
"organization",
"project",
}[s]
}
func (s ScopeType) Prefix() string {
return [...]string{
"unknown",
"o",
"p",
}[s]
}
func stringToScopeType(s string) ScopeType {
switch s {
case OrganizationScope.String():
return OrganizationScope
case ProjectScope.String():
return ProjectScope
default:
return UnknownScope
}
}
// Scope is used to create a hierarchy of "containers" that encompass the scope of
// an IAM resource. Scopes are Organizations and Projects.
type Scope struct {
@@ -62,14 +29,14 @@ var _ db.VetForWriter = (*Scope)(nil)
var _ Clonable = (*Scope)(nil)
func NewOrganization(opt ...Option) (*Scope, error) {
return newScope(OrganizationScope, opt...)
return newScope(scope.Organization, opt...)
}
func NewProject(organizationPublicId string, opt ...Option) (*Scope, error) {
org := allocScope()
org.PublicId = organizationPublicId
opt = append(opt, withScope(&org))
p, err := newScope(ProjectScope, opt...)
p, err := newScope(scope.Project, opt...)
if err != nil {
return nil, fmt.Errorf("error creating new project: %w", err)
}
@@ -79,15 +46,15 @@ func NewProject(organizationPublicId string, opt ...Option) (*Scope, error) {
// newScope creates a new Scope of the specified ScopeType with options:
// WithName specifies the Scope's friendly name. WithDescription specifies the
// scope's description. WithScope specifies the Scope's parent
func newScope(scopeType ScopeType, opt ...Option) (*Scope, error) {
func newScope(scopeType scope.Type, opt ...Option) (*Scope, error) {
opts := getOpts(opt...)
if scopeType == UnknownScope {
if scopeType == scope.Unknown {
return nil, fmt.Errorf("new scope: unknown scope type %w", db.ErrInvalidParameter)
}
if opts.withScope != nil && opts.withScope.PublicId == "" {
return nil, fmt.Errorf("new scope: with scope's parent id is missing %w", db.ErrInvalidParameter)
}
if scopeType == ProjectScope && opts.withScope == nil {
if scopeType == scope.Project && opts.withScope == nil {
return nil, fmt.Errorf("new scope: project scope is missing its parent %w", db.ErrInvalidParameter)
}
s := &Scope{
@@ -121,7 +88,7 @@ func (s *Scope) Clone() interface{} {
// this function is intended to be callled by a db.Writer (Create and Update) to validate
// the scope before writing it to the db.
func (s *Scope) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error {
if s.Type == UnknownScope.String() {
if s.Type == scope.Unknown.String() {
return errors.New("unknown scope type for scope write")
}
if s.PublicId == "" {
@@ -139,16 +106,16 @@ func (s *Scope) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType,
}
}
if opType == db.CreateOp {
if s.ParentId == "" && s.Type == ProjectScope.String() {
if s.ParentId == "" && s.Type == scope.Project.String() {
return errors.New("project has no organization")
}
if s.Type == ProjectScope.String() {
if s.Type == scope.Project.String() {
parentScope := allocScope()
parentScope.PublicId = s.ParentId
if err := r.LookupByPublicId(ctx, &parentScope, opt...); err != nil {
return fmt.Errorf("unable to verify project's organization scope: %w", err)
}
if parentScope.Type != OrganizationScope.String() {
if parentScope.Type != scope.Organization.String() {
return errors.New("project parent scope is not an organization")
}
}
@@ -157,19 +124,19 @@ func (s *Scope) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType,
}
// ResourceType returns the type of scope
func (s *Scope) ResourceType() ResourceType {
func (s *Scope) ResourceType() resource.Type {
switch s.Type {
case OrganizationScope.String():
return ResourceTypeOrganization
case ProjectScope.String():
return ResourceTypeProject
case scope.Organization.String():
return resource.Organization
case scope.Project.String():
return resource.Project
default:
return ResourceTypeScope
return resource.Scope
}
}
// Actions returns the available actions for Scopes
func (*Scope) Actions() map[string]Action {
func (*Scope) Actions() map[string]action.Type {
return CrudActions()
}
@@ -188,9 +155,9 @@ func (s *Scope) GetScope(ctx context.Context, r db.Reader) (*Scope, error) {
}
// HANDLE_ORG
switch s.Type {
case OrganizationScope.String():
case scope.Organization.String():
return nil, nil
case ProjectScope.String():
case scope.Project.String():
var p Scope
switch s.ParentId {
case "":
+17 -45
View File
@@ -5,6 +5,9 @@ import (
"testing"
"github.com/hashicorp/watchtower/internal/db"
"github.com/hashicorp/watchtower/internal/types/action"
"github.com/hashicorp/watchtower/internal/types/resource"
"github.com/hashicorp/watchtower/internal/types/scope"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
@@ -25,7 +28,7 @@ func Test_NewScope(t *testing.T) {
s, err := NewOrganization()
require.NoError(err)
require.NotNil(s.Scope)
s.PublicId, err = newScopeId(OrganizationScope)
s.PublicId, err = newScopeId(scope.Organization)
require.NoError(err)
err = w.Create(context.Background(), s)
require.NoError(err)
@@ -40,7 +43,7 @@ func Test_NewScope(t *testing.T) {
})
t.Run("unknown-scope", func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
s, err := newScope(UnknownScope)
s, err := newScope(scope.Unknown)
require.Error(err)
require.Nil(s)
assert.Equal(err.Error(), "new scope: unknown scope type invalid parameter")
@@ -68,7 +71,7 @@ func Test_ScopeCreate(t *testing.T) {
s, err := NewOrganization()
require.NoError(err)
require.NotNil(s.Scope)
s.PublicId, err = newScopeId(OrganizationScope)
s.PublicId, err = newScopeId(scope.Organization)
require.NoError(err)
err = w.Create(context.Background(), s)
require.NoError(err)
@@ -80,7 +83,7 @@ func Test_ScopeCreate(t *testing.T) {
s, err := NewOrganization()
require.NoError(err)
require.NotNil(s.Scope)
s.PublicId, err = newScopeId(OrganizationScope)
s.PublicId, err = newScopeId(scope.Organization)
require.NoError(err)
err = w.Create(context.Background(), s)
require.NoError(err)
@@ -92,7 +95,7 @@ func Test_ScopeCreate(t *testing.T) {
require.NotNil(project.Scope)
assert.Equal(project.Scope.ParentId, s.PublicId)
assert.Equal(project.GetDescription(), id)
project.PublicId, err = newScopeId(OrganizationScope)
project.PublicId, err = newScopeId(scope.Organization)
require.NoError(err)
err = w.Create(context.Background(), project)
@@ -125,7 +128,7 @@ func Test_ScopeUpdate(t *testing.T) {
w := db.New(conn)
s, _ := TestScopes(t, conn)
s.Type = ProjectScope.String()
s.Type = scope.Project.String()
updatedRows, err := w.Update(context.Background(), s, []string{"Type"}, nil)
require.Error(err)
assert.Equal(0, updatedRows)
@@ -155,7 +158,7 @@ func Test_ScopeGetScope(t *testing.T) {
p := allocScope()
p.PublicId = proj.PublicId
p.Type = ProjectScope.String()
p.Type = scope.Project.String()
projectOrg, err = p.GetScope(context.Background(), w)
require.NoError(err)
assert.Equal(org.PublicId, projectOrg.PublicId)
@@ -166,20 +169,20 @@ func TestScope_Actions(t *testing.T) {
assert := assert.New(t)
s := &Scope{}
a := s.Actions()
assert.Equal(a[ActionCreate.String()], ActionCreate)
assert.Equal(a[ActionUpdate.String()], ActionUpdate)
assert.Equal(a[ActionRead.String()], ActionRead)
assert.Equal(a[ActionDelete.String()], ActionDelete)
assert.Equal(a[action.Create.String()], action.Create)
assert.Equal(a[action.Update.String()], action.Update)
assert.Equal(a[action.Read.String()], action.Read)
assert.Equal(a[action.Delete.String()], action.Delete)
if _, ok := a[ActionList.String()]; ok {
t.Errorf("scopes should not include %s as an action", ActionList.String())
if _, ok := a[action.List.String()]; ok {
t.Errorf("scopes should not include %s as an action", action.List.String())
}
}
func TestScope_ResourceType(t *testing.T) {
o, err := NewOrganization()
require.NoError(t, err)
assert.Equal(t, o.ResourceType(), ResourceTypeOrganization)
assert.Equal(t, o.ResourceType(), resource.Organization)
}
func TestScope_Clone(t *testing.T) {
@@ -205,34 +208,3 @@ func TestScope_Clone(t *testing.T) {
assert.True(!proto.Equal(cp.(*Scope).Scope, s2.Scope))
})
}
func Test_stringToScopeType(t *testing.T) {
tests := []struct {
name string
s string
want ScopeType
}{
{
name: "org",
s: "organization",
want: OrganizationScope,
},
{
name: "proj",
s: "project",
want: ProjectScope,
},
{
name: "unknown",
s: "org",
want: UnknownScope,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
got := stringToScopeType(tt.s)
assert.Equal(tt.want, got)
})
}
}
+7 -4
View File
@@ -6,6 +6,9 @@ import (
"github.com/hashicorp/watchtower/internal/db"
"github.com/hashicorp/watchtower/internal/iam/store"
"github.com/hashicorp/watchtower/internal/types/action"
"github.com/hashicorp/watchtower/internal/types/resource"
"github.com/hashicorp/watchtower/internal/types/scope"
"google.golang.org/protobuf/proto"
)
@@ -64,8 +67,8 @@ func (u *User) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, o
return nil
}
func (u *User) validScopeTypes() []ScopeType {
return []ScopeType{OrganizationScope}
func (u *User) validScopeTypes() []scope.Type {
return []scope.Type{scope.Organization}
}
// GetScope returns the scope for the User
@@ -74,10 +77,10 @@ func (u *User) GetScope(ctx context.Context, r db.Reader) (*Scope, error) {
}
// ResourceType returns the type of the User
func (*User) ResourceType() ResourceType { return ResourceTypeUser }
func (*User) ResourceType() resource.Type { return resource.User }
// Actions returns the available actions for Users
func (*User) Actions() map[string]Action {
func (*User) Actions() map[string]action.Type {
return CrudActions()
}
+9 -7
View File
@@ -6,6 +6,8 @@ import (
"testing"
"github.com/hashicorp/watchtower/internal/db"
"github.com/hashicorp/watchtower/internal/types/action"
"github.com/hashicorp/watchtower/internal/types/resource"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
@@ -362,18 +364,18 @@ func TestUser_Actions(t *testing.T) {
assert := assert.New(t)
u := &User{}
a := u.Actions()
assert.Equal(a[ActionCreate.String()], ActionCreate)
assert.Equal(a[ActionUpdate.String()], ActionUpdate)
assert.Equal(a[ActionRead.String()], ActionRead)
assert.Equal(a[ActionDelete.String()], ActionDelete)
assert.Equal(a[action.Create.String()], action.Create)
assert.Equal(a[action.Update.String()], action.Update)
assert.Equal(a[action.Read.String()], action.Read)
assert.Equal(a[action.Delete.String()], action.Delete)
if _, ok := a[ActionList.String()]; ok {
t.Errorf("users should not include %s as an action", ActionList.String())
if _, ok := a[action.List.String()]; ok {
t.Errorf("users should not include %s as an action", action.List.String())
}
}
func TestUser_ResourceType(t *testing.T) {
t.Parallel()
u := allocUser()
assert.Equal(t, ResourceTypeUser, u.ResourceType())
assert.Equal(t, resource.User, u.ResourceType())
}
+11 -10
View File
@@ -19,7 +19,8 @@ construction is thus synthesizing something reasonable from a set of Grants.
*/
import (
"github.com/hashicorp/watchtower/internal/iam"
"github.com/hashicorp/watchtower/internal/types/action"
"github.com/hashicorp/watchtower/internal/types/resource"
)
// ACL provides an entry point into the permissions engine for determining if an
@@ -48,7 +49,7 @@ type Resource struct {
Id string
// Type of resource.
Type string
Type resource.Type
// Pin if defined would constrain the resource within the collection of the
// pin id.
@@ -69,32 +70,32 @@ func NewACL(grants ...Grant) ACL {
}
// Allowed determines if the grants for an ACL allow an action for a resource.
func (a ACL) Allowed(resource Resource, action iam.Action) (results ACLResults) {
func (a ACL) Allowed(r Resource, aType action.Type) (results ACLResults) {
// First, get the grants within the specified scope
grants := a.scopeMap[resource.ScopeId]
grants := a.scopeMap[r.ScopeId]
results.scopeMap = a.scopeMap
// Now, go through and check the cases indicated above
for _, grant := range grants {
if !(grant.actions[action] || grant.actions[iam.ActionAll]) {
if !(grant.actions[aType] || grant.actions[action.All]) {
continue
}
switch {
// type=<resource.type>;actions=<action>
case grant.id == "" &&
grant.typ == resource.Type:
grant.typ == r.Type:
results.Allowed = true
return
// id=<resource.id>;actions=<action>
case (grant.id == resource.Id || grant.id == "*") &&
grant.typ == "":
case (grant.id == r.Id || grant.id == "*") &&
grant.typ == resource.Unknown:
results.Allowed = true
return
// id=<pin>;type=<resource.type>;actions=<action>
case grant.id == resource.Pin &&
grant.typ == resource.Type:
case grant.id == r.Pin &&
grant.typ == r.Type:
results.Allowed = true
return
}
+57 -55
View File
@@ -3,7 +3,9 @@ package perms
import (
"testing"
"github.com/hashicorp/watchtower/internal/iam"
"github.com/hashicorp/watchtower/internal/types/action"
"github.com/hashicorp/watchtower/internal/types/resource"
"github.com/hashicorp/watchtower/internal/types/scope"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -16,7 +18,7 @@ func Test_ACLAllowed(t *testing.T) {
grants []string
}
type actionAllowed struct {
action iam.Action
action action.Type
allowed bool
}
type input struct {
@@ -30,7 +32,7 @@ func Test_ACLAllowed(t *testing.T) {
// A set of common grants to use in the following tests
commonGrants := []scopeGrant{
{
scope: Scope{Type: iam.OrganizationScope, Id: "org_a"},
scope: Scope{Type: scope.Organization, Id: "org_a"},
grants: []string{
"id=a_bar;actions=read,update",
"id=a_foo;type=host-catalog;actions=update,delete",
@@ -38,7 +40,7 @@ func Test_ACLAllowed(t *testing.T) {
},
},
{
scope: Scope{Type: iam.OrganizationScope, Id: "org_b"},
scope: Scope{Type: scope.Organization, Id: "org_b"},
grants: []string{
"type=host-set;actions=list,create",
"type=host;actions=*",
@@ -46,7 +48,7 @@ func Test_ACLAllowed(t *testing.T) {
},
},
{
scope: Scope{Type: iam.OrganizationScope, Id: "org_c"},
scope: Scope{Type: scope.Organization, Id: "org_c"},
grants: []string{
"id={{user.id }};actions=create,update",
},
@@ -58,19 +60,19 @@ func Test_ACLAllowed(t *testing.T) {
tests := []input{
{
name: "no grants",
resource: Resource{ScopeId: "foo", Id: "bar", Type: "typ"},
resource: Resource{ScopeId: "foo", Id: "bar", Type: resource.HostCatalog},
actionsAllowed: []actionAllowed{
{action: iam.ActionCreate},
{action: iam.ActionRead},
{action: action.Create},
{action: action.Read},
},
},
{
name: "no overlap",
resource: Resource{ScopeId: "foo", Id: "bar", Type: "typ"},
resource: Resource{ScopeId: "foo", Id: "bar", Type: resource.HostCatalog},
scopeGrants: commonGrants,
actionsAllowed: []actionAllowed{
{action: iam.ActionCreate},
{action: iam.ActionRead},
{action: action.Create},
{action: action.Read},
},
},
{
@@ -78,8 +80,8 @@ func Test_ACLAllowed(t *testing.T) {
resource: Resource{ScopeId: "org_a", Id: "a_foo"},
scopeGrants: commonGrants,
actionsAllowed: []actionAllowed{
{action: iam.ActionUpdate},
{action: iam.ActionDelete},
{action: action.Update},
{action: action.Delete},
},
},
{
@@ -87,19 +89,19 @@ func Test_ACLAllowed(t *testing.T) {
resource: Resource{ScopeId: "org_a", Id: "a_bar"},
scopeGrants: commonGrants,
actionsAllowed: []actionAllowed{
{action: iam.ActionRead, allowed: true},
{action: iam.ActionUpdate, allowed: true},
{action: iam.ActionDelete},
{action: action.Read, allowed: true},
{action: action.Update, allowed: true},
{action: action.Delete},
},
},
{
name: "matching scope and id and all action",
resource: Resource{ScopeId: "org_b", Type: "host"},
resource: Resource{ScopeId: "org_b", Type: resource.Host},
scopeGrants: commonGrants,
actionsAllowed: []actionAllowed{
{action: iam.ActionRead, allowed: true},
{action: iam.ActionUpdate, allowed: true},
{action: iam.ActionDelete, allowed: true},
{action: action.Read, allowed: true},
{action: action.Update, allowed: true},
{action: action.Delete, allowed: true},
},
},
{
@@ -107,78 +109,78 @@ func Test_ACLAllowed(t *testing.T) {
resource: Resource{ScopeId: "org_b", Id: "id_g"},
scopeGrants: commonGrants,
actionsAllowed: []actionAllowed{
{action: iam.ActionRead},
{action: iam.ActionUpdate},
{action: iam.ActionDelete},
{action: action.Read},
{action: action.Update},
{action: action.Delete},
},
},
{
name: "matching scope and not matching type",
resource: Resource{ScopeId: "org_a", Type: "host-catalog"},
resource: Resource{ScopeId: "org_a", Type: resource.HostCatalog},
scopeGrants: commonGrants,
actionsAllowed: []actionAllowed{
{action: iam.ActionUpdate},
{action: iam.ActionDelete},
{action: action.Update},
{action: action.Delete},
},
},
{
name: "matching scope and matching type",
resource: Resource{ScopeId: "org_a", Type: "host-set"},
resource: Resource{ScopeId: "org_a", Type: resource.HostSet},
scopeGrants: commonGrants,
actionsAllowed: []actionAllowed{
{action: iam.ActionList, allowed: true},
{action: iam.ActionCreate, allowed: true},
{action: iam.ActionDelete},
{action: action.List, allowed: true},
{action: action.Create, allowed: true},
{action: action.Delete},
},
},
{
name: "matching scope, type, action, bad pin",
resource: Resource{ScopeId: "org_a", Id: "a_foo", Type: "host-catalog"},
resource: Resource{ScopeId: "org_a", Id: "a_foo", Type: resource.HostCatalog},
scopeGrants: commonGrants,
actionsAllowed: []actionAllowed{
{action: iam.ActionUpdate},
{action: iam.ActionDelete},
{action: iam.ActionRead},
{action: action.Update},
{action: action.Delete},
{action: action.Read},
},
},
{
name: "matching scope, type, action, random id and bad pin",
resource: Resource{ScopeId: "org_a", Id: "anything", Type: "host-catalog", Pin: "a_bar"},
resource: Resource{ScopeId: "org_a", Id: "anything", Type: resource.HostCatalog, Pin: "a_bar"},
scopeGrants: commonGrants,
actionsAllowed: []actionAllowed{
{action: iam.ActionUpdate},
{action: iam.ActionDelete},
{action: iam.ActionRead},
{action: action.Update},
{action: action.Delete},
{action: action.Read},
},
},
{
name: "matching scope, type, action, random id and good pin",
resource: Resource{ScopeId: "org_a", Id: "anything", Type: "host-catalog", Pin: "a_foo"},
resource: Resource{ScopeId: "org_a", Id: "anything", Type: resource.HostCatalog, Pin: "a_foo"},
scopeGrants: commonGrants,
actionsAllowed: []actionAllowed{
{action: iam.ActionUpdate, allowed: true},
{action: iam.ActionDelete, allowed: true},
{action: iam.ActionRead},
{action: action.Update, allowed: true},
{action: action.Delete, allowed: true},
{action: action.Read},
},
},
{
name: "wrong scope and matching type",
resource: Resource{ScopeId: "org_bad", Type: "host-set"},
resource: Resource{ScopeId: "org_bad", Type: resource.HostSet},
scopeGrants: commonGrants,
actionsAllowed: []actionAllowed{
{action: iam.ActionList},
{action: iam.ActionCreate},
{action: iam.ActionDelete},
{action: action.List},
{action: action.Create},
{action: action.Delete},
},
},
{
name: "any id",
resource: Resource{ScopeId: "org_b", Type: "auth-method"},
resource: Resource{ScopeId: "org_b", Type: resource.AuthMethod},
scopeGrants: commonGrants,
actionsAllowed: []actionAllowed{
{action: iam.ActionList},
{action: iam.ActionAuthen, allowed: true},
{action: iam.ActionDelete},
{action: action.List},
{action: action.Authen, allowed: true},
{action: action.Delete},
},
},
{
@@ -186,9 +188,9 @@ func Test_ACLAllowed(t *testing.T) {
resource: Resource{ScopeId: "org_c"},
scopeGrants: commonGrants,
actionsAllowed: []actionAllowed{
{action: iam.ActionList},
{action: iam.ActionAuthen},
{action: iam.ActionDelete},
{action: action.List},
{action: action.Authen},
{action: action.Delete},
},
userId: "u_abcd1234",
},
@@ -197,8 +199,8 @@ func Test_ACLAllowed(t *testing.T) {
resource: Resource{ScopeId: "org_c", Id: "u_abcd1234"},
scopeGrants: commonGrants,
actionsAllowed: []actionAllowed{
{action: iam.ActionCreate, allowed: true},
{action: iam.ActionUpdate, allowed: true},
{action: action.Create, allowed: true},
{action: action.Update, allowed: true},
},
userId: "u_abcd1234",
},
+41 -48
View File
@@ -7,20 +7,9 @@ import (
"sort"
"strings"
"github.com/hashicorp/watchtower/internal/iam"
)
const (
TypeNone = ""
TypeAll = "*"
TypeRole = "role"
TypeGroup = "group"
TypeUser = "user"
TypeAuthMethod = "auth-method"
TypeHostCatalog = "host-catalog"
TypeHostSet = "host-set"
TypeHost = "host"
TypeTarget = "target"
"github.com/hashicorp/watchtower/internal/types/action"
"github.com/hashicorp/watchtower/internal/types/resource"
"github.com/hashicorp/watchtower/internal/types/scope"
)
// Scope provides an in-memory representation of iam.Scope without the
@@ -30,7 +19,7 @@ type Scope struct {
Id string
// Type is the scope's type (org or project)
Type iam.ScopeType
Type scope.Type
}
// Grant is a Go representation of a parsed grant
@@ -42,10 +31,10 @@ type Grant struct {
id string
// The type, if provided
typ string
typ resource.Type
// The set of actions being granted
actions map[iam.Action]bool
actions map[action.Type]bool
// This is used as a temporary staging area before validating permissions to
// allow the same validation code across grant string formats
@@ -62,7 +51,7 @@ func (g Grant) clone() *Grant {
ret.actionsBeingParsed = append(ret.actionsBeingParsed, g.actionsBeingParsed...)
}
if g.actions != nil {
ret.actions = make(map[iam.Action]bool, len(g.actions))
ret.actions = make(map[action.Type]bool, len(g.actions))
for action := range g.actions {
ret.actions[action] = true
}
@@ -78,8 +67,8 @@ func (g Grant) CanonicalString() string {
builder = append(builder, fmt.Sprintf("id=%s", g.id))
}
if g.typ != TypeNone {
builder = append(builder, fmt.Sprintf("type=%s", g.typ))
if g.typ != resource.Unknown {
builder = append(builder, fmt.Sprintf("type=%s", g.typ.String()))
}
if len(g.actions) > 0 {
@@ -100,8 +89,8 @@ func (g Grant) MarshalJSON() ([]byte, error) {
if g.id != "" {
res["id"] = g.id
}
if g.typ != "" {
res["type"] = g.typ
if g.typ != resource.Unknown {
res["type"] = g.typ.String()
}
if len(g.actions) > 0 {
actions := make([]string, 0, len(g.actions))
@@ -134,7 +123,7 @@ func (g *Grant) unmarshalJSON(data []byte) error {
if !ok {
return fmt.Errorf("unable to interpret %q as string", "type")
}
g.typ = strings.ToLower(typ)
g.typ = resource.StringToResourceType(typ)
}
if rawActions, ok := raw["actions"]; ok {
interfaceActions, ok := rawActions.([]interface{})
@@ -179,7 +168,11 @@ func (g *Grant) unmarshalText(grantString string) error {
g.id = strings.ToLower(kv[1])
case "type":
g.typ = strings.ToLower(kv[1])
typeString := strings.ToLower(kv[1])
g.typ = resource.StringToResourceType(typeString)
if g.typ == resource.Unknown {
return fmt.Errorf("unknown type specifier %q", typeString)
}
case "actions":
actions := strings.Split(kv[1], ",")
@@ -205,23 +198,23 @@ func (g *Grant) unmarshalText(grantString string) error {
//
// The scope must be the org and project where this grant originated, not the
// request.
func Parse(scope Scope, userId, grantString string) (Grant, error) {
func Parse(s Scope, userId, grantString string) (Grant, error) {
if len(grantString) == 0 {
return Grant{}, errors.New("grant string is empty")
}
switch scope.Type {
case iam.ProjectScope, iam.OrganizationScope:
switch s.Type {
case scope.Project, scope.Organization:
default:
return Grant{}, errors.New("invalid scope type")
}
if scope.Id == "" {
if s.Id == "" {
return Grant{}, errors.New("no scope ID provided")
}
grant := Grant{
scope: scope,
scope: s,
}
switch {
@@ -249,7 +242,7 @@ func Parse(scope Scope, userId, grantString string) (Grant, error) {
}
}
if grant.id == "" && grant.typ == "" {
if grant.id == "" && grant.typ == resource.Unknown {
return Grant{}, errors.New(`"id" and "type" cannot both be empty`)
}
@@ -266,16 +259,16 @@ func Parse(scope Scope, userId, grantString string) (Grant, error) {
func (g Grant) validateType() error {
switch g.typ {
case TypeNone,
TypeAll,
TypeRole,
TypeGroup,
TypeUser,
TypeAuthMethod,
TypeHostCatalog,
TypeHostSet,
TypeHost,
TypeTarget:
case resource.Unknown,
resource.All,
resource.Role,
resource.StaticGroup,
resource.User,
resource.AuthMethod,
resource.HostCatalog,
resource.HostSet,
resource.Host,
resource.Target:
return nil
}
return fmt.Errorf("unknown type specifier %q", g.typ)
@@ -286,22 +279,22 @@ func (g *Grant) parseAndValidateActions() error {
return errors.New("no actions specified")
}
for _, action := range g.actionsBeingParsed {
if action == "" {
for _, a := range g.actionsBeingParsed {
if a == "" {
return errors.New("empty action found")
}
if g.actions == nil {
g.actions = make(map[iam.Action]bool, len(g.actionsBeingParsed))
g.actions = make(map[action.Type]bool, len(g.actionsBeingParsed))
}
if a := iam.ActionMap[action]; a == iam.ActionUnknown {
return fmt.Errorf("unknown action %q", action)
if am := action.Map[a]; am == action.Unknown {
return fmt.Errorf("unknown action %q", a)
} else {
g.actions[a] = true
g.actions[am] = true
}
}
if len(g.actions) > 1 && g.actions[iam.ActionAll] {
return fmt.Errorf("%q cannot be specified with other actions", iam.ActionAll.String())
if len(g.actions) > 1 && g.actions[action.All] {
return fmt.Errorf("%q cannot be specified with other actions", action.All.String())
}
g.actionsBeingParsed = nil
+56 -56
View File
@@ -4,7 +4,9 @@ import (
"strings"
"testing"
"github.com/hashicorp/watchtower/internal/iam"
"github.com/hashicorp/watchtower/internal/types/action"
"github.com/hashicorp/watchtower/internal/types/resource"
"github.com/hashicorp/watchtower/internal/types/scope"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -44,8 +46,8 @@ func Test_ActionParsingValidation(t *testing.T) {
actionsBeingParsed: []string{"*"},
},
result: Grant{
actions: map[iam.Action]bool{
iam.ActionAll: true,
actions: map[action.Type]bool{
action.All: true,
},
},
},
@@ -62,14 +64,14 @@ func Test_ActionParsingValidation(t *testing.T) {
actionsBeingParsed: []string{"list", "create", "update", "read", "delete", "authen", "connect"},
},
result: Grant{
actions: map[iam.Action]bool{
iam.ActionList: true,
iam.ActionCreate: true,
iam.ActionUpdate: true,
iam.ActionRead: true,
iam.ActionDelete: true,
iam.ActionAuthen: true,
iam.ActionConnect: true,
actions: map[action.Type]bool{
action.List: true,
action.Create: true,
action.Update: true,
action.Read: true,
action.Delete: true,
action.Authen: true,
action.Connect: true,
},
},
},
@@ -105,14 +107,14 @@ func Test_ValidateType(t *testing.T) {
{
name: "unknown specifier",
input: Grant{
typ: "foobar",
typ: resource.RoleGrant,
},
errResult: `unknown type specifier "foobar"`,
errResult: `unknown type specifier "role-grant"`,
},
{
name: "valid specifier",
input: Grant{
typ: TypeHostCatalog,
typ: resource.HostCatalog,
},
},
}
@@ -145,7 +147,7 @@ func Test_MarshallingAndCloning(t *testing.T) {
name: "empty",
input: Grant{
scope: Scope{
Type: iam.OrganizationScope,
Type: scope.Organization,
},
},
jsonOutput: `{}`,
@@ -156,29 +158,29 @@ func Test_MarshallingAndCloning(t *testing.T) {
input: Grant{
id: "baz",
scope: Scope{
Type: iam.ProjectScope,
Type: scope.Project,
},
typ: TypeGroup,
typ: resource.StaticGroup,
},
jsonOutput: `{"id":"baz","type":"group"}`,
canonicalString: `id=baz;type=group`,
jsonOutput: `{"id":"baz","type":"static-group"}`,
canonicalString: `id=baz;type=static-group`,
},
{
name: "everything",
input: Grant{
id: "baz",
scope: Scope{
Type: iam.ProjectScope,
Type: scope.Project,
},
typ: TypeGroup,
actions: map[iam.Action]bool{
iam.ActionCreate: true,
iam.ActionRead: true,
typ: resource.StaticGroup,
actions: map[action.Type]bool{
action.Create: true,
action.Read: true,
},
actionsBeingParsed: []string{"create", "read"},
},
jsonOutput: `{"actions":["create","read"],"id":"baz","type":"group"}`,
canonicalString: `id=baz;type=group;actions=create,read`,
jsonOutput: `{"actions":["create","read"],"id":"baz","type":"static-group"}`,
canonicalString: `id=baz;type=static-group;actions=create,read`,
},
}
@@ -243,7 +245,7 @@ func Test_Unmarshaling(t *testing.T) {
{
name: "good type",
expected: Grant{
typ: "host-catalog",
typ: resource.HostCatalog,
},
jsonInput: `{"type":"host-catalog"}`,
textInput: `type=host-catalog`,
@@ -284,11 +286,10 @@ func Test_Unmarshaling(t *testing.T) {
},
}
assert := assert.New(t)
require := require.New(t)
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
var g Grant
if test.jsonInput != "" {
err := g.unmarshalJSON([]byte(test.jsonInput))
@@ -362,13 +363,13 @@ func Test_Parse(t *testing.T) {
expected: Grant{
scope: Scope{
Id: "scope",
Type: iam.OrganizationScope,
Type: scope.Organization,
},
id: "foobar",
typ: "host-catalog",
actions: map[iam.Action]bool{
iam.ActionCreate: true,
iam.ActionRead: true,
typ: resource.HostCatalog,
actions: map[action.Type]bool{
action.Create: true,
action.Read: true,
},
},
},
@@ -378,13 +379,13 @@ func Test_Parse(t *testing.T) {
expected: Grant{
scope: Scope{
Id: "scope",
Type: iam.OrganizationScope,
Type: scope.Organization,
},
id: "foobar",
typ: "host-catalog",
actions: map[iam.Action]bool{
iam.ActionCreate: true,
iam.ActionRead: true,
typ: resource.HostCatalog,
actions: map[action.Type]bool{
action.Create: true,
action.Read: true,
},
},
},
@@ -401,38 +402,37 @@ func Test_Parse(t *testing.T) {
expected: Grant{
scope: Scope{
Id: "scope",
Type: iam.OrganizationScope,
Type: scope.Organization,
},
id: "u_abcd1234",
actions: map[iam.Action]bool{
iam.ActionCreate: true,
iam.ActionRead: true,
actions: map[action.Type]bool{
action.Create: true,
action.Read: true,
},
},
},
}
assert := assert.New(t)
require := require.New(t)
_, err := Parse(Scope{}, "", "")
require.Error(err)
assert.Equal("grant string is empty", err.Error())
require.Error(t, err)
assert.Equal(t, "grant string is empty", err.Error())
_, err = Parse(Scope{}, "", "{}")
require.Error(err)
assert.Equal("invalid scope type", err.Error())
require.Error(t, err)
assert.Equal(t, "invalid scope type", err.Error())
_, err = Parse(Scope{Type: iam.OrganizationScope}, "", "{}")
require.Error(err)
assert.Equal("no scope ID provided", err.Error())
_, err = Parse(Scope{Type: scope.Organization}, "", "{}")
require.Error(t, err)
assert.Equal(t, "no scope ID provided", err.Error())
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
grant, err := Parse(Scope{Id: "scope", Type: iam.OrganizationScope}, test.userId, test.input)
assert := assert.New(t)
require := require.New(t)
grant, err := Parse(Scope{Id: "scope", Type: scope.Organization}, test.userId, test.input)
if test.err != "" {
require.Error(err)
assert.True(strings.HasPrefix(err.Error(), test.err))
assert.True(strings.Contains(err.Error(), test.err))
} else {
require.NoError(err)
assert.Equal(test.expected, grant)
@@ -12,6 +12,7 @@ import (
pbs "github.com/hashicorp/watchtower/internal/gen/controller/api/services"
"github.com/hashicorp/watchtower/internal/iam"
"github.com/hashicorp/watchtower/internal/servers/controller/handlers"
"github.com/hashicorp/watchtower/internal/types/scope"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/wrapperspb"
@@ -347,7 +348,7 @@ func validateAncestors(r ancestorProvider) map[string]string {
if r.GetOrgId() == "" {
return map[string]string{orgIdFieldName: "Missing organization id."}
}
if !validId(r.GetOrgId(), iam.OrganizationScope.Prefix()+"_") {
if !validId(r.GetOrgId(), scope.Organization.Prefix()+"_") {
return map[string]string{orgIdFieldName: "Improperly formatted identifier."}
}
return map[string]string{}
@@ -12,6 +12,7 @@ import (
pbs "github.com/hashicorp/watchtower/internal/gen/controller/api/services"
"github.com/hashicorp/watchtower/internal/iam"
"github.com/hashicorp/watchtower/internal/servers/controller/handlers/groups"
"github.com/hashicorp/watchtower/internal/types/scope"
"google.golang.org/genproto/protobuf/field_mask"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
@@ -155,14 +156,14 @@ func TestList(t *testing.T) {
},
{
name: "Invalid Org Id",
req: &pbs.ListGroupsRequest{OrgId: iam.OrganizationScope.Prefix() + "_this is invalid"},
req: &pbs.ListGroupsRequest{OrgId: scope.Organization.Prefix() + "_this is invalid"},
res: nil,
errCode: codes.InvalidArgument,
},
// TODO: When an org doesn't exist, we should return a 404 instead of an empty list.
{
name: "Unfound Org",
req: &pbs.ListGroupsRequest{OrgId: iam.OrganizationScope.Prefix() + "_DoesntExis"},
req: &pbs.ListGroupsRequest{OrgId: scope.Organization.Prefix() + "_DoesntExis"},
res: &pbs.ListGroupsResponse{},
errCode: codes.OK,
},
@@ -9,9 +9,9 @@ import (
pb "github.com/hashicorp/watchtower/internal/gen/controller/api/resources/hosts"
pbs "github.com/hashicorp/watchtower/internal/gen/controller/api/services"
"github.com/hashicorp/watchtower/internal/host/static"
"github.com/hashicorp/watchtower/internal/iam"
"github.com/hashicorp/watchtower/internal/servers/controller/common"
"github.com/hashicorp/watchtower/internal/servers/controller/handlers"
"github.com/hashicorp/watchtower/internal/types/scope"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/wrapperspb"
@@ -391,10 +391,10 @@ func validateAncestors(r ancestorProvider) map[string]string {
return badFields
}
if !validId(r.GetOrgId(), iam.OrganizationScope.Prefix()+"_") {
if !validId(r.GetOrgId(), scope.Organization.Prefix()+"_") {
badFields[orgIdFieldName] = "The field is incorrectly formatted."
}
if !validId(r.GetProjectId(), iam.ProjectScope.Prefix()+"_") {
if !validId(r.GetProjectId(), scope.Project.Prefix()+"_") {
badFields[projectIdFieldName] = "The field is incorrectly formatted."
}
return badFields
@@ -11,6 +11,7 @@ import (
"github.com/hashicorp/watchtower/internal/iam"
"github.com/hashicorp/watchtower/internal/servers/controller/common"
"github.com/hashicorp/watchtower/internal/servers/controller/handlers"
"github.com/hashicorp/watchtower/internal/types/scope"
"google.golang.org/protobuf/types/known/wrapperspb"
)
@@ -109,7 +110,7 @@ func toProto(in *iam.Scope) *pb.Organization {
// * There are no conflicting parameters provided
func validateGetRequest(req *pbs.GetOrganizationRequest) error {
badFields := make(map[string]string)
if !validId(req.GetId(), iam.OrganizationScope.Prefix()+"_") {
if !validId(req.GetId(), scope.Organization.Prefix()+"_") {
badFields["id"] = "Invalid formatted organization id."
}
if len(badFields) > 0 {
@@ -10,6 +10,7 @@ import (
pbs "github.com/hashicorp/watchtower/internal/gen/controller/api/services"
"github.com/hashicorp/watchtower/internal/iam"
"github.com/hashicorp/watchtower/internal/servers/controller/handlers/organizations"
"github.com/hashicorp/watchtower/internal/types/scope"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
@@ -77,7 +78,7 @@ func TestGet(t *testing.T) {
},
{
name: "Get a non existing organization",
req: &pbs.GetOrganizationRequest{Id: iam.OrganizationScope.Prefix() + "_DoesntExis"},
req: &pbs.GetOrganizationRequest{Id: scope.Organization.Prefix() + "_DoesntExis"},
res: nil,
errCode: codes.NotFound,
},
@@ -89,7 +90,7 @@ func TestGet(t *testing.T) {
},
{
name: "space in id",
req: &pbs.GetOrganizationRequest{Id: iam.OrganizationScope.Prefix() + "_1 23456789"},
req: &pbs.GetOrganizationRequest{Id: scope.Organization.Prefix() + "_1 23456789"},
res: nil,
errCode: codes.InvalidArgument,
},
@@ -11,6 +11,7 @@ import (
"github.com/hashicorp/watchtower/internal/iam"
"github.com/hashicorp/watchtower/internal/servers/controller/common"
"github.com/hashicorp/watchtower/internal/servers/controller/handlers"
"github.com/hashicorp/watchtower/internal/types/scope"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/wrapperspb"
@@ -246,7 +247,7 @@ func toProto(in *iam.Scope) *pb.Project {
// * There are no conflicting parameters provided
func validateGetRequest(req *pbs.GetProjectRequest) error {
badFields := validateAncestors(req)
if !validId(req.GetId(), iam.ProjectScope.Prefix()+"_") {
if !validId(req.GetId(), scope.Project.Prefix()+"_") {
badFields["id"] = "Invalid formatted project id."
}
if len(badFields) > 0 {
@@ -275,7 +276,7 @@ func validateCreateRequest(req *pbs.CreateProjectRequest) error {
func validateUpdateRequest(req *pbs.UpdateProjectRequest) error {
badFields := validateAncestors(req)
if !validId(req.GetId(), iam.ProjectScope.Prefix()+"_") {
if !validId(req.GetId(), scope.Project.Prefix()+"_") {
badFields["project_id"] = "Improperly formatted path identifier."
}
if req.GetUpdateMask() == nil {
@@ -309,7 +310,7 @@ func validateUpdateRequest(req *pbs.UpdateProjectRequest) error {
func validateDeleteRequest(req *pbs.DeleteProjectRequest) error {
badFields := validateAncestors(req)
if !validId(req.GetId(), iam.ProjectScope.Prefix()+"_") {
if !validId(req.GetId(), scope.Project.Prefix()+"_") {
badFields["id"] = "Incorrectly formatted project."
}
if len(badFields) > 0 {
@@ -343,7 +344,7 @@ func validateAncestors(r ancestorProvider) map[string]string {
if r.GetOrgId() == "" {
return map[string]string{orgIdFieldName: "Missing organization id."}
}
if !validId(r.GetOrgId(), iam.OrganizationScope.Prefix()+"_") {
if !validId(r.GetOrgId(), scope.Organization.Prefix()+"_") {
return map[string]string{orgIdFieldName: "Improperly formatted identifier."}
}
return map[string]string{}
@@ -13,6 +13,7 @@ import (
pbs "github.com/hashicorp/watchtower/internal/gen/controller/api/services"
"github.com/hashicorp/watchtower/internal/iam"
"github.com/hashicorp/watchtower/internal/servers/controller/handlers/projects"
"github.com/hashicorp/watchtower/internal/types/scope"
"google.golang.org/genproto/protobuf/field_mask"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
@@ -172,14 +173,14 @@ func TestList(t *testing.T) {
},
{
name: "Invalid Org Id",
req: &pbs.ListProjectsRequest{OrgId: iam.OrganizationScope.Prefix() + "_this is invalid"},
req: &pbs.ListProjectsRequest{OrgId: scope.Organization.Prefix() + "_this is invalid"},
res: nil,
errCode: codes.InvalidArgument,
},
// TODO: When an org doesn't exist, we should return a 404 instead of an empty list.
{
name: "Unfound Org",
req: &pbs.ListProjectsRequest{OrgId: iam.OrganizationScope.Prefix() + "_DoesntExis"},
req: &pbs.ListProjectsRequest{OrgId: scope.Organization.Prefix() + "_DoesntExis"},
res: &pbs.ListProjectsResponse{},
errCode: codes.OK,
},
@@ -12,6 +12,7 @@ import (
pbs "github.com/hashicorp/watchtower/internal/gen/controller/api/services"
"github.com/hashicorp/watchtower/internal/iam"
"github.com/hashicorp/watchtower/internal/servers/controller/handlers"
"github.com/hashicorp/watchtower/internal/types/scope"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/wrapperspb"
@@ -347,7 +348,7 @@ func validateAncestors(r ancestorProvider) map[string]string {
if r.GetOrgId() == "" {
return map[string]string{orgIdFieldName: "Missing organization id."}
}
if !validId(r.GetOrgId(), iam.OrganizationScope.Prefix()+"_") {
if !validId(r.GetOrgId(), scope.Organization.Prefix()+"_") {
return map[string]string{orgIdFieldName: "Improperly formatted identifier."}
}
return map[string]string{}
@@ -12,6 +12,7 @@ import (
pbs "github.com/hashicorp/watchtower/internal/gen/controller/api/services"
"github.com/hashicorp/watchtower/internal/iam"
"github.com/hashicorp/watchtower/internal/servers/controller/handlers/roles"
"github.com/hashicorp/watchtower/internal/types/scope"
"google.golang.org/genproto/protobuf/field_mask"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
@@ -155,14 +156,14 @@ func TestList(t *testing.T) {
},
{
name: "Invalid Org Id",
req: &pbs.ListRolesRequest{OrgId: iam.OrganizationScope.Prefix() + "_this is invalid"},
req: &pbs.ListRolesRequest{OrgId: scope.Organization.Prefix() + "_this is invalid"},
res: nil,
errCode: codes.InvalidArgument,
},
// TODO: When an org doesn't exist, we should return a 404 instead of an empty list.
{
name: "Unfound Org",
req: &pbs.ListRolesRequest{OrgId: iam.OrganizationScope.Prefix() + "_DoesntExis"},
req: &pbs.ListRolesRequest{OrgId: scope.Organization.Prefix() + "_DoesntExis"},
res: &pbs.ListRolesResponse{},
errCode: codes.OK,
},
@@ -12,6 +12,7 @@ import (
pbs "github.com/hashicorp/watchtower/internal/gen/controller/api/services"
"github.com/hashicorp/watchtower/internal/iam"
"github.com/hashicorp/watchtower/internal/servers/controller/handlers"
"github.com/hashicorp/watchtower/internal/types/scope"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/wrapperspb"
@@ -316,7 +317,7 @@ func validateAncestors(r ancestorProvider) map[string]string {
if r.GetOrgId() == "" {
return map[string]string{orgIdFieldName: "Missing organization id."}
}
if !validId(r.GetOrgId(), iam.OrganizationScope.Prefix()+"_") {
if !validId(r.GetOrgId(), scope.Organization.Prefix()+"_") {
return map[string]string{orgIdFieldName: "Improperly formatted identifier."}
}
return map[string]string{}
+44
View File
@@ -0,0 +1,44 @@
package action
// Type defines a type for the Actions of Resources
// actions are also stored as a lookup db table named iam_action
type Type int
// not using iota intentionally, since the values are stored in the db as well.
const (
Unknown Type = 0
List Type = 1
Create Type = 2
Update Type = 3
Read Type = 4
Delete Type = 5
Authen Type = 6
All Type = 7
Connect Type = 8
)
var Map = map[string]Type{
"unknown": Unknown,
"list": List,
"create": Create,
"update": Update,
"read": Read,
"delete": Delete,
"authen": Authen,
"*": All,
"connect": Connect,
}
func (a Type) String() string {
return [...]string{
"unknown",
"list",
"create",
"update",
"read",
"delete",
"authen",
"*",
"connect",
}[a]
}
+57
View File
@@ -0,0 +1,57 @@
package action
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestAction(t *testing.T) {
tests := []struct {
action Type
want string
}{
{
action: Unknown,
want: "unknown",
},
{
action: List,
want: "list",
},
{
action: Create,
want: "create",
},
{
action: Update,
want: "update",
},
{
action: Read,
want: "read",
},
{
action: Delete,
want: "delete",
},
{
action: Authen,
want: "authen",
},
{
action: All,
want: "*",
},
{
action: Connect,
want: "connect",
},
}
for _, tt := range tests {
t.Run(tt.want, func(t *testing.T) {
assert.Equalf(t, tt.want, tt.action.String(), "unexpected action string for %s", tt.want)
assert.Equalf(t, tt.action, Map[tt.want], "unexpected action from map for %s", tt.want)
})
}
}
+93
View File
@@ -0,0 +1,93 @@
package resource
// Type defines the types of resources in the system
type Type int
const (
Unknown Type = 0
Scope Type = 1
User Type = 2
StaticGroup Type = 3
Role Type = 4
Organization Type = 5
StaticGroupMember Type = 6
StaticGroupUserMember Type = 7
AssignedRole Type = 8
AssignedUserRole Type = 9
AssignedStaticGroupRole Type = 10
RoleGrant Type = 11
AuthMethod Type = 12
Project Type = 13
All Type = 14
HostCatalog Type = 15
HostSet Type = 16
Host Type = 17
Target Type = 18
)
func (r Type) String() string {
return [...]string{
"unknown",
"scope",
"user",
"static-group",
"role",
"organization",
"static-group-member",
"static-group-user-member",
"assigned-role",
"assigned-user-role",
"assigned-static-group-role",
"role-grant",
"auth-method",
"project",
"*",
"host-catalog",
"host-set",
"host",
"target",
}[r]
}
func StringToResourceType(s string) Type {
switch s {
case Scope.String():
return Scope
case User.String():
return User
case StaticGroup.String():
return StaticGroup
case Role.String():
return Role
case Organization.String():
return Organization
case StaticGroupMember.String():
return StaticGroupMember
case StaticGroupUserMember.String():
return StaticGroupUserMember
case AssignedRole.String():
return AssignedRole
case AssignedUserRole.String():
return AssignedUserRole
case AssignedStaticGroupRole.String():
return AssignedStaticGroupRole
case RoleGrant.String():
return RoleGrant
case AuthMethod.String():
return AuthMethod
case Project.String():
return Project
case All.String():
return All
case HostCatalog.String():
return HostCatalog
case HostSet.String():
return HostSet
case Host.String():
return Host
case Target.String():
return Target
default:
return Unknown
}
}
+98
View File
@@ -0,0 +1,98 @@
package resource
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_Resource(t *testing.T) {
tests := []struct {
typeString string
want Type
}{
{
typeString: "unknown",
want: Unknown,
},
{
typeString: "scope",
want: Scope,
},
{
typeString: "user",
want: User,
},
{
typeString: "static-group",
want: StaticGroup,
},
{
typeString: "role",
want: Role,
},
{
typeString: "organization",
want: Organization,
},
{
typeString: "static-group-member",
want: StaticGroupMember,
},
{
typeString: "static-group-user-member",
want: StaticGroupUserMember,
},
{
typeString: "assigned-role",
want: AssignedRole,
},
{
typeString: "assigned-user-role",
want: AssignedUserRole,
},
{
typeString: "assigned-static-group-role",
want: AssignedStaticGroupRole,
},
{
typeString: "role-grant",
want: RoleGrant,
},
{
typeString: "auth-method",
want: AuthMethod,
},
{
typeString: "project",
want: Project,
},
{
typeString: "*",
want: All,
},
{
typeString: "host-catalog",
want: HostCatalog,
},
{
typeString: "host-set",
want: HostSet,
},
{
typeString: "host",
want: Host,
},
{
typeString: "target",
want: Target,
},
}
for _, tt := range tests {
t.Run(tt.typeString, func(t *testing.T) {
assert.Equalf(t, tt.want, StringToResourceType(tt.typeString), "unexpected type for %s", tt.typeString)
assert.Equalf(t, tt.typeString, tt.want.String(), "unexpected string for %s", tt.typeString)
})
}
}
+37
View File
@@ -0,0 +1,37 @@
package scope
// Type defines the possible types for Scopes
type Type uint32
const (
Unknown Type = 0
Organization Type = 1
Project Type = 2
)
func (s Type) String() string {
return [...]string{
"unknown",
"organization",
"project",
}[s]
}
func (s Type) Prefix() string {
return [...]string{
"unknown",
"o",
"p",
}[s]
}
func StringToScopeType(s string) Type {
switch s {
case Organization.String():
return Organization
case Project.String():
return Project
default:
return Unknown
}
}
+43
View File
@@ -0,0 +1,43 @@
package scope
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_StringToScopeType(t *testing.T) {
tests := []struct {
name string
s string
want Type
wantPrefix string
}{
{
name: "org",
s: "organization",
want: Organization,
wantPrefix: "o",
},
{
name: "proj",
s: "project",
want: Project,
wantPrefix: "p",
},
{
name: "unknown",
s: "org",
want: Unknown,
wantPrefix: "unknown",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
got := StringToScopeType(tt.s)
assert.Equal(tt.want, got)
assert.Equalf(tt.wantPrefix, got.Prefix(), "unexpected prefix for %s", tt.s)
})
}
}