Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resource: postgresql_schema privilege support #10808

Merged
merged 22 commits into from
Dec 28, 2016
Merged

Resource: postgresql_schema privilege support #10808

merged 22 commits into from
Dec 28, 2016

Conversation

sean-
Copy link
Contributor

@sean- sean- commented Dec 17, 2016

Updates to the PostgreSQL provider.

  • Update postgresql_role to include the skip_drop_role and skip_reassign_owned attributes, which are required when deleting a PostgreSQL database where a ROLE is used across multiple database instances within a single PostgreSQL cluster.
  • Update the postgresql_schema resource to include a policy block that allows for setting permissions on a per-schema basis to zero or more roles (one per block).

Note: due to the way that locking is handled within PostgreSQL, it is required to run these tests with a parallelism of 1 because concurrent access for updating permissions results in error messages about concurrent updates to system catalogs.

@sean- sean- requested a review from stack72 December 17, 2016 02:04

resource "postgresql_role" "app1" {
name = "app1"
# depends_on = ["postgresql_schema_policy.foo_allow"]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stack72 e.g. This is what should be required, but only on destroy. ?

@apparentlymart
Copy link
Contributor

@sean- The dependency edges in the DAG are dealt with in the opposite order during the destroy phase, so if a postgresql_schema_policy depends on a postgresql_role then the latter will precede the former on create, but the former will be destroyed before the latter on destroy.

This means we don't need to explicitly draw the opposing dependency to deal with the destroy case, and so the need for a cycle is avoided. As long as all of the relevant objects were created by Terraform, Terraform should be able to unwind them all in an appropriate order automatically.

A tricky situation is if a role and a policy are created by two separate Terraform configs or the role policy was created manually outside of Terraform. In that case Terraform can't know it needs to delete the policy first because it doesn't know the policy exists. But this is generally desirable behavior because Terraform does its best to guarantee that it will never interfere with a resource it didn't create.

It seems like you've already been working with @stack72 on this, so I'll leave him to do a full review but I hope the above info is helpful in answering your question about how to deal with the destroy process.

@sean-
Copy link
Contributor Author

sean- commented Dec 17, 2016

@apparentlymart Yup, understood. The problem I'm trying to figure out is how to deal with a quirk in PostgreSQL where the create/destroy order of objects is asymmetric. By that I mean:

postgres@grit=# CREATE SCHEMA foo;
CREATE SCHEMA
postgres@grit=# CREATE ROLE bar;
CREATE ROLE
postgres@grit=# GRANT USAGE ON SCHEMA foo TO bar;
GRANT
postgres@grit=# DROP ROLE bar;
ERROR:  2BP01: role "bar" cannot be dropped because some objects depend on it
DETAIL:  privileges for schema foo
LOCATION:  DropRole, user.c:1045

Which in Terraform is written out like:

resource "postgresql_schema" "foo" {
  name = "foo"
}

resource "postgresql_role" "bar" {
  name = "bar"
}

resource "postgresql_schema_policy" "foo" {
  create = true

  schema = "${postgresql_schema.foo.name}"
  role = "${postgresql_role.bar.name}"
}

I've been chewing on this a bit and started out thinking I wanted a postgresql_schema_policy resource that would be defined like:

resource "postgresql_schema_policy" "foo_allow_res" {
  create_with_grant = true
  usage_with_grant = true

  schema = "${postgresql_schema.foo_res.name}"
  role = "${postgresql_role.app1_res.name}"
}

Which creates a DAG roughly like: database -> schema -> role -> schema_policy, which is how you would normally construct a database. It's possible to preserve the correct ordering of a DAG using something like:

resource "postgresql_schema" "foo" {
  name = "foo"
  owner = "${postgresql_role.dba.name}"
  policies = [
    "${postgresql_schema_policy.foo_allow}",
    "${postgresql_schema_policy.foo_deny}",
  ]
}

resource "postgresql_schema_policy" "foo_allow" {
  create_with_grant = true
  usage_with_grant = true

  role = "${postgresql_role.app1.name}"
}

resource "postgresql_schema_policy" "foo_deny" {
  role = "${postgresql_role.app2.name}"
}

But I don't like the aesthetics of that. What I'm going to do instead, and this resolves my own initial question above, is to move forward toward the following:

resource "postgresql_schema" "foo" {
  name = "foo"
  owner = "${postgresql_role.dba.name}"

  policy {
    role = "${postgresql_role.app1.name}"
    create = true
    usage_with_grant = true
  }

  policy {
    roles = ["${postgresql_role.app2.name}"]
    usage = true
  }
}

Which I like a lot more.

@sean-
Copy link
Contributor Author

sean- commented Dec 25, 2016

There's another quirk with PostgreSQL and Terraform: PostgreSQL has its own dependency graph and resolves things just fine on its own! Said differently in Terraform-speak:

resource "postgresql_role" "myrole" {
  name = "myrole"
}

resource "postgresql_schema" "test_schema" {
  name = "test_schema"

  policy {
    create = true
    usage = true
    role = "${postgresql_role.myrole.name}"
  }

When you go to drop the psotgresql_schema via TF, because myrole is dropped first, the revocation of the GRANTs fails:

LOG:  statement: DROP ROLE "all_with_grantdrop"
LOG:  statement: REVOKE CREATE ON SCHEMA "test_schema" FROM "myrole"
ERROR:  role "myrole" does not exist

Which is correct, but irritating from Terraform's perspective because you have no way of knowing that myrole no longer exists without testing first before each and every DCL. This is really only a problem when the PostgreSQL ROLE has been removed first, which in theory shouldn't happen too often. I've added a guard to prevent this from happening by querying the DB to test and see if the ROLE exists or not, but I wish there was something more elegant that was possible since this check will run for every role. Not the end of the world.

@sean-
Copy link
Contributor Author

sean- commented Dec 25, 2016

@stack72 whenever you're back and have time. The only thing that I'm not happy about with this is the need to individually spell out the policies, one per role. For instance:

resource "postgresql_schema" "test_schema" {
  name = "test_schema"

  policy {
    create = true
    usage = true
    role = "${postgresql_role.myrole.name}"
  }
}

What I was originally aiming to do was:

resource "postgresql_schema" "test_schema" {
  name = "test_schema"

  policy {
    create = true
    usage = true
    roles = ["${postgresql_role.myrole.name}"]
  }
}

so that an array of roles could receive the same target permission. Internally this was very problematic because PostgreSQL stores this per-policy. What I attempted to do was flatten the input of a list of roles into synthesized list of policies, however there isn't a delete method that allows for easy rewriting of the resource data. Anyway, for another day. Regardless, PTAL. Thanks in advance!

sean- added 11 commits December 25, 2016 06:13
issues with cycles when an object is destroyed.
removing a PostgreSQL role.

Add manual overrides if this isn't the desired behavior, but it should
universally be the desired outcome except when a ROLE name is reused
across multiple databases in the same PostgreSQL cluster, in which case
the `skip_drop_role` is necessary for all but the last PostgreSQL
provider.
It is now possible to begin specifying DCL for schemas.
applied.

This is only a problem when the ROLE is removed before the revokation of
the schema's policies on the doomed PostgreSQL ROLE.  Comment out the
tests for now to get unit tests to pass.  Using hard-coded values that
exist outside of the Terraform unit test work.
@sean- sean- force-pushed the f-postgresql-owner branch from 4bc264e to d92a3ca Compare December 25, 2016 14:14
@sean- sean- changed the title postgresql_schema_policy WIP Resource: postgresql_schema privilege support Dec 26, 2016
@jen20
Copy link
Contributor

jen20 commented Dec 27, 2016

Which is correct, but irritating from Terraform's perspective because you have no way of knowing that myrole no longer exists without testing first before each and every DCL

@sean- This is a fairly common case where things which are logically subresources are modelled as top level resources. It is also needed to deal with divergence caused by operator intervention - there is a specific operation for Exists which can be used on refresh for this.

This commit modifies the GNUmakefile used for starting PostgreSQL for
testing purposes to prefer the version installed via Homebrew, followed
by the version installed via MacPorts. POSTGRES, PSQL and INITDB may be
overriden as Make variables if neither of these package management
systems is in use.
Copy link
Contributor

@jen20 jen20 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few minor changes, but otherwise LGTM! Thanks @sean-!

@@ -70,7 +81,7 @@ func (c *Config) NewClient() (*Client, error) {
func (c *Client) Connect() (*sql.DB, error) {
db, err := sql.Open("postgres", c.connStr)
if err != nil {
return nil, fmt.Errorf("Error connecting to PostgreSQL server: %s", err)
return nil, fmt.Errorf("Error connecting to PostgreSQL server: %v", err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should switch all of these to use the err wrap.Wrapf pattern as we have been rolling through the rest of the code base.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There were only two call sites where fmt.Errorf() was still being used. Fixed the remaining two sites.

@@ -46,7 +46,7 @@ func resourcePostgreSQLDatabase() *schema.Resource {
Type: schema.TypeString,
Optional: true,
Computed: true,
Description: "The role name of the user who will own the new database",
Description: "The ROLE name who owns the database",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/who/that/ or s/who/which/? "Who" seems unusual for a non-person.

@@ -8,37 +8,86 @@ description: |-

# postgresql\_schema

The ``postgresql_schema`` resource creates and manages a schema within a
PostgreSQL database.
The ``postgresql_schema`` resource creates and manages a [schema
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/a// or s/objects/object

* `usage` - (Optional) Should the specified ROLE have USAGE privileges to the specified SCHEMA.
* `usage_with_grant` - (Optional) Should the specified ROLE have USAGE privileges to the specified SCHEMA and the ability to GRANT the USAGE privilege to other ROLEs.

~> **NOTE on `policy`:** The permissions of a role specified in multiple policy blocks is cumulative. For example, if the same role is specified in two different `policy` each with different permissions (e.g. `create` and `usage_with_grant`, respectively), then the specified role with have both `create` and `usage_with_grant` privileges.

## Import Example

`postgresql_schema` supports importing resources. Supposing the following
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We usually give the command line to import rather than the configuration code for import examples.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started there but added the examples because the import without a config is opaque to new users who want to move to TF. I can remove the import sample if you'd like but think it's a net win for users for the time being. Happy to go either way, however.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we at least need the import command line as well as the example code.

…Mutex.

Some of the checks didn't support concurrent updates.  This should
improve the reliability of the provider.
Copy link
Contributor

@jen20 jen20 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, with exception of last comment on import documentation.

* `usage` - (Optional) Should the specified ROLE have USAGE privileges to the specified SCHEMA.
* `usage_with_grant` - (Optional) Should the specified ROLE have USAGE privileges to the specified SCHEMA and the ability to GRANT the USAGE privilege to other ROLEs.

~> **NOTE on `policy`:** The permissions of a role specified in multiple policy blocks is cumulative. For example, if the same role is specified in two different `policy` each with different permissions (e.g. `create` and `usage_with_grant`, respectively), then the specified role with have both `create` and `usage_with_grant` privileges.

## Import Example

`postgresql_schema` supports importing resources. Supposing the following
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we at least need the import command line as well as the example code.

@sean- sean- merged commit 9dc7131 into master Dec 28, 2016
@sean- sean- deleted the f-postgresql-owner branch December 28, 2016 00:08
@sean- sean- removed the request for review from stack72 December 28, 2016 00:45
@ghost
Copy link

ghost commented Apr 18, 2020

I'm going to lock this issue because it has been closed for 30 days ⏳. This helps our maintainers find and focus on the active issues.

If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.

@ghost ghost locked and limited conversation to collaborators Apr 18, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants