Skip to content

Commit

Permalink
feat(usergroup): adds usergroup command
Browse files Browse the repository at this point in the history
Rapyuta.io supports creating user groups for granting users access to
project and perform access management. The CLI lacked support for it and
this commit introduces the command for usergroup management. Usergroup
can be created using the rio apply manifest. You can run the following
to see how to build a manifest

       rio explain usergroup

Here's how the command usage looks like:

Usage: rio usergroup [OPTIONS] COMMAND [ARGS]...

  Create and manage usergroups in organization

Options:
  --help  Show this message and exit.

Commands:
  delete   Delete usergroup from organization
  inspect  Inspect the usergroup resource
  list     List all user groups in selected organization
  • Loading branch information
swagnikdutta authored and pallabpain committed Aug 3, 2023
1 parent e06d99f commit c57891e
Show file tree
Hide file tree
Showing 13 changed files with 670 additions and 2 deletions.
1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ Rapyuta CLI has commands for all rapyuta.io resources. You can read more about t
Rosbag <rosbag>
Secret <secret>
Static Route <static_route>
User Group <usergroup>
VPN <vpn>


Expand Down
10 changes: 10 additions & 0 deletions docs/source/usergroup.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
User Group
============

.. toctree::
:maxdepth: 3
:caption: Contents:

.. click:: riocli.usergroup:usergroup
:prog: rio usergroup
:nested: full
18 changes: 18 additions & 0 deletions riocli/apply/manifests/usergroup.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
apiVersion: api.rapyuta.io/v2
kind: UserGroup
metadata:
name: usergroup_name
organization: org-bqgpmsafgnvnawlkuvxtxohs
labels:
key: value
spec:
description: This is a sample user group description
members:
- emailID: [email protected]
- emailID: [email protected]
admins:
- emailID: [email protected]
projects:
- name: project01
- name: project02
14 changes: 12 additions & 2 deletions riocli/apply/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from riocli.project.model import Project
from riocli.secret.model import Secret
from riocli.static_route.model import StaticRoute
from riocli.usergroup.model import UserGroup


class _Singleton(type):
Expand All @@ -55,7 +56,8 @@ class ResolverCache(object, metaclass=_Singleton):
'Package': Package,
'Disk': Disk,
'Deployment': Deployment,
"ManagedService": ManagedService
"ManagedService": ManagedService,
'UserGroup': UserGroup,
}

KIND_REGEX = {
Expand Down Expand Up @@ -99,7 +101,12 @@ def find_depends(self, depends, *args):
elif 'guid' in depends and depends['kind'] not in ('network', 'managedservice'):
return depends['guid'], None
elif 'nameOrGUID' in depends:
obj_list = self._list_functors(depends['kind'])()
if depends['kind'] == 'usergroup':
org_guid = depends['organization']
obj_list = self._list_functors(depends['kind'])(org_guid)
else:
obj_list = self._list_functors(depends['kind'])()

obj_match = list(self._find_functors(depends['kind'])(
depends['nameOrGUID'], obj_list, *args))
if not obj_list or (isinstance(obj_list, list) and len(obj_list) == 0):
Expand All @@ -123,6 +130,7 @@ def _guid_functor(self, kind):
"disk": lambda x: munchify(x)['internalDeploymentGUID'],
"device": lambda x: munchify(x)['uuid'],
"managedservice": lambda x: munchify(x)['metadata']['name'],
"usergroup": lambda x: munchify(x).guid
}
return mapping[kind]

Expand All @@ -140,6 +148,7 @@ def _list_functors(self, kind):
"disk": self._list_disks,
"device": self.client.get_all_devices,
"managedservice": self._list_managedservices,
"usergroup": self.client.list_usergroups
}

return mapping[kind]
Expand All @@ -158,6 +167,7 @@ def _find_functors(self, kind):
"disk": self._generate_find_guid_functor(),
"device": self._generate_find_guid_functor(),
"managedservice": lambda name, instances: filter(lambda i: i.metadata.name == name, instances),
"usergroup": lambda name, groups: filter(lambda i: i.name == name, groups),
}

return mapping[kind]
Expand Down
2 changes: 2 additions & 0 deletions riocli/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
from riocli.secret import secret
from riocli.shell import shell, deprecated_repl
from riocli.static_route import static_route
from riocli.usergroup import usergroup
from riocli.vpn import vpn


Expand Down Expand Up @@ -97,3 +98,4 @@ def version():
cli.add_command(template)
cli.add_command(organization)
cli.add_command(vpn)
cli.add_command(usergroup)
113 changes: 113 additions & 0 deletions riocli/jsonschema/schemas/usergroup-schema.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
---
$schema: http://json-schema.org/draft-07/schema#
title: UserGroup
description: A construct in rapyuta.io that allows one to grant access to projects to multiple users at once
$ref: "#/definitions/usergroup"
definitions:
usergroup:
type: object
properties:
apiVersion:
const: api.rapyuta.io/v2
default: api.rapyuta.io/v2
kind:
const: UserGroup
metadata:
"$ref": "#/definitions/metadata"
spec:
"$ref": "#/definitions/usergroupSpec"

required:
- apiVersion
- kind
- metadata
- spec

metadata:
type: object
properties:
name:
type: string
guid:
$ref: "#/definitions/uuid"
creator:
$ref: "#/definitions/uuid"
project:
$ref: "#/definitions/projectGUID"
organization:
$ref: "#/definitions/organizationGUID"
labels:
$ref: "#/definitions/stringMap"
uniqueItems: true

required:
- name
- organization

usergroupSpec:
type: object
properties:
description:
type: string
members:
type: array
items:
"$ref": "#/definitions/member"
admins:
type: array
items:
"$ref": "#/definitions/member"
projects:
type: array
items:
"$ref": "#/definitions/project"
additionalProperties: false

member:
type: object
properties:
guid:
$ref: "#/definitions/uuid"
emailID:
$ref: "#/definitions/email"

oneOf:
- required:
- guid
- required:
- emailID

project:
type: object
properties:
guid:
type: string
pattern: "^project-([a-z0-9]{20}|[a-z]{24})$"
name:
type: string
oneOf:
- required:
- guid
- required:
- name

stringMap:
type: object
additionalProperties:
type: string

projectGUID:
type: string
pattern: "^project-([a-z0-9]{20}|[a-z]{24})$"

organizationGUID:
type: string
pattern: "^org-([a-z0-9]{20}|[a-z]{24})$"

uuid:
type: string
pattern: "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$"

email:
type: string
pattern: "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9]{2,}$"
55 changes: 55 additions & 0 deletions riocli/organization/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Copyright 2023 Rapyuta Robotics
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import typing

from rapyuta_io.utils import RestClient
from rapyuta_io.utils.rest_client import HttpMethod

from riocli.config import Configuration


def _api_call(
method: str,
path: typing.Union[str, None] = None,
payload: typing.Union[typing.Dict, None] = None,
load_response: bool = True,
) -> typing.Any:
config = Configuration()
coreapi_host = config.data.get(
'core_api_host',
'https://gaapiserver.apps.rapyuta.io'
)

url = '{}/api/organization'.format(coreapi_host)
if path:
url = '{}/{}'.format(url, path)

headers = config.get_auth_header()
response = RestClient(url).method(method).headers(headers).execute(
payload=payload)

data = None

if load_response:
data = response.json()

if not response.ok:
err_msg = data.get('error')
raise Exception(err_msg)

return data


def get_organization_details(organization_guid):
return _api_call(HttpMethod.GET, '{}/get'.format(organization_guid))
39 changes: 39 additions & 0 deletions riocli/usergroup/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Copyright 2023 Rapyuta Robotics
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import click
from click_help_colors import HelpColorsGroup

from riocli.constants import Colors
from riocli.usergroup.delete import delete_usergroup
from riocli.usergroup.inspect import inspect_usergroup
from riocli.usergroup.list import list_usergroup


@click.group(
invoke_without_command=False,
cls=HelpColorsGroup,
help_headers_color=Colors.YELLOW,
help_options_color=Colors.GREEN,
)
def usergroup() -> None:
"""
Manage usergroups on rapyuta.io
"""
pass


usergroup.add_command(list_usergroup)
usergroup.add_command(inspect_usergroup)
usergroup.add_command(delete_usergroup)
60 changes: 60 additions & 0 deletions riocli/usergroup/delete.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Copyright 2023 Rapyuta Robotics
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import click
from click_help_colors import HelpColorsCommand
from yaspin.api import Yaspin

from riocli.config import new_client
from riocli.constants import Colors, Symbols
from riocli.usergroup.util import name_to_guid
from riocli.utils.spinner import with_spinner


@click.command(
'delete',
cls=HelpColorsCommand,
help_headers_color=Colors.YELLOW,
help_options_color=Colors.GREEN,
)
@click.option('--force', '-f', '--silent', 'force', is_flag=True,
default=False, help='Skip confirmation')
@click.argument('group-name')
@click.pass_context
@with_spinner(text="Deleting user group...")
@name_to_guid
def delete_usergroup(
ctx: click.Context,
group_name: str,
group_guid: str,
force: bool,
spinner: Yaspin = None,
) -> None:
"""
Delete usergroup from organization
"""
if not force:
with spinner.hidden():
click.confirm('Deleting usergroup {} ({})'.format(group_name, group_guid), abort=True)

try:
client = new_client()
org_guid = ctx.obj.data.get('organization_id')
client.delete_usergroup(org_guid, group_guid)
spinner.text = click.style('User group deleted successfully.', fg=Colors.GREEN)
spinner.green.ok(Symbols.SUCCESS)
except Exception as e:
spinner.text = click.style('Failed to delete usergroup: {}'.format(e), Colors.RED)
spinner.red.fail(Symbols.ERROR)
raise SystemExit(1) from e
Loading

0 comments on commit c57891e

Please sign in to comment.