mirror of
https://github.com/apache/cloudstack
synced 2026-08-02 05:26:35 +00:00
UI for API Key Pair Management (#13225)
Some checks are pending
Build / build (push) Waiting to run
Simulator CI / build (push) Waiting to run
Simulator CI / test (component/find_hosts_for_migration component/test_acl_isolatednetwork component/test_acl_isolatednetwork_delete component/test_acl_listsnapshot) (push) Blocked by required conditions
Simulator CI / test (component/test_acl_listvm component/test_acl_listvolume) (push) Blocked by required conditions
Simulator CI / test (component/test_acl_sharednetwork component/test_acl_sharednetwork_deployVM-impersonation component/test_user_private_gateway component/test_user_shared_network) (push) Blocked by required conditions
Simulator CI / test (component/test_affinity_groups_projects component/test_allocation_states component/test_assign_vm) (push) Blocked by required conditions
Simulator CI / test (component/test_concurrent_snapshots_limit component/test_cpu_domain_limits component/test_cpu_limits component/test_cpu_max_limits component/test_cpu_project_limits component/test_deploy_vm_userdata_multi_nic component/test_deploy_vm_lease) (push) Blocked by required conditions
Simulator CI / test (component/test_egress_fw_rules component/test_invalid_gw_nm component/test_ip_reservation) (push) Blocked by required conditions
Simulator CI / test (component/test_lb_secondary_ip component/test_list_nics component/test_list_pod component/test_memory_limits) (push) Blocked by required conditions
Simulator CI / test (component/test_mm_domain_limits component/test_mm_max_limits component/test_mm_project_limits component/test_network_offering component/test_non_contiguous_vlan) (push) Blocked by required conditions
Simulator CI / test (component/test_persistent_networks component/test_project_configs component/test_project_limits component/test_project_resources) (push) Blocked by required conditions
Simulator CI / test (component/test_project_usage component/test_protocol_number_security_group component/test_public_ip component/test_resource_limits component/test_resource_limit_tags) (push) Blocked by required conditions
Simulator CI / test (component/test_regions_accounts component/test_routers component/test_snapshots component/test_stopped_vm component/test_tags component/test_templates component/test_updateResourceCount component/test_update_vm) (push) Blocked by required conditions
Simulator CI / test (component/test_volumes component/test_vpc component/test_vpc_distributed_routing_offering component/test_vpc_network component/test_vpc_offerings component/test_vpc_routers component/test_vpn_users component/test_vpc_network_lbrules) (push) Blocked by required conditions
Simulator CI / test (smoke/test_accounts smoke/test_account_access smoke/test_affinity_groups smoke/test_affinity_groups_projects smoke/test_annotations smoke/test_async_job smoke/test_attach_multiple_volumes smoke/test_backup_recovery_dummy smoke/test_certauthority_… (push) Blocked by required conditions
Simulator CI / test (smoke/test_cluster_drs smoke/test_dynamicroles smoke/test_enable_account_settings_for_domain smoke/test_enable_role_based_users_in_projects smoke/test_events_resource smoke/test_global_settings smoke/test_guest_vlan_range smoke/test_host_maintena… (push) Blocked by required conditions
Simulator CI / test (smoke/test_list_accounts smoke/test_list_disk_offerings smoke/test_list_domains smoke/test_list_hosts smoke/test_list_service_offerings smoke/test_list_storage_pools smoke/test_list_volumes) (push) Blocked by required conditions
Simulator CI / test (smoke/test_network smoke/test_network_acl smoke/test_network_ipv6 smoke/test_network_permissions smoke/test_nic smoke/test_nic_adapter_type smoke/test_non_contigiousvlan smoke/test_object_stores smoke/test_outofbandmanagement smoke/test_outofband… (push) Blocked by required conditions
Simulator CI / test (smoke/test_router_dhcphosts smoke/test_router_dns smoke/test_router_dnsservice smoke/test_routers smoke/test_routers_iptables_default_policy smoke/test_routers_network_ops smoke/test_scale_vm smoke/test_secondary_storage smoke/test_service_offeri… (push) Blocked by required conditions
CodeQL Analysis / CodeQL (actions) (push) Waiting to run
Docker Image Build / build (push) Waiting to run
Sonar Quality Check (Main) / Sonar JaCoCo Coverage (push) Waiting to run
License Check / build (push) Waiting to run
UI Build / build (push) Waiting to run
Some checks are pending
Build / build (push) Waiting to run
Simulator CI / build (push) Waiting to run
Simulator CI / test (component/find_hosts_for_migration component/test_acl_isolatednetwork component/test_acl_isolatednetwork_delete component/test_acl_listsnapshot) (push) Blocked by required conditions
Simulator CI / test (component/test_acl_listvm component/test_acl_listvolume) (push) Blocked by required conditions
Simulator CI / test (component/test_acl_sharednetwork component/test_acl_sharednetwork_deployVM-impersonation component/test_user_private_gateway component/test_user_shared_network) (push) Blocked by required conditions
Simulator CI / test (component/test_affinity_groups_projects component/test_allocation_states component/test_assign_vm) (push) Blocked by required conditions
Simulator CI / test (component/test_concurrent_snapshots_limit component/test_cpu_domain_limits component/test_cpu_limits component/test_cpu_max_limits component/test_cpu_project_limits component/test_deploy_vm_userdata_multi_nic component/test_deploy_vm_lease) (push) Blocked by required conditions
Simulator CI / test (component/test_egress_fw_rules component/test_invalid_gw_nm component/test_ip_reservation) (push) Blocked by required conditions
Simulator CI / test (component/test_lb_secondary_ip component/test_list_nics component/test_list_pod component/test_memory_limits) (push) Blocked by required conditions
Simulator CI / test (component/test_mm_domain_limits component/test_mm_max_limits component/test_mm_project_limits component/test_network_offering component/test_non_contiguous_vlan) (push) Blocked by required conditions
Simulator CI / test (component/test_persistent_networks component/test_project_configs component/test_project_limits component/test_project_resources) (push) Blocked by required conditions
Simulator CI / test (component/test_project_usage component/test_protocol_number_security_group component/test_public_ip component/test_resource_limits component/test_resource_limit_tags) (push) Blocked by required conditions
Simulator CI / test (component/test_regions_accounts component/test_routers component/test_snapshots component/test_stopped_vm component/test_tags component/test_templates component/test_updateResourceCount component/test_update_vm) (push) Blocked by required conditions
Simulator CI / test (component/test_volumes component/test_vpc component/test_vpc_distributed_routing_offering component/test_vpc_network component/test_vpc_offerings component/test_vpc_routers component/test_vpn_users component/test_vpc_network_lbrules) (push) Blocked by required conditions
Simulator CI / test (smoke/test_accounts smoke/test_account_access smoke/test_affinity_groups smoke/test_affinity_groups_projects smoke/test_annotations smoke/test_async_job smoke/test_attach_multiple_volumes smoke/test_backup_recovery_dummy smoke/test_certauthority_… (push) Blocked by required conditions
Simulator CI / test (smoke/test_cluster_drs smoke/test_dynamicroles smoke/test_enable_account_settings_for_domain smoke/test_enable_role_based_users_in_projects smoke/test_events_resource smoke/test_global_settings smoke/test_guest_vlan_range smoke/test_host_maintena… (push) Blocked by required conditions
Simulator CI / test (smoke/test_list_accounts smoke/test_list_disk_offerings smoke/test_list_domains smoke/test_list_hosts smoke/test_list_service_offerings smoke/test_list_storage_pools smoke/test_list_volumes) (push) Blocked by required conditions
Simulator CI / test (smoke/test_network smoke/test_network_acl smoke/test_network_ipv6 smoke/test_network_permissions smoke/test_nic smoke/test_nic_adapter_type smoke/test_non_contigiousvlan smoke/test_object_stores smoke/test_outofbandmanagement smoke/test_outofband… (push) Blocked by required conditions
Simulator CI / test (smoke/test_router_dhcphosts smoke/test_router_dns smoke/test_router_dnsservice smoke/test_routers smoke/test_routers_iptables_default_policy smoke/test_routers_network_ops smoke/test_scale_vm smoke/test_secondary_storage smoke/test_service_offeri… (push) Blocked by required conditions
CodeQL Analysis / CodeQL (actions) (push) Waiting to run
Docker Image Build / build (push) Waiting to run
Sonar Quality Check (Main) / Sonar JaCoCo Coverage (push) Waiting to run
License Check / build (push) Waiting to run
UI Build / build (push) Waiting to run
Co-authored-by: Fabricio Duarte <fabricio.duarte.jr@gmail.com>
This commit is contained in:
parent
ce2d890e38
commit
8ef036e717
@ -50,7 +50,7 @@ public class RegisterUserKeysCmd extends BaseAsyncCmd {
|
||||
@Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "API key pair name.")
|
||||
private String name;
|
||||
|
||||
@Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, description = "API key pair description.")
|
||||
@Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, description = "API key pair description.", length = 1024)
|
||||
private String description;
|
||||
|
||||
@Parameter(name = ApiConstants.START_DATE, type = CommandType.DATE, description = "Start date of the API key pair. " +
|
||||
@ -138,6 +138,9 @@ public class RegisterUserKeysCmd extends BaseAsyncCmd {
|
||||
|
||||
String description = detail.get(ApiConstants.DESCRIPTION);
|
||||
if (StringUtils.isNotEmpty(description)) {
|
||||
if (description.length() > 255) {
|
||||
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Rule description cannot be longer than 255 characters.");
|
||||
}
|
||||
ruleDetails.put(ApiConstants.DESCRIPTION, description);
|
||||
}
|
||||
|
||||
|
||||
@ -70,7 +70,7 @@ public class ApiKeyPairVO implements ApiKeyPair {
|
||||
@Temporal(value = TemporalType.TIMESTAMP)
|
||||
private Date created = Date.from(Instant.now());
|
||||
|
||||
@Column(name = "description")
|
||||
@Column(name = "description", length = 1024)
|
||||
private String description = "";
|
||||
|
||||
@Column(name = "api_key", nullable = false)
|
||||
|
||||
@ -73,7 +73,7 @@ CREATE TABLE IF NOT EXISTS `cloud`.`api_keypair` (
|
||||
`user_id` bigint(20) unsigned NOT NULL,
|
||||
`start_date` datetime,
|
||||
`end_date` datetime,
|
||||
`description` varchar(100),
|
||||
`description` varchar(1024),
|
||||
`api_key` varchar(255) NOT NULL,
|
||||
`secret_key` varchar(255) NOT NULL,
|
||||
`created` datetime NOT NULL,
|
||||
@ -107,11 +107,15 @@ WHERE user.api_key IS NOT NULL AND user.secret_key IS NOT NULL;
|
||||
-- Drop API keys from user table
|
||||
ALTER TABLE `cloud`.`user` DROP COLUMN api_key, DROP COLUMN secret_key;
|
||||
|
||||
-- Grant access to the "deleteUserKeys" API to the "User", "Domain Admin" and "Resource Admin" roles, similarly to the "registerUserKeys" API
|
||||
-- Grant access to the "deleteUserKeys" and "listUserKeyRules" APIs to the "User", "Domain Admin" and "Resource Admin" roles, similarly to the "registerUserKeys" API
|
||||
CALL `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION`('User', 'deleteUserKeys', 'ALLOW');
|
||||
CALL `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION`('Domain Admin', 'deleteUserKeys', 'ALLOW');
|
||||
CALL `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION`('Resource Admin', 'deleteUserKeys', 'ALLOW');
|
||||
|
||||
CALL `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION`('User', 'listUserKeyRules', 'ALLOW');
|
||||
CALL `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION`('Domain Admin', 'listUserKeyRules', 'ALLOW');
|
||||
CALL `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION`('Resource Admin', 'listUserKeyRules', 'ALLOW');
|
||||
|
||||
-- Add conserve mode for VPC offerings
|
||||
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.vpc_offerings','conserve_mode', 'tinyint(1) unsigned NULL DEFAULT 0 COMMENT ''True if the VPC offering is IP conserve mode enabled, allowing public IP services to be used across multiple VPC tiers'' ');
|
||||
|
||||
|
||||
@ -62,6 +62,7 @@
|
||||
"label.action.attach.disk": "Attach Disk",
|
||||
"label.action.attach.iso": "Attach ISO",
|
||||
"label.action.attach.to.instance": "Attach to Instance",
|
||||
"label.action.bulk.delete.api.keys": "Bulk delete API key pairs",
|
||||
"label.action.bulk.delete.egress.firewall.rules": "Bulk delete egress firewall rules",
|
||||
"label.action.bulk.delete.firewall.rules": "Bulk delete firewall rules",
|
||||
"label.action.bulk.delete.ip.v6.firewall.rules": "Bulk remove IPv6 firewall rules",
|
||||
@ -82,6 +83,7 @@
|
||||
"label.action.copy.iso": "Copy ISO",
|
||||
"label.action.copy.snapshot": "Copy Snapshot",
|
||||
"label.action.copy.template": "Copy Template",
|
||||
"label.action.create.api.key": "Create API key pair for user",
|
||||
"label.action.create.backup.schedule": "Create Backup Schedule",
|
||||
"label.action.create.recurring.snapshot": "Create Recurring Snapshot",
|
||||
"label.action.create.snapshot.from.vmsnapshot": "Create Snapshot from Instance Snapshot",
|
||||
@ -116,6 +118,7 @@
|
||||
"label.action.delete.snapshot": "Delete Snapshot",
|
||||
"label.action.delete.template": "Delete Template",
|
||||
"label.action.delete.tungsten.router.table": "Remove Tungsten Fabric route table from Network",
|
||||
"label.action.delete.keypair": "Delete API key pair",
|
||||
"label.action.delete.user": "Delete User",
|
||||
"label.action.delete.vgpu.profile": "Delete vGPU profile",
|
||||
"label.action.delete.volume": "Delete Volume",
|
||||
@ -409,6 +412,11 @@
|
||||
"label.api.docs.count": "APIs available for your account",
|
||||
"label.api.version": "API version",
|
||||
"label.apikey": "API key",
|
||||
"label.apikeypairs": "API Key Pairs",
|
||||
"label.apikeypair.description": "Description of the API key pair",
|
||||
"label.apikeypair.name": "Name of the API key pair",
|
||||
"label.apikeypair.startdate": "API key pair valid from",
|
||||
"label.apikeypair.enddate": "API key pair valid until",
|
||||
"label.app.cookie": "AppCookie",
|
||||
"label.app.name": "CloudStack",
|
||||
"label.application.policy.set": "Application Policy Set",
|
||||
@ -624,6 +632,7 @@
|
||||
"label.configure.ldap": "Configure LDAP",
|
||||
"label.configure.ovs": "Configure Ovs",
|
||||
"label.configure.sticky.policy": "Configure sticky policy",
|
||||
"label.confirm.delete.api.keys": "Please confirm you wish to delete the selected API key pairs",
|
||||
"label.confirm.delete.egress.firewall.rules": "Please confirm you wish to delete the selected egress firewall rules.",
|
||||
"label.confirm.delete.firewall.rules": "Please confirm you wish to delete the selected firewall rules.",
|
||||
"label.confirm.delete.ip.v6.firewall.rules": "Please confirm you wish to delete the selected IPv6 firewall rules",
|
||||
@ -786,6 +795,7 @@
|
||||
"label.delete.alerts": "Delete alerts",
|
||||
"label.delete.asnrange": "Delete AS Range",
|
||||
"label.delete.autoscale.vmgroup": "Delete AutoScaling Group",
|
||||
"label.delete.all.rules": "Delete all API key pair rules",
|
||||
"label.delete.backup": "Delete backup",
|
||||
"label.delete.backup.schedule": "Delete backup schedule",
|
||||
"label.delete.bgp.peer": "Delete BGP peer",
|
||||
@ -843,6 +853,7 @@
|
||||
"label.deleting": "Deleting",
|
||||
"label.deleting.failed": "Deleting failed",
|
||||
"label.deleting.iso": "Deleting ISO",
|
||||
"label.deleting.keypair": "Deleting API key pair",
|
||||
"label.deleting.snapshot": "Deleting Snapshot",
|
||||
"label.deleting.template": "Deleting Template",
|
||||
"label.deleteprotection": "Delete protection",
|
||||
@ -1643,6 +1654,7 @@
|
||||
"message.memory.usage.info.hypervisor.additionals": "The data shown may not reflect the actual memory usage if the Instance does not have the additional hypervisor tools installed",
|
||||
"message.memory.usage.info.negative.value": "If the Instance's memory usage cannot be obtained from the hypervisor, the lines for free memory in the raw data graph and memory usage in the percentage graph will be disabled",
|
||||
"message.migrate.volume.tooltip": "Volume can be migrated to any suitable storage pool. Admin has to choose the appropriate disk offering to replace, that supports the new storage pool",
|
||||
"message.register.keypair.failed": "Failed to register API key pair",
|
||||
"label.migrate.with.storage": "Migrate with storage",
|
||||
"label.migrating": "Migrating",
|
||||
"label.migrating.data": "Migrating data",
|
||||
@ -2083,12 +2095,14 @@
|
||||
"label.redundantvpcrouter": "Redundant VPC",
|
||||
"label.refresh": "Refresh",
|
||||
"label.region": "Region",
|
||||
"label.register.api.key": "Register API key pair",
|
||||
"label.register.extension": "Register Extension",
|
||||
"label.register.oauth": "Register OAuth",
|
||||
"label.register.template": "Register Template",
|
||||
"label.register.user.data": "Register User Data",
|
||||
"label.register.cni.config": "Register CNI Configuration",
|
||||
"label.register.user.data.details": "Enter the User Data in plain text or in Base64 encoding. Up to 32KB of Base64 encoded User Data can be sent by default. The setting vm.userdata.max.length can be used to increase the limit to upto 1MB.",
|
||||
"label.registering.keypair": "Registering API key pair for user \"{user}\"",
|
||||
"label.reinstall.vm": "Reinstall Instance",
|
||||
"label.reject": "Reject",
|
||||
"label.related": "Related",
|
||||
@ -2403,6 +2417,7 @@
|
||||
"label.unregister.extension": "Unregister Extension",
|
||||
"label.usediops": "IOPS used",
|
||||
"label.userdata": "User Data",
|
||||
"label.user.api.key.rules": "API key pair rules",
|
||||
"label.user.data.id": "User Data ID",
|
||||
"label.user.data.name": "User Data name",
|
||||
"label.user.data.details": "User Data details",
|
||||
@ -2936,7 +2951,7 @@
|
||||
"label.versioning": "Versioning",
|
||||
"label.objectlocking": "Object Lock",
|
||||
"label.bucket.policy": "Bucket Policy",
|
||||
"label.usersecretkey": "Secret Key",
|
||||
"label.usersecretkey": "API Secret Key",
|
||||
"label.create.bucket": "Create Bucket",
|
||||
"label.cniconfiguration": "CNI Configuration",
|
||||
"label.cniconfigname": "Associated CNI Configuration",
|
||||
@ -3350,6 +3365,8 @@
|
||||
"message.delete.failed": "Delete fail",
|
||||
"message.delete.gateway": "Please confirm you want to delete the gateway.",
|
||||
"message.delete.ip.v6.prefix.processing": "Deleting IPv6 prefix...",
|
||||
"message.delete.keypair": "Please confirm that you would like to delete this API key pair.",
|
||||
"message.delete.keypair.failed": "Failed to delete API key pair",
|
||||
"message.delete.port.forward.processing": "Deleting port forwarding rule...",
|
||||
"message.delete.project": "Are you sure you want to delete this project?",
|
||||
"message.delete.rule.processing": "Deleting rule...",
|
||||
@ -3755,6 +3772,8 @@
|
||||
"message.new.version.available": "A new version of CloudStack is available. Click here to check the details",
|
||||
"message.no.data.to.show.for.period": "No data to show for the selected period.",
|
||||
"message.no.description": "No description entered.",
|
||||
"message.note.about.keypair.permissions.title": "Note about API key pair rule permissions",
|
||||
"message.note.about.keypair.permissions.body": "During the creation of API key pairs, it is possible to define a corresponding set of rule permissions. If a rule set is defined, the API key pair will only have access to APIs for which access has been explicitly granted (i.e., APIs whose corresponding rules are marked as allowed). On the other hand, if no rule set is specified, the API key pair permissions will follow and adapt to the permission set of the user's account role.",
|
||||
"message.offering.internet.protocol.warning": "WARNING: IPv6 supported Networks use static routing and will require upstream routes to be configured manually.",
|
||||
"message.offering.ipv6.warning": "Please refer documentation for creating IPv6 enabled Network/VPC offering <a href='http://docs.cloudstack.apache.org/en/latest/plugins/ipv6.html#isolated-network-and-vpc-tier'>IPv6 support in CloudStack - Isolated Networks and VPC Network Tiers</a>",
|
||||
"message.oobm.configured": "Successfully configured out-of-band management for host",
|
||||
@ -3963,6 +3982,7 @@
|
||||
"message.success.delete.gpu.devices": "Successfully deleted GPU device(s)",
|
||||
"message.success.delete.icon": "Successfully deleted icon of",
|
||||
"message.success.delete.interface.static.route": "Successfully removed interface Static Route",
|
||||
"message.success.delete.keypair": "Success deleting API key pair",
|
||||
"message.success.delete.ipv4.subnet": "Successfully removed IPv4 subnet",
|
||||
"message.success.delete.network.static.route": "Successfully removed Network Static Route",
|
||||
"message.success.delete.node": "Successfully deleted node",
|
||||
@ -3994,6 +4014,7 @@
|
||||
"message.success.register.keypair": "Successfully registered SSH key pair",
|
||||
"message.success.register.template": "Successfully registered Template",
|
||||
"message.success.register.user.data": "Successfully registered User Data",
|
||||
"message.success.register.user.keypair": "Successfully registered API key pair for user \"{user}\"",
|
||||
"message.success.release.ip": "Successfully released IP",
|
||||
"message.success.release.dedicated.bgp.peer": "Successfully released dedicated BGP peer",
|
||||
"message.success.release.dedicated.ipv4.subnet": "Successfully released dedicated IPv4 subnet",
|
||||
|
||||
@ -51,6 +51,7 @@
|
||||
"label.action": "A\u00e7\u00e3o",
|
||||
"label.action.attach.disk": "Anexar disco",
|
||||
"label.action.attach.iso": "Anexar ISO",
|
||||
"label.action.bulk.delete.api.keys": "Apagar em massa as chaves de acesso \u00e0 API.",
|
||||
"label.action.bulk.delete.egress.firewall.rules": "Apagar em massa as regras de sa\u00edda do firewall.",
|
||||
"label.action.bulk.delete.firewall.rules": "Apagar em massa as regras do firewall.",
|
||||
"label.action.bulk.delete.ip.v6.firewall.rules": "Apagar em massa as regras de firewall IPv6.",
|
||||
@ -70,6 +71,7 @@
|
||||
"label.action.copy.iso": "Copiar ISO",
|
||||
"label.action.copy.snapshot": "Copiar Snapshot",
|
||||
"label.action.copy.template": "Copiar template",
|
||||
"label.action.create.api.key": "Criar um par de chaves de API para usu\u00e1rio",
|
||||
"label.action.create.snapshot.from.vmsnapshot": "Criar snapshot a partir de uma snapshot de VM",
|
||||
"label.action.create.template.from.volume": "Criar template a partir do disco",
|
||||
"label.action.create.volume": "Criar disco",
|
||||
@ -86,6 +88,7 @@
|
||||
"label.action.delete.interface.static.route": "Remover rota est\u00e1tica da interface Tungsten Fabric",
|
||||
"label.action.delete.ip.range": "Remover intervalo de IPs",
|
||||
"label.action.delete.iso": "Remover ISO",
|
||||
"label.action.delete.keypair": "Remover par de chaves",
|
||||
"label.action.delete.load.balancer": "Remover regra do balanceador de carga",
|
||||
"label.action.delete.network": "Remover rede",
|
||||
"label.action.delete.network.permission": "Excluir permiss\u00e3o de Rede",
|
||||
@ -379,6 +382,11 @@
|
||||
"label.api.docs.count": "APIs dispon\u00edveis para sua conta",
|
||||
"label.api.version": "Vers\u00e3o da API",
|
||||
"label.apikey": "Chave da API",
|
||||
"label.apikeypairs": "Par de chaves de API",
|
||||
"label.apikeypair.description": "Descri\u00e7\u00e3o do par de chaves de API",
|
||||
"label.apikeypair.name": "Nome do par de chaves de API",
|
||||
"label.apikeypair.startdate": "Data de in\u00edcio da validade do par de chaves de API",
|
||||
"label.apikeypair.enddate": "Data de expira\u00e7\u00e3o do par de chaves de API",
|
||||
"label.app.cookie": "AppCookie",
|
||||
"label.app.name": "CloudStack",
|
||||
"label.application.policy.set": "Conjunto de Pol\u00edticas de Aplica\u00e7\u00e3o",
|
||||
@ -564,6 +572,7 @@
|
||||
"label.configure.ldap": "Configurar LDAP",
|
||||
"label.configure.ovs": "Configure Ovs",
|
||||
"label.configure.sticky.policy": "Configurar sticky policy",
|
||||
"label.confirm.delete.api.keys": "Por favor, confirme se deseja apagar as chaves de acesso \u00e0 API selecionadas",
|
||||
"label.confirm.delete.egress.firewall.rules": "Por favor, confirme se deseja apagar as regras de firewall de sa\u00edda selecionadas",
|
||||
"label.confirm.delete.firewall.rules": "Por favor, confirme se deseja apagar as regras de firewall selecionadas",
|
||||
"label.confirm.delete.ip.v6.firewall.rules": "Por favor, confirme que deseja excluir as regras de firewall IPv6 selecionadas",
|
||||
@ -709,6 +718,7 @@
|
||||
"label.delete.acl": "Apagar lista ACL",
|
||||
"label.delete.affinity.group": "Apagar grupo de afinidade",
|
||||
"label.delete.alerts": "Remover alertas",
|
||||
"label.delete.all.rules": "Apagar todas as regras",
|
||||
"label.delete.asnrange": "Excluir Faixa AS",
|
||||
"label.delete.autoscale.vmgroup": "Excluir grupo de auto escalonamento horizontal",
|
||||
"label.delete.backup": "Apagar backup",
|
||||
@ -763,6 +773,7 @@
|
||||
"label.deleteconfirm": "Por favor, confirme que voc\u00ea deseja apagar isto",
|
||||
"label.deleting": "Removendo",
|
||||
"label.deleting.failed": "Falha ao remover",
|
||||
"label.deleting.keypair": "Deletando uma chave de API",
|
||||
"label.deleting.iso": "Removendo ISO",
|
||||
"label.deleting.template": "Remover template",
|
||||
"label.deleting.snapshot": "Excluindo Snapshot",
|
||||
@ -1890,6 +1901,7 @@
|
||||
"label.register.oauth": "Registrar OAuth",
|
||||
"label.register.user.data": "Registrar dados de usu\u00e1rio",
|
||||
"label.register.template": "Registrar template",
|
||||
"label.registering.keypair": "Registrando par de chaves de API para o usu\u00e1rio \"{user}\"",
|
||||
"label.reinstall.vm": "Reinstalar VM",
|
||||
"label.reject": "Rejeitar",
|
||||
"label.related": "Relacionado",
|
||||
@ -2050,6 +2062,7 @@
|
||||
"label.secondarystoragelimit": "Limites do armazenamento secund\u00e1rio (GiB)",
|
||||
"label.secretkey": "Chave secreta",
|
||||
"label.secret.key": "Chave secreta",
|
||||
"label.apikeyaccess": "Acesso a pares de chaves de API",
|
||||
"label.secured": "Protegido",
|
||||
"label.security.groups": "Grupos de seguran\u00e7a",
|
||||
"label.securitygroup": "Grupo de seguran\u00e7a",
|
||||
@ -2466,10 +2479,12 @@
|
||||
"label.usenewdiskoffering": "Substituir a oferta de disco?",
|
||||
"label.user": "Usu\u00e1rio",
|
||||
"label.user.conflict": "Conflito",
|
||||
"label.usersecretkey": "Chave Secreta de API",
|
||||
"label.user.data": "Dados de usu\u00e1rio",
|
||||
"label.username": "Nome de usu\u00e1rio",
|
||||
"label.users": "Usu\u00e1rios",
|
||||
"label.usersource": "Tipo de usu\u00e1rio",
|
||||
"label.user.api.key.rules": "Regras do par de chaves de API",
|
||||
"label.using.cli": "Usando CLI",
|
||||
"label.utilization": "Utiliza\u00e7\u00e3o",
|
||||
"label.uuid": "ID",
|
||||
@ -3365,6 +3380,8 @@
|
||||
"message.nfs.mount.options.description": "Lista separada por v\u00edrgulas de op\u00e7\u00f5es de montagem NFS para hosts KVM. Op\u00e7\u00f5es suportadas : vers=[3,4.0,4.1,4.2], nconnect=[1...16]",
|
||||
"message.no.data.to.show.for.period": "Nenhum dado para mostrar no per\u00edodo selecionado.",
|
||||
"message.no.description": "Nenhuma descri\u00e7\u00e3o inserida.",
|
||||
"message.note.about.keypair.permissions.title": "Observa\u00e7\u00e3o sobre as permiss\u00f5es de regras de pares de chaves de API",
|
||||
"message.note.about.keypair.permissions.body": "Durante a cria\u00e7\u00e3o de pares de chaves de API, é possível definir um conjunto correspondente de permiss\u00f5es de regras. Se um conjunto de regras for definido, o par de chaves de API terá acesso apenas às APIs para as quais o acesso foi explicitamente concedido (ou seja, APIs cujas regras correspondentes estejam marcadas como permitidas). Por outro lado, se nenhum conjunto de regras for especificado, as permiss\u00f5es do par de chaves de API seguir\u00e3o e se adaptar\u00e3o ao conjunto de permiss\u00f5es da role da conta do usu\u00e1rio.",
|
||||
"message.offering.internet.protocol.warning": "AVISO: Redes suportadas por IPv6 usam roteamento est\u00e1tico e exigir\u00e3o que rotas upstream sejam configuradas manualmente.",
|
||||
"message.offering.ipv6.warning": "Por favor, consulte a documenta\u00e7\u00e3o para criar oferta de Rede/VPC habilitada para IPv6 <a href='http://docs.cloudstack.apache.org/en/latest/plugins/ipv6.html#isolated-network-and-vpc-tier'>Suporte IPv6 no CloudStack - Redes Isoladas e Camadas de VPC</a>",
|
||||
"message.ovf.configurations": "H\u00e1 propriedades OVF dispon\u00edveis para a personaliza\u00e7\u00e3o do aparelho selecionado. Por favor, edite os valores de forma apropriada.As ofertas incompat\u00edveis de computa\u00e7\u00e3o ser\u00e3o desativadas.",
|
||||
@ -3391,6 +3408,7 @@
|
||||
"message.read.accept.license.agreements": "Leia e aceite os termos dos contratos de licen\u00e7a.",
|
||||
"message.read.admin.guide.scaling.up": "Por favor leia a sess\u00e3o sobre escalonamento din\u00e2mico no guia do administrador antes de escalonar.",
|
||||
"message.recover.vm": "Por favor, confirme a recupera\u00e7\u00e3o desta VM.",
|
||||
"message.register.keypair.failed": "Falha ao registrar par de chave de API",
|
||||
"message.reinstall.vm": "NOTA: proceda com cuidado. Isso far\u00e1 com que a m\u00e1quina virtual seja re-instalada a partir do template. Todos os dados do disco root ser\u00e3o perdidos. Se houver volumes de dados adicionais, eles n\u00e3o ser\u00e3o alterados.",
|
||||
"message.release.ip.failed": "Falha ao liberar IP",
|
||||
"message.releasing.dedicated.cluster": "Liberando cluster dedicado...",
|
||||
@ -3542,7 +3560,6 @@
|
||||
"message.success.create.keypair": "Par de chaves SSH criado com sucesso",
|
||||
"message.success.create.kubernetes.cluster": "Cluster Kubernetes criado com sucesso",
|
||||
"message.success.create.l2.network": "Rede L2 criada com sucesso",
|
||||
"message.success.create.password": "Par de chave de acesso \u00e0 API criada com sucesso",
|
||||
"message.success.create.sharedfs": "Sistema de Arquivos Compartilhado criado com sucesso",
|
||||
"message.success.create.snapshot.from.vmsnapshot": "Snapshot de volume a partir da snapshot de VM criada com sucesso",
|
||||
"message.success.create.template": "Template criado com sucesso",
|
||||
@ -3586,7 +3603,7 @@
|
||||
"message.success.register.keypair": "Par de chaves SSH registrado com sucesso",
|
||||
"message.success.register.template": "Template cadastrado com sucesso",
|
||||
"message.success.register.user.data": "Userdata registrado com sucesso",
|
||||
"message.success.register.user.keypair": "Novo par de chave de API criado com sucesso",
|
||||
"message.success.register.user.keypair": "Novo par de chaves de API registrado com sucesso para o usu\u00e1rio \"{user}\"",
|
||||
"message.success.release.dedicated.bgp.peer": "Par BGP dedicado liberado com sucesso",
|
||||
"message.success.release.dedicated.ipv4.subnet": "Sub-rede IPv4 dedicada liberada com sucesso",
|
||||
"message.success.release.ip": "IP liberado com sucesso",
|
||||
|
||||
451
ui/src/components/view/ApiKeyPairsTab.vue
Normal file
451
ui/src/components/view/ApiKeyPairsTab.vue
Normal file
@ -0,0 +1,451 @@
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you 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.
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<a-spin :spinning="fetchLoading">
|
||||
<a-button
|
||||
v-if="'registerUserKeys' in $store.getters.apis"
|
||||
type="dashed"
|
||||
style="width: 100%; margin-bottom: 15px"
|
||||
@click="onShowAddKeyPair()">
|
||||
<template #icon><plus-outlined /></template>
|
||||
{{ $t('label.register.api.key') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="this.selectedRowKeys.length > 0 && ('deleteUserKeys' in $store.getters.apis)"
|
||||
type="primary"
|
||||
danger
|
||||
style="width: 100%; margin-bottom: 15px"
|
||||
@click="bulkActionConfirmation()">
|
||||
<template #icon><delete-outlined /></template>
|
||||
{{ $t('label.action.bulk.delete.api.keys') }}
|
||||
</a-button>
|
||||
<a-table
|
||||
size="small"
|
||||
style="overflow-y: auto"
|
||||
:columns="columns"
|
||||
:dataSource="keypairs"
|
||||
:rowKey="item => item.id"
|
||||
:rowSelection="rowSelection()"
|
||||
:pagination="false" >
|
||||
<template #name="{ record }">
|
||||
<div>
|
||||
<router-link :to="{ path: '/keypair/' + record.id }" >
|
||||
{{ record.name }}
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
<template #apikey="{ record }">
|
||||
<strong>
|
||||
<tooltip-button
|
||||
tooltipPlacement="right"
|
||||
:tooltip="$t('label.copy')"
|
||||
icon="CopyOutlined"
|
||||
type="dashed"
|
||||
size="small"
|
||||
@onClick="$message.success($t('label.copied.clipboard'))"
|
||||
:copyResource="record.apikey" />
|
||||
</strong>
|
||||
<div>
|
||||
{{ record.apikey.substring(0, 20) }}...
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #secretkey="{ record }">
|
||||
<strong>
|
||||
<tooltip-button
|
||||
tooltipPlacement="right"
|
||||
:tooltip="$t('label.copy')"
|
||||
icon="CopyOutlined"
|
||||
type="dashed"
|
||||
size="small"
|
||||
@onClick="$message.success($t('label.copied.clipboard'))"
|
||||
:copyResource="record.secretkey" />
|
||||
</strong>
|
||||
<div>
|
||||
{{ record.secretkey.substring(0, 20) }}...
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #startdate="{ record }">
|
||||
<div> {{ $toLocaleDate(record.startdate) }} </div>
|
||||
</template>
|
||||
|
||||
<template #enddate="{ record }">
|
||||
<div> {{ $toLocaleDate(record.enddate)}} </div>
|
||||
</template>
|
||||
|
||||
<template #created="{ record }">
|
||||
<div> {{ $toLocaleDate(record.created) }} </div>
|
||||
</template>
|
||||
|
||||
</a-table>
|
||||
<a-divider/>
|
||||
<a-pagination
|
||||
class="row-element pagination"
|
||||
size="small"
|
||||
:current="page"
|
||||
:pageSize="pageSize"
|
||||
:total="totalKeypairs"
|
||||
:showTotal="total => `${$t('label.total')} ${total} ${$t('label.items')}`"
|
||||
:pageSizeOptions="['10', '20', '40', '80', '100']"
|
||||
@change="changePage"
|
||||
@showSizeChange="changePageSize"
|
||||
showSizeChanger>
|
||||
<template #buildOptionText="props">
|
||||
<span>{{ props.value }} / {{ $t('label.page') }}</span>
|
||||
</template>
|
||||
</a-pagination>
|
||||
</a-spin>
|
||||
<bulk-action-view
|
||||
v-if="(showConfirmationAction || showGroupActionModal)"
|
||||
:showConfirmationAction="showConfirmationAction"
|
||||
:showGroupActionModal="showGroupActionModal"
|
||||
:items="keypairs"
|
||||
:selectedRowKeys="selectedRowKeys"
|
||||
:selectedItems="selectedItems"
|
||||
:columns="columns"
|
||||
:selectedColumns="selectedColumns"
|
||||
action="eraseKeypairs"
|
||||
:loading="loading"
|
||||
:message="bulkDeleteMessage"
|
||||
@group-action="eraseKeypairs"
|
||||
@handle-cancel="handleCancelBulk"
|
||||
@close-modal="closeModalBulk" />
|
||||
<generate-api-key-pair
|
||||
:showAddKeyPair="showAddKeyPair"
|
||||
:resource="resource"
|
||||
@fetch-data="fetchData"
|
||||
@refresh-data="handleRefreshData"
|
||||
@close-modal="closeModalAddKeyPair" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { getAPI, postAPI } from '@/api'
|
||||
import TooltipButton from '@/components/widgets/TooltipButton'
|
||||
import BulkActionView from '@/components/view/BulkActionView.vue'
|
||||
import eventBus from '@/config/eventBus'
|
||||
import GenerateApiKeyPair from '@/views/iam/GenerateApiKeyPair.vue'
|
||||
|
||||
export default {
|
||||
name: 'ApiKeyPairsTab',
|
||||
components: {
|
||||
TooltipButton,
|
||||
BulkActionView,
|
||||
GenerateApiKeyPair
|
||||
},
|
||||
props: {
|
||||
resource: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
fetchLoading: false,
|
||||
keypairs: [],
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
totalKeypairs: 0,
|
||||
selectedRowKeys: [],
|
||||
selectedItems: [],
|
||||
selectedColumns: [],
|
||||
filterColumns: ['Action'],
|
||||
showConfirmationAction: false,
|
||||
showAddKeyPair: false,
|
||||
showGroupActionModal: false,
|
||||
bulkDeleteMessage: {
|
||||
title: this.$t('label.action.bulk.delete.api.keys'),
|
||||
confirmMessage: this.$t('label.confirm.delete.api.keys')
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
title: this.$t('label.name'),
|
||||
dataIndex: 'name',
|
||||
slots: { customRender: 'name' }
|
||||
},
|
||||
{
|
||||
title: this.$t('label.apikey'),
|
||||
dataIndex: 'apikey',
|
||||
slots: { customRender: 'apikey' }
|
||||
},
|
||||
{
|
||||
title: this.$t('label.secretkey'),
|
||||
dataIndex: 'secretkey',
|
||||
slots: { customRender: 'secretkey' }
|
||||
},
|
||||
{
|
||||
title: this.$t('label.start.date'),
|
||||
dataIndex: 'startdate',
|
||||
slots: { customRender: 'startdate' }
|
||||
},
|
||||
{
|
||||
title: this.$t('label.end.date'),
|
||||
dataIndex: 'enddate',
|
||||
slots: { customRender: 'enddate' }
|
||||
},
|
||||
{
|
||||
title: this.$t('label.created'),
|
||||
dataIndex: 'created',
|
||||
slots: { customRender: 'created' }
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.fetchData()
|
||||
},
|
||||
watch: {
|
||||
resource: {
|
||||
deep: true,
|
||||
handler (newItem) {
|
||||
if (!newItem || !newItem.id) {
|
||||
return
|
||||
}
|
||||
this.fetchData()
|
||||
}
|
||||
}
|
||||
},
|
||||
inject: ['parentFetchData'],
|
||||
methods: {
|
||||
fetchData () {
|
||||
const params = {
|
||||
listall: true,
|
||||
page: this.page,
|
||||
pagesize: this.pageSize,
|
||||
userid: this.resource.id
|
||||
}
|
||||
this.fetchLoading = true
|
||||
getAPI('listUserKeys', params).then(json => {
|
||||
this.totalKeypairs = json.listuserkeysresponse.count || 0
|
||||
this.keypairs = json.listuserkeysresponse.userapikey || []
|
||||
}).finally(() => {
|
||||
this.fetchLoading = false
|
||||
})
|
||||
},
|
||||
setSelection (selection) {
|
||||
this.selectedRowKeys = selection
|
||||
this.$emit('selection-change', this.selectedRowKeys)
|
||||
this.selectedItems = (this.keypairs.filter(function (item) {
|
||||
return selection.indexOf(item.id) !== -1
|
||||
}))
|
||||
},
|
||||
changePage (page, pageSize) {
|
||||
this.page = page
|
||||
this.pageSize = pageSize
|
||||
this.fetchData()
|
||||
},
|
||||
changePageSize (currentPage, pageSize) {
|
||||
this.page = currentPage
|
||||
this.pageSize = pageSize
|
||||
this.fetchData()
|
||||
},
|
||||
onShowAddKeyPair () {
|
||||
this.showAddKeyPair = true
|
||||
},
|
||||
eraseKeypairs () {
|
||||
this.selectedColumns.splice(0, 0, {
|
||||
dataIndex: 'status',
|
||||
title: this.$t('label.operation.status'),
|
||||
slots: { customRender: 'status' },
|
||||
filters: [
|
||||
{ text: this.$t('state.inprogress'), value: 'InProgress' },
|
||||
{ text: this.$t('label.success'), value: 'success' },
|
||||
{ text: this.$t('label.failed'), value: 'failed' }
|
||||
]
|
||||
})
|
||||
if (this.selectedRowKeys.length > 0) {
|
||||
this.showGroupActionModal = true
|
||||
}
|
||||
this.deleteKeypairs(this.selectedItems)
|
||||
},
|
||||
async deleteKeypairs (keypairs) {
|
||||
if (!keypairs || keypairs.length === 0) {
|
||||
this.fetchLoading = false
|
||||
return
|
||||
}
|
||||
|
||||
this.fetchLoading = true
|
||||
try {
|
||||
await Promise.all(keypairs.map(async keypair => {
|
||||
try {
|
||||
const jobId = await this.deleteKeyPair({
|
||||
keypairid: keypair.id
|
||||
})
|
||||
await this.$pollJob({
|
||||
jobId,
|
||||
action: {
|
||||
isFetchData: false
|
||||
},
|
||||
successMethod: () => {
|
||||
eventBus.emit('update-resource-state', { selectedItems: this.selectedItems, resource: keypair.id, state: 'success' })
|
||||
},
|
||||
catchMethod: () => {
|
||||
eventBus.emit('update-resource-state', { selectedItems: this.selectedItems, resource: keypair.id, state: 'failed' })
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
eventBus.emit('update-resource-state', { selectedItems: this.selectedItems, resource: keypair.id, state: 'failed' })
|
||||
}
|
||||
}))
|
||||
} finally {
|
||||
this.fetchLoading = false
|
||||
}
|
||||
},
|
||||
async deleteKeyPair (args) {
|
||||
const response = await postAPI('deleteUserKeys', args)
|
||||
return response.deleteuserkeysresponse.jobid
|
||||
},
|
||||
bulkActionConfirmation () {
|
||||
this.showConfirmationAction = true
|
||||
this.selectedColumns = this.columns.filter(column => {
|
||||
return !this.filterColumns.includes(column.title)
|
||||
})
|
||||
this.selectedItems = this.selectedItems.map(v => ({ ...v, status: 'InProgress' }))
|
||||
},
|
||||
handleCancelBulk () {
|
||||
eventBus.emit('update-bulk-job-status', { items: this.selectedItems, action: false })
|
||||
this.showGroupActionModal = false
|
||||
this.selectedItems = []
|
||||
this.selectedColumns = []
|
||||
this.selectedRowKeys = []
|
||||
this.parentFetchData()
|
||||
},
|
||||
closeModalBulk () {
|
||||
this.showConfirmationAction = false
|
||||
},
|
||||
closeModalAddKeyPair () {
|
||||
this.showAddKeyPair = false
|
||||
},
|
||||
handleRefreshData () {
|
||||
this.$emit('refresh-data')
|
||||
},
|
||||
rowSelection () {
|
||||
if ('deleteUserKeys' in this.$store.getters.apis) {
|
||||
return {
|
||||
selectedRowKeys: this.selectedRowKeys,
|
||||
onChange: this.setSelection
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.list {
|
||||
max-height: 95vh;
|
||||
width: 95vw;
|
||||
overflow-y: scroll;
|
||||
margin: -24px;
|
||||
|
||||
@media (min-width: 1000px) {
|
||||
max-height: 70vh;
|
||||
width: 900px;
|
||||
}
|
||||
|
||||
&__header,
|
||||
&__footer {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
|
||||
.ant-select {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
&__col {
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
&--full {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&__footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
button {
|
||||
&:not(:last-child) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__item {
|
||||
padding-right: 20px;
|
||||
padding-left: 20px;
|
||||
|
||||
&--selected {
|
||||
background-color: #e6f7ff;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&__outer-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
|
||||
@media (min-width: 760px) {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&__row {
|
||||
margin-bottom: 10px;
|
||||
|
||||
@media (min-width: 760px) {
|
||||
margin-right: 20px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__radio {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
@ -97,7 +97,7 @@
|
||||
<span v-if="['USER.LOGIN', 'USER.LOGOUT', 'ROUTER.HEALTH.CHECKS', 'FIREWALL.CLOSE', 'ALERT.SERVICE.DOMAINROUTER'].includes(dataResource[item])">{{ $t(dataResource[item].toLowerCase()) }}</span>
|
||||
<span v-else>{{ dataResource[item] }}</span>
|
||||
</div>
|
||||
<div v-else-if="['created', 'sent', 'lastannotated', 'collectiontime', 'lastboottime', 'lastserverstart', 'lastserverstop', 'removed', 'effectiveDate', 'endDate'].includes(item)">
|
||||
<div v-else-if="['created', 'sent', 'lastannotated', 'collectiontime', 'lastboottime', 'lastserverstart', 'lastserverstop', 'removed', 'effectiveDate', 'endDate', 'startdate', 'enddate'].includes(item)">
|
||||
{{ $toLocaleDate(dataResource[item]) }}
|
||||
</div>
|
||||
<div style="white-space: pre-wrap;" v-else-if="$route.meta.name === 'quotatariff' && item === 'description'">{{ dataResource[item] }}</div>
|
||||
@ -168,7 +168,7 @@
|
||||
<div>{{ dataResource[item] }}</div>
|
||||
</div>
|
||||
</a-list-item>
|
||||
<a-list-item v-else-if="['startdate', 'enddate'].includes(item)">
|
||||
<a-list-item v-else-if="['startdate', 'enddate'].includes(item) && dataResource[item]">
|
||||
<div>
|
||||
<strong>{{ $t('label.' + item.replace('date', '.date.and.time'))}}</strong>
|
||||
<br/>
|
||||
|
||||
@ -120,12 +120,18 @@
|
||||
|
||||
<a-divider/>
|
||||
|
||||
<div class="resource-detail-item" v-if="(resource.state || resource.status) && $route.meta.name !== 'zone'">
|
||||
<div class="resource-detail-item" v-if="(resource.state || resource.status) && !['zone', 'keypair'].includes($route.meta.name)">
|
||||
<div class="resource-detail-item__label">{{ $t('label.status') }}</div>
|
||||
<div class="resource-detail-item__details">
|
||||
<status class="status" :text="resource.state || resource.status" displayText/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource-detail-item" v-if="resource.apikeyaccess && $route.meta.name === 'accountuser'">
|
||||
<div class="resource-detail-item__label">{{ $t('label.apikeyaccess') }}</div>
|
||||
<div class="resource-detail-item__details">
|
||||
<status class="status" :text="resource.apikeyaccess" displayText/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource-detail-item" v-if="resource.allocationstate">
|
||||
<div class="resource-detail-item__label">{{ $t('label.allocationstate') }}</div>
|
||||
<div class="resource-detail-item__details">
|
||||
@ -159,6 +165,42 @@
|
||||
<span style="margin-left: 10px;"><copy-label :label="resource.id" /></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource-detail-item" v-if="resource.apikey && resource.secretkey">
|
||||
<div class="user-keys">
|
||||
<key-outlined />
|
||||
<strong>
|
||||
{{ $t('label.apikey') }}
|
||||
<tooltip-button
|
||||
tooltipPlacement="right"
|
||||
:tooltip="$t('label.copy') + ' ' + $t('label.apikey')"
|
||||
icon="CopyOutlined"
|
||||
type="dashed"
|
||||
size="small"
|
||||
@onClick="$message.success($t('label.copied.clipboard'))"
|
||||
:copyResource="resource.apikey" />
|
||||
</strong>
|
||||
<div>
|
||||
{{ resource.apikey.substring(0, 20) }}...
|
||||
</div>
|
||||
</div> <br/>
|
||||
<div class="user-keys">
|
||||
<lock-outlined />
|
||||
<strong>
|
||||
{{ $t('label.secretkey') }}
|
||||
<tooltip-button
|
||||
tooltipPlacement="right"
|
||||
:tooltip="$t('label.copy') + ' ' + $t('label.secretkey')"
|
||||
icon="CopyOutlined"
|
||||
type="dashed"
|
||||
size="small"
|
||||
@onClick="$message.success($t('label.copied.clipboard'))"
|
||||
:copyResource="resource.secretkey" />
|
||||
</strong>
|
||||
<div>
|
||||
{{ resource.secretkey.substring(0, 20) }}...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource-detail-item" v-if="(resource.ostypename || resource.osdisplayname) && resource.ostypeid">
|
||||
<div class="resource-detail-item__label">{{ $t('label.ostypename') }}</div>
|
||||
<div class="resource-detail-item__details">
|
||||
@ -796,6 +838,14 @@
|
||||
<span v-else>{{ resource.account }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource-detail-item" v-if="resource.userid && $route.meta.name === 'keypair'">
|
||||
<div class="resource-detail-item__label">{{ $t('label.user') }}</div>
|
||||
<div class="resource-detail-item__details">
|
||||
<user-outlined />
|
||||
<router-link v-if="!isStatic && $router.resolve('/accountuser/' + resource.userid).matched[0].redirect !== '/exception/404'" :to="{ path: '/accountuser/' + resource.userid }">{{ resource.username }}</router-link>
|
||||
<span v-else>{{ resource.username }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource-detail-item" v-if="resource.roleid">
|
||||
<div class="resource-detail-item__label">{{ $t('label.role') }}</div>
|
||||
<div class="resource-detail-item__details">
|
||||
@ -883,54 +933,6 @@
|
||||
:osCategoryId="osCategoryId" />
|
||||
</div>
|
||||
|
||||
<div class="account-center-tags" v-if="showKeys || resource.apikeyaccess">
|
||||
<a-divider/>
|
||||
</div>
|
||||
<div class="account-center-tags" v-if="resource.apikeyaccess && resource.account">
|
||||
<div class="resource-detail-item">
|
||||
<div class="resource-detail-item__label">{{ $t('label.apikeyaccess') }}</div>
|
||||
<div class="resource-detail-item__details">
|
||||
<status class="status" :text="resource.apikeyaccess" displayText/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="account-center-tags" v-if="showKeys">
|
||||
<div class="user-keys">
|
||||
<key-outlined />
|
||||
<strong>
|
||||
{{ $t('label.apikey') }}
|
||||
<tooltip-button
|
||||
tooltipPlacement="right"
|
||||
:tooltip="$t('label.copy') + ' ' + $t('label.apikey')"
|
||||
icon="CopyOutlined"
|
||||
type="dashed"
|
||||
size="small"
|
||||
@onClick="$message.success($t('label.copied.clipboard'))"
|
||||
:copyResource="resource.apikey" />
|
||||
</strong>
|
||||
<div>
|
||||
{{ resource.apikey.substring(0, 20) }}...
|
||||
</div>
|
||||
</div> <br/>
|
||||
<div class="user-keys">
|
||||
<lock-outlined />
|
||||
<strong>
|
||||
{{ $t('label.secretkey') }}
|
||||
<tooltip-button
|
||||
tooltipPlacement="right"
|
||||
:tooltip="$t('label.copy') + ' ' + $t('label.secretkey')"
|
||||
icon="CopyOutlined"
|
||||
type="dashed"
|
||||
size="small"
|
||||
@onClick="$message.success($t('label.copied.clipboard'))"
|
||||
:copyResource="resource.secretkey" />
|
||||
</strong>
|
||||
<div>
|
||||
{{ resource.secretkey.substring(0, 20) }}...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="account-center-tags" v-if="!isStatic && resourceType && tagsSupportingResourceTypes.includes(this.resourceType) && 'listTags' in $store.getters.apis">
|
||||
<a-divider/>
|
||||
<a-spin :spinning="loadingTags">
|
||||
@ -1077,7 +1079,7 @@ export default {
|
||||
this.setData()
|
||||
this.validLinks = await validateLinksAsync(this.$router, this.isStatic, this.resource)
|
||||
|
||||
if ('apikey' in this.resource) {
|
||||
if (this.$route.meta.name === 'accountuser' && 'apikey' in this.resource) {
|
||||
this.getUserKeys()
|
||||
}
|
||||
this.updateResourceAdditionalData()
|
||||
@ -1256,8 +1258,6 @@ export default {
|
||||
return
|
||||
}
|
||||
getAPI('getUserKeys', { id: this.resource.id }).then(json => {
|
||||
this.showKeys = true
|
||||
this.newResource.secretkey = json.getuserkeysresponse.userkeys.secretkey
|
||||
if (!this.isAdmin()) {
|
||||
this.newResource.apikeyaccess = json.getuserkeysresponse.userkeys.apikeyaccess ? 'Enabled' : 'Disabled'
|
||||
}
|
||||
|
||||
@ -795,7 +795,7 @@
|
||||
{{ record.enabled ? 'Enabled' : 'Disabled' }}
|
||||
</template>
|
||||
<template
|
||||
v-if="['created', 'sent', 'removed', 'effectiveDate', 'endDate', 'allocated'].includes(column.key) || (['startdate'].includes(column.key) && ['webhook'].includes($route.path.split('/')[1])) || (column.key === 'allocated' && ['asnumbers', 'publicip', 'ipv4subnets'].includes($route.meta.name) && text)"
|
||||
v-if="['created', 'sent', 'removed', 'effectiveDate', 'endDate', 'allocated', 'startdate', 'enddate'].includes(column.key) || (['startdate'].includes(column.key) && ['webhook'].includes($route.path.split('/')[1])) || (column.key === 'allocated' && ['asnumbers', 'publicip', 'ipv4subnets'].includes($route.meta.name) && text)"
|
||||
>
|
||||
{{ text && $toLocaleDate(text) }}
|
||||
</template>
|
||||
|
||||
@ -31,6 +31,7 @@ import image from '@/config/section/image'
|
||||
import project from '@/config/section/project'
|
||||
import event from '@/config/section/event'
|
||||
import user from '@/config/section/user'
|
||||
import keyPair from '@/config/section/keypair'
|
||||
import account from '@/config/section/account'
|
||||
import domain from '@/config/section/domain'
|
||||
import role from '@/config/section/role'
|
||||
@ -221,6 +222,7 @@ export function asyncRouterMap () {
|
||||
generateRouterMap(event),
|
||||
generateRouterMap(project),
|
||||
generateRouterMap(user),
|
||||
generateRouterMap(keyPair),
|
||||
generateRouterMap(role),
|
||||
generateRouterMap(account),
|
||||
generateRouterMap(domain),
|
||||
|
||||
69
ui/src/config/section/keypair.js
Normal file
69
ui/src/config/section/keypair.js
Normal file
@ -0,0 +1,69 @@
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you 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 { shallowRef, defineAsyncComponent } from 'vue'
|
||||
import store from '@/store'
|
||||
|
||||
export default {
|
||||
name: 'keypair',
|
||||
identifier: 'keypairid',
|
||||
title: 'label.apikeypairs',
|
||||
icon: 'key-outlined',
|
||||
hidden: true,
|
||||
docHelp: 'adminguide/accounts.html#keypairs',
|
||||
permission: ['listUserKeys'],
|
||||
columns: [
|
||||
'name',
|
||||
{ field: 'startdate', customTitle: 'apikeypair.startdate' },
|
||||
{ field: 'enddate', customTitle: 'apikeypair.enddate' },
|
||||
'username', 'rolename'
|
||||
],
|
||||
details: [
|
||||
'id', 'name', 'description',
|
||||
'domain', 'role', 'roletype',
|
||||
{ field: 'accountname', customTitle: 'account' }, 'username',
|
||||
{ field: 'startdate', customTitle: 'apikeypair.startdate' },
|
||||
{ field: 'enddate', customTitle: 'apikeypair.enddate' },
|
||||
'created'
|
||||
],
|
||||
tabs: [{
|
||||
name: 'details',
|
||||
component: shallowRef(defineAsyncComponent(() => import('@/components/view/DetailsTab.vue')))
|
||||
}, {
|
||||
name: 'rules',
|
||||
component: shallowRef(defineAsyncComponent(() => import('@/views/iam/ApiKeyPairPermissionTable.vue'))),
|
||||
show: () => { return 'listUserKeyRules' in store.getters.apis }
|
||||
}],
|
||||
actions: [
|
||||
{
|
||||
api: 'deleteUserKeys',
|
||||
icon: 'delete-outlined',
|
||||
label: 'label.action.delete.keypair',
|
||||
message: 'message.delete.keypair',
|
||||
dataView: true,
|
||||
args: ['keypairid'],
|
||||
mapping: {
|
||||
keypairid: {
|
||||
value: (record) => { return record.id }
|
||||
}
|
||||
},
|
||||
show: () => {
|
||||
return 'deleteUserKeys' in store.getters.apis
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -63,6 +63,12 @@ export default {
|
||||
resourceType: 'User',
|
||||
component: shallowRef(defineAsyncComponent(() => import('@/components/view/EventsTab.vue'))),
|
||||
show: () => { return 'listEvents' in store.getters.apis }
|
||||
},
|
||||
{
|
||||
name: 'apikeypairs',
|
||||
resourceType: 'User',
|
||||
component: shallowRef(defineAsyncComponent(() => import('@/components/view/ApiKeyPairsTab.vue'))),
|
||||
show: () => { return 'listUserKeys' in store.getters.apis }
|
||||
}
|
||||
],
|
||||
actions: [
|
||||
|
||||
@ -1119,7 +1119,6 @@ export default {
|
||||
this.loading = true
|
||||
if (this.$route.path.startsWith('/cniconfiguration')) {
|
||||
params.forcks = true
|
||||
console.log('here')
|
||||
}
|
||||
if (this.$route.params && this.$route.params.id) {
|
||||
params.id = this.$route.params.id
|
||||
@ -1132,6 +1131,10 @@ export default {
|
||||
params.name = this.$route.params.id
|
||||
}
|
||||
}
|
||||
if (['listUserKeys'].includes(this.apiName)) {
|
||||
delete params.listall
|
||||
params.keypairid = this.$route.params.id
|
||||
}
|
||||
if (['listPublicIpAddresses'].includes(this.apiName)) {
|
||||
params.allocatedonly = false
|
||||
}
|
||||
@ -1253,7 +1256,7 @@ export default {
|
||||
if (this.items.length <= 0 && this.dataView) {
|
||||
this.$router.push({ path: '/exception/404' })
|
||||
}
|
||||
if (!this.showAction || this.dataView) {
|
||||
if (!this.showAction || this.dataView || (this.items.length === 1 && this.apiName === 'getUserKeys')) {
|
||||
this.resource = this.items?.[0] || {}
|
||||
this.$emit('change-resource', this.resource)
|
||||
}
|
||||
|
||||
518
ui/src/views/iam/ApiKeyPairPermissionTable.vue
Normal file
518
ui/src/views/iam/ApiKeyPairPermissionTable.vue
Normal file
@ -0,0 +1,518 @@
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you 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.
|
||||
|
||||
<template>
|
||||
<loading-outlined v-if="loadingTable" class="main-loading-spinner" />
|
||||
<div v-else>
|
||||
<div v-if="!disabled" class="rules-list ant-list ant-list-bordered">
|
||||
<div class="rules-table-item ant-list-item">
|
||||
<div class="rules-table__col rules-table__col--grab" />
|
||||
<div class="rules-table__col rules-table__col--rule rules-table__col--new">
|
||||
<a-auto-complete
|
||||
:key="autocompleteKey"
|
||||
v-focus="true"
|
||||
:filterOption="filterOption"
|
||||
:options="apis"
|
||||
v-model:value="newRule"
|
||||
:placeholder="$t('label.rule')" />
|
||||
</div>
|
||||
<div class="rules-table__col rules-table__col--permission">
|
||||
<permission-editable
|
||||
:default-value="newRulePermission"
|
||||
:value="newRulePermission"
|
||||
@onChange="updateNewPermission()" />
|
||||
</div>
|
||||
<div class="rules-table__col rules-table__col--description">
|
||||
<a-input v-model:value="newRuleDescription" :placeholder="$t('label.description')" />
|
||||
</div>
|
||||
<div class="rules-table__col rules-table__col--actions">
|
||||
<tooltip-button
|
||||
tooltipPlacement="bottom"
|
||||
:tooltip="$t('label.save.new.rule')"
|
||||
icon="plus-outlined"
|
||||
type="primary"
|
||||
@onClick="onRuleSave" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<draggable
|
||||
v-model="rules"
|
||||
@change="updateRules"
|
||||
handle=".drag-handle"
|
||||
ghostClass="drag-ghost"
|
||||
:component-data="{type: 'transition'}"
|
||||
item-key="rule">
|
||||
<template #item="{element, index}">
|
||||
<div class="rules-table-item ant-list-item">
|
||||
<div class="rules-table__col rules-table__col--grab drag-handle">
|
||||
<drag-outlined />
|
||||
</div>
|
||||
<div class="rules-table__col rules-table__col--rule">
|
||||
{{ element.rule }}
|
||||
</div>
|
||||
<div class="rules-table__col rules-table__col--permission">
|
||||
<permission-editable
|
||||
:default-value="element.permission"
|
||||
@onChange="onPermissionChange(element, $event, index)" />
|
||||
</div>
|
||||
<div class="rules-table__col rules-table__col--description">
|
||||
<div v-if="element.description">
|
||||
{{ element.description }}
|
||||
</div>
|
||||
<div v-else class="no-description">
|
||||
{{ $t('message.no.description') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rules-table__col rules-table__col--actions">
|
||||
<tooltip-button
|
||||
:tooltip="$t('label.delete.rule')"
|
||||
tooltipPlacement="bottom"
|
||||
type="primary"
|
||||
:danger="true"
|
||||
icon="delete-outlined"
|
||||
:disabled="false"
|
||||
@onClick="onRuleDelete(element.rule, index)" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
|
||||
<div :style="{width: '100%', display: 'flex', marginTop: this.rules.length > 0 ? '12px' : '0'}" v-if="this.rules.length > 0 && !disabled">
|
||||
<a-button
|
||||
style="width: 100%;"
|
||||
danger
|
||||
@click="deleteAllRules()">
|
||||
<template #icon><delete-outlined /></template>
|
||||
{{ $t('label.delete.all.rules') }}
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
v-else-if="disabled"
|
||||
:columns="columns"
|
||||
:dataSource="rules"
|
||||
rowKey="rule"
|
||||
size="large"
|
||||
:pagination="pagination"
|
||||
@change="handlePaginationChange">
|
||||
<template #customFilterDropdown="{ setSelectedKeys, selectedKeys, confirm, clearFilters, column }">
|
||||
<div style="padding: 8px">
|
||||
<a-input
|
||||
ref="searchInput"
|
||||
:placeholder="$t('label.search')"
|
||||
:value="selectedKeys[0]"
|
||||
style="width: 100%; margin-bottom: 8px; display: block"
|
||||
@change="e => setSelectedKeys(e.target.value ? [e.target.value] : [])"
|
||||
@pressEnter="handleSearch(selectedKeys, confirm, column.dataIndex)"
|
||||
/>
|
||||
<div style="display: flex; gap: 8px">
|
||||
<a-button
|
||||
type="primary"
|
||||
size="small"
|
||||
style="width: 112px;"
|
||||
@click="handleSearch(selectedKeys, confirm, column.dataIndex)">
|
||||
<template #icon>
|
||||
<search-outlined />
|
||||
</template>
|
||||
{{ $t('label.search') }}
|
||||
</a-button>
|
||||
|
||||
<a-button
|
||||
size="small"
|
||||
style="width: 112px;"
|
||||
@click="handleReset(clearFilters)">
|
||||
{{ $t('label.reset') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #customFilterIcon="{ filtered }">
|
||||
<search-outlined :style="{ color: filtered ? '#1890ff' : '', fontSize: '14px' }" />
|
||||
</template>
|
||||
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'permission'">
|
||||
<a-tag
|
||||
class="permission-tag"
|
||||
:style="{
|
||||
backgroundColor: record.permission === 'allow' ? '#d9f7be' : '#fff2f0',
|
||||
color: record.permission === 'allow' ? '#135200' : '#cf1322'
|
||||
}">
|
||||
<check-outlined v-if="record.permission === 'allow'" />
|
||||
<close-outlined v-else />
|
||||
{{ record.permission === 'allow' ? $t('label.allow') : $t('label.deny') }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'description' && record.description">
|
||||
{{ record.description }}
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getAPI } from '@/api'
|
||||
import draggable from 'vuedraggable'
|
||||
import PermissionEditable from './PermissionEditable'
|
||||
import TooltipButton from '@/components/widgets/TooltipButton'
|
||||
import { genericCompare } from '@/utils/sort'
|
||||
|
||||
export default {
|
||||
name: 'ApiKeyPairPermissionTable',
|
||||
components: {
|
||||
PermissionEditable,
|
||||
draggable,
|
||||
TooltipButton
|
||||
},
|
||||
props: {
|
||||
resource: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
loadingTable: true,
|
||||
disabled: false,
|
||||
rules: [],
|
||||
newRule: '',
|
||||
newRulePermission: 'allow',
|
||||
newRuleDescription: '',
|
||||
drag: false,
|
||||
apis: [],
|
||||
currRules: new Set(),
|
||||
searchText: '',
|
||||
searchedColumn: '',
|
||||
columns: [
|
||||
{
|
||||
title: this.$t('label.rule'),
|
||||
dataIndex: 'rule',
|
||||
key: 'rule',
|
||||
ellipsis: true,
|
||||
customFilterDropdown: true,
|
||||
onFilter: (value, record) => {
|
||||
return record.rule.toString().toLowerCase().includes(value.toLowerCase())
|
||||
},
|
||||
sorter: (a, b) => { return genericCompare(a.rule || '', b.rule || '') },
|
||||
width: 480
|
||||
},
|
||||
{
|
||||
title: this.$t('label.permission'),
|
||||
dataIndex: 'permission',
|
||||
key: 'permission',
|
||||
width: 160,
|
||||
align: 'center',
|
||||
sorter: (a, b) => { return genericCompare(a.permission || '', b.permission || '') }
|
||||
},
|
||||
{
|
||||
title: this.$t('label.description'),
|
||||
dataIndex: 'description',
|
||||
key: 'description'
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
pageSize: this.$store.getters.defaultListViewPageSize,
|
||||
pageSizeOptions: ['10', '20', '40', '80', '100', '200'],
|
||||
showSizeChanger: true
|
||||
},
|
||||
autocompleteKey: 0
|
||||
}
|
||||
},
|
||||
async created () {
|
||||
if (this.$route.path.startsWith('/keypair')) {
|
||||
await this.fetchKeyData()
|
||||
this.disabled = true
|
||||
} else {
|
||||
this.getApis()
|
||||
}
|
||||
this.loadingTable = false
|
||||
},
|
||||
methods: {
|
||||
handleSearch (selectedKeys, confirm, dataIndex) {
|
||||
confirm()
|
||||
this.searchText = selectedKeys[0]
|
||||
this.searchedColumn = dataIndex
|
||||
},
|
||||
handleReset (clearFilters) {
|
||||
clearFilters()
|
||||
this.searchText = ''
|
||||
},
|
||||
handlePaginationChange (pagination) {
|
||||
this.pagination.pageSize = pagination.pageSize
|
||||
this.pagination.current = pagination.current
|
||||
},
|
||||
filterOption (input, option) {
|
||||
return option.value.toUpperCase().indexOf(input.toUpperCase()) >= 0
|
||||
},
|
||||
async fetchKeyData () {
|
||||
try {
|
||||
const response = await getAPI('listUserKeyRules', { keypairid: this.resource.id })
|
||||
this.rules = response?.listuserkeyrulesresponse?.keypermission ?? []
|
||||
} catch (e) {
|
||||
this.$notifyError(e)
|
||||
}
|
||||
},
|
||||
getApis () {
|
||||
this.apis = Object.keys(this.$store.getters.apis).sort((a, b) => a.localeCompare(b)).map(value => { return { value: value } })
|
||||
},
|
||||
onRuleDelete (rule, idx) {
|
||||
this.rules.splice(idx, 1)
|
||||
this.currRules.delete(rule)
|
||||
this.updateRules()
|
||||
},
|
||||
onRuleSave () {
|
||||
if (!this.newRule || this.currRules.has(this.newRule)) {
|
||||
return
|
||||
}
|
||||
this.rules.push({
|
||||
rule: this.newRule,
|
||||
permission: this.newRulePermission,
|
||||
description: this.newRuleDescription,
|
||||
roleid: this.resource.id
|
||||
})
|
||||
this.currRules.add(this.newRule)
|
||||
this.newRule = ''
|
||||
this.newRuleDescription = ''
|
||||
this.autocompleteKey += 1
|
||||
this.updateRules()
|
||||
},
|
||||
async deleteAllRules () {
|
||||
this.loadingTable = true
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
this.rules = []
|
||||
this.currRules = new Set()
|
||||
this.updateRules()
|
||||
resolve()
|
||||
this.loadingTable = false
|
||||
})
|
||||
})
|
||||
},
|
||||
updateRules () {
|
||||
this.$emit('update-rules', this.rules)
|
||||
},
|
||||
updateNewPermission () {
|
||||
this.newRulePermission = (this.newRulePermission === 'allow') ? 'deny' : 'allow'
|
||||
},
|
||||
onPermissionChange (record, value, idx) {
|
||||
if (!record) return
|
||||
this.rules[idx].permission = value
|
||||
record.permission = value
|
||||
this.updateRules()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.main-loading-spinner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 30px;
|
||||
}
|
||||
.role-add-btn {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.new-role-controls {
|
||||
display: flex;
|
||||
|
||||
button {
|
||||
&:not(:last-child) {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rules-list {
|
||||
max-height: 600px;
|
||||
overflow: auto;
|
||||
|
||||
&--overflow-hidden {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.rules-table {
|
||||
&-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
padding: 0;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@media (min-width: 760px) {
|
||||
flex-wrap: nowrap;
|
||||
padding-right: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
&__col {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
|
||||
@media (min-width: 760px) {
|
||||
padding: 15px 0;
|
||||
|
||||
&:not(:first-child) {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
border-right: 1px solid #e8e8e8;
|
||||
padding-right: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&--grab {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
|
||||
@media (min-width: 760px) {
|
||||
position: relative;
|
||||
top: auto;
|
||||
width: 35px;
|
||||
padding-left: 25px;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
&--rule,
|
||||
&--description {
|
||||
word-break: break-all;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
|
||||
@media (min-width: 760px) {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&--rule {
|
||||
padding-left: 60px;
|
||||
background-color: rgba(#e6f7ff, 0.7);
|
||||
|
||||
@media (min-width: 760px) {
|
||||
padding-left: 20px;
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
&--permission {
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
|
||||
.ant-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 760px) {
|
||||
width: auto;
|
||||
|
||||
.ant-select {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--actions {
|
||||
max-width: 60px;
|
||||
width: 100%;
|
||||
padding-right: 0;
|
||||
|
||||
@media (min-width: 760px) {
|
||||
width: auto;
|
||||
max-width: 70px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
&--new {
|
||||
padding-left: 15px;
|
||||
background-color: transparent;
|
||||
|
||||
div {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-description {
|
||||
opacity: 0.4;
|
||||
font-size: 0.7rem;
|
||||
|
||||
@media (min-width: 760px) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.drag-ghost {
|
||||
opacity: 0.5;
|
||||
background: #f0f2f5;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 3rem;
|
||||
color: #39A7DE;
|
||||
background-color: rgba(#fff, 0.8);
|
||||
}
|
||||
|
||||
.permission-tag {
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
padding: 2px 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.rules-table__col--new {
|
||||
.ant-select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.rule-dropdown-error {
|
||||
.ant-input {
|
||||
border-color: #ff0000
|
||||
}
|
||||
}
|
||||
</style>
|
||||
226
ui/src/views/iam/GenerateApiKeyPair.vue
Normal file
226
ui/src/views/iam/GenerateApiKeyPair.vue
Normal file
@ -0,0 +1,226 @@
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you 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.
|
||||
|
||||
<template>
|
||||
<div class="form-layout" v-ctrl-enter="handleSubmit">
|
||||
<a-modal
|
||||
v-if="showAddKeyPair"
|
||||
:visible="showAddKeyPair"
|
||||
:closable="true"
|
||||
:maskClosable="false"
|
||||
:okText="$t('label.ok')"
|
||||
:cancelText="$t('label.cancel')"
|
||||
style="top: 20px;"
|
||||
width="50vw"
|
||||
@cancel="closeModal"
|
||||
@ok="handleSubmit"
|
||||
:ok-button-props="{props: { type: 'default' } }"
|
||||
:cancel-button-props="{props: { type: 'primary' } }"
|
||||
centered>
|
||||
<template #title>
|
||||
{{ $t('label.action.create.api.key') }}
|
||||
</template>
|
||||
<a-spin :spinning="loading">
|
||||
<a-form
|
||||
:ref="formRef"
|
||||
:model="form"
|
||||
layout="vertical"
|
||||
@finish="handleSubmit">
|
||||
<a-alert
|
||||
style="margin-bottom: 10px; "
|
||||
:message="$t('message.note.about.keypair.permissions.title')"
|
||||
:description="$t('message.note.about.keypair.permissions.body')"
|
||||
type="info"
|
||||
show-icon
|
||||
/>
|
||||
<a-form-item name="name" ref="name">
|
||||
<template #label>
|
||||
<tooltip-label :title="$t('label.name')" :tooltip="apiParams.name.description"/>
|
||||
</template>
|
||||
<a-input
|
||||
v-focus="true"
|
||||
:placeholder="$t('label.apikeypair.name')"
|
||||
v-model:value="form.name" />
|
||||
</a-form-item>
|
||||
<a-form-item name="description" ref="description">
|
||||
<template #label>
|
||||
<tooltip-label :title="$t('label.description')" :tooltip="apiParams.description.description"/>
|
||||
</template>
|
||||
<a-input
|
||||
v-model:value="form.description"
|
||||
:placeholder="$t('label.apikeypair.description')" />
|
||||
</a-form-item>
|
||||
<a-row>
|
||||
<a-form-item ref="startDate" name="startDate">
|
||||
<template #label>
|
||||
<tooltip-label :title="$t('label.start.date')" :tooltip="apiParams.startdate.description"/>
|
||||
</template>
|
||||
<a-date-picker
|
||||
v-model:value="form.startDate"
|
||||
:disabled-date="disabledStartDate"
|
||||
show-time
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item ref="endDate" name="endDate" style="margin: 0 8px">
|
||||
<template #label>
|
||||
<tooltip-label :title="$t('label.end.date')" :tooltip="apiParams.enddate.description"/>
|
||||
</template>
|
||||
<a-date-picker
|
||||
:disabled-date="disabledEndDate"
|
||||
v-model:value="form.endDate"
|
||||
show-time />
|
||||
</a-form-item>
|
||||
</a-row>
|
||||
<a-form-item>
|
||||
<template #label>
|
||||
<tooltip-label :title="$t('label.rules')" :tooltip="apiParams.rules.description"/>
|
||||
</template>
|
||||
<api-key-pair-permission-table
|
||||
:resource="resource"
|
||||
@update-rules="updateRules"/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, reactive, toRaw } from 'vue'
|
||||
import { postAPI } from '@/api'
|
||||
import TooltipLabel from '@/components/widgets/TooltipLabel'
|
||||
import ApiKeyPairPermissionTable from '@/views/iam/ApiKeyPairPermissionTable.vue'
|
||||
import { dayjs, parseDayJsObject } from '@/utils/date'
|
||||
|
||||
export default {
|
||||
name: 'GenerateApiKeyPair',
|
||||
components: {
|
||||
TooltipLabel,
|
||||
ApiKeyPairPermissionTable
|
||||
},
|
||||
props: {
|
||||
showAddKeyPair: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
resource: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
rules: [],
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
beforeCreate () {
|
||||
this.apiParams = this.$getApiParams('registerUserKeys')
|
||||
},
|
||||
created () {
|
||||
this.initForm()
|
||||
},
|
||||
methods: {
|
||||
initForm () {
|
||||
this.formRef = ref()
|
||||
this.form = reactive({})
|
||||
},
|
||||
buildRequestParams () {
|
||||
const values = toRaw(this.form)
|
||||
this.loading = true
|
||||
const params = {
|
||||
name: values.name,
|
||||
id: this.resource.id,
|
||||
description: values.description ? values.description : null,
|
||||
startdate: values.startDate ? parseDayJsObject({ value: values.startDate }) : null,
|
||||
enddate: values.endDate ? parseDayJsObject({ value: values.endDate }) : null
|
||||
}
|
||||
for (const i in this.rules) {
|
||||
const rule = this.rules[i]
|
||||
params['rules[' + i + '].rule'] = rule.rule ? rule.rule : ''
|
||||
params['rules[' + i + '].permission'] = rule.permission ? rule.permission : 'deny'
|
||||
params['rules[' + i + '].description'] = rule.description ? rule.description : ''
|
||||
}
|
||||
return params
|
||||
},
|
||||
handleSubmit (e) {
|
||||
if (e && typeof e.preventDefault === 'function') {
|
||||
e.preventDefault()
|
||||
}
|
||||
if (this.loading) return
|
||||
this.formRef.value.validate().then(() => {
|
||||
const params = this.buildRequestParams()
|
||||
this.loading = true
|
||||
postAPI('registerUserKeys', params).then(response => {
|
||||
this.$pollJob({
|
||||
jobId: response.registeruserkeysresponse.jobid,
|
||||
successMessage: this.$t('message.success.register.user.keypair', { user: this.resource.username }),
|
||||
successMethod: () => {
|
||||
this.fetchData()
|
||||
},
|
||||
errorMessage: this.$t('message.register.keypair.failed'),
|
||||
errorMethod: () => {
|
||||
this.fetchData()
|
||||
},
|
||||
loadingMessage: this.$t('label.registering.keypair', { user: this.resource.username }),
|
||||
catchMessage: this.$t('error.fetching.async.job.result')
|
||||
})
|
||||
}).catch(error => {
|
||||
this.$notification.error({
|
||||
message: this.$t('message.request.failed'),
|
||||
description: (error.response && error.response.headers && error.response.headers['x-description']) || error.message,
|
||||
duration: 0
|
||||
})
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
this.closeModal()
|
||||
})
|
||||
})
|
||||
},
|
||||
closeModal () {
|
||||
this.form.name = null
|
||||
this.form.description = null
|
||||
this.form.startDate = null
|
||||
this.form.endDate = null
|
||||
this.rules = []
|
||||
this.$emit('close-modal')
|
||||
},
|
||||
fetchData () {
|
||||
this.$emit('fetch-data')
|
||||
},
|
||||
updateRules (rules) {
|
||||
this.rules = rules
|
||||
},
|
||||
disabledStartDate (current) {
|
||||
return current < dayjs().startOf('day')
|
||||
},
|
||||
disabledEndDate (current) {
|
||||
return current < (this.form.startDate || dayjs().startOf('day'))
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.form-layout {
|
||||
width: 80vw;
|
||||
|
||||
@media (min-width: 600px) {
|
||||
width: 450px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue
Block a user