From 69283beb8603c3afcf62c2e8fa09908d1495f304 Mon Sep 17 00:00:00 2001 From: obs-gh-abhinavpappu <141665106+obs-gh-abhinavpappu@users.noreply.github.com> Date: Tue, 8 Oct 2024 21:30:34 -0700 Subject: [PATCH] feat: add new observe_grant resource (#160) --- .../meta/operation/rbac_statement.graphql | 1 + client/internal/meta/schema/rbac.graphql | 38 +- client/meta/genqlient.generated.go | 13 +- docs/data-sources/rbac_group.md | 8 +- docs/resources/grant.md | 93 +++++ .../observe_rbac_group/data-source.tf | 6 + examples/resources/observe_grant/import.sh | 1 + examples/resources/observe_grant/resource.tf | 45 +++ observe/data_source_rbac_group.go | 13 +- observe/descriptions/grant.yaml | 15 + observe/helpers.go | 13 + observe/provider.go | 1 + observe/resource_bookmark_group.go | 6 +- observe/resource_dataset.go | 6 +- observe/resource_grant.go | 367 ++++++++++++++++++ observe/resource_grant_test.go | 160 ++++++++ 16 files changed, 774 insertions(+), 12 deletions(-) create mode 100644 docs/resources/grant.md create mode 100644 examples/resources/observe_grant/import.sh create mode 100644 examples/resources/observe_grant/resource.tf create mode 100644 observe/descriptions/grant.yaml create mode 100644 observe/resource_grant.go create mode 100644 observe/resource_grant_test.go diff --git a/client/internal/meta/operation/rbac_statement.graphql b/client/internal/meta/operation/rbac_statement.graphql index b75ac43e..7d3eff24 100644 --- a/client/internal/meta/operation/rbac_statement.graphql +++ b/client/internal/meta/operation/rbac_statement.graphql @@ -16,6 +16,7 @@ fragment RbacStatement on RbacStatement { all } role + version } mutation createRbacStatement($config: RbacStatementInput!) { diff --git a/client/internal/meta/schema/rbac.graphql b/client/internal/meta/schema/rbac.graphql index ec66d26b..b823a709 100644 --- a/client/internal/meta/schema/rbac.graphql +++ b/client/internal/meta/schema/rbac.graphql @@ -15,20 +15,31 @@ extend type Query { rbacStatement(id: ORN!): RbacStatement! """ - All groups defined in this tenant. + All groups defined. """ rbacGroups: [RbacGroup!]! """ - All group memberships defined in this tenant. + All group memberships defined. """ rbacGroupmembers: [RbacGroupmember!]! """ - All RBAC statements defined in this tenant. + All RBAC statements defined. """ rbacStatements: [RbacStatement!]! + + """ + Get all RBAC Role Statements + """ + rbacRoleStatements: [RbacStatement!]! + + """ + Get all RBAC resource statements for several objects at once + """ + rbacResourceStatements(ids: [ObjectId!]!): [RbacStatement!]! + """ Given a particular user, and a particular object/role request, return what would happen. Note that we assume that the customer owning the object is the current @@ -66,6 +77,11 @@ extend type Query { Get the group users will be assigned to by default """ rbacDefaultGroup: RbacGroup! + + """ + Get the group users will be assigned to by default + """ + rbacDefaultSharingGroups: [RbacDefaultSharingGroup!]! } extend type User { @@ -116,6 +132,8 @@ extend type Mutation { """ setRbacDefaultGroup(id: ORN!): ResultStatus! unsetRbacDefaultGroup: ResultStatus! + + setRbacDefaultSharingGroups(shares: [RbacDefaultSharingGroupInput!]): ResultStatus! } scalar ORN @goModel(model: "observe/authorization/id.ORN") @@ -212,6 +230,8 @@ type RbacStatement implements AuditedObject @goModel(model: "observe/rbac/policy object: RbacObject! role: RbacRole! + version: Int + createdBy: UserId! createdByInfo: UserInfo! @goField(forceResolver: true) createdDate: Time! @@ -225,6 +245,7 @@ input RbacStatementInput @goModel(model: "observe/rbac/policy.Statement") { subject: RbacSubjectInput! object: RbacObjectInput! role: RbacRole! + version: Int } input UpdateRbacStatementInput @goModel(model: "observe/rbac/policy.Statement") { @@ -233,6 +254,7 @@ input UpdateRbacStatementInput @goModel(model: "observe/rbac/policy.Statement") subject: RbacSubjectInput! object: RbacObjectInput! role: RbacRole! + version: Int } type MutateRbacStatementsResponse @goModel(model: "observe/meta/metatypes.MutateRbacStatementsResponse") { @@ -241,6 +263,16 @@ type MutateRbacStatementsResponse @goModel(model: "observe/meta/metatypes.Mutate deletedStatements: [ORN!]! } +type RbacDefaultSharingGroup @goModel(model: "observe/meta/metatypes.RbacDefaultSharingGroup") { + groupId: ORN! + allowEdit: Boolean! +} + +input RbacDefaultSharingGroupInput @goModel(model: "observe/meta/metatypes.RbacDefaultSharingGroup") { + groupId: ORN! + allowEdit: Boolean! +} + """ A RequestSubject is different from a Subject, because the RequestSubject provides all of the values, such that each Statement can match against diff --git a/client/meta/genqlient.generated.go b/client/meta/genqlient.generated.go index dcef88d6..fe513b21 100644 --- a/client/meta/genqlient.generated.go +++ b/client/meta/genqlient.generated.go @@ -7214,6 +7214,7 @@ type RbacStatement struct { Subject RbacStatementSubjectRbacSubject `json:"subject"` Object RbacStatementObjectRbacObject `json:"object"` Role RbacRole `json:"role"` + Version *int `json:"version"` } // GetId returns RbacStatement.Id, and is useful for accessing the field via an interface. @@ -7231,11 +7232,15 @@ func (v *RbacStatement) GetObject() RbacStatementObjectRbacObject { return v.Obj // GetRole returns RbacStatement.Role, and is useful for accessing the field via an interface. func (v *RbacStatement) GetRole() RbacRole { return v.Role } +// GetVersion returns RbacStatement.Version, and is useful for accessing the field via an interface. +func (v *RbacStatement) GetVersion() *int { return v.Version } + type RbacStatementInput struct { Description string `json:"description"` Subject RbacSubjectInput `json:"subject"` Object RbacObjectInput `json:"object"` Role RbacRole `json:"role"` + Version *int `json:"version"` } // GetDescription returns RbacStatementInput.Description, and is useful for accessing the field via an interface. @@ -7250,6 +7255,9 @@ func (v *RbacStatementInput) GetObject() RbacObjectInput { return v.Object } // GetRole returns RbacStatementInput.Role, and is useful for accessing the field via an interface. func (v *RbacStatementInput) GetRole() RbacRole { return v.Role } +// GetVersion returns RbacStatementInput.Version, and is useful for accessing the field via an interface. +func (v *RbacStatementInput) GetVersion() *int { return v.Version } + // RbacStatementObjectRbacObject includes the requested fields of the GraphQL type RbacObject. type RbacStatementObjectRbacObject struct { ObjectId *string `json:"objectId"` @@ -10811,7 +10819,7 @@ func (v *getRbacGroupmemberResponse) GetRbacGroupmember() RbacGroupmember { retu // getRbacGroupsResponse is returned by getRbacGroups on success. type getRbacGroupsResponse struct { - // All groups defined in this tenant. + // All groups defined. RbacGroups []RbacGroup `json:"rbacGroups"` } @@ -13289,6 +13297,7 @@ fragment RbacStatement on RbacStatement { all } role + version } ` @@ -16689,6 +16698,7 @@ fragment RbacStatement on RbacStatement { all } role + version } ` @@ -20312,6 +20322,7 @@ fragment RbacStatement on RbacStatement { all } role + version } ` diff --git a/docs/data-sources/rbac_group.md b/docs/data-sources/rbac_group.md index 1e874df3..da2076a3 100644 --- a/docs/data-sources/rbac_group.md +++ b/docs/data-sources/rbac_group.md @@ -16,6 +16,12 @@ Fetches metadata for an existing Observe RbacGroup. data "observe_rbac_group" "example" { name = "example" } + +// In RBAC v2, "everyone" is a special pre-defined group that always includes all users. +// Reach out to Observe to enable this feature. +data "observe_rbac_group" "everyone" { + name = "everyone" +} ``` @@ -24,7 +30,7 @@ data "observe_rbac_group" "example" { ### Optional - `id` (String) RbacGroup ID. Either `name` or `id` must be provided. -- `name` (String) RbacGroup Name. Either `name` or `id` must be provided +- `name` (String) RbacGroup Name. Either `name` or `id` must be provided. ### Read-Only diff --git a/docs/resources/grant.md b/docs/resources/grant.md new file mode 100644 index 00000000..b830dd76 --- /dev/null +++ b/docs/resources/grant.md @@ -0,0 +1,93 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "observe_grant Resource - terraform-provider-observe" +subcategory: "" +description: |- + NOTE: This feature is still under development. It is not meant for customer use yet. + Manages an Observe grant. Grants allow configuring permissions for users and groups by + assigning roles. A grant may also optionally be qualified by an object id. Replaces + rbac_statement. Reach out to Observe to enable this feature. +--- +# observe_grant + +NOTE: This feature is still under development. It is not meant for customer use yet. + +Manages an Observe grant. Grants allow configuring permissions for users and groups by +assigning roles. A grant may also optionally be qualified by an object id. Replaces +rbac_statement. Reach out to Observe to enable this feature. +## Example Usage +```terraform +data "observe_workspace" "default" { + name = "Default" +} + +data "observe_user" "example" { + email = "example@domain.com" +} + +data "observe_rbac_group" "example" { + name = "engineering" +} + +// "everyone" is a special pre-defined group that always includes all users +data "observe_rbac_group" "everyone" { + name = "everyone" +} + +data "observe_dataset" "example" { + workspace = data.observe_workspace.default.oid + name = "Engineering Logs" +} + +// Allow user example to create worksheets +resource "observe_grant" "user_example" { + subject = data.observe_user.example.oid + role = "worksheet_creator" +} + +// Allow group engineering to edit dataset Engineering Logs +resource "observe_grant" "group_example" { + subject = data.observe_rbac_group.example.oid + role = "dataset_editor" + qualifier { + oid = data.observe_dataset.example.oid + } +} + +// Allow everyone to view dataset Engineering Logs +resource "observe_grant" "everyone_example" { + subject = data.observe_rbac_group.everyone.oid + role = "dataset_viewer" + qualifier { + oid = data.observe_dataset.example.oid + } +} +``` + +## Schema + +### Required + +- `role` (String) The role to grant. +- `subject` (String) OID of the subject. Must be a user or a group. + +### Optional + +- `qualifier` (Block List, Max: 1) (see [below for nested schema](#nestedblock--qualifier)) + +### Read-Only + +- `id` (String) The ID of this resource. +- `oid` (String) + + +### Nested Schema for `qualifier` + +Optional: + +- `oid` (String) OID of the object this grant applies to. +## Import +Import is supported using the following syntax: +```shell +terraform import observe_grant.example 1414010 +``` diff --git a/examples/data-sources/observe_rbac_group/data-source.tf b/examples/data-sources/observe_rbac_group/data-source.tf index 134796ae..ca87ea7f 100644 --- a/examples/data-sources/observe_rbac_group/data-source.tf +++ b/examples/data-sources/observe_rbac_group/data-source.tf @@ -1,3 +1,9 @@ data "observe_rbac_group" "example" { name = "example" } + +// In RBAC v2, "everyone" is a special pre-defined group that always includes all users. +// Reach out to Observe to enable this feature. +data "observe_rbac_group" "everyone" { + name = "everyone" +} diff --git a/examples/resources/observe_grant/import.sh b/examples/resources/observe_grant/import.sh new file mode 100644 index 00000000..f00ce4a1 --- /dev/null +++ b/examples/resources/observe_grant/import.sh @@ -0,0 +1 @@ +terraform import observe_grant.example 1414010 diff --git a/examples/resources/observe_grant/resource.tf b/examples/resources/observe_grant/resource.tf new file mode 100644 index 00000000..a6dd7b7d --- /dev/null +++ b/examples/resources/observe_grant/resource.tf @@ -0,0 +1,45 @@ +data "observe_workspace" "default" { + name = "Default" +} + +data "observe_user" "example" { + email = "example@domain.com" +} + +data "observe_rbac_group" "example" { + name = "engineering" +} + +// "everyone" is a special pre-defined group that always includes all users +data "observe_rbac_group" "everyone" { + name = "everyone" +} + +data "observe_dataset" "example" { + workspace = data.observe_workspace.default.oid + name = "Engineering Logs" +} + +// Allow user example to create worksheets +resource "observe_grant" "user_example" { + subject = data.observe_user.example.oid + role = "worksheet_creator" +} + +// Allow group engineering to edit dataset Engineering Logs +resource "observe_grant" "group_example" { + subject = data.observe_rbac_group.example.oid + role = "dataset_editor" + qualifier { + oid = data.observe_dataset.example.oid + } +} + +// Allow everyone to view dataset Engineering Logs +resource "observe_grant" "everyone_example" { + subject = data.observe_rbac_group.everyone.oid + role = "dataset_viewer" + qualifier { + oid = data.observe_dataset.example.oid + } +} diff --git a/observe/data_source_rbac_group.go b/observe/data_source_rbac_group.go index b40cf727..71c6dd22 100644 --- a/observe/data_source_rbac_group.go +++ b/observe/data_source_rbac_group.go @@ -12,7 +12,7 @@ import ( const ( schemaRbacGroupIdDescription = "RbacGroup ID. Either `name` or `id` must be provided." schemaRbacGroupOIDDescription = "The Observe ID for rbacGroup." - schemaRbacGroupNameDescription = "RbacGroup Name. Either `name` or `id` must be provided" + schemaRbacGroupNameDescription = "RbacGroup Name. Either `name` or `id` must be provided." schemaRbacGroupDescriptionDescription = "RbacGroup description." ) @@ -63,6 +63,17 @@ func dataSourceRbacGroupRead(ctx context.Context, data *schema.ResourceData, met r, err = client.GetRbacGroup(ctx, explicitId) } else if name != "" { r, err = client.LookupRbacGroup(ctx, name) + + // In RBAC v2, "everyone" is a special group with id "1" that always includes all users. + // To prevent issues for customers who have a real group named "everyone", only + // return this special group if the lookup failed. + if err != nil && name == "everyone" { + r = &gql.RbacGroup{ + Id: "1", + Name: "everyone", + } + err = nil + } } if err != nil { diff --git a/observe/descriptions/grant.yaml b/observe/descriptions/grant.yaml new file mode 100644 index 00000000..78514dd7 --- /dev/null +++ b/observe/descriptions/grant.yaml @@ -0,0 +1,15 @@ +description: | + NOTE: This feature is still under development. It is not meant for customer use yet. + + Manages an Observe grant. Grants allow configuring permissions for users and groups by + assigning roles. A grant may also optionally be qualified by an object id. Replaces + rbac_statement. Reach out to Observe to enable this feature. + +schema: + subject: | + OID of the subject. Must be a user or a group. + role: | + The role to grant. + qualifier: + oid: | + OID of the object this grant applies to. diff --git a/observe/helpers.go b/observe/helpers.go index d4b83eab..dc54a3da 100644 --- a/observe/helpers.go +++ b/observe/helpers.go @@ -484,3 +484,16 @@ func validateDatasetName() schema.SchemaValidateDiagFunc { func validateDatastreamName() schema.SchemaValidateDiagFunc { return validateDatasetName() } + +func asPointer[T any](val T) *T { + return &val +} + +func sliceContains[T comparable](slice []T, val T) bool { + for _, v := range slice { + if v == val { + return true + } + } + return false +} diff --git a/observe/provider.go b/observe/provider.go index 1164f4fa..65a721ec 100644 --- a/observe/provider.go +++ b/observe/provider.go @@ -175,6 +175,7 @@ func Provider() *schema.Provider { "observe_rbac_default_group": resourceRbacDefaultGroup(), "observe_rbac_group_member": resourceRbacGroupmember(), "observe_rbac_statement": resourceRbacStatement(), + "observe_grant": resourceGrant(), "observe_filedrop": resourceFiledrop(), "observe_snowflake_outbound_share": resourceSnowflakeOutboundShare(), "observe_dataset_outbound_share": resourceDatasetOutboundShare(), diff --git a/observe/resource_bookmark_group.go b/observe/resource_bookmark_group.go index 12d8fc04..e56aac3f 100644 --- a/observe/resource_bookmark_group.go +++ b/observe/resource_bookmark_group.go @@ -62,9 +62,9 @@ func resourceBookmarkGroup() *schema.Resource { Description: descriptions.Get("bookmark_group", "schema", "presentation"), }, "is_home": { - Type: schema.TypeBool, - Optional: true, - Default: false, + Type: schema.TypeBool, + Optional: true, + Default: false, Description: descriptions.Get("bookmark_group", "schema", "is_home"), }, }, diff --git a/observe/resource_dataset.go b/observe/resource_dataset.go index 467c2ea9..edff8753 100644 --- a/observe/resource_dataset.go +++ b/observe/resource_dataset.go @@ -100,11 +100,11 @@ func resourceDataset() *schema.Resource { Description: descriptions.Get("transform", "schema", "inputs"), }, "data_table_view_state": { - Type: schema.TypeString, - Optional: true, + Type: schema.TypeString, + Optional: true, ValidateDiagFunc: validateStringIsJSON, DiffSuppressFunc: diffSuppressJSON, - Description: descriptions.Get("dataset", "schema", "data_table_view_state"), + Description: descriptions.Get("dataset", "schema", "data_table_view_state"), }, "stage": { Type: schema.TypeList, diff --git a/observe/resource_grant.go b/observe/resource_grant.go new file mode 100644 index 00000000..80921bba --- /dev/null +++ b/observe/resource_grant.go @@ -0,0 +1,367 @@ +package observe + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + observe "github.com/observeinc/terraform-provider-observe/client" + gql "github.com/observeinc/terraform-provider-observe/client/meta" + "github.com/observeinc/terraform-provider-observe/client/meta/types" + "github.com/observeinc/terraform-provider-observe/client/oid" + "github.com/observeinc/terraform-provider-observe/observe/descriptions" +) + +func resourceGrant() *schema.Resource { + return &schema.Resource{ + Description: descriptions.Get("grant", "description"), + CreateContext: resourceGrantCreate, + UpdateContext: resourceGrantUpdate, + ReadContext: resourceGrantRead, + DeleteContext: resourceGrantDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: map[string]*schema.Schema{ + "subject": { + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validateOID(oid.TypeUser, oid.TypeRbacGroup), + Description: descriptions.Get("grant", "schema", "subject"), + }, + "role": { + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validateEnums(validGrantRoles), + Description: descriptions.Get("grant", "schema", "role"), + }, + "qualifier": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "oid": { + Type: schema.TypeString, + Optional: true, + ValidateDiagFunc: validateOID(validRbacV2Types...), + DiffSuppressFunc: diffSuppressOIDVersion, + Description: descriptions.Get("grant", "schema", "qualifier", "oid"), + }, + // in the future, will contain other qualifiers such as "tags" + }, + }, + }, + "oid": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +// for now, translates grants into rbac v1 statements until api support is added +func newGrantInput(data *schema.ResourceData) (input *gql.RbacStatementInput, diags diag.Diagnostics) { + input = &gql.RbacStatementInput{ + Version: intPtr(2), + } + + // subject + subject, err := oid.NewOID(data.Get("subject").(string)) + if err != nil { + return nil, diag.Errorf("error parsing subject: %s", err.Error()) + } + input.Subject.All = boolPtr(false) + if subject.Type == oid.TypeUser { + uid, err := types.StringToUserIdScalar(subject.Id) + if err != nil { + return nil, diag.Errorf("error parsing subject user: %s", err.Error()) + } + input.Subject.UserId = &uid + } else if subject.Type == oid.TypeRbacGroup { + if subject.Id == "1" { + input.Subject.All = boolPtr(true) + } else { + input.Subject.GroupId = &subject.Id + } + } + + // role + role := GrantRole(toCamel(data.Get("role").(string))) + input.Role, err = role.ToRbacRole() + if err != nil { + return nil, diag.Errorf(err.Error()) + } + + // object + var resourceId *string + resourceOidStr, ok := data.GetOk("qualifier.0.oid") + if ok { + resourceOid, err := oid.NewOID(resourceOidStr.(string)) + if err != nil { + return nil, diag.Errorf("error parsing qualifier.oid: %s", err.Error()) + } + resourceId = &resourceOid.Id + } + input.Object, err = role.ToRbacObject(resourceId) + if err != nil { + return nil, diag.Errorf(err.Error()) + } + + return input, diags +} + +// for now, receives an rbac v1 statement and translates it into a grant until api support is added +func grantToResourceData(stmt *gql.RbacStatement, data *schema.ResourceData) (diags diag.Diagnostics) { + data.SetId(stmt.Id) + + if stmt.Version == nil || *stmt.Version != 2 { + diags = append(diags, diag.Errorf("observe_grant only works with rbac v2 statements")...) + } + + // subject + subject := "" + if stmt.Subject.All != nil && *stmt.Subject.All { + subject = oid.RbacGroupOid("1").String() + } else if stmt.Subject.UserId != nil { + subject = oid.UserOid(*stmt.Subject.UserId).String() + } else if stmt.Subject.GroupId != nil { + subject = oid.RbacGroupOid(*stmt.Subject.GroupId).String() + } else { + diags = append(diags, diag.Errorf("invalid subject")...) + } + if err := data.Set("subject", subject); err != nil { + diags = append(diags, diag.FromErr(err)...) + } + + // role and qualifier + var role string + qualifier := make(map[string]interface{}, 0) + if stmt.Role == gql.RbacRoleManager && stmt.Object.All != nil && *stmt.Object.All { + role = toSnake(string(Administrator)) + } else if stmt.Object.Type != nil { + objType := oid.Type(*stmt.Object.Type) + if !sliceContains(validRbacV2Types, objType) { + diags = append(diags, diag.Errorf("invalid object type for v2 statment: %s", objType)...) + } + + if stmt.Object.ObjectId != nil { + resourceOid := oid.OID{Type: objType, Id: *stmt.Object.ObjectId} + qualifier["oid"] = resourceOid.String() + + if stmt.Role == gql.RbacRoleViewer { + if grantRole, ok := viewGrantRoleForType[objType]; ok { + role = toSnake(string(grantRole)) + } + } else if stmt.Role == gql.RbacRoleEditor { + if grantRole, ok := editGrantRoleForType[objType]; ok { + role = toSnake(string(grantRole)) + } + } + } else { + // editor without object id is create + if stmt.Role == gql.RbacRoleEditor { + if grantRole, ok := createGrantRoleForType[objType]; ok { + role = toSnake(string(grantRole)) + } + } + } + } + if role == "" { + diags = append(diags, diag.Errorf("invalid statement")...) + } + if err := data.Set("role", string(role)); err != nil { + diags = append(diags, diag.FromErr(err)...) + } + // since qualifier is optional, we want it to be nil unless it actually has values + var qualifierSlice []interface{} + if len(qualifier) > 0 { + qualifierSlice = []interface{}{qualifier} + } + if err := data.Set("qualifier", qualifierSlice); err != nil { + diags = append(diags, diag.FromErr(err)...) + } + + if err := data.Set("oid", stmt.Oid().String()); err != nil { + diags = append(diags, diag.FromErr(err)...) + } + return diags +} + +func resourceGrantCreate(ctx context.Context, data *schema.ResourceData, meta interface{}) (diags diag.Diagnostics) { + client := meta.(*observe.Client) + + input, diags := newGrantInput(data) + if diags.HasError() { + return diags + } + + result, err := client.CreateRbacStatement(ctx, input) + if err != nil { + return diag.Errorf("failed to create grant: %s", err.Error()) + } + + data.SetId(result.Id) + return append(diags, resourceGrantRead(ctx, data, meta)...) +} + +func resourceGrantUpdate(ctx context.Context, data *schema.ResourceData, meta interface{}) (diags diag.Diagnostics) { + client := meta.(*observe.Client) + + input, diags := newGrantInput(data) + if diags.HasError() { + return diags + } + + _, err := client.UpdateRbacStatement(ctx, data.Id(), input) + if err != nil { + return diag.Errorf("failed to update grant: %s", err.Error()) + } + + return append(diags, resourceGrantRead(ctx, data, meta)...) +} + +func resourceGrantRead(ctx context.Context, data *schema.ResourceData, meta interface{}) (diags diag.Diagnostics) { + client := meta.(*observe.Client) + + stmt, err := client.GetRbacStatement(ctx, data.Id()) + if err != nil { + if gql.HasErrorCode(err, gql.ErrNotFound) { + data.SetId("") + return nil + } + return diag.Errorf("failed to read grant: %s", err.Error()) + } + return grantToResourceData(stmt, data) +} + +func resourceGrantDelete(ctx context.Context, data *schema.ResourceData, meta interface{}) (diags diag.Diagnostics) { + client := meta.(*observe.Client) + if err := client.DeleteRbacStatement(ctx, data.Id()); err != nil { + return diag.Errorf("failed to delete grant: %s", err.Error()) + } + return diags +} + +type GrantRole string + +const ( + Administrator GrantRole = "Administrator" + DashboardCreator GrantRole = "DashboardCreator" + DashboardEditor GrantRole = "DashboardEditor" + DashboardViewer GrantRole = "DashboardViewer" + DatasetCreator GrantRole = "DatasetCreator" + DatasetEditor GrantRole = "DatasetEditor" + DatasetViewer GrantRole = "DatasetViewer" + DatastreamCreator GrantRole = "DatastreamCreator" + DatastreamEditor GrantRole = "DatastreamEditor" + DatastreamViewer GrantRole = "DatastreamViewer" + MonitorCreator GrantRole = "MonitorCreator" + MonitorEditor GrantRole = "MonitorEditor" + MonitorViewer GrantRole = "MonitorViewer" + WorksheetCreator GrantRole = "WorksheetCreator" + WorksheetEditor GrantRole = "WorksheetEditor" + WorksheetViewer GrantRole = "WorksheetViewer" +) + +var validGrantRoles = []GrantRole{ + Administrator, + DashboardCreator, + DashboardEditor, + DashboardViewer, + DatasetCreator, + DatasetEditor, + DatasetViewer, + DatastreamCreator, + DatastreamEditor, + DatastreamViewer, + MonitorCreator, + MonitorEditor, + MonitorViewer, + WorksheetCreator, + WorksheetEditor, + WorksheetViewer, +} + +var createGrantRoles = []GrantRole{DashboardCreator, DatasetCreator, DatastreamCreator, MonitorCreator, WorksheetCreator} +var editGrantRoles = []GrantRole{DashboardEditor, DatasetEditor, DatastreamEditor, MonitorEditor, WorksheetEditor} +var viewGrantRoles = []GrantRole{DashboardViewer, DatasetViewer, DatastreamViewer, MonitorViewer, WorksheetViewer} + +var validRbacV2Types = []oid.Type{oid.TypeDashboard, oid.TypeDataset, oid.TypeDatastream, oid.TypeMonitor, oid.TypeWorksheet} + +var createGrantRoleForType = map[oid.Type]GrantRole{ + oid.TypeDashboard: DashboardCreator, + oid.TypeDataset: DatasetCreator, + oid.TypeDatastream: DatastreamCreator, + oid.TypeMonitor: MonitorCreator, + oid.TypeWorksheet: WorksheetCreator, +} + +var editGrantRoleForType = map[oid.Type]GrantRole{ + oid.TypeDashboard: DashboardEditor, + oid.TypeDataset: DatasetEditor, + oid.TypeDatastream: DatastreamEditor, + oid.TypeMonitor: MonitorEditor, + oid.TypeWorksheet: WorksheetEditor, +} + +var viewGrantRoleForType = map[oid.Type]GrantRole{ + oid.TypeDashboard: DashboardViewer, + oid.TypeDataset: DatasetViewer, + oid.TypeDatastream: DatastreamViewer, + oid.TypeMonitor: MonitorViewer, + oid.TypeWorksheet: WorksheetViewer, +} + +func (r GrantRole) ToRbacRole() (gql.RbacRole, error) { + if r == Administrator { + return gql.RbacRoleManager, nil + } else if sliceContains(createGrantRoles, r) || sliceContains(editGrantRoles, r) { + return gql.RbacRoleEditor, nil + } else if sliceContains(viewGrantRoles, r) { + return gql.RbacRoleViewer, nil + } else { + return "", fmt.Errorf("invalid role: %s", r) + } +} + +func (r GrantRole) ToType() *oid.Type { + switch r { + case DashboardCreator, DashboardEditor, DashboardViewer: + return asPointer(oid.TypeDashboard) + case DatasetCreator, DatasetEditor, DatasetViewer: + return asPointer(oid.TypeDataset) + case DatastreamCreator, DatastreamEditor, DatastreamViewer: + return asPointer(oid.TypeDatastream) + case MonitorCreator, MonitorEditor, MonitorViewer: + return asPointer(oid.TypeMonitor) + case WorksheetCreator, WorksheetEditor, WorksheetViewer: + return asPointer(oid.TypeWorksheet) + default: + return nil + } +} + +func (r GrantRole) ToRbacObject(resourceId *string) (gql.RbacObjectInput, error) { + objectInput := gql.RbacObjectInput{ + Owner: boolPtr(false), + All: boolPtr(false), + } + // an oid qualifier is only valid for edit roles and view roles + isResourceRole := sliceContains(editGrantRoles, r) || sliceContains(viewGrantRoles, r) + if isResourceRole && resourceId == nil { + return objectInput, fmt.Errorf("role %s must be qualified with an object id", r) + } + if !isResourceRole && resourceId != nil { + return objectInput, fmt.Errorf("role %s cannot be qualified with an object id", r) + } + switch r { + case Administrator: + objectInput.All = boolPtr(true) + default: + objectInput.Type = (*string)(r.ToType()) + objectInput.ObjectId = resourceId + } + return objectInput, nil +} diff --git a/observe/resource_grant_test.go b/observe/resource_grant_test.go new file mode 100644 index 00000000..1a8c1fe6 --- /dev/null +++ b/observe/resource_grant_test.go @@ -0,0 +1,160 @@ +package observe + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccObserveGrantGroupDatasetCreate(t *testing.T) { + randomPrefix := acctest.RandomWithPrefix("tf") + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(configPreamble+` + resource "observe_rbac_group" "example" { + name = "%[1]s" + } + + resource "observe_grant" "example" { + subject = observe_rbac_group.example.oid + role = "dataset_creator" + } + `, randomPrefix), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("observe_grant.example", "subject"), + resource.TestCheckResourceAttr("observe_grant.example", "role", "dataset_creator"), + ), + }, + }, + }) +} + +func TestAccObserveGrantUserDatastreamEdit(t *testing.T) { + randomPrefix := acctest.RandomWithPrefix("tf") + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(configPreamble+datastreamConfigPreamble+` + data "observe_user" "system" { + email = "%[2]s" + } + + resource "observe_grant" "example" { + subject = data.observe_user.system.oid + role = "datastream_editor" + qualifier { + oid = observe_datastream.test.oid + } + } + `, randomPrefix, systemUser()), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("observe_grant.example", "subject"), + resource.TestCheckResourceAttr("observe_grant.example", "role", "datastream_editor"), + resource.TestCheckResourceAttr("observe_grant.example", "qualifier.#", "1"), + resource.TestCheckResourceAttrSet("observe_grant.example", "qualifier.0.oid"), + ), + }, + { + Config: fmt.Sprintf(configPreamble+datastreamConfigPreamble+` + data "observe_user" "system" { + email = "%[2]s" + } + + resource "observe_grant" "example" { + subject = data.observe_user.system.oid + role = "datastream_viewer" + qualifier { + oid = observe_datastream.test.oid + } + } + `, randomPrefix, systemUser()), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("observe_grant.example", "subject"), + ), + }, + }, + }) +} + +func TestAccObserveGrantEveryoneWorksheetView(t *testing.T) { + randomPrefix := acctest.RandomWithPrefix("tf") + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(configPreamble+datastreamConfigPreamble+` + data "observe_rbac_group" "everyone" { + name = "everyone" + } + + data "observe_oid" "dataset" { + oid = observe_datastream.test.dataset + } + + resource "observe_worksheet" "example" { + workspace = data.observe_workspace.default.oid + name = "%[1]s" + queries = <<-EOF + [{ + "id": "stage1", + "input": [{ + "inputName": "kubernetes/Container Logs", + "inputRole": "Data", + "datasetId": "${data.observe_oid.dataset.id}" + }] + }] + EOF + } + + resource "observe_grant" "example" { + subject = data.observe_rbac_group.everyone.oid + role = "worksheet_viewer" + qualifier { + oid = observe_worksheet.example.oid + } + } + `, randomPrefix), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("observe_grant.example", "subject"), + resource.TestCheckResourceAttr("observe_grant.example", "role", "worksheet_viewer"), + resource.TestCheckResourceAttr("observe_grant.example", "qualifier.#", "1"), + resource.TestCheckResourceAttrSet("observe_grant.example", "qualifier.0.oid"), + ), + }, + }, + }) +} + +func TestAccObserveGrantGroupAdminWorkspace(t *testing.T) { + randomPrefix := acctest.RandomWithPrefix("tf") + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(configPreamble+` + resource "observe_rbac_group" "example" { + name = "%[1]s" + } + + resource "observe_grant" "example" { + subject = observe_rbac_group.example.oid + role = "administrator" + } + `, randomPrefix), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("observe_grant.example", "subject"), + resource.TestCheckResourceAttr("observe_grant.example", "role", "administrator"), + ), + }, + }, + }) +}