From 8a5197e39e08ede9ec77d29bb30a0d83f0b7abf3 Mon Sep 17 00:00:00 2001 From: Jim Date: Fri, 8 May 2020 11:36:28 -0400 Subject: [PATCH] implement schema, store and repository for IAM scopes of organization and project --- Makefile | 2 +- internal/db/domains_test.go | 4 +- internal/db/id.go | 20 ++ internal/db/id_test.go | 51 ++++ .../postgres/02_domain_types.up.sql | 2 +- .../db/migrations/postgres/04_iam.down.sql | 7 + internal/db/migrations/postgres/04_iam.up.sql | 86 +++++++ internal/db/option.go | 34 ++- internal/db/option_test.go | 12 +- internal/db/read_writer.go | 34 +-- internal/iam/action.go | 53 ++++ internal/iam/action_test.go | 46 ++++ internal/iam/option_test.go | 36 +++ internal/iam/options.go | 58 +++++ internal/iam/repository.go | 128 ++++++++++ internal/iam/repository_scope.go | 56 +++++ internal/iam/repository_scope_test.go | 135 ++++++++++ internal/iam/repository_test.go | 198 +++++++++++++++ internal/iam/resource.go | 100 ++++++++ internal/iam/scope.go | 208 +++++++++++++++ internal/iam/scope_test.go | 215 ++++++++++++++++ internal/iam/store/scanner_test.go | 73 ++++++ internal/iam/store/scanners.go | 33 +++ internal/iam/store/scope.pb.go | 238 ++++++++++++++++++ internal/iam/store/timestamp.pb.go | 165 ++++++++++++ .../storage/iam/store/v1/scope.proto | 36 +++ .../storage/iam/store/v1/timestamp.proto | 15 ++ 27 files changed, 2007 insertions(+), 38 deletions(-) create mode 100644 internal/db/id.go create mode 100644 internal/db/id_test.go create mode 100644 internal/db/migrations/postgres/04_iam.down.sql create mode 100644 internal/db/migrations/postgres/04_iam.up.sql create mode 100644 internal/iam/action.go create mode 100644 internal/iam/action_test.go create mode 100644 internal/iam/option_test.go create mode 100644 internal/iam/options.go create mode 100644 internal/iam/repository.go create mode 100644 internal/iam/repository_scope.go create mode 100644 internal/iam/repository_scope_test.go create mode 100644 internal/iam/repository_test.go create mode 100644 internal/iam/resource.go create mode 100644 internal/iam/scope.go create mode 100644 internal/iam/scope_test.go create mode 100644 internal/iam/store/scanner_test.go create mode 100644 internal/iam/store/scanners.go create mode 100644 internal/iam/store/scope.pb.go create mode 100644 internal/iam/store/timestamp.pb.go create mode 100644 internal/proto/local/controller/storage/iam/store/v1/scope.proto create mode 100644 internal/proto/local/controller/storage/iam/store/v1/timestamp.proto diff --git a/Makefile b/Makefile index 0b1ba78bf..76a7c84be 100644 --- a/Makefile +++ b/Makefile @@ -42,9 +42,9 @@ protobuild: @protoc --proto_path=internal/proto/local --proto_path=internal/proto/third_party --swagger_out=logtostderr=true,disable_default_errors=true,include_package_in_tags=true,fqn_for_swagger_name=true,allow_merge,merge_file_name=controller:internal/gen/. internal/proto/local/controller/api/services/v1/*.proto @protoc-go-inject-tag -input=./internal/oplog/store/oplog.pb.go @protoc-go-inject-tag -input=./internal/oplog/oplog_test/oplog_test.pb.go + @protoc-go-inject-tag -input=./internal/iam/store/scope.pb.go @protoc-go-inject-tag -input=./internal/db/db_test/db_test.pb.go - cleanup: @rm -R ${TMP_DIR} diff --git a/internal/db/domains_test.go b/internal/db/domains_test.go index ea5e5dda4..8e9605b39 100644 --- a/internal/db/domains_test.go +++ b/internal/db/domains_test.go @@ -9,7 +9,7 @@ import ( func TestPublicIdDomain(t *testing.T) { const ( createTable = ` -create table test_table ( +create table if not exists test_table ( id bigint generated always as identity primary key, public_id wt_public_id ); @@ -33,7 +33,7 @@ returning id; failTests := []string{ " ", "bar", - "0000000001000000000200000000031", + "00000009", } for _, tt := range failTests { value := tt diff --git a/internal/db/id.go b/internal/db/id.go new file mode 100644 index 000000000..a7258f4cf --- /dev/null +++ b/internal/db/id.go @@ -0,0 +1,20 @@ +package db + +import ( + "errors" + "fmt" + + "github.com/hashicorp/vault/sdk/helper/base62" +) + +// NewPublicId creates a new public id with the prefix +func NewPublicId(prefix string) (string, error) { + if prefix == "" { + return "", errors.New("error no prefix for new public id") + } + publicId, err := base62.Random(10) + if err != nil { + return "", fmt.Errorf("unable to generate public id: %w", err) + } + return fmt.Sprintf("%s_%s", prefix, publicId), nil +} diff --git a/internal/db/id_test.go b/internal/db/id_test.go new file mode 100644 index 000000000..60bcace0f --- /dev/null +++ b/internal/db/id_test.go @@ -0,0 +1,51 @@ +package db + +import ( + "strings" + "testing" +) + +func TestNewPublicId(t *testing.T) { + type args struct { + prefix string + } + tests := []struct { + name string + args args + wantErr bool + wantLen int + }{ + { + name: "valid", + args: args{ + prefix: "id", + }, + wantErr: false, + wantLen: 10 + len("id_"), + }, + { + name: "bad-prefix", + args: args{ + prefix: "", + }, + wantErr: true, + wantLen: 0, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewPublicId(tt.args.prefix) + if (err != nil) != tt.wantErr { + t.Errorf("NewPublicId() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && !strings.HasPrefix(got, tt.args.prefix+"_") { + t.Errorf("NewPublicId() = %v, wanted it to start with %v", got, tt.args.prefix) + } + if len(got) != tt.wantLen { + t.Errorf("NewPublicId() = %v, with len of %d and wanted len of %v", got, len(got), tt.wantLen) + } + + }) + } +} diff --git a/internal/db/migrations/postgres/02_domain_types.up.sql b/internal/db/migrations/postgres/02_domain_types.up.sql index 7c98e3341..0d6a3d95b 100644 --- a/internal/db/migrations/postgres/02_domain_types.up.sql +++ b/internal/db/migrations/postgres/02_domain_types.up.sql @@ -2,7 +2,7 @@ begin; create domain wt_public_id as text check( - length(trim(value)) > 31 + length(trim(value)) > 10 ); comment on domain wt_public_id is 'Random ID generated with github.com/hashicorp/vault/sdk/helper/base62'; diff --git a/internal/db/migrations/postgres/04_iam.down.sql b/internal/db/migrations/postgres/04_iam.down.sql new file mode 100644 index 000000000..12cabee1b --- /dev/null +++ b/internal/db/migrations/postgres/04_iam.down.sql @@ -0,0 +1,7 @@ +BEGIN; + +drop table if exists iam_scope CASCADE; +drop trigger if exists iam_scope_insert; +drop function if exists iam_sub_scopes_func; + +COMMIT; \ No newline at end of file diff --git a/internal/db/migrations/postgres/04_iam.up.sql b/internal/db/migrations/postgres/04_iam.up.sql new file mode 100644 index 000000000..a7fc1508c --- /dev/null +++ b/internal/db/migrations/postgres/04_iam.up.sql @@ -0,0 +1,86 @@ +BEGIN; + +CREATE TABLE if not exists iam_scope_type_enm ( + string text NOT NULL primary key CHECK(string IN ('unknown', 'organization', 'project')) +); +INSERT INTO iam_scope_type_enm (string) +values + ('unknown'), + ('organization'), + ('project'); + + +CREATE TABLE if not exists iam_scope ( + public_id wt_public_id primary key, + create_time timestamp with time zone default current_timestamp, + update_time timestamp with time zone default current_timestamp, + name text, + type text NOT NULL REFERENCES iam_scope_type_enm(string) CHECK( + ( + type = 'organization' + and parent_id = NULL + ) + or ( + type = 'project' + and parent_id IS NOT NULL + ) + ), + description text, + parent_id text REFERENCES iam_scope(public_id) ON DELETE CASCADE ON UPDATE CASCADE + ); +create table if not exists iam_scope_organization ( + scope_id wt_public_id NOT NULL UNIQUE REFERENCES iam_scope(public_id) ON DELETE CASCADE ON UPDATE CASCADE, + name text UNIQUE, + primary key(scope_id) + ); +create table if not exists iam_scope_project ( + scope_id wt_public_id NOT NULL REFERENCES iam_scope(public_id) ON DELETE CASCADE ON UPDATE CASCADE, + parent_id wt_public_id NOT NULL REFERENCES iam_scope_organization(scope_id) ON DELETE CASCADE ON UPDATE CASCADE, + name text, + unique(parent_id, name), + primary key(scope_id, parent_id) + ); + + +CREATE + OR REPLACE FUNCTION iam_sub_scopes_func() RETURNS TRIGGER +SET SCHEMA + 'public' LANGUAGE plpgsql AS $$ DECLARE parent_type INT; +BEGIN IF new.type = 'organization' THEN +insert into iam_scope_organization (scope_id, name) +values + (new.public_id, new.name); +return NEW; +END IF; +IF new.type = 'project' THEN +insert into iam_scope_project (scope_id, parent_id, name) +values + (new.public_id, new.parent_id, new.name); +return NEW; +END IF; +RAISE EXCEPTION 'unknown scope type'; +END; +$$; + + +CREATE TRIGGER iam_scope_insert +AFTER +insert ON iam_scope FOR EACH ROW EXECUTE PROCEDURE iam_sub_scopes_func(); + + +CREATE + OR REPLACE FUNCTION iam_immutable_scope_type_func() RETURNS TRIGGER +SET SCHEMA + 'public' LANGUAGE plpgsql AS $$ DECLARE parent_type INT; +BEGIN IF new.type != old.type THEN +RAISE EXCEPTION 'scope type cannot be updated'; +END IF; +return NEW; +END; +$$; + +CREATE TRIGGER iam_scope_update +BEFORE +update ON iam_scope FOR EACH ROW EXECUTE PROCEDURE iam_immutable_scope_type_func(); + +COMMIT; \ No newline at end of file diff --git a/internal/db/option.go b/internal/db/option.go index 31176f577..fd9c9622e 100644 --- a/internal/db/option.go +++ b/internal/db/option.go @@ -5,8 +5,8 @@ import ( "github.com/hashicorp/watchtower/internal/oplog" ) -// getOpts - iterate the inbound Options and return a struct -func getOpts(opt ...Option) options { +// GetOpts - iterate the inbound Options and return a struct +func GetOpts(opt ...Option) Options { opts := getDefaultOptions() for _, o := range opt { o(&opts) @@ -15,14 +15,16 @@ func getOpts(opt ...Option) options { } // Option - how Options are passed as arguments -type Option func(*options) +type Option func(*Options) -// options = how options are represented -type options struct { +// Options = how Options are represented +type Options struct { withOplog bool oplogOpts oplogOpts withDebug bool withLookup bool + // WithFieldMaskPaths must be accessible from other packages + WithFieldMaskPaths []string } type oplogOpts struct { @@ -30,35 +32,36 @@ type oplogOpts struct { metadata oplog.Metadata } -func getDefaultOptions() options { - return options{ +func getDefaultOptions() Options { + return Options{ withOplog: false, oplogOpts: oplogOpts{ wrapper: nil, metadata: oplog.Metadata{}, }, - withDebug: false, - withLookup: false, + withDebug: false, + withLookup: false, + WithFieldMaskPaths: []string{}, } } // WithLookup enables a lookup func WithLookup(enable bool) Option { - return func(o *options) { + return func(o *Options) { o.withLookup = enable } } // WithDebug enables debug func WithDebug(enable bool) Option { - return func(o *options) { + return func(o *Options) { o.withDebug = enable } } // WithOplog provides an option to write an oplog entry func WithOplog(wrapper wrapping.Wrapper, md oplog.Metadata) Option { - return func(o *options) { + return func(o *Options) { o.withOplog = true o.oplogOpts = oplogOpts{ wrapper: wrapper, @@ -66,3 +69,10 @@ func WithOplog(wrapper wrapping.Wrapper, md oplog.Metadata) Option { } } } + +// WithOplog provides an option to provide field mask paths +func WithFieldMaskPaths(paths []string) Option { + return func(o *Options) { + o.WithFieldMaskPaths = paths + } +} diff --git a/internal/db/option_test.go b/internal/db/option_test.go index f714edc1a..03863fc05 100644 --- a/internal/db/option_test.go +++ b/internal/db/option_test.go @@ -14,7 +14,7 @@ func Test_getOpts(t *testing.T) { t.Parallel() t.Run("WithOplog", func(t *testing.T) { // test default of false - opts := getOpts() + opts := GetOpts() testOpts := getDefaultOptions() testOpts.withOplog = false assert.True(reflect.DeepEqual(opts, testOpts)) @@ -26,7 +26,7 @@ func Test_getOpts(t *testing.T) { "project": []string{"central-info-systems", "local-info-systems"}, } // try setting to true - opts = getOpts(WithOplog(wrapper, md)) + opts = GetOpts(WithOplog(wrapper, md)) testOpts = getDefaultOptions() testOpts.withOplog = true testOpts.oplogOpts = oplogOpts{ @@ -37,13 +37,13 @@ func Test_getOpts(t *testing.T) { }) t.Run("WithLookup", func(t *testing.T) { // test default of true - opts := getOpts() + opts := GetOpts() testOpts := getDefaultOptions() testOpts.withLookup = false assert.True(reflect.DeepEqual(opts, testOpts)) // try setting to false - opts = getOpts(WithLookup(true)) + opts = GetOpts(WithLookup(true)) testOpts = getDefaultOptions() testOpts.withLookup = true assert.True(reflect.DeepEqual(opts, testOpts)) @@ -51,13 +51,13 @@ func Test_getOpts(t *testing.T) { }) t.Run("WithDebug", func(t *testing.T) { // test default of false - opts := getOpts() + opts := GetOpts() testOpts := getDefaultOptions() testOpts.withDebug = false assert.True(reflect.DeepEqual(opts, testOpts)) // try setting to true - opts = getOpts(WithDebug(true)) + opts = GetOpts(WithDebug(true)) testOpts = getDefaultOptions() testOpts.withDebug = true assert.True(reflect.DeepEqual(opts, testOpts)) diff --git a/internal/db/read_writer.go b/internal/db/read_writer.go index 3d6e862e7..ac4f326d2 100644 --- a/internal/db/read_writer.go +++ b/internal/db/read_writer.go @@ -63,6 +63,10 @@ type Writer interface { DB() (*sql.DB, error) } +const ( + StdRetryCnt = 20 +) + // RetryInfo provides information on the retries of a transaction type RetryInfo struct { Retries int @@ -93,7 +97,7 @@ const ( // VetForWriter provides an interface that Create and Update can use to vet the resource before sending it to the db type VetForWriter interface { - VetForWrite(ctx context.Context, r Reader, opType OpType) error + VetForWrite(ctx context.Context, r Reader, opType OpType, opt ...Option) error } // GormReadWriter uses a gorm DB connection for read/write @@ -118,15 +122,10 @@ func (rw *GormReadWriter) ScanRows(rows *sql.Rows, result interface{}) error { return rw.Tx.ScanRows(rows, result) } -// CreateConstraint will create a db constraint if it doesn't already exist -func (w *GormReadWriter) CreateConstraint(tableName string, constraintName string, constraint string) error { - return w.Tx.Exec("create_constraint_if_not_exists(?, ?, ?)", tableName, constraintName, constraint).Error -} - var ErrNotResourceWithId = errors.New("not a resource with an id") func (rw *GormReadWriter) lookupAfterWrite(ctx context.Context, i interface{}, opt ...Option) error { - opts := getOpts(opt...) + opts := GetOpts(opt...) withLookup := opts.withLookup if !withLookup { @@ -146,7 +145,7 @@ func (rw *GormReadWriter) Create(ctx context.Context, i interface{}, opt ...Opti if rw.Tx == nil { return errors.New("create tx is nil") } - opts := getOpts(opt...) + opts := GetOpts(opt...) withOplog := opts.withOplog withDebug := opts.withDebug if withDebug { @@ -158,7 +157,7 @@ func (rw *GormReadWriter) Create(ctx context.Context, i interface{}, opt ...Opti } if vetter, ok := i.(VetForWriter); ok { if err := vetter.VetForWrite(ctx, rw, CreateOp); err != nil { - return fmt.Errorf("error on Create %w", err) + return fmt.Errorf("error on create %w", err) } } if err := rw.Tx.Create(i).Error; err != nil { @@ -181,7 +180,7 @@ func (rw *GormReadWriter) Update(ctx context.Context, i interface{}, fieldMaskPa if rw.Tx == nil { return errors.New("update tx is nil") } - opts := getOpts(opt...) + opts := GetOpts(opt...) withDebug := opts.withDebug withOplog := opts.withOplog if withDebug { @@ -192,8 +191,8 @@ func (rw *GormReadWriter) Update(ctx context.Context, i interface{}, fieldMaskPa return errors.New("update interface is nil") } if vetter, ok := i.(VetForWriter); ok { - if err := vetter.VetForWrite(ctx, rw, UpdateOp); err != nil { - return fmt.Errorf("error on Create %w", err) + if err := vetter.VetForWrite(ctx, rw, UpdateOp, WithFieldMaskPaths(fieldMaskPaths)); err != nil { + return fmt.Errorf("error on update %w", err) } } if len(fieldMaskPaths) == 0 { @@ -246,6 +245,9 @@ func (rw *GormReadWriter) Update(ctx context.Context, i interface{}, fieldMaskPa return err } } + // we need to force a lookupAfterWrite so the resource returned is correctly initialized + // from the db + opt = append(opt, WithLookup(true)) if err := rw.lookupAfterWrite(ctx, i, opt...); err != nil { return fmt.Errorf("lookup error after update: %w", err) } @@ -260,7 +262,7 @@ func (rw *GormReadWriter) Delete(ctx context.Context, i interface{}, opt ...Opti if i == nil { return errors.New("delete interface is nil") } - opts := getOpts(opt...) + opts := GetOpts(opt...) withDebug := opts.withDebug withOplog := opts.withOplog if withDebug { @@ -278,7 +280,7 @@ func (rw *GormReadWriter) Delete(ctx context.Context, i interface{}, opt ...Opti return nil } -func (rw *GormReadWriter) addOplog(ctx context.Context, opType OpType, opts options, i interface{}) error { +func (rw *GormReadWriter) addOplog(ctx context.Context, opType OpType, opts Options, i interface{}) error { oplogArgs := opts.oplogOpts if oplogArgs.wrapper == nil { return errors.New("error wrapper is nil for WithWrapper") @@ -386,7 +388,7 @@ func (rw *GormReadWriter) LookupByName(ctx context.Context, resource ResourceNam if rw.Tx == nil { return errors.New("error tx nil for lookup by name") } - opts := getOpts(opt...) + opts := GetOpts(opt...) withDebug := opts.withDebug if withDebug { rw.Tx.LogMode(true) @@ -406,7 +408,7 @@ func (rw *GormReadWriter) LookupByPublicId(ctx context.Context, resource Resourc if rw.Tx == nil { return errors.New("error tx nil for lookup by public id") } - opts := getOpts(opt...) + opts := GetOpts(opt...) withDebug := opts.withDebug if withDebug { rw.Tx.LogMode(true) diff --git a/internal/iam/action.go b/internal/iam/action.go new file mode 100644 index 000000000..7bb06ca58 --- /dev/null +++ b/internal/iam/action.go @@ -0,0 +1,53 @@ +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 +) + +func (a Action) String() string { + return [...]string{ + "unknown", + "list", + "create", + "update", + "read", + "delete", + "authen", + "all", + "connect", + }[a] +} + +// 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, + } +} + +// 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, + } +} diff --git a/internal/iam/action_test.go b/internal/iam/action_test.go new file mode 100644 index 000000000..e53ffe15f --- /dev/null +++ b/internal/iam/action_test.go @@ -0,0 +1,46 @@ +package iam + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// Test_CrudActions provides unit tests for CrudActions() +func Test_CrudActions(t *testing.T) { + t.Parallel() + assert := assert.New(t) + t.Run("valid", func(t *testing.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) + }) + t.Run("invalid", func(t *testing.T) { + a := CrudlActions() + action, ok := a["invalid"] + assert.Equal(ok, false) + assert.Equal(action, ActionUnknown) + }) +} + +// Test_CrudlActions provides unit tests for CrudlActions() +func Test_CrudlActions(t *testing.T) { + t.Parallel() + assert := assert.New(t) + t.Run("valid", func(t *testing.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) + }) + t.Run("invalid", func(t *testing.T) { + a := CrudlActions() + action, ok := a["invalid"] + assert.Equal(ok, false) + assert.Equal(action, ActionUnknown) + }) +} diff --git a/internal/iam/option_test.go b/internal/iam/option_test.go new file mode 100644 index 000000000..3304fd18a --- /dev/null +++ b/internal/iam/option_test.go @@ -0,0 +1,36 @@ +package iam + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/assert" +) + +// Test_GetOpts provides unit tests for GetOpts and all the options +func Test_GetOpts(t *testing.T) { + t.Parallel() + assert := assert.New(t) + t.Run("WithName", func(t *testing.T) { + opts := getOpts(WithName("test")) + testOpts := getDefaultOptions() + testOpts.withName = "test" + assert.True(reflect.DeepEqual(opts, testOpts)) + }) + t.Run("withScope", func(t *testing.T) { + s, err := NewOrganization() + assert.Nil(err) + assert.True(s.Scope != nil) + + opts := getOpts(withScope(s)) + testOpts := getDefaultOptions() + testOpts.withScope = s + assert.True(reflect.DeepEqual(opts, testOpts)) + }) + t.Run("WithDescription", func(t *testing.T) { + opts := getOpts(WithDescription("test desc")) + testOpts := getDefaultOptions() + testOpts.withDescription = "test desc" + assert.True(reflect.DeepEqual(opts, testOpts)) + }) +} diff --git a/internal/iam/options.go b/internal/iam/options.go new file mode 100644 index 000000000..0cd2600f0 --- /dev/null +++ b/internal/iam/options.go @@ -0,0 +1,58 @@ +package iam + +// getOpts - iterate the inbound Options and return a struct +func getOpts(opt ...Option) options { + opts := getDefaultOptions() + for _, o := range opt { + o(&opts) + } + return opts +} + +// Option - how Options are passed as arguments +type Option func(*options) + +// options = how options are represented +type options struct { + withPublicId string + withName string + withScope *Scope + withDescription string +} + +func getDefaultOptions() options { + return options{ + withPublicId: "", + withScope: nil, + withDescription: "", + withName: "", + } +} + +// WithPublicId provides an optional public id +func WithPublicId(id string) Option { + return func(o *options) { + o.withPublicId = id + } +} + +// WithDescription provides an optional description +func WithDescription(desc string) Option { + return func(o *options) { + o.withDescription = desc + } +} + +// withScope provides an optional scope and used within the package +func withScope(s *Scope) Option { + return func(o *options) { + o.withScope = s + } +} + +// WithName provides an option to search by a friendly name +func WithName(name string) Option { + return func(o *options) { + o.withName = name + } +} diff --git a/internal/iam/repository.go b/internal/iam/repository.go new file mode 100644 index 000000000..d46bed2dc --- /dev/null +++ b/internal/iam/repository.go @@ -0,0 +1,128 @@ +package iam + +import ( + "context" + "errors" + "fmt" + "strconv" + + wrapping "github.com/hashicorp/go-kms-wrapping" + "github.com/hashicorp/watchtower/internal/db" + "github.com/hashicorp/watchtower/internal/oplog" +) + +// Repository is the iam database repository +type Repository struct { + reader db.Reader + writer db.Writer + wrapper wrapping.Wrapper +} + +// NewRepository creates a new iam Repository +func NewRepository(r db.Reader, w db.Writer, wrapper wrapping.Wrapper) (*Repository, error) { + if r == nil { + return nil, errors.New("error creating db repository with nil reader") + } + if w == nil { + return nil, errors.New("error creating db repository with nil writer") + } + if wrapper == nil { + return nil, errors.New("error creating db repository with nil wrapper") + } + return &Repository{ + reader: r, + writer: w, + wrapper: wrapper, + }, nil +} + +// Create will create a new iam resource in the db repository with an oplog entry +func (r *Repository) create(ctx context.Context, resource Resource, opt ...Option) (Resource, error) { + if resource == nil { + return nil, errors.New("error creating resource that is nil") + } + resourceCloner, ok := resource.(ClonableResource) + if !ok { + return nil, errors.New("error resource is not clonable for create") + } + metadata, err := r.stdMetadata(ctx, resource) + if err != nil { + return nil, fmt.Errorf("error getting metadata for create: %w", err) + } + metadata["op-type"] = []string{strconv.Itoa(int(oplog.OpType_OP_TYPE_CREATE))} + + var returnedResource Resource + _, err = r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(w db.Writer) error { + returnedResource = resourceCloner.Clone() + return w.Create( + ctx, + returnedResource, + db.WithOplog(r.wrapper, metadata), + ) + }, + ) + return returnedResource, err +} + +// Update will update an iam resource in the db repository with an oplog entry +func (r *Repository) update(ctx context.Context, resource Resource, fieldMaskPaths []string, opt ...Option) (Resource, error) { + if resource == nil { + return nil, errors.New("error updating resource that is nil") + } + resourceCloner, ok := resource.(ClonableResource) + if !ok { + return nil, errors.New("error resource is not clonable for update") + } + metadata, err := r.stdMetadata(ctx, resource) + if err != nil { + return nil, fmt.Errorf("error getting metadata for update: %w", err) + } + metadata["op-type"] = []string{strconv.Itoa(int(oplog.OpType_OP_TYPE_UPDATE))} + + var returnedResource Resource + _, err = r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(w db.Writer) error { + returnedResource = resourceCloner.Clone() + return w.Update( + ctx, + returnedResource, + fieldMaskPaths, + db.WithOplog(r.wrapper, metadata), + ) + }, + ) + return returnedResource, err +} + +func (r *Repository) stdMetadata(ctx context.Context, resource Resource) (oplog.Metadata, error) { + if s, ok := resource.(*Scope); ok { + if s.Type == OrganizationScope.String() { + return oplog.Metadata{ + "resource-public-id": []string{resource.GetPublicId()}, + "scope-id": []string{s.PublicId}, + "scope-type": []string{s.Type}, + "resource-type": []string{resource.ResourceType().String()}, + }, nil + } + } + scope, err := resource.GetScope(ctx, r.reader) + if err != nil { + return nil, fmt.Errorf("unable to get scope for standard metadata: %w", err) + } + if scope == nil { + return nil, errors.New("scope was nil for standard metadata") + } + return oplog.Metadata{ + "resource-public-id": []string{resource.GetPublicId()}, + "scope-id": []string{scope.PublicId}, + "scope-type": []string{scope.Type}, + "resource-type": []string{resource.ResourceType().String()}, + }, nil +} diff --git a/internal/iam/repository_scope.go b/internal/iam/repository_scope.go new file mode 100644 index 000000000..753a6818b --- /dev/null +++ b/internal/iam/repository_scope.go @@ -0,0 +1,56 @@ +package iam + +import ( + "context" + "errors" + "fmt" +) + +// CreateScope will create a scope in the repository and return the written scope +func (r *Repository) CreateScope(ctx context.Context, scope *Scope, opt ...Option) (*Scope, error) { + if scope == nil { + return nil, errors.New("error scope is nil for create") + } + resource, err := r.create(ctx, scope) + if err != nil { + return nil, fmt.Errorf("failed to create scope: %w", err) + } + return resource.(*Scope), err +} + +// UpdateScope will update a scope in the repository and return the written scope +func (r *Repository) UpdateScope(ctx context.Context, scope *Scope, fieldMaskPaths []string, opt ...Option) (*Scope, error) { + if scope == nil { + return nil, errors.New("error scope is nil for update") + } + resource, err := r.update(ctx, scope, fieldMaskPaths) + if err != nil { + return nil, fmt.Errorf("failed to update scope: %w", err) + } + return resource.(*Scope), err +} + +// LookupScope will look up a scope in the respository +func (r *Repository) LookupScope(ctx context.Context, opt ...Option) (*Scope, error) { + opts := getOpts(opt...) + withPublicId := opts.withPublicId + withName := opts.withName + + if withPublicId != "" { + scope := allocScope() + scope.PublicId = withPublicId + if err := r.reader.LookupByPublicId(ctx, &scope); err != nil { + return nil, err + } + return &scope, nil + } + if withName != "" { + scope := allocScope() + scope.Name = withName + if err := r.reader.LookupByName(ctx, &scope); err != nil { + return nil, err + } + return &scope, nil + } + return nil, errors.New("you must look up scopes by id or name") +} diff --git a/internal/iam/repository_scope_test.go b/internal/iam/repository_scope_test.go new file mode 100644 index 000000000..31c13fcc0 --- /dev/null +++ b/internal/iam/repository_scope_test.go @@ -0,0 +1,135 @@ +package iam + +import ( + "context" + "testing" + + "github.com/hashicorp/go-uuid" + "github.com/hashicorp/watchtower/internal/db" + "github.com/hashicorp/watchtower/internal/oplog" + "github.com/hashicorp/watchtower/internal/oplog/store" + "github.com/stretchr/testify/assert" +) + +func Test_Repository_CreateScope(t *testing.T) { + t.Parallel() + cleanup, conn := db.TestSetup(t, "../db/migrations/postgres") + defer cleanup() + assert := assert.New(t) + defer conn.Close() + + t.Run("valid-scope", func(t *testing.T) { + rw := &db.GormReadWriter{Tx: conn} + wrapper := db.TestWrapper(t) + repo, err := NewRepository(rw, rw, wrapper) + id, err := uuid.GenerateUUID() + assert.Nil(err) + + s, err := NewOrganization(WithName("fname-" + id)) + s, err = repo.CreateScope(context.Background(), s) + assert.Nil(err) + assert.True(s != nil) + assert.True(s.GetPublicId() != "") + assert.Equal(s.GetName(), "fname-"+id) + + foundScope, err := repo.LookupScope(context.Background(), WithPublicId(s.PublicId)) + assert.Nil(err) + assert.Equal(foundScope.GetPublicId(), s.GetPublicId()) + + foundScope, err = repo.LookupScope(context.Background(), WithName("fname-"+id)) + assert.Nil(err) + assert.Equal(foundScope.GetPublicId(), s.GetPublicId()) + + var metadata store.Metadata + err = conn.Where("key = ? and value = ?", "resource-public-id", s.PublicId).First(&metadata).Error + assert.Nil(err) + + var foundEntry oplog.Entry + err = conn.Where("id = ?", metadata.EntryId).First(&foundEntry).Error + assert.Nil(err) + }) +} + +func Test_Repository_UpdateScope(t *testing.T) { + t.Parallel() + cleanup, conn := db.TestSetup(t, "../db/migrations/postgres") + defer cleanup() + assert := assert.New(t) + defer conn.Close() + + t.Run("valid-scope", func(t *testing.T) { + rw := &db.GormReadWriter{Tx: conn} + wrapper := db.TestWrapper(t) + repo, err := NewRepository(rw, rw, wrapper) + id, err := uuid.GenerateUUID() + assert.Nil(err) + + s, err := NewOrganization(WithName("fname-" + id)) + s, err = repo.CreateScope(context.Background(), s) + assert.Nil(err) + assert.True(s != nil) + assert.True(s.GetPublicId() != "") + assert.Equal(s.GetName(), "fname-"+id) + + foundScope, err := repo.LookupScope(context.Background(), WithPublicId(s.PublicId)) + assert.Nil(err) + assert.Equal(foundScope.GetPublicId(), s.GetPublicId()) + + foundScope, err = repo.LookupScope(context.Background(), WithName("fname-"+id)) + assert.Nil(err) + assert.Equal(foundScope.GetPublicId(), s.GetPublicId()) + + var metadata store.Metadata + err = conn.Where("key = ? and value = ?", "resource-public-id", s.PublicId).First(&metadata).Error + assert.Nil(err) + + var foundEntry oplog.Entry + err = conn.Where("id = ?", metadata.EntryId).First(&foundEntry).Error + assert.Nil(err) + + s.Name = "fname-" + id + s.Description = "desc-id" // not in the field mask paths + s, err = repo.UpdateScope(context.Background(), s, []string{"Name"}) + assert.Nil(err) + assert.True(s != nil) + assert.Equal(s.GetName(), "fname-"+id) + assert.Equal(foundScope.GetDescription(), "") // should be "" after update in db + + foundScope, err = repo.LookupScope(context.Background(), WithName("fname-"+id)) + assert.Nil(err) + assert.Equal(foundScope.GetPublicId(), s.GetPublicId()) + assert.Equal(foundScope.GetDescription(), "") + + err = conn.Where("key = ? and value = ?", "resource-public-id", s.PublicId).First(&metadata).Error + assert.Nil(err) + + err = conn.Where("id = ?", metadata.EntryId).First(&foundEntry).Error + assert.Nil(err) + }) + t.Run("bad-parent-scope", func(t *testing.T) { + rw := &db.GormReadWriter{Tx: conn} + wrapper := db.TestWrapper(t) + repo, err := NewRepository(rw, rw, wrapper) + id, err := uuid.GenerateUUID() + assert.Nil(err) + + s, err := NewOrganization(WithName("fname-" + id)) + assert.Nil(err) + s, err = repo.CreateScope(context.Background(), s) + assert.Nil(err) + assert.True(s != nil) + assert.True(s.GetPublicId() != "") + assert.Equal(s.GetName(), "fname-"+id) + + project, err := NewProject(s.PublicId) + assert.Nil(err) + project, err = repo.CreateScope(context.Background(), project) + assert.Nil(err) + assert.True(project != nil) + + project.ParentId = project.PublicId + project, err = repo.UpdateScope(context.Background(), project, []string{"ParentId"}) + assert.True(err != nil) + assert.Equal("failed to update scope: error on update you cannot change a scope's parent", err.Error()) + }) +} diff --git a/internal/iam/repository_test.go b/internal/iam/repository_test.go new file mode 100644 index 000000000..100189bfc --- /dev/null +++ b/internal/iam/repository_test.go @@ -0,0 +1,198 @@ +package iam + +import ( + "context" + "reflect" + "testing" + + wrapping "github.com/hashicorp/go-kms-wrapping" + "github.com/hashicorp/go-uuid" + "github.com/hashicorp/watchtower/internal/db" + "github.com/hashicorp/watchtower/internal/oplog" + "github.com/hashicorp/watchtower/internal/oplog/store" + "github.com/stretchr/testify/assert" +) + +func TestNewRepository(t *testing.T) { + t.Parallel() + cleanup, conn := db.TestSetup(t, "../db/migrations/postgres") + defer cleanup() + assert := assert.New(t) + defer conn.Close() + + rw := &db.GormReadWriter{Tx: conn} + wrapper := db.TestWrapper(t) + type args struct { + r db.Reader + w db.Writer + wrapper wrapping.Wrapper + } + tests := []struct { + name string + args args + want *Repository + wantErr bool + wantErrString string + }{ + { + name: "valid", + args: args{ + r: rw, + w: rw, + wrapper: wrapper, + }, + want: &Repository{ + reader: rw, + writer: rw, + wrapper: wrapper, + }, + wantErr: false, + }, + { + name: "nil-wrapper", + args: args{ + r: rw, + w: rw, + wrapper: nil, + }, + want: nil, + wantErr: true, + wantErrString: "error creating db repository with nil wrapper", + }, + { + name: "nil-writer", + args: args{ + r: rw, + w: nil, + wrapper: wrapper, + }, + want: nil, + wantErr: true, + wantErrString: "error creating db repository with nil writer", + }, + { + name: "nil-reader", + args: args{ + r: nil, + w: rw, + wrapper: wrapper, + }, + want: nil, + wantErr: true, + wantErrString: "error creating db repository with nil reader", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewRepository(tt.args.r, tt.args.w, tt.args.wrapper) + if (err != nil) != tt.wantErr { + t.Errorf("NewRepository() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewRepository() = %v, want %v", got, tt.want) + } + if err != nil { + assert.Equal(err.Error(), tt.wantErrString) + } + }) + } +} +func Test_Repository_create(t *testing.T) { + t.Parallel() + cleanup, conn := db.TestSetup(t, "../db/migrations/postgres") + defer cleanup() + assert := assert.New(t) + defer conn.Close() + + t.Run("valid-scope", func(t *testing.T) { + rw := &db.GormReadWriter{Tx: conn} + wrapper := db.TestWrapper(t) + repo, err := NewRepository(rw, rw, wrapper) + id, err := uuid.GenerateUUID() + assert.Nil(err) + + s, err := NewOrganization(WithName("fname-" + id)) + retScope, err := repo.create(context.Background(), s) + assert.Nil(err) + assert.True(retScope != nil) + assert.True(retScope.GetPublicId() != "") + assert.Equal(retScope.GetName(), "fname-"+id) + + foundScope, err := repo.LookupScope(context.Background(), WithPublicId(s.PublicId)) + assert.Nil(err) + assert.Equal(foundScope.GetPublicId(), retScope.GetPublicId()) + + foundScope.Name = "fname-" + id + foundScope, err = repo.LookupScope(context.Background(), WithName("fname-"+id)) + assert.Nil(err) + assert.Equal(foundScope.GetPublicId(), retScope.GetPublicId()) + + var metadata store.Metadata + err = conn.Where("key = ? and value = ?", "resource-public-id", s.PublicId).First(&metadata).Error + assert.Nil(err) + + var foundEntry oplog.Entry + err = conn.Where("id = ?", metadata.EntryId).First(&foundEntry).Error + assert.Nil(err) + }) + t.Run("nil-resource", func(t *testing.T) { + rw := &db.GormReadWriter{Tx: conn} + wrapper := db.TestWrapper(t) + repo, err := NewRepository(rw, rw, wrapper) + resource, err := repo.create(context.Background(), nil) + assert.True(err != nil) + assert.True(resource == nil) + assert.Equal(err.Error(), "error creating resource that is nil") + }) +} + +func Test_dbRepository_update(t *testing.T) { + t.Parallel() + cleanup, conn := db.TestSetup(t, "../db/migrations/postgres") + defer cleanup() + assert := assert.New(t) + defer conn.Close() + + t.Run("valid-scope", func(t *testing.T) { + rw := &db.GormReadWriter{Tx: conn} + wrapper := db.TestWrapper(t) + repo, err := NewRepository(rw, rw, wrapper) + id, err := uuid.GenerateUUID() + assert.Nil(err) + + s, err := NewOrganization() + retScope, err := repo.create(context.Background(), s) + assert.Nil(err) + assert.True(retScope != nil) + assert.True(retScope.GetPublicId() != "") + assert.Equal(retScope.GetName(), "") + + retScope.(*Scope).Name = "fname-" + id + retScope, err = repo.update(context.Background(), retScope, []string{"Name"}) + assert.Nil(err) + assert.True(retScope != nil) + assert.Equal(retScope.GetName(), "fname-"+id) + + foundScope, err := repo.LookupScope(context.Background(), WithName("fname-"+id)) + assert.Nil(err) + assert.Equal(foundScope.GetPublicId(), retScope.GetPublicId()) + + var metadata store.Metadata + err = conn.Where("key = ? and value = ?", "resource-public-id", s.PublicId).First(&metadata).Error + assert.Nil(err) + + var foundEntry oplog.Entry + err = conn.Where("id = ?", metadata.EntryId).First(&foundEntry).Error + assert.Nil(err) + }) + t.Run("nil-resource", func(t *testing.T) { + rw := &db.GormReadWriter{Tx: conn} + wrapper := db.TestWrapper(t) + repo, err := NewRepository(rw, rw, wrapper) + resource, err := repo.update(context.Background(), nil, nil) + assert.True(err != nil) + assert.True(resource == nil) + assert.Equal(err.Error(), "error updating resource that is nil") + }) +} diff --git a/internal/iam/resource.go b/internal/iam/resource.go new file mode 100644 index 000000000..570fd06d1 --- /dev/null +++ b/internal/iam/resource.go @@ -0,0 +1,100 @@ +package iam + +import ( + "context" + "errors" + "fmt" + + "github.com/hashicorp/watchtower/internal/db" +) + +// Resource declares the shared behavior of IAM Resources +type Resource interface { + // GetPublicId is the resource ID used to access the resource via an API + GetPublicId() string + + // GetName is the optional friendly name used to + // access the resource via an API + GetName() string + + // GetDescription is the optional description of the resource + GetDescription() string + + // GetScope is the Scope that owns the Resource + GetScope(ctx context.Context, r db.Reader) (*Scope, error) + + // Type of Resource (Target, Policy, User, Group, etc) + ResourceType() ResourceType + + // 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] +} + +type ClonableResource interface { + Clone() Resource +} + +// ResourceWithScope defines an interface for Resources that have a scope +type ResourceWithScope interface { + GetPublicId() string + GetScopeId() string +} + +// LookupScope looks up the resource's scope +func LookupScope(ctx context.Context, reader db.Reader, resource ResourceWithScope) (*Scope, error) { + if reader == nil { + return nil, errors.New("error reader is nil for LookupScope") + } + if resource == nil { + return nil, errors.New("error resource is nil for LookupScope") + } + if resource.GetScopeId() == "" { + return nil, errors.New("error scope is unset for LookupScope") + } + var p Scope + if err := reader.LookupWhere(ctx, &p, "public_id = ?", resource.GetScopeId()); err != nil { + return nil, fmt.Errorf("error getting scope for LookupScope: %w", err) + } + return &p, nil +} diff --git a/internal/iam/scope.go b/internal/iam/scope.go new file mode 100644 index 000000000..f071c8f77 --- /dev/null +++ b/internal/iam/scope.go @@ -0,0 +1,208 @@ +package iam + +import ( + "context" + "errors" + "fmt" + + "github.com/hashicorp/watchtower/internal/db" + "github.com/hashicorp/watchtower/internal/iam/store" + "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] +} + +// 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 { + *store.Scope + + // tableName which is used to support overriding the table name in the db + // and making the Scope a ReplayableMessage + tableName string `gorm:"-"` +} + +// ensure that Scope implements the interfaces of: Resource, ClonableResource, and db.VetForWriter +var _ Resource = (*Scope)(nil) +var _ db.VetForWriter = (*Scope)(nil) +var _ ClonableResource = (*Scope)(nil) + +func NewOrganization(opt ...Option) (*Scope, error) { + return newScope(OrganizationScope, opt...) +} + +func NewProject(organizationPublicId string, opt ...Option) (*Scope, error) { + org := allocScope() + org.PublicId = organizationPublicId + opt = append(opt, withScope(&org)) + p, err := newScope(ProjectScope, opt...) + if err != nil { + return nil, fmt.Errorf("error creating new project: %w", err) + } + return p, nil +} + +// newScope creates a new Scope of the specified ScopeType with options: +// WithName specifies the Scope's friendly name. +// WithScope specifies the Scope's parent +func newScope(scopeType ScopeType, opt ...Option) (*Scope, error) { + opts := getOpts(opt...) + withName := opts.withName + withScope := opts.withScope + withDescription := opts.withDescription + + if scopeType == UnknownScope { + return nil, errors.New("error unknown scope type for new scope") + } + if scopeType == ProjectScope { + if withScope == nil { + return nil, errors.New("error project scope must be with a scope") + } + if withScope.PublicId == "" { + return nil, errors.New("error project scope parent id is unset") + } + } + prefix := "p" + if scopeType == OrganizationScope { + prefix = "o" + } + + publicId, err := db.NewPublicId(prefix) + if err != nil { + return nil, fmt.Errorf("error generating public id %w for new scope", err) + } + s := &Scope{ + Scope: &store.Scope{ + PublicId: publicId, + Type: scopeType.String(), + }, + } + if withScope != nil { + if withScope.PublicId == "" { + return nil, errors.New("error assigning scope parent id to a scope with unset id") + } + s.ParentId = withScope.PublicId + } + if withName != "" { + s.Name = withName + } + if withDescription != "" { + s.Description = withDescription + } + return s, nil +} + +func allocScope() Scope { + return Scope{ + Scope: &store.Scope{}, + } +} + +// Clone creates a clone of the Scope +func (s *Scope) Clone() Resource { + cp := proto.Clone(s.Scope) + return &Scope{ + Scope: cp.(*store.Scope), + } +} + +// VetForWrite implements db.VetForWrite() interface for scopes +// 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() { + return errors.New("unknown scope type for scope write") + } + if s.PublicId == "" { + return errors.New("public id is empty string for scope write") + } + if opType == db.UpdateOp { + dbOptions := db.GetOpts(opt...) + for _, path := range dbOptions.WithFieldMaskPaths { + switch path { + case "ParentId": + return errors.New("you cannot change a scope's parent") + case "Type": + return errors.New("you cannot change a scope's type") + } + } + } + if opType == db.CreateOp { + if s.ParentId == "" && s.Type == ProjectScope.String() { + return errors.New("project has no organization") + } + if s.Type == ProjectScope.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() { + return errors.New("project parent scope is not an organization") + } + } + } + return nil +} + +// ResourceType returns the type of scope +func (s *Scope) ResourceType() ResourceType { + switch s.Type { + case OrganizationScope.String(): + return ResourceTypeOrganization + case ProjectScope.String(): + return ResourceTypeProject + default: + return ResourceTypeScope + } +} + +// Actions returns the available actions for Scopes +func (*Scope) Actions() map[string]Action { + return CrudActions() +} + +// GetScope returns the scope for the "scope" if there is one defined +func (s *Scope) GetScope(ctx context.Context, r db.Reader) (*Scope, error) { + if r == nil { + return nil, errors.New("error db is nil for get scope") + } + if s.ParentId == "" { + return nil, nil + } + var p Scope + if err := r.LookupWhere(ctx, &p, "public_id = ?", s.ParentId); err != nil { + return nil, fmt.Errorf("error getting scope %w", err) + } + return &p, nil +} + +// TableName returns the tablename to override the default gorm table name +func (s *Scope) TableName() string { + if s.tableName != "" { + return s.tableName + } + return "iam_scope" +} + +// SetTableName sets the tablename and satisfies the ReplayableMessage interface +func (s *Scope) SetTableName(n string) { + if n != "" { + s.tableName = n + } +} diff --git a/internal/iam/scope_test.go b/internal/iam/scope_test.go new file mode 100644 index 000000000..42e3f5616 --- /dev/null +++ b/internal/iam/scope_test.go @@ -0,0 +1,215 @@ +package iam + +import ( + "context" + "testing" + + "github.com/hashicorp/go-uuid" + "github.com/hashicorp/watchtower/internal/db" + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/proto" +) + +func Test_NewScope(t *testing.T) { + t.Parallel() + cleanup, conn := db.TestSetup(t, "../db/migrations/postgres") + defer cleanup() + assert := assert.New(t) + defer conn.Close() + + t.Run("valid-org-with-project", func(t *testing.T) { + w := db.GormReadWriter{Tx: conn} + s, err := NewOrganization() + assert.Nil(err) + assert.True(s.Scope != nil) + err = w.Create(context.Background(), s) + assert.Nil(err) + assert.True(s.PublicId != "") + + id, err := uuid.GenerateUUID() + assert.Nil(err) + projScope, err := NewProject(s.PublicId, WithDescription(id)) + assert.Nil(err) + assert.True(projScope.Scope != nil) + assert.Equal(projScope.GetParentId(), s.PublicId) + assert.Equal(projScope.GetDescription(), id) + }) + t.Run("unknown-scope", func(t *testing.T) { + s, err := newScope(UnknownScope) + assert.True(err != nil) + assert.True(s == nil) + assert.Equal(err.Error(), "error unknown scope type for new scope") + }) + t.Run("proj-scope-with-no-org", func(t *testing.T) { + s, err := NewProject("") + assert.True(err != nil) + assert.True(s == nil) + assert.Equal(err.Error(), "error creating new project: error project scope parent id is unset") + }) +} +func Test_ScopeCreate(t *testing.T) { + t.Parallel() + cleanup, conn := db.TestSetup(t, "../db/migrations/postgres") + defer cleanup() + assert := assert.New(t) + defer conn.Close() + + t.Run("valid", func(t *testing.T) { + w := db.GormReadWriter{Tx: conn} + s, err := NewOrganization() + assert.Nil(err) + assert.True(s.Scope != nil) + err = w.Create(context.Background(), s) + assert.Nil(err) + assert.True(s.PublicId != "") + }) + t.Run("valid-with-parent", func(t *testing.T) { + w := db.GormReadWriter{Tx: conn} + s, err := NewOrganization() + assert.Nil(err) + assert.True(s.Scope != nil) + err = w.Create(context.Background(), s) + assert.Nil(err) + assert.True(s.PublicId != "") + + id, err := uuid.GenerateUUID() + assert.Nil(err) + + project, err := NewProject(s.PublicId, WithDescription(id)) + assert.Nil(err) + assert.True(project.Scope != nil) + assert.Equal(project.Scope.ParentId, s.PublicId) + assert.Equal(project.GetDescription(), id) + + err = w.Create(context.Background(), project) + assert.Nil(err) + assert.Equal(project.ParentId, s.PublicId) + }) +} + +func Test_ScopeUpdate(t *testing.T) { + t.Parallel() + cleanup, conn := db.TestSetup(t, "../db/migrations/postgres") + defer cleanup() + assert := assert.New(t) + defer conn.Close() + + t.Run("valid", func(t *testing.T) { + w := db.GormReadWriter{Tx: conn} + s, err := NewOrganization() + assert.Nil(err) + assert.True(s.Scope != nil) + err = w.Create(context.Background(), s) + assert.Nil(err) + assert.True(s.PublicId != "") + + id, err := uuid.GenerateUUID() + assert.Nil(err) + s.Name = id + err = w.Update(context.Background(), s, []string{"Name"}) + assert.Nil(err) + }) + t.Run("type-update-not-allowed", func(t *testing.T) { + w := db.GormReadWriter{Tx: conn} + s, err := NewOrganization() + assert.Nil(err) + assert.True(s.Scope != nil) + err = w.Create(context.Background(), s) + assert.Nil(err) + assert.True(s.PublicId != "") + + s.Type = ProjectScope.String() + err = w.Update(context.Background(), s, []string{"Type"}) + assert.True(err != nil) + }) +} +func Test_ScopeGetScope(t *testing.T) { + t.Parallel() + cleanup, conn := db.TestSetup(t, "../db/migrations/postgres") + defer cleanup() + assert := assert.New(t) + defer conn.Close() + t.Run("valid-scope", func(t *testing.T) { + w := db.GormReadWriter{Tx: conn} + s, err := NewOrganization() + assert.Nil(err) + assert.True(s.Scope != nil) + err = w.Create(context.Background(), s) + assert.Nil(err) + assert.True(s.PublicId != "") + + project, err := NewProject(s.PublicId) + assert.Nil(err) + assert.True(project.Scope != nil) + assert.Equal(project.ParentId, s.PublicId) + err = w.Create(context.Background(), project) + assert.Nil(err) + + projectOrg, err := project.GetScope(context.Background(), &w) + assert.Nil(err) + assert.True(projectOrg != nil) + assert.Equal(projectOrg.PublicId, project.ParentId) + }) +} + +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) + + if _, ok := a[ActionList.String()]; ok { + t.Errorf("scopes should not include %s as an action", ActionList.String()) + } +} + +func TestScope_ResourceType(t *testing.T) { + assert := assert.New(t) + o, err := NewOrganization() + assert.Nil(err) + ty := o.ResourceType() + assert.Equal(ty, ResourceTypeOrganization) +} + +func TestScope_Clone(t *testing.T) { + t.Parallel() + cleanup, conn := db.TestSetup(t, "../db/migrations/postgres") + defer cleanup() + assert := assert.New(t) + defer conn.Close() + + t.Run("valid", func(t *testing.T) { + w := db.GormReadWriter{Tx: conn} + s, err := NewOrganization() + assert.Nil(err) + assert.True(s.Scope != nil) + err = w.Create(context.Background(), s) + assert.Nil(err) + assert.True(s.PublicId != "") + + cp := s.Clone() + assert.True(proto.Equal(cp.(*Scope).Scope, s.Scope)) + }) + t.Run("not-equal", func(t *testing.T) { + w := db.GormReadWriter{Tx: conn} + s, err := NewOrganization() + assert.Nil(err) + assert.True(s.Scope != nil) + err = w.Create(context.Background(), s) + assert.Nil(err) + assert.True(s.PublicId != "") + + s2, err := NewOrganization() + assert.Nil(err) + assert.True(s2.Scope != nil) + err = w.Create(context.Background(), s2) + assert.Nil(err) + assert.True(s2.PublicId != "") + + cp := s.Clone() + assert.True(!proto.Equal(cp.(*Scope).Scope, s2.Scope)) + }) +} diff --git a/internal/iam/store/scanner_test.go b/internal/iam/store/scanner_test.go new file mode 100644 index 000000000..3eb4c05cb --- /dev/null +++ b/internal/iam/store/scanner_test.go @@ -0,0 +1,73 @@ +package store + +import ( + "reflect" + "testing" + "time" + + "github.com/golang/protobuf/ptypes/timestamp" + "github.com/stretchr/testify/assert" +) + +func Test_TimestampValue(t *testing.T) { + t.Parallel() + assert := assert.New(t) + t.Run("valid", func(t *testing.T) { + ts := Timestamp{Timestamp: ×tamp.Timestamp{Seconds: 0, Nanos: 0}} + v, err := ts.Value() + assert.Nil(err) + assert.Equal(v, utcDate(1970, 1, 1)) + }) + t.Run("valid nil ts", func(t *testing.T) { + var ts *Timestamp + v, err := ts.Value() + assert.Nil(err) + assert.Equal(v, nil) + }) + t.Run("invalid ts", func(t *testing.T) { + ts := Timestamp{Timestamp: ×tamp.Timestamp{Seconds: maxValidSeconds, Nanos: 0}} + v, err := ts.Value() + assert.True(err != nil) + assert.Equal(err.Error(), "timestamp: seconds:253402300800 after 10000-01-01") + assert.Equal(v, utcDate(10000, 1, 1)) + }) +} + +func Test_TimestampScan(t *testing.T) { + t.Parallel() + assert := assert.New(t) + t.Run("valid", func(t *testing.T) { + v := time.Unix(0, 0) + ts := Timestamp{} + err := ts.Scan(v) + assert.Nil(err) + assert.True(reflect.DeepEqual(ts.Timestamp, ×tamp.Timestamp{Seconds: 0, Nanos: 0})) + }) + t.Run("valid default time", func(t *testing.T) { + var v time.Time + ts := Timestamp{} + err := ts.Scan(v) + assert.Nil(err) + assert.True(reflect.DeepEqual(ts.Timestamp, ×tamp.Timestamp{Seconds: -62135596800, Nanos: 0})) + }) + t.Run("invalid type", func(t *testing.T) { + v := 1 + ts := Timestamp{} + err := ts.Scan(v) + assert.True(err != nil) + assert.Equal(err.Error(), "Not a protobuf Timestamp") + }) + t.Run("invalid time", func(t *testing.T) { + v := time.Unix(maxValidSeconds, 0) + ts := Timestamp{} + err := ts.Scan(v) + assert.True(err != nil) + assert.Equal(err.Error(), "error converting the timestamp: timestamp: seconds:253402300800 after 10000-01-01") + }) +} + +const maxValidSeconds = 253402300800 + +func utcDate(year, month, day int) time.Time { + return time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC) +} diff --git a/internal/iam/store/scanners.go b/internal/iam/store/scanners.go new file mode 100644 index 000000000..f438b1e6e --- /dev/null +++ b/internal/iam/store/scanners.go @@ -0,0 +1,33 @@ +package store + +import ( + "database/sql/driver" + "errors" + "fmt" + "time" + + "github.com/golang/protobuf/ptypes" +) + +// Scan supports Timestamps for oplogs +func (ts *Timestamp) Scan(value interface{}) error { + switch t := value.(type) { + case time.Time: + var err error + ts.Timestamp, err = ptypes.TimestampProto(t) // google proto version + if err != nil { + return fmt.Errorf("error converting the timestamp: %w", err) + } + default: + return errors.New("Not a protobuf Timestamp") + } + return nil +} + +// Value supports Timestamps for oplogs +func (ts *Timestamp) Value() (driver.Value, error) { + if ts == nil { + return nil, nil + } + return ptypes.Timestamp(ts.Timestamp) +} diff --git a/internal/iam/store/scope.pb.go b/internal/iam/store/scope.pb.go new file mode 100644 index 000000000..497ac6359 --- /dev/null +++ b/internal/iam/store/scope.pb.go @@ -0,0 +1,238 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.21.0 +// protoc v3.11.4 +// source: controller/storage/iam/store/v1/scope.proto + +package store + +import ( + proto "github.com/golang/protobuf/proto" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// This is a compile-time assertion that a sufficiently up-to-date version +// of the legacy proto package is being used. +const _ = proto.ProtoPackageIsVersion4 + +type Scope struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // public_id is the used to access the Scope via an API + // @inject_tag: gorm:"primary_key" + PublicId string `protobuf:"bytes,1,opt,name=public_id,json=publicId,proto3" json:"public_id,omitempty" gorm:"primary_key"` + // create_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *Timestamp `protobuf:"bytes,2,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` + // update_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + UpdateTime *Timestamp `protobuf:"bytes,3,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty" gorm:"default:current_timestamp"` + // name is the optional friendly name used to + // access the Scope via an API + // @inject_tag: `gorm:"default:null"` + Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty" gorm:"default:null"` + // Type of scope + Type string `protobuf:"bytes,5,opt,name=type,proto3" json:"type,omitempty"` + // parent_id is the id (primary key in the Scope aggregate) of scope's parent + // @inject_tag: `gorm:"default:0"` + ParentId string `protobuf:"bytes,6,opt,name=parent_id,json=parentId,proto3" json:"parent_id,omitempty" gorm:"default:0"` + // description of scope + // @inject_tag: `gorm:"default:null"` + Description string `protobuf:"bytes,7,opt,name=description,proto3" json:"description,omitempty" gorm:"default:null"` +} + +func (x *Scope) Reset() { + *x = Scope{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_iam_store_v1_scope_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Scope) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Scope) ProtoMessage() {} + +func (x *Scope) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_iam_store_v1_scope_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Scope.ProtoReflect.Descriptor instead. +func (*Scope) Descriptor() ([]byte, []int) { + return file_controller_storage_iam_store_v1_scope_proto_rawDescGZIP(), []int{0} +} + +func (x *Scope) GetPublicId() string { + if x != nil { + return x.PublicId + } + return "" +} + +func (x *Scope) GetCreateTime() *Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *Scope) GetUpdateTime() *Timestamp { + if x != nil { + return x.UpdateTime + } + return nil +} + +func (x *Scope) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Scope) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *Scope) GetParentId() string { + if x != nil { + return x.ParentId + } + return "" +} + +func (x *Scope) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +var File_controller_storage_iam_store_v1_scope_proto protoreflect.FileDescriptor + +var file_controller_storage_iam_store_v1_scope_proto_rawDesc = []byte{ + 0x0a, 0x2b, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x2f, 0x69, 0x61, 0x6d, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x76, + 0x31, 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1f, 0x63, + 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, + 0x65, 0x2e, 0x69, 0x61, 0x6d, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x2f, + 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, + 0x67, 0x65, 0x2f, 0x69, 0x61, 0x6d, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x76, 0x31, 0x2f, + 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, + 0xa5, 0x02, 0x0a, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, + 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, + 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, + 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, + 0x2e, 0x69, 0x61, 0x6d, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, + 0x69, 0x6d, 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, + 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, + 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x69, 0x61, + 0x6d, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, + 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x72, 0x65, + 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x72, + 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, + 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x3a, 0x5a, 0x38, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, + 0x77, 0x61, 0x74, 0x63, 0x68, 0x74, 0x6f, 0x77, 0x65, 0x72, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x2f, 0x69, 0x61, 0x6d, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, + 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_controller_storage_iam_store_v1_scope_proto_rawDescOnce sync.Once + file_controller_storage_iam_store_v1_scope_proto_rawDescData = file_controller_storage_iam_store_v1_scope_proto_rawDesc +) + +func file_controller_storage_iam_store_v1_scope_proto_rawDescGZIP() []byte { + file_controller_storage_iam_store_v1_scope_proto_rawDescOnce.Do(func() { + file_controller_storage_iam_store_v1_scope_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_storage_iam_store_v1_scope_proto_rawDescData) + }) + return file_controller_storage_iam_store_v1_scope_proto_rawDescData +} + +var file_controller_storage_iam_store_v1_scope_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_controller_storage_iam_store_v1_scope_proto_goTypes = []interface{}{ + (*Scope)(nil), // 0: controller.storage.iam.store.v1.Scope + (*Timestamp)(nil), // 1: controller.storage.iam.store.v1.Timestamp +} +var file_controller_storage_iam_store_v1_scope_proto_depIdxs = []int32{ + 1, // 0: controller.storage.iam.store.v1.Scope.create_time:type_name -> controller.storage.iam.store.v1.Timestamp + 1, // 1: controller.storage.iam.store.v1.Scope.update_time:type_name -> controller.storage.iam.store.v1.Timestamp + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_controller_storage_iam_store_v1_scope_proto_init() } +func file_controller_storage_iam_store_v1_scope_proto_init() { + if File_controller_storage_iam_store_v1_scope_proto != nil { + return + } + file_controller_storage_iam_store_v1_timestamp_proto_init() + if !protoimpl.UnsafeEnabled { + file_controller_storage_iam_store_v1_scope_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Scope); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_controller_storage_iam_store_v1_scope_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controller_storage_iam_store_v1_scope_proto_goTypes, + DependencyIndexes: file_controller_storage_iam_store_v1_scope_proto_depIdxs, + MessageInfos: file_controller_storage_iam_store_v1_scope_proto_msgTypes, + }.Build() + File_controller_storage_iam_store_v1_scope_proto = out.File + file_controller_storage_iam_store_v1_scope_proto_rawDesc = nil + file_controller_storage_iam_store_v1_scope_proto_goTypes = nil + file_controller_storage_iam_store_v1_scope_proto_depIdxs = nil +} diff --git a/internal/iam/store/timestamp.pb.go b/internal/iam/store/timestamp.pb.go new file mode 100644 index 000000000..c2460a612 --- /dev/null +++ b/internal/iam/store/timestamp.pb.go @@ -0,0 +1,165 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.21.0 +// protoc v3.11.4 +// source: controller/storage/iam/store/v1/timestamp.proto + +package store + +import ( + proto "github.com/golang/protobuf/proto" + timestamp "github.com/golang/protobuf/ptypes/timestamp" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// This is a compile-time assertion that a sufficiently up-to-date version +// of the legacy proto package is being used. +const _ = proto.ProtoPackageIsVersion4 + +// Timestamp for iam messages. We've defined a new local type wrapper +// of google.protobuf.Timestamp so we can implement sql.Scanner and sql.Valuer +// interfaces. See: +// https://golang.org/pkg/database/sql/#Scanner +// https://golang.org/pkg/database/sql/driver/#Valuer +type Timestamp struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Timestamp *timestamp.Timestamp `protobuf:"bytes,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` +} + +func (x *Timestamp) Reset() { + *x = Timestamp{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_iam_store_v1_timestamp_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Timestamp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Timestamp) ProtoMessage() {} + +func (x *Timestamp) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_iam_store_v1_timestamp_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Timestamp.ProtoReflect.Descriptor instead. +func (*Timestamp) Descriptor() ([]byte, []int) { + return file_controller_storage_iam_store_v1_timestamp_proto_rawDescGZIP(), []int{0} +} + +func (x *Timestamp) GetTimestamp() *timestamp.Timestamp { + if x != nil { + return x.Timestamp + } + return nil +} + +var File_controller_storage_iam_store_v1_timestamp_proto protoreflect.FileDescriptor + +var file_controller_storage_iam_store_v1_timestamp_proto_rawDesc = []byte{ + 0x0a, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x2f, 0x69, 0x61, 0x6d, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x76, + 0x31, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x12, 0x1f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, + 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x69, 0x61, 0x6d, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, + 0x76, 0x31, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x22, 0x45, 0x0a, 0x09, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, + 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x42, 0x3a, 0x5a, 0x38, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, + 0x72, 0x70, 0x2f, 0x77, 0x61, 0x74, 0x63, 0x68, 0x74, 0x6f, 0x77, 0x65, 0x72, 0x2f, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x69, 0x61, 0x6d, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, + 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_controller_storage_iam_store_v1_timestamp_proto_rawDescOnce sync.Once + file_controller_storage_iam_store_v1_timestamp_proto_rawDescData = file_controller_storage_iam_store_v1_timestamp_proto_rawDesc +) + +func file_controller_storage_iam_store_v1_timestamp_proto_rawDescGZIP() []byte { + file_controller_storage_iam_store_v1_timestamp_proto_rawDescOnce.Do(func() { + file_controller_storage_iam_store_v1_timestamp_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_storage_iam_store_v1_timestamp_proto_rawDescData) + }) + return file_controller_storage_iam_store_v1_timestamp_proto_rawDescData +} + +var file_controller_storage_iam_store_v1_timestamp_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_controller_storage_iam_store_v1_timestamp_proto_goTypes = []interface{}{ + (*Timestamp)(nil), // 0: controller.storage.iam.store.v1.Timestamp + (*timestamp.Timestamp)(nil), // 1: google.protobuf.Timestamp +} +var file_controller_storage_iam_store_v1_timestamp_proto_depIdxs = []int32{ + 1, // 0: controller.storage.iam.store.v1.Timestamp.timestamp:type_name -> google.protobuf.Timestamp + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_controller_storage_iam_store_v1_timestamp_proto_init() } +func file_controller_storage_iam_store_v1_timestamp_proto_init() { + if File_controller_storage_iam_store_v1_timestamp_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_controller_storage_iam_store_v1_timestamp_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Timestamp); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_controller_storage_iam_store_v1_timestamp_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controller_storage_iam_store_v1_timestamp_proto_goTypes, + DependencyIndexes: file_controller_storage_iam_store_v1_timestamp_proto_depIdxs, + MessageInfos: file_controller_storage_iam_store_v1_timestamp_proto_msgTypes, + }.Build() + File_controller_storage_iam_store_v1_timestamp_proto = out.File + file_controller_storage_iam_store_v1_timestamp_proto_rawDesc = nil + file_controller_storage_iam_store_v1_timestamp_proto_goTypes = nil + file_controller_storage_iam_store_v1_timestamp_proto_depIdxs = nil +} diff --git a/internal/proto/local/controller/storage/iam/store/v1/scope.proto b/internal/proto/local/controller/storage/iam/store/v1/scope.proto new file mode 100644 index 000000000..95d5a69b9 --- /dev/null +++ b/internal/proto/local/controller/storage/iam/store/v1/scope.proto @@ -0,0 +1,36 @@ +syntax = "proto3"; + +package controller.storage.iam.store.v1; +option go_package = "github.com/hashicorp/watchtower/internal/iam/store;store"; + +import "controller/storage/iam/store/v1/timestamp.proto"; + +message Scope { + // public_id is the used to access the Scope via an API + // @inject_tag: gorm:"primary_key" + string public_id = 1; + + // create_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + Timestamp create_time = 2; + + // update_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + Timestamp update_time = 3; + + // name is the optional friendly name used to + // access the Scope via an API + // @inject_tag: `gorm:"default:null"` + string name = 4; + + // Type of scope + string type = 5; + + // parent_id is the id (primary key in the Scope aggregate) of scope's parent + // @inject_tag: `gorm:"default:0"` + string parent_id = 6; + + // description of scope + // @inject_tag: `gorm:"default:null"` + string description = 7; +} \ No newline at end of file diff --git a/internal/proto/local/controller/storage/iam/store/v1/timestamp.proto b/internal/proto/local/controller/storage/iam/store/v1/timestamp.proto new file mode 100644 index 000000000..21ab807ec --- /dev/null +++ b/internal/proto/local/controller/storage/iam/store/v1/timestamp.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package controller.storage.iam.store.v1; +option go_package = "github.com/hashicorp/watchtower/internal/iam/store;store"; + +import "google/protobuf/timestamp.proto"; + +// Timestamp for iam messages. We've defined a new local type wrapper +// of google.protobuf.Timestamp so we can implement sql.Scanner and sql.Valuer +// interfaces. See: +// https://golang.org/pkg/database/sql/#Scanner +// https://golang.org/pkg/database/sql/driver/#Valuer +message Timestamp { + google.protobuf.Timestamp timestamp = 1; +}