Skip to main content

Contas de Service

Introdução

Service Account é o recurso utilizado para fornecer a uma aplicação ou serviço um meio de se identificar a recursos que necessitam de uma autenticação e checagem de autorização dessa identidade para que esses possam ser acessados.

Caso de Uso

Permitir que o serviço DataCrusher execute operações em apenas um bucket BigBucket existente na Organização Big Organization.

Requisições contra API de Object Storage usualmente requerem que tais requests sejam identificadas por uma Key ID e assinadas por uma Key Secret. A Key ID está diretamente relacionada a uma Account que identifica quem é o responsável por aquele acesso e também o Tenant a qual o recurso pertencer.

(Nota: Recursos estarāo sempre associados a um Tenant ao qual uma ou mais Accounts (de pessoas ou serviços) podem gerenciar seus recursos).

Para o cenário desse caso de uso, uma idéia seria um Admin criar uma chave de acesso (composta por Api Key, Api Key ID e Api Key Secret ) e desse tal chave ao DataCrusher para que esse pudesse acessar (apenas) o BigBucket.

É nesse momento que temos um problema: Essa chave está associada ao Account de um Admin, o que significa que requests recebidas com essa chave serão identificadas como Admin, não permitindo as APIs saberem que a request originou a partir do DataCrusher não permitindo limitar seus acessos.

Para endereçar esse problema, foi implementada o Service Account, que nada mais é que uma Account que está associada a um Tenant e é gerenciada pelas pessoas Admins desse Tenant. Essas pessoas podem também criar Api Keys para essa Service Account que agora, quando utilizadas na execução de requests, as APIs podem identificar a Service Account (pela claim "sa" contida no token de acesso) que podem agora possuir seu próprio conjunto de autorizações (permissões).

Gerenciamento de Contas de Serviço

Cloud Portal IAM

Em desenvolvimento

Magalu Cloud CLI

Em desenvolvimento

Chamadas de APIs

Em muitos cenários, pode ser conveniente o regenciamento de Contas de Serviço diretamente pelas APIs diponíveis, como por exemplo um sistema que automatiza criação de Contas de Serviço, criação de Chaves para essa conta de serviço, configuração de ambientes que utilizarão essas chaves, criação de privilégios, revogações, etc.

A maneira mais fácil de fazer isso, é ser criada uma Api Key que dê a essa aplicação escopo para gerenciar Service Accounts e utilizá-la em seus processos para obterem um Access Token e a partir daí, utilizar diretamente as APIs de Service Account

Para criarmos essa chave, devemos seguir os seguintes passos:

  • Efetuar Login no ID Magalu (https://id.magalu.com) e mudar o perfil para a Organização a qual deseja gerenciar as Service Accounts Mudança para Organização

No perfil da Organização desejada, acessar o menu Avançado -> Api Keys

Perfil Organização

  • Clicar no botão + Criar API Key Gerenciamento APIKey

  • Preencher as informações solicidas e nas permissões ir em Magalu Cloud -> ID Magalu -> Manage Service Account Criação APIKey

  • Anotar os dados da Api Key

Tela APIKey

ATENÇÃO: Essa API Key não é uma API Key de conta de serviço. Esta API Key pode ser trocada por um Access Token que poderá ser utilizada para gerenciar Service Account mas a identidade presente no Token (Subscriber) é o da pessoa que criou essa Api Key.

Python Snippets

Os snippets abaixo exemplificam como obter um token com escopo de gerenciamento de Service Account a partir de uma Api Key e como utilizá-lo para criar e lista uma Service Account.

"""
Basic Imports and Consts
"""
import requests # pip install requests
import jwt # pip install pyjwt

APIKEY = "9d2b3580-6cf2-44ce-8466-7d98a4ecaab3"
PA_URL = "https://id.magalu.com/account"
"""
Request Access Token
"""
def get_management_access_token():
path = "/api/v1/validate-request"
url = f"{PA_URL}{path}"
headers = {"x-api-key": APIKEY}

resp = requests.get(url, headers=headers)
resp.raise_for_status()

return resp.json().get("accessToken")
"""
Create Service Account
"""
def create():
path = "/api/v1/service-accounts"
url = f"{PA_URL}{path}"
headers = {"Authorization": get_management_access_token()}
payload = dict(
name="Service Account 1",
description="Service Account 1 Description",
email_prefix="sa_001"
)


resp = requests.post(url, json=payload, headers=headers)
resp.raise_for_status()

return resp.json()

>>> create()
{
'uuid': '8ff5bc5a-9914-4de6-9f77-ddea1a3320b9',
'email': 'sa_001@MGI-2765.sa.idmagalu.com'
}

def get():
path = "/api/v1/service-accounts"
url = f"{PA_URL}{path}"
headers = {"Authorization": get_management_access_token()}

resp = requests.get(url, headers=headers)
resp.raise_for_status()

return resp.json()

>>> get()
{
"metadata": {
"page": 1,
"limit": 50
},
"data": [
{
"uuid": "8ff5bc5a-9914-4de6-9f77-ddea1a3320b9",
"name": "Service Account 1",
"description": "Service Account 1 Description",
"email": "sa_001@MGI-2765.sa.idmagalu.com",
"tenant": {
"uuid": "a3c7dc30-742d-4882-98a8-ce0d9e2640cb",
"legal_name": "Debonzi PreProd Com"
}
},
]
}

Python CLI

O código abaixo implementa uma CLI totalmente funcional para o gerenciamento geral de Service Account e Service Account Api Key, todavia, a intenção desse código é servir apenas de referência.

"""
Exemplo CLI (quase) completa para gerenciamento de Service Accounts.\
"""
import json

import requests # pip install requests
import jwt # pip install pyjwt
import click # pip install click


APIKEY = "9d2b3580-6cf2-44ce-8466-7d98a4ecaab3"
PA_URL = "https://id.magalu.com/account"


########################################################################
# ID Magalu Service Account SDK
########################################################################
class APIKeyBasedClientSDK:
class Client:
def __init__(self, pa_url, apikey):
self.pa_url = pa_url
self.apikey = apikey
self.access_token = None

def get_endpoint(self, path):
return f"{self.pa_url}{path}"

def check_access_token(self, key):
path = "/api/v1/validate-request"
url, headers = self.prepare_request(path, key)

resp = requests.get(url, headers=headers)
resp.raise_for_status()

return resp.json().get("accessToken")

def refresh_access_token(self):
# TODO: Implement a proper refresh.
if self.access_token:
return self.access_token

self.access_token = self.check_access_token(self.apikey)
return self.access_token

def prepare_request(self, path, key=None):
url = self.get_endpoint(path)
headers = (
{"x-api-key": key}
if key
else {"Authorization": self.refresh_access_token()}
)

return url, headers

def __init__(self, pa_url, apikey):
self.client = APIKeyBasedClientSDK.Client(pa_url, apikey)
self.sa = ServiceAccount(self.client)
self.sakey = ServiceAccountAPIKey(self.client)

def decode_access_token(self, token=None):
return jwt.decode(
token if token else self.access_token,
algorithms=["RS256"],
options={"verify_signature": False},
)


class ServiceAccount:
def __init__(self, client):
self.client = client

def create(self, name, description, email_prefix):
path = "/api/v1/service-accounts"
url, headers = self.client.prepare_request(path)
payload = dict(
name=name,
description=description,
email_prefix=email_prefix
)

resp = requests.post(url, json=payload, headers=headers)
resp.raise_for_status()

return resp.json()

def list(self):
path = "/api/v1/service-accounts"
url, headers = self.client.prepare_request(path)

resp = requests.get(url, headers=headers)
resp.raise_for_status()

return resp.json()

def edit(self, sa_uuid, name, description):
path = f"/api/v1/service-accounts/{sa_uuid}"
url, headers = self.client.prepare_request(path)

payload = {}
if name:
payload.update(dict(name=name))
if description:
payload.update(dict(description=description))

resp = requests.patch(url, json=payload, headers=headers)
resp.raise_for_status()

return resp.json()

def delete(self, sa_uuid):
path = f"/api/v1/service-accounts/{sa_uuid}"
url, headers = self.client.prepare_request(path)

resp = requests.delete(url, headers=headers)
resp.raise_for_status()

return resp.json()


class ServiceAccountAPIKey:
def __init__(self, client):
self.client = client

def create(self, sa_uuid, name, description, scopes):
path = f"/api/v1/service-accounts/{sa_uuid}/api-keys"
url, headers = self.client.prepare_request(path)

_scopes = [dict(name=s) for s in scopes.split(",")]
payload = dict(name=name, description=description, scopes=_scopes)

resp = requests.post(url, json=payload, headers=headers)
resp.raise_for_status()

return resp.json()

def edit(self, sa_uuid, key_uuid, name=None, description=None, scopes=None):
path = f"/api/v1/service-accounts/{sa_uuid}/api-keys/{key_uuid}"
url, headers = self.client.prepare_request(path)

payload = {}
if name:
payload.update(dict(name=name))
if description:
payload.update(dict(description=description))
if scopes:
_scopes = [dict(name=s) for s in scopes.split(",")]
payload.update(dict(scopes=_scopes))

resp = requests.patch(url, json=payload, headers=headers)
resp.raise_for_status()

return resp.json()

def list(self, sa_uuid):
path = f"/api/v1/service-accounts/{sa_uuid}/api-keys"
url, headers = self.client.prepare_request(path)

resp = requests.get(url, headers=headers)
resp.raise_for_status()

return resp.json()

def check(self, key):
return self.client.check_access_token(key)

def revoke(self, sa_uuid, key_uuid):
path = f"/api/v1/service-accounts/{sa_uuid}/api-keys/{key_uuid}"
url, headers = self.client.prepare_request(path)

resp = requests.delete(url, headers=headers)
resp.raise_for_status()

return resp.json()


########################################################################
########################################################################


########################################################################
# Helpers
def print_json(body):
print(json.dumps(body, indent=4))


########################################################################
# Click Config
@click.group()
@click.pass_context
def cli(ctx):
ctx.obj = APIKeyBasedClientSDK(PA_URL, APIKEY)


pass_sdk = click.make_pass_decorator(APIKeyBasedClientSDK)


@cli.group()
def sa():
pass


@cli.group()
def key():
pass


########################################################################
# CLI Root
@cli.command
@pass_sdk
def show_token(sdk):
access_t = sdk.client.refresh_access_token()
decoded = sdk.decode_access_token(access_t)
print_json(decoded)

########################################################################
# CLI Service Account
@sa.command
@click.argument("name")
@click.argument("description")
@click.argument("email_prefix")
@pass_sdk
def create(sdk, name, description, email_prefix):
print_json(sdk.sa.create(name, description, email_prefix))


@sa.command
@pass_sdk
def list(sdk):
print_json(sdk.sa.list())


@sa.command
@click.argument("sa_uuid")
@pass_sdk
def delete(sdk, sa_uuid):
print_json(sdk.sa.delete(sa_uuid))


@sa.command
@click.argument("sa_uuid")
@click.option("-n", "--name", default=None)
@click.option("-d", "--description", default=None)
@pass_sdk
def edit(sdk, sa_uuid, name, description):
print_json(sdk.sa.edit(sa_uuid, name, description))

########################################################################
# CLI Service Account APIKey
@key.command
@click.argument("sa_uuid")
@click.argument("name")
@click.argument("description")
@click.argument("scopes")
@pass_sdk
def create(sdk, sa_uuid, name, description, scopes):
print_json(sdk.sakey.create(sa_uuid, name, description, scopes))


@key.command
@click.option("-d", "--description", default=None)
@click.option("-s", "--scopes", default="")
@pass_sdk
def edit(sdk, sa_uuid, key_uuid, name, description, scopes):
print_json(sdk.sakey.edit(sa_uuid, key_uuid, name, description, scopes))


@key.command
@click.argument("sa_uuid")
@pass_sdk
def list(sdk, sa_uuid):
print_json(sdk.sakey.list(sa_uuid))


@key.command
@click.argument("key")
@pass_sdk
def check(sdk, key):
token = sdk.sakey.check(key)
print_json(sdk.decode_access_token(token))


@key.command
@click.argument("sa_uuid")
@click.argument("key_uuid")
@pass_sdk
def revoke(sdk, sa_uuid, key_uuid):
print_json(sdk.sakey.revoke(sa_uuid, key_uuid))


if __name__ == "__main__":
cli()