ApiTokenServiceImpl.java

/*
 * Copyright 2023 Global Crop Diversity Trust
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.genesys.blocks.tokenauth.service.impl;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.DigestUtils;
import org.genesys.blocks.security.model.AclSid;
import org.genesys.blocks.tokenauth.model.ApiToken;
import org.genesys.blocks.tokenauth.persistence.ApiTokenPersistence;
import org.genesys.blocks.tokenauth.service.ApiTokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;

import java.time.Instant;
import java.util.List;
import java.util.UUID;

/**
 * API Token services
 */
@Service
@Slf4j
public class ApiTokenServiceImpl implements ApiTokenService {

	@Value("${apitoken.salt:hellothere}")
	private String tokenSalt;

	@Autowired
	private ApiTokenPersistence apiTokenPersistence;

	@Override
	@Cacheable(cacheNames = { "api.tokenauth.encoded" }, key = "#token", unless = "#result == null")
	public String encodeToken(@NonNull String token) {
		log.debug("Encoding token {}", token);
		return Base64.encodeBase64String(DigestUtils.sha256(tokenSalt.concat(token)));
	}

	@Override
	@Transactional(readOnly = true)
	@PostAuthorize("hasRole('ADMINISTRATOR') || returnObject.sid.id == principal.id") // or hasPermission(#id, 'org.genesys.blocks.tokenauth.model.ApiToken', 'READ')")
	public ApiToken loadById(Long id) {
		return apiTokenPersistence.findById(id).orElse(null);
	}

	@Override
	@Transactional(readOnly = true)
	@Cacheable(cacheNames = { "api.tokenauth.tokens" }, key = "#encodedToken", unless = "#result == null")
	public ApiToken getToken(String encodedToken) {
		log.debug("Loading token from database {}", encodedToken);
		return apiTokenPersistence.findByToken(encodedToken).orElse(null);
	}

	@Override
	@Transactional(readOnly = true)
	@PreAuthorize("hasRole('ADMINISTRATOR') || (hasRole('VETTEDUSER') && #sid.id == principal.id)") // Admins and vetted users can list tokens
	public List<ApiToken> listTokensForSid(AclSid sid) {
		return apiTokenPersistence.findAllBySid(sid);
	}

	@Override
	@Transactional(readOnly = true)
	@PreAuthorize("hasRole('ADMINISTRATOR')")
	public Page<ApiToken> listTokens(Pageable page) {
		return apiTokenPersistence.findAll(page);
	}

	@Override
	@Transactional
	@PreAuthorize("hasRole('ADMINISTRATOR') || (hasRole('VETTEDUSER') && #sid.id == principal.id)") // Admins and vetted users can create tokens
	public ApiToken createToken(AclSid sid, String label, Instant expires) {
		ApiToken apiToken = new ApiToken();
		apiToken.setSid(sid);
		apiToken.setExpires(expires);
		apiToken.setLabel(label);
		String token = null;
		for (int i = 0; i < 10; i++) {
			token = UUID.randomUUID().toString();
			apiToken.setToken(encodeToken(token));
			if (apiTokenPersistence.findByToken(apiToken.getToken()).isEmpty())
				break;
			log.info("Token already exists, generating a new one");
		}
		var saved = apiTokenPersistence.save(apiToken);
		saved.setToken(token);
		log.info("Created a new token for {} with label={} expires={}", apiToken.getSid().getSid(), apiToken.getLabel(), apiToken.getExpires());
		return saved;
	}

	@Override
	@Transactional
	@PostAuthorize("hasRole('ADMINISTRATOR') || (hasRole('VETTEDUSER') && returnObject.sid.id == principal.id)")
	@CacheEvict(cacheNames = { "api.tokenauth.tokens" }, key = "#result.token", condition = "#result != null")
	public ApiToken remove(ApiToken apiToken) {
		assert (apiToken != null && apiToken.getId() != null);
		ApiToken token = apiTokenPersistence.findById(apiToken.getId()).orElse(null);
		if (token == null) {
			return null;
		} else {
			log.info("Deleting token for {} with label={}", token.getSid().getSid(), token.getLabel());
			apiTokenPersistence.delete(token);
			return token;
		}
	}

	@Override
	@Transactional
	@PostAuthorize("hasRole('ADMINISTRATOR') || (hasRole('VETTEDUSER') && returnObject.sid.id == principal.id)")
	@CacheEvict(cacheNames = { "api.tokenauth.tokens" }, key = "#result.token", condition = "#result != null")
	public ApiToken update(ApiToken apiToken) {
		ApiToken toUpdate = apiTokenPersistence.findById(apiToken.getId()).orElse(null);

		if (toUpdate == null) {
			return null;
		}
		log.info("Updating token for {} with label={}", toUpdate.getSid().getSid(), toUpdate.getLabel());
		toUpdate.apply(apiToken);
		return apiTokenPersistence.save(toUpdate);
	}
}