diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index df4762b9..dbb1a95a 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -7,4 +7,4 @@ on:
jobs:
build:
- uses: valitydev/java-workflow/.github/workflows/maven-service-build.yml@v2.0.11
+ uses: valitydev/java-workflow/.github/workflows/maven-service-build.yml@v3
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index dfef0b70..9f366b27 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -7,7 +7,7 @@ on:
jobs:
build-and-deploy:
- uses: valitydev/java-workflow/.github/workflows/maven-service-deploy.yml@v2
+ uses: valitydev/java-workflow/.github/workflows/maven-service-deploy.yml@v3
secrets:
github-token: ${{ secrets.GITHUB_TOKEN }}
mm-webhook-url: ${{ secrets.MATTERMOST_WEBHOOK_URL }}
diff --git a/pom.xml b/pom.xml
index d189c453..7dc98193 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
dev.vality
service-parent-pom
- 2.1.8
+ 3.1.7
org-manager
@@ -16,19 +16,19 @@
org-manager
- 17
- 17
+ 21
+ 21
UTF-8
UTF-8
8022
8080
8023
${server.port} ${server.rest.port} ${management.port}
- 5.6.12
17.0.0
4.34.0
- 1.19-8707f87-server
+ 1.38-8c9b968
2.32.0
+ 1.685-5c25c2e
@@ -36,7 +36,7 @@
dev.vality.woody
woody-thrift
- ${woody.version}
+ 2.1.0
dev.vality
@@ -45,17 +45,18 @@
dev.vality
- swag-organizations
- 1.25-de0cd06-server
+ swag-organizations-server
+ ${swag.organizations.version}
dev.vality
damsel
+ ${damsel.version}
dev.vality
org-management-proto
- 1.11-03a269d
+ 1.18-6b1eb95
dev.vality
@@ -66,7 +67,7 @@
dev.vality
bouncer-proto
- 1.41-5628c61
+ 1.57-31866c3
@@ -94,26 +95,10 @@
org.springframework.boot
spring-boot-starter-security
-
-
- org.springframework
- spring-security-config
-
-
- org.springframework
- spring-security-web
-
-
-
-
- org.springframework.security
- spring-security-web
- ${spring-security.version}
- org.springframework.security
- spring-security-config
- ${spring-security.version}
+ org.springframework.boot
+ spring-boot-starter-oauth2-resource-server
net.javacrumbs.shedlock
@@ -128,37 +113,8 @@
org.keycloak
- keycloak-admin-client
+ keycloak-core
${keycloak.version}
-
-
- org.jboss.resteasy
- resteasy-client
-
-
- org.jboss.resteasy
- resteasy-multipart-provider
-
-
- org.jboss.resteasy
- resteasy-jackson2-provider
-
-
- org.jboss.resteasy
- resteasy-jaxb-provider
-
-
-
-
- org.keycloak
- keycloak-spring-security-adapter
- ${keycloak.version}
-
-
- org.bouncycastle
- bcprov-jdk15on
-
-
org.bouncycastle
@@ -177,14 +133,9 @@
micrometer-registry-prometheus
- javax.servlet
- javax.servlet-api
- 4.0.1
-
-
- javax.validation
- validation-api
- 2.0.1.Final
+ jakarta.validation
+ jakarta.validation-api
+ 3.1.1
org.projectlombok
@@ -193,12 +144,15 @@
org.postgresql
postgresql
- 42.4.3
org.flywaydb
flyway-core
+
+ org.flywaydb
+ flyway-database-postgresql
+
com.google.guava
guava
@@ -231,7 +185,7 @@
org.springframework.cloud
spring-cloud-contract-wiremock
- 3.1.1
+ 4.1.5
test
@@ -319,6 +273,22 @@
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.8.1
+
+ 21
+ 21
+
+
+ org.projectlombok
+ lombok
+ 1.18.36
+
+
+
+
diff --git a/src/main/java/dev/vality/orgmanager/config/ConfigResolverConfig.java b/src/main/java/dev/vality/orgmanager/config/ConfigResolverConfig.java
index bf7af82b..7152cfd0 100644
--- a/src/main/java/dev/vality/orgmanager/config/ConfigResolverConfig.java
+++ b/src/main/java/dev/vality/orgmanager/config/ConfigResolverConfig.java
@@ -2,51 +2,65 @@
import com.google.common.base.Strings;
import dev.vality.orgmanager.config.properties.KeyCloakProperties;
-import org.keycloak.adapters.KeycloakConfigResolver;
-import org.keycloak.adapters.KeycloakDeployment;
-import org.keycloak.adapters.KeycloakDeploymentBuilder;
-import org.keycloak.adapters.springsecurity.config.KeycloakSpringConfigResolverWrapper;
-import org.keycloak.representations.adapters.config.AdapterConfig;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
-import org.springframework.context.annotation.DependsOn;
+import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
+import org.springframework.security.oauth2.core.OAuth2TokenValidator;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.jwt.JwtDecoder;
+import org.springframework.security.oauth2.jwt.JwtValidators;
+import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import java.io.IOException;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
import java.nio.file.Files;
import java.nio.file.Paths;
+import java.util.Base64;
import java.util.List;
import java.util.stream.Collectors;
@Configuration
+@ConditionalOnProperty(value = "auth.enabled", havingValue = "true")
public class ConfigResolverConfig {
@Bean
- public KeycloakConfigResolver keycloakConfigResolver(KeyCloakProperties keyCloakProperties) {
- return facade -> {
- KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(adapterConfig(keyCloakProperties));
- deployment.setNotBefore(keyCloakProperties.getNotBefore());
- return deployment;
- };
+ public JwtDecoder jwtDecoder(KeyCloakProperties keyCloakProperties) {
+ RSAPublicKey publicKey = readPublicKey(keycloakRealmPublicKey(keyCloakProperties));
+ NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey(publicKey).build();
+
+ String issuer = String.format(
+ "%s/realms/%s",
+ keyCloakProperties.getAuthServerUrl(),
+ keyCloakProperties.getRealm()
+ );
+ OAuth2TokenValidator withIssuer = JwtValidators.createDefaultWithIssuer(issuer);
+ OAuth2TokenValidator validator = new DelegatingOAuth2TokenValidator<>(withIssuer);
+ jwtDecoder.setJwtValidator(validator);
+ return jwtDecoder;
}
- private AdapterConfig adapterConfig(KeyCloakProperties keyCloakProperties) {
- String keycloakRealmPublicKey;
+ private String keycloakRealmPublicKey(KeyCloakProperties keyCloakProperties) {
if (!Strings.isNullOrEmpty(keyCloakProperties.getRealmPublicKeyFilePath())) {
- keycloakRealmPublicKey = readKeyFromFile(keyCloakProperties.getRealmPublicKeyFilePath());
- } else {
- keycloakRealmPublicKey = keyCloakProperties.getRealmPublicKey();
+ return readKeyFromFile(keyCloakProperties.getRealmPublicKeyFilePath());
}
+ return keyCloakProperties.getRealmPublicKey();
+ }
- AdapterConfig adapterConfig = new AdapterConfig();
- adapterConfig.setRealm(keyCloakProperties.getRealm());
- adapterConfig.setRealmKey(keycloakRealmPublicKey);
- adapterConfig.setResource(keyCloakProperties.getResource());
- adapterConfig.setAuthServerUrl(keyCloakProperties.getAuthServerUrl());
- adapterConfig.setUseResourceRoleMappings(true);
- adapterConfig.setBearerOnly(true);
- adapterConfig.setSslRequired(keyCloakProperties.getSslRequired());
- return adapterConfig;
+ private RSAPublicKey readPublicKey(String publicKey) {
+ String normalizedPublicKey = publicKey.replaceAll("\\s", "");
+ byte[] decoded = Base64.getDecoder().decode(normalizedPublicKey);
+ X509EncodedKeySpec spec = new X509EncodedKeySpec(decoded);
+ try {
+ KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+ return (RSAPublicKey) keyFactory.generatePublic(spec);
+ } catch (NoSuchAlgorithmException | InvalidKeySpecException ex) {
+ throw new IllegalStateException("Unable to parse keycloak realm public key", ex);
+ }
}
private String readKeyFromFile(String filePath) {
diff --git a/src/main/java/dev/vality/orgmanager/config/PartyManagementConfig.java b/src/main/java/dev/vality/orgmanager/config/PartyManagementConfig.java
deleted file mode 100644
index c810f800..00000000
--- a/src/main/java/dev/vality/orgmanager/config/PartyManagementConfig.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package dev.vality.orgmanager.config;
-
-import dev.vality.damsel.payment_processing.PartyManagementSrv;
-import dev.vality.woody.thrift.impl.http.THSpawnClientBuilder;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.core.io.Resource;
-
-import java.io.IOException;
-
-@Configuration
-public class PartyManagementConfig {
-
- @Bean
- public PartyManagementSrv.Iface partyManagementClient(
- @Value("${party-management.url}") Resource resource,
- @Value("${party-management.networkTimeout}") int networkTimeout
- ) throws IOException {
- return new THSpawnClientBuilder()
- .withNetworkTimeout(networkTimeout)
- .withAddress(resource.getURI())
- .build(PartyManagementSrv.Iface.class);
- }
-
-}
diff --git a/src/main/java/dev/vality/orgmanager/config/SecurityConfig.java b/src/main/java/dev/vality/orgmanager/config/SecurityConfig.java
index a596a0b8..71c11b5a 100644
--- a/src/main/java/dev/vality/orgmanager/config/SecurityConfig.java
+++ b/src/main/java/dev/vality/orgmanager/config/SecurityConfig.java
@@ -1,66 +1,47 @@
package dev.vality.orgmanager.config;
-import org.keycloak.adapters.springsecurity.KeycloakSecurityComponents;
-import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
-import org.keycloak.adapters.springsecurity.management.HttpSessionManager;
-import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.context.annotation.*;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
-import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
-import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
-import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
-import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;
-import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@Configuration
-@EnableWebSecurity
-@ComponentScan(
- basePackageClasses = KeycloakSecurityComponents.class,
- excludeFilters = @ComponentScan.Filter(
- type = FilterType.REGEX,
- pattern = "org.keycloak.adapters.springsecurity.management.HttpSessionManager"
- )
-)
-@EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true)
+@EnableMethodSecurity(prePostEnabled = true)
@ConditionalOnProperty(value = "auth.enabled", havingValue = "true")
-@DependsOn("configResolverConfig")
-public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
-
- @Override
- protected HttpSessionManager httpSessionManager() {
- return super.httpSessionManager();
- }
+public class SecurityConfig {
@Bean
- @Override
- protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
- return new NullAuthenticatedSessionStrategy();
- }
-
- @Override
- protected void configure(HttpSecurity http) throws Exception {
- super.configure(http);
+ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
- .cors().and()
- .csrf().disable()
- .authorizeRequests()
- .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
- .antMatchers(HttpMethod.GET, "/**/health").permitAll()
- .antMatchers(HttpMethod.GET, "/**/health/readiness").permitAll()
- .antMatchers(HttpMethod.GET, "/**/health/liveness").permitAll()
- .antMatchers("/**/auth-context").permitAll()
- .antMatchers(HttpMethod.GET, "/**/prometheus").permitAll()
- .anyRequest().authenticated();
- }
-
- @Autowired
- public void configureGlobal(AuthenticationManagerBuilder auth) {
- auth.authenticationProvider(keycloakAuthenticationProvider());
+ .cors(Customizer.withDefaults())
+ .csrf(AbstractHttpConfigurer::disable)
+ .sessionManagement(config -> config.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+ .authorizeHttpRequests(config -> config
+ .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
+ .requestMatchers(
+ HttpMethod.GET,
+ "/health",
+ "/health/readiness",
+ "/health/liveness",
+ "/actuator/health",
+ "/actuator/health/readiness",
+ "/actuator/health/liveness",
+ "/prometheus",
+ "/actuator/prometheus"
+ ).permitAll()
+ .requestMatchers("/auth-context").permitAll()
+ .anyRequest().authenticated())
+ .oauth2ResourceServer(config -> config.jwt(Customizer.withDefaults()));
+ return http.build();
}
@Bean
diff --git a/src/main/java/dev/vality/orgmanager/config/SecurityDisabledConfig.java b/src/main/java/dev/vality/orgmanager/config/SecurityDisabledConfig.java
new file mode 100644
index 00000000..0637eaf4
--- /dev/null
+++ b/src/main/java/dev/vality/orgmanager/config/SecurityDisabledConfig.java
@@ -0,0 +1,21 @@
+package dev.vality.orgmanager.config;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.web.SecurityFilterChain;
+
+@Configuration
+@ConditionalOnProperty(value = "auth.enabled", havingValue = "false")
+public class SecurityDisabledConfig {
+
+ @Bean
+ public SecurityFilterChain disabledSecurityFilterChain(HttpSecurity http) throws Exception {
+ http
+ .csrf(AbstractHttpConfigurer::disable)
+ .authorizeHttpRequests(config -> config.anyRequest().permitAll());
+ return http.build();
+ }
+}
diff --git a/src/main/java/dev/vality/orgmanager/controller/OrgsController.java b/src/main/java/dev/vality/orgmanager/controller/OrgsController.java
index 9aaf8aeb..9d102764 100644
--- a/src/main/java/dev/vality/orgmanager/controller/OrgsController.java
+++ b/src/main/java/dev/vality/orgmanager/controller/OrgsController.java
@@ -11,8 +11,8 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RestController;
-import javax.validation.Valid;
-import javax.validation.constraints.Size;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.Size;
import java.util.Objects;
@Slf4j
@@ -128,15 +128,15 @@ public ResponseEntity listInvitations(String requestId,
public ResponseEntity revokeInvitation(String requestId,
String orgId,
String invitationId,
- InlineObject1 inlineObject1) {
+ RevokeInvitationRequest request) {
log.info("Revoke invitation: requestId={}, orgId={}, invitationId={}, payload={}",
- requestId, orgId, invitationId, inlineObject1);
+ requestId, orgId, invitationId, request);
ResourceDto resource = ResourceDto.builder()
.orgId(orgId)
.invitationId(invitationId)
.build();
resourceAccessService.checkRights(resource);
- invitationService.revoke(orgId, invitationId, inlineObject1);
+ invitationService.revoke(orgId, invitationId, request);
return ResponseEntity
.status(HttpStatus.NO_CONTENT)
.build();
@@ -167,12 +167,12 @@ public ResponseEntity listOrgRoles(String requestId, St
}
@Override
- public ResponseEntity patchOrg(String requestId, String orgId, InlineObject inlineObject) {
+ public ResponseEntity patchOrg(String requestId, String orgId, PatchOrgRequest request) {
ResourceDto resource = ResourceDto.builder()
.orgId(orgId)
.build();
resourceAccessService.checkRights(resource);
- Organization modifiedOrganization = organizationService.modify(orgId, inlineObject.getName());
+ Organization modifiedOrganization = organizationService.modify(orgId, request.getName());
return ResponseEntity.ok(modifiedOrganization);
}
diff --git a/src/main/java/dev/vality/orgmanager/controller/UserController.java b/src/main/java/dev/vality/orgmanager/controller/UserController.java
index c4c2e240..668de83e 100644
--- a/src/main/java/dev/vality/orgmanager/controller/UserController.java
+++ b/src/main/java/dev/vality/orgmanager/controller/UserController.java
@@ -12,7 +12,7 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RestController;
-import javax.validation.Valid;
+import jakarta.validation.Valid;
@Slf4j
@RestController
diff --git a/src/main/java/dev/vality/orgmanager/entity/InvitationEntity.java b/src/main/java/dev/vality/orgmanager/entity/InvitationEntity.java
index 8d3f4266..520f2a66 100644
--- a/src/main/java/dev/vality/orgmanager/entity/InvitationEntity.java
+++ b/src/main/java/dev/vality/orgmanager/entity/InvitationEntity.java
@@ -2,7 +2,7 @@
import lombok.*;
-import javax.persistence.*;
+import jakarta.persistence.*;
import java.io.Serializable;
import java.time.LocalDateTime;
diff --git a/src/main/java/dev/vality/orgmanager/entity/MemberContextEntity.java b/src/main/java/dev/vality/orgmanager/entity/MemberContextEntity.java
index 41b07c47..2a558ade 100644
--- a/src/main/java/dev/vality/orgmanager/entity/MemberContextEntity.java
+++ b/src/main/java/dev/vality/orgmanager/entity/MemberContextEntity.java
@@ -5,7 +5,7 @@
import lombok.Data;
import lombok.NoArgsConstructor;
-import javax.persistence.*;
+import jakarta.persistence.*;
import java.io.Serializable;
diff --git a/src/main/java/dev/vality/orgmanager/entity/MemberEntity.java b/src/main/java/dev/vality/orgmanager/entity/MemberEntity.java
index ccc7951c..6130f590 100644
--- a/src/main/java/dev/vality/orgmanager/entity/MemberEntity.java
+++ b/src/main/java/dev/vality/orgmanager/entity/MemberEntity.java
@@ -2,7 +2,7 @@
import lombok.*;
-import javax.persistence.*;
+import jakarta.persistence.*;
import java.io.Serializable;
import java.util.HashSet;
diff --git a/src/main/java/dev/vality/orgmanager/entity/MemberRoleEntity.java b/src/main/java/dev/vality/orgmanager/entity/MemberRoleEntity.java
index 3ba6c34a..6ed55000 100644
--- a/src/main/java/dev/vality/orgmanager/entity/MemberRoleEntity.java
+++ b/src/main/java/dev/vality/orgmanager/entity/MemberRoleEntity.java
@@ -2,9 +2,9 @@
import lombok.*;
-import javax.persistence.Entity;
-import javax.persistence.Id;
-import javax.persistence.Table;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
import java.io.Serializable;
diff --git a/src/main/java/dev/vality/orgmanager/entity/OrganizationEntity.java b/src/main/java/dev/vality/orgmanager/entity/OrganizationEntity.java
index b279eb9b..c5ccf292 100644
--- a/src/main/java/dev/vality/orgmanager/entity/OrganizationEntity.java
+++ b/src/main/java/dev/vality/orgmanager/entity/OrganizationEntity.java
@@ -4,7 +4,7 @@
import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode;
-import javax.persistence.*;
+import jakarta.persistence.*;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.HashSet;
diff --git a/src/main/java/dev/vality/orgmanager/entity/OrganizationRoleEntity.java b/src/main/java/dev/vality/orgmanager/entity/OrganizationRoleEntity.java
index a43e5803..eb563913 100644
--- a/src/main/java/dev/vality/orgmanager/entity/OrganizationRoleEntity.java
+++ b/src/main/java/dev/vality/orgmanager/entity/OrganizationRoleEntity.java
@@ -2,7 +2,7 @@
import lombok.*;
-import javax.persistence.*;
+import jakarta.persistence.*;
import java.io.Serializable;
import java.util.Set;
diff --git a/src/main/java/dev/vality/orgmanager/entity/ScopeEntity.java b/src/main/java/dev/vality/orgmanager/entity/ScopeEntity.java
index a2b8c58a..b507851a 100644
--- a/src/main/java/dev/vality/orgmanager/entity/ScopeEntity.java
+++ b/src/main/java/dev/vality/orgmanager/entity/ScopeEntity.java
@@ -5,9 +5,9 @@
import lombok.Data;
import lombok.NoArgsConstructor;
-import javax.persistence.Entity;
-import javax.persistence.Id;
-import javax.persistence.Table;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
import java.io.Serializable;
@Entity
diff --git a/src/main/java/dev/vality/orgmanager/exception/RestExceptionHandler.java b/src/main/java/dev/vality/orgmanager/exception/RestExceptionHandler.java
index 4dffa5f3..8fc6c146 100644
--- a/src/main/java/dev/vality/orgmanager/exception/RestExceptionHandler.java
+++ b/src/main/java/dev/vality/orgmanager/exception/RestExceptionHandler.java
@@ -1,7 +1,8 @@
package dev.vality.orgmanager.exception;
-import dev.vality.swag.organizations.model.InlineResponse422;
-import dev.vality.swag.organizations.model.InlineResponse4222;
+import dev.vality.swag.organizations.model.JoinOrg422Response;
+import dev.vality.swag.organizations.model.RemoveMemberRole422Response;
+import dev.vality.swag.organizations.model.RevokeInvitation422Response;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@@ -37,33 +38,33 @@ protected ResponseEntity