Skip to content

Commit

Permalink
feat: Add manageState permission (#1313)
Browse files Browse the repository at this point in the history
  • Loading branch information
stanleyz authored Sep 25, 2024
1 parent 7b291c6 commit de4b739
Show file tree
Hide file tree
Showing 20 changed files with 290 additions and 105 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.terrakube.api.plugin.security.state;

import java.util.List;
import java.util.UUID;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.stereotype.Service;
import org.terrakube.api.repository.TeamRepository;
import org.terrakube.api.rs.team.Team;

@Service
public class StateService {
@Autowired
private TeamRepository teamRepository;

public boolean hasManageStatePermission(Authentication authentication, String orgnizationId) {
Object groupNames = ((JwtAuthenticationToken) authentication).getTokenAttributes().get("groups");
if (groupNames == null) {
return false;
}
@SuppressWarnings("unchecked")
List<Team> teams = teamRepository.findAllByOrganizationIdAndNameIn(UUID.fromString(orgnizationId), (List<String>) groupNames);
for (Team team : teams) {
if (team.isManageState()) {
return true;
}
}

return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public ResponseEntity<String> ping() {
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.set("TFP-API-Version", "2.5");
responseHeaders.set("TFP-AppName", "Terrakube");
ResponseEntity response = new ResponseEntity<>(responseHeaders, HttpStatus.NOT_FOUND);
ResponseEntity<String> response = new ResponseEntity<>(responseHeaders, HttpStatus.NOT_FOUND);
return response;
}

Expand Down Expand Up @@ -159,7 +159,7 @@ public ResponseEntity<WorkspaceData> lockWorkspace(@PathVariable("workspaceId")
log.info("Lock {}", workspaceId);
if (remoteTfeService.isWorkspaceLocked(workspaceId)) {
WorkspaceData workspaceData = new WorkspaceData();
workspaceData.setErrors(new ArrayList());
workspaceData.setErrors(new ArrayList<WorkspaceError>());
WorkspaceError workspaceError = new WorkspaceError();
workspaceError.setStatus("409");
workspaceError.setTitle("conflict");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -312,12 +312,12 @@ WorkspaceData getWorkspace(String organizationName, String workspaceName, Map<St
defaultAttributes.put("can-lock", isManageWorkspace);
defaultAttributes.put("can-manage-run-tasks", isManageWorkspace);
defaultAttributes.put("can-manage-tags", isManageWorkspace);
defaultAttributes.put("can-queue-apply", isManageWorkspace);
defaultAttributes.put("can-queue-apply", true);
defaultAttributes.put("can-queue-destroy", isManageWorkspace);
defaultAttributes.put("can-queue-run", isManageWorkspace);
defaultAttributes.put("can-read-settings", isManageWorkspace);
defaultAttributes.put("can-queue-run", true);
defaultAttributes.put("can-read-settings", true);
defaultAttributes.put("can-read-state-versions", isManageWorkspace);
defaultAttributes.put("can-read-variable", isManageWorkspace);
defaultAttributes.put("can-read-variable", true);
defaultAttributes.put("can-unlock", isManageWorkspace);
defaultAttributes.put("can-update", isManageWorkspace);
defaultAttributes.put("can-update-variable", isManageWorkspace);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
package org.terrakube.api.plugin.storage.controller;

import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import java.util.UUID;

import org.apache.commons.io.IOUtils;
import org.springframework.http.ResponseEntity;
import org.terrakube.api.plugin.storage.StorageTypeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.terrakube.api.plugin.security.state.StateService;
import org.terrakube.api.plugin.storage.StorageTypeService;
import org.terrakube.api.repository.ArchiveRepository;
import org.terrakube.api.repository.HistoryRepository;
import org.terrakube.api.repository.WorkspaceRepository;
import org.terrakube.api.rs.workspace.history.History;
import org.terrakube.api.rs.workspace.history.archive.Archive;

import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import java.util.UUID;
import lombok.extern.slf4j.Slf4j;

@RestController
@Slf4j
Expand All @@ -28,6 +37,9 @@ public class TerraformStateController {
private final ArchiveRepository archiveRepository;
private final WorkspaceRepository workspaceRepository;
private final HistoryRepository historyRepository;
@SuppressWarnings("unused")
@Autowired
private StateService stateService;
private final String hostname;

public TerraformStateController(StorageTypeService storageTypeService,
Expand All @@ -49,12 +61,14 @@ public TerraformStateController(StorageTypeService storageTypeService,
}

@GetMapping(value = "/organization/{organizationId}/workspace/{workspaceId}/state/{stateFilename}.json", produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("@stateService.hasManageStatePermission(authentication, #organizationId)")
public @ResponseBody byte[] getTerraformStateJson(@PathVariable("organizationId") String organizationId,
@PathVariable("workspaceId") String workspaceId, @PathVariable("stateFilename") String stateFilename) {
return storageTypeService.getTerraformStateJson(organizationId, workspaceId, stateFilename);
}

@GetMapping(value = "/organization/{organizationId}/workspace/{workspaceId}/state/terraform.tfstate", produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("@stateService.hasManageStatePermission(authentication, #organizationId)")
public @ResponseBody byte[] getCurrentTerraformState(@PathVariable("organizationId") String organizationId,
@PathVariable("workspaceId") String workspaceId) {
return storageTypeService.getCurrentTerraformState(organizationId, workspaceId);
Expand Down Expand Up @@ -105,6 +119,7 @@ public ResponseEntity<String> uploadJsonHostedState(HttpServletRequest httpServl
}

@PutMapping(value = "/organization/{organizationId}/workspace/{workspaceId}/rollback/{stateFilename}.json", produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("@stateService.hasManageStatePermission(authentication, #organizationId)")
public ResponseEntity<String> rollbackToState(
@PathVariable("organizationId") String organizationId,
@PathVariable("workspaceId") String workspaceId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
package org.terrakube.api.plugin.token.team;

import java.security.Principal;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.terrakube.api.repository.TeamRepository;
import org.terrakube.api.rs.token.group.Group;

import java.security.Principal;
import java.util.ArrayList;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RestController
Expand All @@ -23,9 +31,11 @@
public class TeamTokenController {

TeamTokenService teamTokenService;
TeamRepository teamRepository;

@PostMapping
public ResponseEntity<TeamToken> createToken(@RequestBody GroupTokenRequest groupTokenRequest, Principal principal) {
public ResponseEntity<TeamToken> createToken(@RequestBody GroupTokenRequest groupTokenRequest,
Principal principal) {
TeamToken teamToken = new TeamToken();
teamToken.setToken(teamTokenService.createTeamToken(
groupTokenRequest.getGroup(),
Expand All @@ -37,25 +47,46 @@ public ResponseEntity<TeamToken> createToken(@RequestBody GroupTokenRequest grou
}

@GetMapping("/current-teams")
public ResponseEntity<CurrentGroupsResponse> SearchTeams(Principal principal){
public ResponseEntity<CurrentGroupsResponse> SearchTeams(Principal principal) {
JwtAuthenticationToken principalJwt = ((JwtAuthenticationToken) principal);
CurrentGroupsResponse groupList = new CurrentGroupsResponse();
groupList.setGroups(new ArrayList());
teamTokenService.getCurrentGroups(principalJwt).forEach(group->{
groupList.setGroups(new ArrayList<String>());
teamTokenService.getCurrentGroups(principalJwt).forEach(group -> {
groupList.getGroups().add(group);
});
return new ResponseEntity<>(groupList, HttpStatus.ACCEPTED);
}

@GetMapping(path = "/permissions/organization/{organizationId}")
public ResponseEntity<PermissionSet> getPermissions(Principal principal,
@PathVariable("organizationId") String organizationId) {
JwtAuthenticationToken principalJwt = ((JwtAuthenticationToken) principal);
PermissionSet permissions = new PermissionSet();
List<String> groups = teamTokenService.getCurrentGroups(principalJwt);
teamRepository.findAllByOrganizationIdAndNameIn(UUID.fromString(organizationId), groups).forEach(group -> {
permissions.setManageState(permissions.manageState || group.isManageState());
permissions.setManageWorkspace(permissions.manageWorkspace || group.isManageWorkspace());
permissions.setManageModule(permissions.manageModule || group.isManageModule());
permissions.setManageProvider(permissions.manageProvider || group.isManageProvider());
permissions.setManageTemplate(permissions.manageTemplate || group.isManageTemplate());
permissions.setManageVcs(permissions.manageVcs || group.isManageVcs());
});
return new ResponseEntity<>(permissions, HttpStatus.ACCEPTED);
}

@Transactional
@DeleteMapping(path = "/{groupTokenId}")
public ResponseEntity<String> deleteToken(@PathVariable("groupTokenId") String groupTokenId){
if (teamTokenService.deleteToken(groupTokenId)) return ResponseEntity.accepted().build(); else return ResponseEntity.badRequest().build();
public ResponseEntity<String> deleteToken(@PathVariable("groupTokenId") String groupTokenId) {
if (teamTokenService.deleteToken(groupTokenId))
return ResponseEntity.accepted().build();
else
return ResponseEntity.badRequest().build();
}

@GetMapping
public ResponseEntity<List<Group>> searchToken(Principal principal){
return new ResponseEntity<>(teamTokenService.searchToken(((JwtAuthenticationToken) principal)), HttpStatus.ACCEPTED);
public ResponseEntity<List<Group>> searchToken(Principal principal) {
return new ResponseEntity<>(teamTokenService.searchToken(((JwtAuthenticationToken) principal)),
HttpStatus.ACCEPTED);
}

@Getter
Expand All @@ -72,12 +103,22 @@ public static class TeamToken {

@Getter
@Setter
private static class GroupTokenRequest{
private static class GroupTokenRequest {
private String group;
private String description;
private int days = 0;
private int minutes = 0;
private int hours = 0;
}

@Getter
@Setter
private class PermissionSet {
private boolean manageState;
private boolean manageWorkspace;
private boolean manageModule;
private boolean manageProvider;
private boolean manageVcs;
private boolean manageTemplate;
}
}
11 changes: 11 additions & 0 deletions api/src/main/java/org/terrakube/api/repository/TeamRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.terrakube.api.repository;

import java.util.List;
import java.util.UUID;

import org.springframework.data.jpa.repository.JpaRepository;
import org.terrakube.api.rs.team.Team;

public interface TeamRepository extends JpaRepository<Team, UUID> {
List<Team> findAllByOrganizationIdAndNameIn(UUID organizationId, List<String> names);
}
28 changes: 20 additions & 8 deletions api/src/main/java/org/terrakube/api/rs/team/Team.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
package org.terrakube.api.rs.team;

import com.yahoo.elide.annotation.*;
import lombok.Getter;
import lombok.Setter;
import java.sql.Types;
import java.util.UUID;

import org.hibernate.annotations.JdbcTypeCode;
import org.terrakube.api.rs.IdConverter;
import org.terrakube.api.rs.Organization;
import org.hibernate.annotations.Type;

import jakarta.persistence.*;

import java.sql.Types;
import java.util.UUID;
import com.yahoo.elide.annotation.CreatePermission;
import com.yahoo.elide.annotation.DeletePermission;
import com.yahoo.elide.annotation.Include;
import com.yahoo.elide.annotation.UpdatePermission;

import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import lombok.Getter;
import lombok.Setter;

@CreatePermission(expression = "user is a superuser")
@UpdatePermission(expression = "user is a superuser")
Expand All @@ -31,6 +40,9 @@ public class Team {
@Column(name = "name")
private String name;

@Column(name = "manage_state")
private boolean manageState;

@Column(name = "manage_workspace")
private boolean manageWorkspace;

Expand Down
21 changes: 13 additions & 8 deletions api/src/main/java/org/terrakube/api/rs/token/group/Group.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
package org.terrakube.api.rs.token.group;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.sql.Types;
import java.util.UUID;

import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.annotations.Type;
import org.terrakube.api.plugin.security.audit.GenericAuditFields;

import jakarta.persistence.*;
import org.terrakube.api.rs.IdConverter;

import java.sql.Types;
import java.util.UUID;
import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@NoArgsConstructor
@Getter
Expand Down
1 change: 1 addition & 0 deletions api/src/main/resources/db/changelog/changelog.xml
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,5 @@
<include file="/db/changelog/local/changelog-2.22.1-module-github-token-id.xml"/>
<include file="/db/changelog/local/changelog-2.22.2-webhook-triggers.xml"/>
<include file="/db/changelog/local/changelog-2.23.0-job-reference-size.xml"/>
<include file="/db/changelog/local/changelog-2.23.0-team-manage-state.xml"/>
</databaseChangeLog>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
<changeSet id="2-23-0-2" author="Stanley Zhang (stanley.zhang@ityin.net)">
<addColumn tableName="team">
<column name="manage_state" type="boolean" defaultValue="false"/>
</addColumn>
</changeSet>
</databaseChangeLog>
2 changes: 1 addition & 1 deletion dynamic-credential-setup/azure/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ resource "random_string" "random" {
}

resource "azuread_application_registration" "terrakube_application" {
display_name = "terrakube-dynamic-creds-${random_string.random.result}"
display_name = "terrakube-dynamic-creds-${random_string.random.result}"
}

resource "azuread_service_principal" "terrakube_service_principal" {
Expand Down
Loading

0 comments on commit de4b739

Please sign in to comment.