b9996e6bbe
Also update the Docs and fixup the HTTP API to return proper errors when someone attempts to use Namespaces with an OSS agent. Add Namespace HTTP API docs Make all API endpoints disallow unknown fields
487 lines
16 KiB
Go
487 lines
16 KiB
Go
package acl
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/consul/agent/structs"
|
|
"github.com/hashicorp/consul/api"
|
|
"github.com/mitchellh/cli"
|
|
)
|
|
|
|
func PrintToken(token *api.ACLToken, ui cli.Ui, showMeta bool) {
|
|
ui.Info(fmt.Sprintf("AccessorID: %s", token.AccessorID))
|
|
ui.Info(fmt.Sprintf("SecretID: %s", token.SecretID))
|
|
if token.Namespace != "" {
|
|
ui.Info(fmt.Sprintf("Namespace: %s", token.Namespace))
|
|
}
|
|
ui.Info(fmt.Sprintf("Description: %s", token.Description))
|
|
ui.Info(fmt.Sprintf("Local: %t", token.Local))
|
|
ui.Info(fmt.Sprintf("Create Time: %v", token.CreateTime))
|
|
if token.ExpirationTime != nil && !token.ExpirationTime.IsZero() {
|
|
ui.Info(fmt.Sprintf("Expiration Time: %v", *token.ExpirationTime))
|
|
}
|
|
if showMeta {
|
|
ui.Info(fmt.Sprintf("Hash: %x", token.Hash))
|
|
ui.Info(fmt.Sprintf("Create Index: %d", token.CreateIndex))
|
|
ui.Info(fmt.Sprintf("Modify Index: %d", token.ModifyIndex))
|
|
}
|
|
if len(token.Policies) > 0 {
|
|
ui.Info(fmt.Sprintf("Policies:"))
|
|
for _, policy := range token.Policies {
|
|
ui.Info(fmt.Sprintf(" %s - %s", policy.ID, policy.Name))
|
|
}
|
|
}
|
|
if len(token.Roles) > 0 {
|
|
ui.Info(fmt.Sprintf("Roles:"))
|
|
for _, role := range token.Roles {
|
|
ui.Info(fmt.Sprintf(" %s - %s", role.ID, role.Name))
|
|
}
|
|
}
|
|
if len(token.ServiceIdentities) > 0 {
|
|
ui.Info(fmt.Sprintf("Service Identities:"))
|
|
for _, svcid := range token.ServiceIdentities {
|
|
if len(svcid.Datacenters) > 0 {
|
|
ui.Info(fmt.Sprintf(" %s (Datacenters: %s)", svcid.ServiceName, strings.Join(svcid.Datacenters, ", ")))
|
|
} else {
|
|
ui.Info(fmt.Sprintf(" %s (Datacenters: all)", svcid.ServiceName))
|
|
}
|
|
}
|
|
}
|
|
if token.Rules != "" {
|
|
ui.Info(fmt.Sprintf("Rules:"))
|
|
ui.Info(token.Rules)
|
|
}
|
|
}
|
|
|
|
func PrintTokenListEntry(token *api.ACLTokenListEntry, ui cli.Ui, showMeta bool) {
|
|
ui.Info(fmt.Sprintf("AccessorID: %s", token.AccessorID))
|
|
if token.Namespace != "" {
|
|
ui.Info(fmt.Sprintf("Namespace: %s", token.Namespace))
|
|
}
|
|
ui.Info(fmt.Sprintf("Description: %s", token.Description))
|
|
ui.Info(fmt.Sprintf("Local: %t", token.Local))
|
|
ui.Info(fmt.Sprintf("Create Time: %v", token.CreateTime))
|
|
if token.ExpirationTime != nil && !token.ExpirationTime.IsZero() {
|
|
ui.Info(fmt.Sprintf("Expiration Time: %v", *token.ExpirationTime))
|
|
}
|
|
ui.Info(fmt.Sprintf("Legacy: %t", token.Legacy))
|
|
if showMeta {
|
|
ui.Info(fmt.Sprintf("Hash: %x", token.Hash))
|
|
ui.Info(fmt.Sprintf("Create Index: %d", token.CreateIndex))
|
|
ui.Info(fmt.Sprintf("Modify Index: %d", token.ModifyIndex))
|
|
}
|
|
if len(token.Policies) > 0 {
|
|
ui.Info(fmt.Sprintf("Policies:"))
|
|
for _, policy := range token.Policies {
|
|
ui.Info(fmt.Sprintf(" %s - %s", policy.ID, policy.Name))
|
|
}
|
|
}
|
|
if len(token.Roles) > 0 {
|
|
ui.Info(fmt.Sprintf("Roles:"))
|
|
for _, role := range token.Roles {
|
|
ui.Info(fmt.Sprintf(" %s - %s", role.ID, role.Name))
|
|
}
|
|
}
|
|
if len(token.ServiceIdentities) > 0 {
|
|
ui.Info(fmt.Sprintf("Service Identities:"))
|
|
for _, svcid := range token.ServiceIdentities {
|
|
if len(svcid.Datacenters) > 0 {
|
|
ui.Info(fmt.Sprintf(" %s (Datacenters: %s)", svcid.ServiceName, strings.Join(svcid.Datacenters, ", ")))
|
|
} else {
|
|
ui.Info(fmt.Sprintf(" %s (Datacenters: all)", svcid.ServiceName))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func PrintPolicy(policy *api.ACLPolicy, ui cli.Ui, showMeta bool) {
|
|
ui.Info(fmt.Sprintf("ID: %s", policy.ID))
|
|
ui.Info(fmt.Sprintf("Name: %s", policy.Name))
|
|
if policy.Namespace != "" {
|
|
ui.Info(fmt.Sprintf("Namespace: %s", policy.Namespace))
|
|
}
|
|
ui.Info(fmt.Sprintf("Description: %s", policy.Description))
|
|
ui.Info(fmt.Sprintf("Datacenters: %s", strings.Join(policy.Datacenters, ", ")))
|
|
if showMeta {
|
|
ui.Info(fmt.Sprintf("Hash: %x", policy.Hash))
|
|
ui.Info(fmt.Sprintf("Create Index: %d", policy.CreateIndex))
|
|
ui.Info(fmt.Sprintf("Modify Index: %d", policy.ModifyIndex))
|
|
}
|
|
ui.Info(fmt.Sprintf("Rules:"))
|
|
ui.Info(policy.Rules)
|
|
}
|
|
|
|
func PrintPolicyListEntry(policy *api.ACLPolicyListEntry, ui cli.Ui, showMeta bool) {
|
|
ui.Info(fmt.Sprintf("%s:", policy.Name))
|
|
ui.Info(fmt.Sprintf(" ID: %s", policy.ID))
|
|
if policy.Namespace != "" {
|
|
ui.Info(fmt.Sprintf(" Namespace: %s", policy.Namespace))
|
|
}
|
|
ui.Info(fmt.Sprintf(" Description: %s", policy.Description))
|
|
ui.Info(fmt.Sprintf(" Datacenters: %s", strings.Join(policy.Datacenters, ", ")))
|
|
if showMeta {
|
|
ui.Info(fmt.Sprintf(" Hash: %x", policy.Hash))
|
|
ui.Info(fmt.Sprintf(" Create Index: %d", policy.CreateIndex))
|
|
ui.Info(fmt.Sprintf(" Modify Index: %d", policy.ModifyIndex))
|
|
}
|
|
}
|
|
|
|
func PrintRole(role *api.ACLRole, ui cli.Ui, showMeta bool) {
|
|
ui.Info(fmt.Sprintf("ID: %s", role.ID))
|
|
ui.Info(fmt.Sprintf("Name: %s", role.Name))
|
|
if role.Namespace != "" {
|
|
ui.Info(fmt.Sprintf("Namespace: %s", role.Namespace))
|
|
}
|
|
ui.Info(fmt.Sprintf("Description: %s", role.Description))
|
|
if showMeta {
|
|
ui.Info(fmt.Sprintf("Hash: %x", role.Hash))
|
|
ui.Info(fmt.Sprintf("Create Index: %d", role.CreateIndex))
|
|
ui.Info(fmt.Sprintf("Modify Index: %d", role.ModifyIndex))
|
|
}
|
|
if len(role.Policies) > 0 {
|
|
ui.Info(fmt.Sprintf("Policies:"))
|
|
for _, policy := range role.Policies {
|
|
ui.Info(fmt.Sprintf(" %s - %s", policy.ID, policy.Name))
|
|
}
|
|
}
|
|
if len(role.ServiceIdentities) > 0 {
|
|
ui.Info(fmt.Sprintf("Service Identities:"))
|
|
for _, svcid := range role.ServiceIdentities {
|
|
if len(svcid.Datacenters) > 0 {
|
|
ui.Info(fmt.Sprintf(" %s (Datacenters: %s)", svcid.ServiceName, strings.Join(svcid.Datacenters, ", ")))
|
|
} else {
|
|
ui.Info(fmt.Sprintf(" %s (Datacenters: all)", svcid.ServiceName))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func PrintRoleListEntry(role *api.ACLRole, ui cli.Ui, showMeta bool) {
|
|
ui.Info(fmt.Sprintf("%s:", role.Name))
|
|
ui.Info(fmt.Sprintf(" ID: %s", role.ID))
|
|
if role.Namespace != "" {
|
|
ui.Info(fmt.Sprintf(" Namespace: %s", role.Namespace))
|
|
}
|
|
ui.Info(fmt.Sprintf(" Description: %s", role.Description))
|
|
if showMeta {
|
|
ui.Info(fmt.Sprintf(" Hash: %x", role.Hash))
|
|
ui.Info(fmt.Sprintf(" Create Index: %d", role.CreateIndex))
|
|
ui.Info(fmt.Sprintf(" Modify Index: %d", role.ModifyIndex))
|
|
}
|
|
if len(role.Policies) > 0 {
|
|
ui.Info(fmt.Sprintf(" Policies:"))
|
|
for _, policy := range role.Policies {
|
|
ui.Info(fmt.Sprintf(" %s - %s", policy.ID, policy.Name))
|
|
}
|
|
}
|
|
if len(role.ServiceIdentities) > 0 {
|
|
ui.Info(fmt.Sprintf(" Service Identities:"))
|
|
for _, svcid := range role.ServiceIdentities {
|
|
if len(svcid.Datacenters) > 0 {
|
|
ui.Info(fmt.Sprintf(" %s (Datacenters: %s)", svcid.ServiceName, strings.Join(svcid.Datacenters, ", ")))
|
|
} else {
|
|
ui.Info(fmt.Sprintf(" %s (Datacenters: all)", svcid.ServiceName))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func PrintAuthMethod(method *api.ACLAuthMethod, ui cli.Ui, showMeta bool) {
|
|
ui.Info(fmt.Sprintf("Name: %s", method.Name))
|
|
ui.Info(fmt.Sprintf("Type: %s", method.Type))
|
|
if method.Namespace != "" {
|
|
ui.Info(fmt.Sprintf("Namespace: %s", method.Namespace))
|
|
}
|
|
ui.Info(fmt.Sprintf("Description: %s", method.Description))
|
|
if showMeta {
|
|
ui.Info(fmt.Sprintf("Create Index: %d", method.CreateIndex))
|
|
ui.Info(fmt.Sprintf("Modify Index: %d", method.ModifyIndex))
|
|
}
|
|
ui.Info(fmt.Sprintf("Config:"))
|
|
output, err := json.MarshalIndent(method.Config, "", " ")
|
|
if err != nil {
|
|
ui.Error(fmt.Sprintf("Error formatting auth method configuration: %s", err))
|
|
}
|
|
ui.Output(string(output))
|
|
}
|
|
|
|
func PrintAuthMethodListEntry(method *api.ACLAuthMethodListEntry, ui cli.Ui, showMeta bool) {
|
|
ui.Info(fmt.Sprintf("%s:", method.Name))
|
|
ui.Info(fmt.Sprintf(" Type: %s", method.Type))
|
|
if method.Namespace != "" {
|
|
ui.Info(fmt.Sprintf(" Namespace: %s", method.Namespace))
|
|
}
|
|
ui.Info(fmt.Sprintf(" Description: %s", method.Description))
|
|
if showMeta {
|
|
ui.Info(fmt.Sprintf(" Create Index: %d", method.CreateIndex))
|
|
ui.Info(fmt.Sprintf(" Modify Index: %d", method.ModifyIndex))
|
|
}
|
|
}
|
|
|
|
func PrintBindingRule(rule *api.ACLBindingRule, ui cli.Ui, showMeta bool) {
|
|
ui.Info(fmt.Sprintf("ID: %s", rule.ID))
|
|
if rule.Namespace != "" {
|
|
ui.Info(fmt.Sprintf("Namespace: %s", rule.Namespace))
|
|
}
|
|
ui.Info(fmt.Sprintf("AuthMethod: %s", rule.AuthMethod))
|
|
ui.Info(fmt.Sprintf("Description: %s", rule.Description))
|
|
ui.Info(fmt.Sprintf("BindType: %s", rule.BindType))
|
|
ui.Info(fmt.Sprintf("BindName: %s", rule.BindName))
|
|
ui.Info(fmt.Sprintf("Selector: %s", rule.Selector))
|
|
if showMeta {
|
|
ui.Info(fmt.Sprintf("Create Index: %d", rule.CreateIndex))
|
|
ui.Info(fmt.Sprintf("Modify Index: %d", rule.ModifyIndex))
|
|
}
|
|
}
|
|
|
|
func PrintBindingRuleListEntry(rule *api.ACLBindingRule, ui cli.Ui, showMeta bool) {
|
|
ui.Info(fmt.Sprintf("%s:", rule.ID))
|
|
if rule.Namespace != "" {
|
|
ui.Info(fmt.Sprintf(" Namespace: %s", rule.Namespace))
|
|
}
|
|
ui.Info(fmt.Sprintf(" AuthMethod: %s", rule.AuthMethod))
|
|
ui.Info(fmt.Sprintf(" Description: %s", rule.Description))
|
|
ui.Info(fmt.Sprintf(" BindType: %s", rule.BindType))
|
|
ui.Info(fmt.Sprintf(" BindName: %s", rule.BindName))
|
|
ui.Info(fmt.Sprintf(" Selector: %s", rule.Selector))
|
|
if showMeta {
|
|
ui.Info(fmt.Sprintf(" Create Index: %d", rule.CreateIndex))
|
|
ui.Info(fmt.Sprintf(" Modify Index: %d", rule.ModifyIndex))
|
|
}
|
|
}
|
|
|
|
func GetTokenIDFromPartial(client *api.Client, partialID string) (string, error) {
|
|
if partialID == "anonymous" {
|
|
return structs.ACLTokenAnonymousID, nil
|
|
}
|
|
|
|
// the full UUID string was given
|
|
if len(partialID) == 36 {
|
|
return partialID, nil
|
|
}
|
|
|
|
tokens, _, err := client.ACL().TokenList(nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
tokenID := ""
|
|
for _, token := range tokens {
|
|
if strings.HasPrefix(token.AccessorID, partialID) {
|
|
if tokenID != "" {
|
|
return "", fmt.Errorf("Partial token ID is not unique")
|
|
}
|
|
tokenID = token.AccessorID
|
|
}
|
|
}
|
|
|
|
if tokenID == "" {
|
|
return "", fmt.Errorf("No such token ID with prefix: %s", partialID)
|
|
}
|
|
|
|
return tokenID, nil
|
|
}
|
|
|
|
func GetPolicyIDFromPartial(client *api.Client, partialID string) (string, error) {
|
|
if partialID == "global-management" {
|
|
return structs.ACLPolicyGlobalManagementID, nil
|
|
}
|
|
// The full UUID string was given
|
|
if len(partialID) == 36 {
|
|
return partialID, nil
|
|
}
|
|
|
|
policies, _, err := client.ACL().PolicyList(nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
policyID := ""
|
|
for _, policy := range policies {
|
|
if strings.HasPrefix(policy.ID, partialID) {
|
|
if policyID != "" {
|
|
return "", fmt.Errorf("Partial policy ID is not unique")
|
|
}
|
|
policyID = policy.ID
|
|
}
|
|
}
|
|
|
|
if policyID == "" {
|
|
return "", fmt.Errorf("No such policy ID with prefix: %s", partialID)
|
|
}
|
|
|
|
return policyID, nil
|
|
}
|
|
|
|
func GetPolicyIDByName(client *api.Client, name string) (string, error) {
|
|
if name == "" {
|
|
return "", fmt.Errorf("No name specified")
|
|
}
|
|
|
|
policies, _, err := client.ACL().PolicyList(nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
for _, policy := range policies {
|
|
if policy.Name == name {
|
|
return policy.ID, nil
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("No such policy with name %s", name)
|
|
}
|
|
|
|
func GetRulesFromLegacyToken(client *api.Client, tokenID string, isSecret bool) (string, error) {
|
|
tokenID, err := GetTokenIDFromPartial(client, tokenID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var token *api.ACLToken
|
|
if isSecret {
|
|
qopts := api.QueryOptions{
|
|
Token: tokenID,
|
|
}
|
|
token, _, err = client.ACL().TokenReadSelf(&qopts)
|
|
} else {
|
|
token, _, err = client.ACL().TokenRead(tokenID, nil)
|
|
}
|
|
|
|
if err != nil {
|
|
return "", fmt.Errorf("Error reading token: %v", err)
|
|
}
|
|
|
|
if token == nil {
|
|
return "", fmt.Errorf("Token not found for ID")
|
|
}
|
|
|
|
if token.Rules == "" {
|
|
return "", fmt.Errorf("Token is not a legacy token with rules")
|
|
}
|
|
|
|
return token.Rules, nil
|
|
}
|
|
|
|
func GetRoleIDFromPartial(client *api.Client, partialID string) (string, error) {
|
|
// the full UUID string was given
|
|
if len(partialID) == 36 {
|
|
return partialID, nil
|
|
}
|
|
|
|
roles, _, err := client.ACL().RoleList(nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
roleID := ""
|
|
for _, role := range roles {
|
|
if strings.HasPrefix(role.ID, partialID) {
|
|
if roleID != "" {
|
|
return "", fmt.Errorf("Partial role ID is not unique")
|
|
}
|
|
roleID = role.ID
|
|
}
|
|
}
|
|
|
|
if roleID == "" {
|
|
return "", fmt.Errorf("No such role ID with prefix: %s", partialID)
|
|
}
|
|
|
|
return roleID, nil
|
|
}
|
|
|
|
func GetRoleIDByName(client *api.Client, name string) (string, error) {
|
|
if name == "" {
|
|
return "", fmt.Errorf("No name specified")
|
|
}
|
|
|
|
roles, _, err := client.ACL().RoleList(nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
for _, role := range roles {
|
|
if role.Name == name {
|
|
return role.ID, nil
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("No such role with name %s", name)
|
|
}
|
|
|
|
func GetBindingRuleIDFromPartial(client *api.Client, partialID string) (string, error) {
|
|
// the full UUID string was given
|
|
if len(partialID) == 36 {
|
|
return partialID, nil
|
|
}
|
|
|
|
rules, _, err := client.ACL().BindingRuleList("", nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
ruleID := ""
|
|
for _, rule := range rules {
|
|
if strings.HasPrefix(rule.ID, partialID) {
|
|
if ruleID != "" {
|
|
return "", fmt.Errorf("Partial rule ID is not unique")
|
|
}
|
|
ruleID = rule.ID
|
|
}
|
|
}
|
|
|
|
if ruleID == "" {
|
|
return "", fmt.Errorf("No such rule ID with prefix: %s", partialID)
|
|
}
|
|
|
|
return ruleID, nil
|
|
}
|
|
|
|
func ExtractServiceIdentities(serviceIdents []string) ([]*api.ACLServiceIdentity, error) {
|
|
var out []*api.ACLServiceIdentity
|
|
for _, svcidRaw := range serviceIdents {
|
|
parts := strings.Split(svcidRaw, ":")
|
|
switch len(parts) {
|
|
case 2:
|
|
out = append(out, &api.ACLServiceIdentity{
|
|
ServiceName: parts[0],
|
|
Datacenters: strings.Split(parts[1], ","),
|
|
})
|
|
case 1:
|
|
out = append(out, &api.ACLServiceIdentity{
|
|
ServiceName: parts[0],
|
|
})
|
|
default:
|
|
return nil, fmt.Errorf("Malformed -service-identity argument: %q", svcidRaw)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// TestKubernetesJWT_A is a valid service account jwt extracted from a minikube setup.
|
|
//
|
|
// {
|
|
// "iss": "kubernetes/serviceaccount",
|
|
// "kubernetes.io/serviceaccount/namespace": "default",
|
|
// "kubernetes.io/serviceaccount/secret.name": "admin-token-qlz42",
|
|
// "kubernetes.io/serviceaccount/service-account.name": "admin",
|
|
// "kubernetes.io/serviceaccount/service-account.uid": "738bc251-6532-11e9-b67f-48e6c8b8ecb5",
|
|
// "sub": "system:serviceaccount:default:admin"
|
|
// }
|
|
const TestKubernetesJWT_A = "eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImFkbWluLXRva2VuLXFsejQyIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImFkbWluIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQudWlkIjoiNzM4YmMyNTEtNjUzMi0xMWU5LWI2N2YtNDhlNmM4YjhlY2I1Iiwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OmRlZmF1bHQ6YWRtaW4ifQ.ixMlnWrAG7NVuTTKu8cdcYfM7gweS3jlKaEsIBNGOVEjPE7rtXtgMkAwjQTdYR08_0QBjkgzy5fQC5ZNyglSwONJ-bPaXGvhoH1cTnRi1dz9H_63CfqOCvQP1sbdkMeRxNTGVAyWZT76rXoCUIfHP4LY2I8aab0KN9FTIcgZRF0XPTtT70UwGIrSmRpxW38zjiy2ymWL01cc5VWGhJqVysmWmYk3wNp0h5N57H_MOrz4apQR4pKaamzskzjLxO55gpbmZFC76qWuUdexAR7DT2fpbHLOw90atN_NlLMY-VrXyW3-Ei5EhYaVreMB9PSpKwkrA4jULITohV-sxpa1LA"
|
|
|
|
// TestKubernetesJWT_B is a valid service account jwt extracted from a minikube setup.
|
|
//
|
|
// {
|
|
// "iss": "kubernetes/serviceaccount",
|
|
// "kubernetes.io/serviceaccount/namespace": "default",
|
|
// "kubernetes.io/serviceaccount/secret.name": "demo-token-kmb9n",
|
|
// "kubernetes.io/serviceaccount/service-account.name": "demo",
|
|
// "kubernetes.io/serviceaccount/service-account.uid": "76091af4-4b56-11e9-ac4b-708b11801cbe",
|
|
// "sub": "system:serviceaccount:default:demo"
|
|
// }
|
|
const TestKubernetesJWT_B = "eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImRlbW8tdG9rZW4ta21iOW4iLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiZGVtbyIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6Ijc2MDkxYWY0LTRiNTYtMTFlOS1hYzRiLTcwOGIxMTgwMWNiZSIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OmRlbW8ifQ.ZiAHjijBAOsKdum0Aix6lgtkLkGo9_Tu87dWQ5Zfwnn3r2FejEWDAnftTft1MqqnMzivZ9Wyyki5ZjQRmTAtnMPJuHC-iivqY4Wh4S6QWCJ1SivBv5tMZR79t5t8mE7R1-OHwst46spru1pps9wt9jsA04d3LpV0eeKYgdPTVaQKklxTm397kIMUugA6yINIBQ3Rh8eQqBgNwEmL4iqyYubzHLVkGkoP9MJikFI05vfRiHtYr-piXz6JFDzXMQj9rW6xtMmrBSn79ChbyvC5nz-Nj2rJPnHsb_0rDUbmXY5PpnMhBpdSH-CbZ4j8jsiib6DtaGJhVZeEQ1GjsFAZwQ"
|