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

feat: Role, ClusterRole, RoleBinding, ClusterRoleBinding L2s #432

Merged
merged 42 commits into from
May 7, 2022

Conversation

Chriscbr
Copy link
Contributor

@Chriscbr Chriscbr commented Mar 11, 2022

Fixes #24
Fixes #25
Fixes #26
Fixes #374

Adds Role and ClusterRole L2s with "allowXxx" methods that resemble those available in the AWS CDK. This allows you to write code like:

import * as kplus from 'cdk8s-plus-23';

declare const deployment: kplus.Deployment;

const role = new kplus.ClusterRole(chart, 'my-cluster-role');
role.allowRead(deployment, kplus.ApiResource.SECRETS, kplus.ApiResource.PODS);

...where the "read" syntactic sugar grants permissions to the "get", "list", and "watch" commands. There are other allow commands available that offer more granularity if needed.

RoleBinding and ClusterRoleBinding L2s have also been created, and they can be instantiated through their constructors, or created using the appropriate bind methods that will attach it to the role automatically:

declare const role: kplus.Role;
declare const clusterRole: kplus.ClusterRole;

const user = new kplus.User({ name: 'alice@example.com' });
const group = new kplus.Group({ name: 'frontend-devs' });

role.bind(user, group); // creates a RoleBinding associated with the same namespace `role` was defined with
clusterRole.bindInNamespace('development', user, group); // creates a RoleBinding associated with namespace "development"
clusterRole.bind(user, group); // creates a ClusterRoleBinding, not associated with any namespace

Future work:

  • Add full pod.grantRead(role) and Resources.fromTypes(ApiResource.POD).grantRead(role) style APIs
  • Add type checking or validation to prevent adding namespaced resources to ClusterRole or cluster-wide resources to Role's etc.
  • Add more constants to ApiResource for granting permissions to subresources
  • Add convenience APIs for adding permissions to the default roles using aggregate-to-<default-role> tags

BREAKING CHANGE: The interface Resources is now named ContainerResources.

Signed-off-by: Christopher Rybicki rybickic@amazon.com

Signed-off-by: Christopher Rybicki <rybickic@amazon.com>
@Chriscbr Chriscbr requested a review from iliapolo March 11, 2022 01:18
github-actions and others added 2 commits March 11, 2022 01:20
Signed-off-by: github-actions <github-actions@github.com>
Signed-off-by: Christopher Rybicki <rybickic@amazon.com>
@Chriscbr Chriscbr marked this pull request as draft March 15, 2022 00:06
@iliapolo
Copy link
Member

Suggestion for API:

const role = new Role(this, 'Role', { namespace: 'default'});
role.grantList(Grantee.fromObjects(deployment, pod));
role.grantList(Grantee.fromKind(Kind.Secrets, Kind.Deployments));
role.grantList({
  objects: [deployment, pod],
});
role.grantList({
  kinds: ['secrets', 'deployments'],
});

const binding = role.bind(user1, group1);
binding.addSubject(serviceAccount);
const role = new kplus.ClusterRole(chart, 'grafana-operator');
    role.addRule({
      apiGroups: [''],
      resources: ['events'],
      verbs: ['get', 'watch', 'list', 'create', 'delete', 'update', 'patch'],
    });
    role.addRule({
      apiGroups: ['integreatly.org'],
      resources: [
        'grafanadashboards',
        'grafanadatasources',
        'grafanadatasources/status',
      ],
      verbs: ['get', 'list', 'create', 'update', 'delete', 'deletecollection', 'watch'],
    });

Would be:

const role = new Role(this, 'Role');
role.grantGet(Kind.EVENTS);
role.grantWatch(Kind.EVENTS);
role.grantList(Kind.EVENTS);

role.grantGet(Kind.custom({ resources: 'grafanadashboards', 'grafanadatasources', apiGroup: 'integreatly.org'} 

Signed-off-by: Christopher Rybicki <rybickic@amazon.com>
@Chriscbr Chriscbr marked this pull request as ready for review March 17, 2022 03:16
Signed-off-by: Christopher Rybicki <rybickic@amazon.com>
Signed-off-by: Christopher Rybicki <rybickic@amazon.com>
Signed-off-by: Christopher Rybicki <rybickic@amazon.com>
Signed-off-by: Christopher Rybicki <rybickic@amazon.com>
Signed-off-by: Christopher Rybicki <rybickic@amazon.com>
@Chriscbr Chriscbr changed the title feat: Role and ClusterRole L2s feat: Role, ClusterRole, RoleBinding, ClusterRoleBinding L2s Mar 19, 2022
@Chriscbr Chriscbr self-assigned this Mar 19, 2022
src/grants.ts Outdated
Comment on lines 34 to 49
/**
* Combine multiple resource kinds into a single grantee that can be used in a
* Role or ClusterRole.
*/
public static fromKinds(...types: ResourceType[]): IGrantee {
const apiGroups = normalizedArray(types
.map(type => type.apiGroups)
.flat(),
);
const resources = normalizedArray(types
.map(type => type.resources)
.filter((x): x is string[] => Boolean(x))
.flat(),
);
return { apiGroups, resources };
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This isn't being used, since I designed ResourceType to implement IGrantee as is. Should I drop it?

Signed-off-by: Christopher Rybicki <rybickic@amazon.com>
@iliapolo
Copy link
Member

iliapolo commented Mar 27, 2022

@Chriscbr I think ApiResource is the eureka name we've been looking for! Good idea.

Both pods and a single pod named foo represent an ApiResource (i.e one that can be managed via the API) - Its just that one has a type, while the other has a name.

So, if we define a common interface:

export interface IApiResource {
  // mandatory, both pods and a single pod has this.
  readonly apiGroup: string;

  // optional - pods will have this
  readonly type?: string;

  // optional - a single will have this
  readonly name?: string;
}

Then, our Resource class actually already implements this interface.

export abstract class Resource extends Construct implements IResource, IApiResource {

If we now make ApiResource implement it:

export class ApiResource implements IApiResource {
  /**
   * API resource information for Binding.
   */
  public static readonly BINDING = new ApiResource({
    apiGroups: [''],
    resources: ['bindings'],
  });
  ....
  ....
}

We can change the allow methods to accept:

public allow(verbs: string[], ...resources: IApiResource[]): void {

And viola, we have the API we wanted:

role.allowRead(deployment, kplus.ApiResource.SECRETS, kplus.ApiResource.PODS);

Thoughts?

Signed-off-by: Christopher Rybicki <rybickic@amazon.com>
Signed-off-by: Christopher Rybicki <rybickic@amazon.com>
Signed-off-by: Christopher Rybicki <rybickic@amazon.com>
Signed-off-by: Christopher Rybicki <rybickic@amazon.com>
Signed-off-by: Christopher Rybicki <rybickic@amazon.com>
Signed-off-by: github-actions <github-actions@github.com>
Chriscbr added 8 commits April 5, 2022 13:50
Signed-off-by: Christopher Rybicki <rybickic@amazon.com>
Signed-off-by: Christopher Rybicki <rybickic@amazon.com>
Signed-off-by: Christopher Rybicki <rybickic@amazon.com>
Signed-off-by: Christopher Rybicki <rybickic@amazon.com>
Signed-off-by: Christopher Rybicki <rybickic@amazon.com>
Signed-off-by: Christopher Rybicki <rybickic@amazon.com>
Signed-off-by: Christopher Rybicki <rybickic@amazon.com>
Signed-off-by: Christopher Rybicki <rybickic@amazon.com>
@Chriscbr Chriscbr requested a review from iliapolo April 5, 2022 21:49
@Chriscbr
Copy link
Contributor Author

Chriscbr commented Apr 5, 2022

Thanks for all of the feedback @iliapolo ! This should be ready for another review 🙂

iliapolo and others added 6 commits April 26, 2022 17:44
Signed-off-by: Eli Polonsky <epolon@amazon.com>
Signed-off-by: Eli Polonsky <epolon@amazon.com>
Signed-off-by: github-actions <github-actions@github.com>
@Chriscbr Following our conversation, I think we need to take a somewhat different approach. 

We've been trying to optimize for code maintenance by creating a common `RoleBase` that gives us some common functionality between a `Role` and a `ClusterRole`. However, we should be optimizing for user experience. 

A `Role` can only define rules for resources, where as a `ClusterRole` can define rules both for resources and non-resources. Trying to express this inherent difference, while still keeping a common base class, is I think what gets us in trouble. The solution we came up with is to have a common `addResourceRule` method in the base class, and have `ClusterRole` add an `addNonResourceRule` method that will only apply to it. 

This will result in the following API:

```ts
const role = new kplus.Role(...);
role.addResourceRule(...);
role.allow(...);

const clusterRole = new kplus.Role(...);
clusterRole.addResourceRule(...);
clusterRole.addNonResourceRule(...);
clusterRole.allow(...);
```

There are several problems with this:

- Naming inconsistency (`addResourceRule` vs `allow` - even though both refer to resource rules). Note that if we change `addResourceRule -> addRule`, it won't be clear why `ClusterRole` has `addRule` and `addNonResourceRule`.
- The `add` methods don't offer any capabilities that the `allow` method don't. It creates a situation where we have two API's offering the same thing - which is confusing. I'm not sure the `add` methods have merit in higher level API's, the whole point is to remove this bulky k8s structure. If users want to use the underlying structure instead, they can always escape hatch. 

If we focus on optimizing user experience for the common use-case, I think the API should look like this: 

```ts
const role = new kplus.Role(...);
// allow reading a specific pod and all secrets
role.allowGet(pod, ApiResource.SECRETS);

const clusterRole = new kplus.ClusterRole(...);
// allow reading a specific pod, all secrets, and the /healthz endpoint
clusterRole.allowGet(pod, ApiResource.SECRETS, ApiEndpoint.of('/healthz'));
```

Note that a `Role` can be given only api resources, but a `ClusterRole` can be given non api resources as well, all with the same API. 

This PR adds an `ApiEndpoint` (this name is also used in k8s docs: *non-resource endpoints (like /healthz)*) interface that allows `ClusterRole` to accept everything it can. It also removes the `add` methods in favor of the existing `allow` ones. 

Note that the base was class was completely removed, which of course resulted in a some duplication, but I think its worth it for the flexibility it gives. 

#### Resources

- https://octopus.com/blog/k8s-rbac-roles-and-bindings
- https://kubernetes.io/docs/reference/access-authn-authz/rbac/
Signed-off-by: Eli Polonsky <epolon@amazon.com>
Signed-off-by: Eli Polonsky <epolon@amazon.com>
@iliapolo iliapolo changed the title feat: Role, ClusterRole, RoleBinding, ClusterRoleBinding L2s feat: Role, ClusterRole, RoleBinding, ClusterRoleBinding L2s May 7, 2022
Signed-off-by: Eli Polonsky <epolon@amazon.com>
@mergify mergify bot merged commit aeaba6e into k8s-22/main May 7, 2022
@mergify mergify bot deleted the rybickic/roles branch May 7, 2022 13:51
@cdk8s-automation
Copy link
Contributor

💔 All backports failed

Status Branch Result
k8s-21/main Backport failed because of merge conflicts
k8s-20/main Backport failed because of merge conflicts

Manual backport

To create the backport manually run:

backport --pr 432

Questions ?

Please refer to the Backport tool documentation

iliapolo pushed a commit that referenced this pull request May 7, 2022
…432)

Fixes #24
Fixes #25
Fixes #26
Fixes #374

Adds Role and ClusterRole L2s with "allowXxx" methods that resemble those available in the AWS CDK. This allows you to write code like:

```ts
import * as kplus from 'cdk8s-plus-23';

declare const deployment: kplus.Deployment;

const role = new kplus.ClusterRole(chart, 'my-cluster-role');
role.allowRead(deployment, kplus.ApiResource.SECRETS, kplus.ApiResource.PODS);
```

...where the "read" syntactic sugar grants permissions to the "get", "list", and "watch" commands. There are other allow commands available that offer more granularity if needed.

RoleBinding and ClusterRoleBinding L2s have also been created, and they can be instantiated through their constructors, or created using the appropriate bind methods that will attach it to the role automatically:

```ts
declare const role: kplus.Role;
declare const clusterRole: kplus.ClusterRole;

const user = new kplus.User({ name: 'alice@example.com' });
const group = new kplus.Group({ name: 'frontend-devs' });

role.bind(user, group); // creates a RoleBinding associated with the same namespace `role` was defined with
clusterRole.bindInNamespace('development', user, group); // creates a RoleBinding associated with namespace "development"
clusterRole.bind(user, group); // creates a ClusterRoleBinding, not associated with any namespace
```

Future work:
- Add full `pod.grantRead(role)` and `Resources.fromTypes(ApiResource.POD).grantRead(role)` style APIs
- Add type checking or validation to prevent adding namespaced resources to ClusterRole or cluster-wide resources to Role's etc.
- Add more constants to `ApiResource` for granting permissions to [subresources](https://stackoverflow.com/questions/57872201/how-to-refer-to-all-subresources-in-a-role-definition)
- Add convenience APIs for adding permissions to the [default roles](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#default-roles-and-role-bindings) using `aggregate-to-<default-role>` tags

BREAKING CHANGE: The interface `Resources` is now named `ContainerResources`.

Signed-off-by: Christopher Rybicki <rybickic@amazon.com>
(cherry picked from commit aeaba6e)
Signed-off-by: Eli Polonsky <epolon@amazon.com>

# Conflicts:
#	.projen/tasks.json
#	package.json
#	src/ingress-v1beta1.ts
@iliapolo
Copy link
Member

iliapolo commented May 7, 2022

💚 All backports created successfully

Status Branch Result
k8s-21/main

Questions ?

Please refer to the Backport tool documentation

iliapolo pushed a commit that referenced this pull request May 7, 2022
…432)

Fixes #24
Fixes #25
Fixes #26
Fixes #374

Adds Role and ClusterRole L2s with "allowXxx" methods that resemble those available in the AWS CDK. This allows you to write code like:

```ts
import * as kplus from 'cdk8s-plus-23';

declare const deployment: kplus.Deployment;

const role = new kplus.ClusterRole(chart, 'my-cluster-role');
role.allowRead(deployment, kplus.ApiResource.SECRETS, kplus.ApiResource.PODS);
```

...where the "read" syntactic sugar grants permissions to the "get", "list", and "watch" commands. There are other allow commands available that offer more granularity if needed.

RoleBinding and ClusterRoleBinding L2s have also been created, and they can be instantiated through their constructors, or created using the appropriate bind methods that will attach it to the role automatically:

```ts
declare const role: kplus.Role;
declare const clusterRole: kplus.ClusterRole;

const user = new kplus.User({ name: 'alice@example.com' });
const group = new kplus.Group({ name: 'frontend-devs' });

role.bind(user, group); // creates a RoleBinding associated with the same namespace `role` was defined with
clusterRole.bindInNamespace('development', user, group); // creates a RoleBinding associated with namespace "development"
clusterRole.bind(user, group); // creates a ClusterRoleBinding, not associated with any namespace
```

Future work:
- Add full `pod.grantRead(role)` and `Resources.fromTypes(ApiResource.POD).grantRead(role)` style APIs
- Add type checking or validation to prevent adding namespaced resources to ClusterRole or cluster-wide resources to Role's etc.
- Add more constants to `ApiResource` for granting permissions to [subresources](https://stackoverflow.com/questions/57872201/how-to-refer-to-all-subresources-in-a-role-definition)
- Add convenience APIs for adding permissions to the [default roles](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#default-roles-and-role-bindings) using `aggregate-to-<default-role>` tags

BREAKING CHANGE: The interface `Resources` is now named `ContainerResources`.

Signed-off-by: Christopher Rybicki <rybickic@amazon.com>
(cherry picked from commit aeaba6e)
Signed-off-by: Eli Polonsky <epolon@amazon.com>

# Conflicts:
#	.projen/tasks.json
#	.projenrc.ts
#	package.json
#	src/ingress-v1beta1.ts
@iliapolo
Copy link
Member

iliapolo commented May 7, 2022

💚 All backports created successfully

Status Branch Result
k8s-20/main

Questions ?

Please refer to the Backport tool documentation

iliapolo added a commit that referenced this pull request May 7, 2022
…432) (#685)

* feat: `Role`, `ClusterRole`, `RoleBinding`, `ClusterRoleBinding` L2s (#432)

Fixes #24
Fixes #25
Fixes #26
Fixes #374

Adds Role and ClusterRole L2s with "allowXxx" methods that resemble those available in the AWS CDK. This allows you to write code like:

```ts
import * as kplus from 'cdk8s-plus-23';

declare const deployment: kplus.Deployment;

const role = new kplus.ClusterRole(chart, 'my-cluster-role');
role.allowRead(deployment, kplus.ApiResource.SECRETS, kplus.ApiResource.PODS);
```

...where the "read" syntactic sugar grants permissions to the "get", "list", and "watch" commands. There are other allow commands available that offer more granularity if needed.

RoleBinding and ClusterRoleBinding L2s have also been created, and they can be instantiated through their constructors, or created using the appropriate bind methods that will attach it to the role automatically:

```ts
declare const role: kplus.Role;
declare const clusterRole: kplus.ClusterRole;

const user = new kplus.User({ name: 'alice@example.com' });
const group = new kplus.Group({ name: 'frontend-devs' });

role.bind(user, group); // creates a RoleBinding associated with the same namespace `role` was defined with
clusterRole.bindInNamespace('development', user, group); // creates a RoleBinding associated with namespace "development"
clusterRole.bind(user, group); // creates a ClusterRoleBinding, not associated with any namespace
```

Future work:
- Add full `pod.grantRead(role)` and `Resources.fromTypes(ApiResource.POD).grantRead(role)` style APIs
- Add type checking or validation to prevent adding namespaced resources to ClusterRole or cluster-wide resources to Role's etc.
- Add more constants to `ApiResource` for granting permissions to [subresources](https://stackoverflow.com/questions/57872201/how-to-refer-to-all-subresources-in-a-role-definition)
- Add convenience APIs for adding permissions to the [default roles](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#default-roles-and-role-bindings) using `aggregate-to-<default-role>` tags

BREAKING CHANGE: The interface `Resources` is now named `ContainerResources`.

Signed-off-by: Christopher Rybicki <rybickic@amazon.com>
(cherry picked from commit aeaba6e)
Signed-off-by: Eli Polonsky <epolon@amazon.com>

# Conflicts:
#	.projen/tasks.json
#	package.json
#	src/ingress-v1beta1.ts

* chore: self mutation

Signed-off-by: github-actions <github-actions@github.com>

Co-authored-by: Christopher Rybicki <rybickic@amazon.com>
Co-authored-by: github-actions <github-actions@github.com>
iliapolo added a commit that referenced this pull request May 7, 2022
…432) (#686)

* feat: `Role`, `ClusterRole`, `RoleBinding`, `ClusterRoleBinding` L2s (#432)

Fixes #24
Fixes #25
Fixes #26
Fixes #374

Adds Role and ClusterRole L2s with "allowXxx" methods that resemble those available in the AWS CDK. This allows you to write code like:

```ts
import * as kplus from 'cdk8s-plus-23';

declare const deployment: kplus.Deployment;

const role = new kplus.ClusterRole(chart, 'my-cluster-role');
role.allowRead(deployment, kplus.ApiResource.SECRETS, kplus.ApiResource.PODS);
```

...where the "read" syntactic sugar grants permissions to the "get", "list", and "watch" commands. There are other allow commands available that offer more granularity if needed.

RoleBinding and ClusterRoleBinding L2s have also been created, and they can be instantiated through their constructors, or created using the appropriate bind methods that will attach it to the role automatically:

```ts
declare const role: kplus.Role;
declare const clusterRole: kplus.ClusterRole;

const user = new kplus.User({ name: 'alice@example.com' });
const group = new kplus.Group({ name: 'frontend-devs' });

role.bind(user, group); // creates a RoleBinding associated with the same namespace `role` was defined with
clusterRole.bindInNamespace('development', user, group); // creates a RoleBinding associated with namespace "development"
clusterRole.bind(user, group); // creates a ClusterRoleBinding, not associated with any namespace
```

Future work:
- Add full `pod.grantRead(role)` and `Resources.fromTypes(ApiResource.POD).grantRead(role)` style APIs
- Add type checking or validation to prevent adding namespaced resources to ClusterRole or cluster-wide resources to Role's etc.
- Add more constants to `ApiResource` for granting permissions to [subresources](https://stackoverflow.com/questions/57872201/how-to-refer-to-all-subresources-in-a-role-definition)
- Add convenience APIs for adding permissions to the [default roles](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#default-roles-and-role-bindings) using `aggregate-to-<default-role>` tags

BREAKING CHANGE: The interface `Resources` is now named `ContainerResources`.

Signed-off-by: Christopher Rybicki <rybickic@amazon.com>
(cherry picked from commit aeaba6e)
Signed-off-by: Eli Polonsky <epolon@amazon.com>

# Conflicts:
#	.projen/tasks.json
#	.projenrc.ts
#	package.json
#	src/ingress-v1beta1.ts

* conflict

Signed-off-by: Eli Polonsky <epolon@amazon.com>

Co-authored-by: Christopher Rybicki <rybickic@amazon.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

RoleBinding L2 ClusterRoleBinding L2 ClusterRole L2 Role L2
3 participants