Skip to content

Commit

Permalink
feat: organization tokens (#528)
Browse files Browse the repository at this point in the history
  • Loading branch information
leg100 authored Jul 24, 2023
1 parent b50056f commit 7ddd416
Show file tree
Hide file tree
Showing 65 changed files with 983 additions and 124 deletions.
8 changes: 8 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ repos:
exclude: '.tfstate|run|testdata'
- repo: local
hooks:
- id: make-actions
name: make actions
entry: make actions
types: [go]
language: "golang"
require_serial: true
pass_filenames: false
description: "Runs `make actions`"
- id: make-vet
name: make vet
entry: make vet
Expand Down
19 changes: 19 additions & 0 deletions docs/auth/org_token.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Organization Tokens

Each organization can have an API token. Only an owner can create or delete the token.

They are equivalent to organization tokens in [Terraform Cloud](https://developer.hashicorp.com/terraform/cloud-docs/users-teams-organizations/api-tokens#organization-api-tokens). They possess the same permissions as those documented for Terraform Cloud.

To manage your tokens, go to your organization main menu and select **organization token**:

![organization main menu](../images/organization_main_menu.png){.screenshot .crop}

Create the token:

![new token](../images/org_token_new.png){.screenshot .crop}

The token is displayed:

![token created](../images/org_token_created.png){.screenshot .crop}

Click the clipboard icon to copy the token to your system clipboard. You can then use the token to authenticate via the [API](https://developer.hashicorp.com/terraform/cloud-docs/api-docs) or the `otf` CLI.
Binary file modified docs/images/connected_workspace_main_page.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/github_login_button.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/modules_confirm.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/modules_list.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/modules_select_provider.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/modules_select_repo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/new_github_vcs_provider_form.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/new_org_created.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/new_org_enter_name.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/newly_created_module_page.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/no_authenticators_site_admin_login.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/oidc_login_button.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/org_token_created.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/org_token_new.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/organization_main_menu.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/owners_team_page.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/run_page_planned_and_finished_state.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/run_page_planned_state.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/run_page_started.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/site_admin_login_enter_token.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/site_admin_profile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/team_permissions_added_workspace_manager.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/terraform_login_consent.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/user_token_created.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/user_token_enter_description.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/user_tokens.png
Binary file modified docs/images/variables_entering_top_secret.png
Binary file modified docs/images/vcs_providers_list.png
Binary file modified docs/images/workspace_main_page.png
Binary file modified docs/images/workspace_page.png
Binary file modified docs/images/workspace_permissions.png
Binary file modified docs/images/workspace_settings.png
Binary file modified docs/images/workspace_vcs_providers_list.png
Binary file modified docs/images/workspace_vcs_repo_list.png
1 change: 1 addition & 0 deletions hack/go-tfe-tests-upstream.bash
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ tests+=('TestOrganizationTagsList/with_no_param_Filter')
#tests+=('TestOrganizationTagsList/with_no_param_Query')
tests+=('TestOrganizationTagsDelete')
tests+=('TestOrganizationTagsAddWorkspace')
tests+=('TestOrganizationTokens')
tests+=('TestWorkspaces_(Add|Remove)Tags')
tests+=('TestWorkspacesList/when_searching_using_a_tag')
tests+=('TestNotificationConfigurationCreate/with_a')
Expand Down
88 changes: 88 additions & 0 deletions internal/api/tokens.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import (

"github.com/DataDog/jsonapi"
"github.com/gorilla/mux"
"github.com/leg100/otf/internal"
"github.com/leg100/otf/internal/api/types"
otfhttp "github.com/leg100/otf/internal/http"
"github.com/leg100/otf/internal/http/decode"
"github.com/leg100/otf/internal/tokens"
)

Expand All @@ -19,6 +21,11 @@ func (a *api) addTokenHandlers(r *mux.Router) {

// Run token routes
r.HandleFunc("/tokens/run/create", a.createRunToken).Methods("POST")

// Organization token routes
r.HandleFunc("/organizations/{organization_name}/authentication-token", a.createOrganizationToken).Methods("POST")
r.HandleFunc("/organizations/{organization_name}/authentication-token", a.getOrganizationToken).Methods("GET")
r.HandleFunc("/organizations/{organization_name}/authentication-token", a.deleteOrganizationToken).Methods("DELETE")
}

func (a *api) createRunToken(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -74,3 +81,84 @@ func (a *api) getCurrentAgent(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-type", mediaType)
w.Write(b)
}

func (a *api) createOrganizationToken(w http.ResponseWriter, r *http.Request) {
org, err := decode.Param("organization_name", r)
if err != nil {
Error(w, err)
return
}
var opts types.OrganizationTokenCreateOptions
if err := unmarshal(r.Body, &opts); err != nil {
Error(w, err)
return
}

ot, token, err := a.CreateOrganizationToken(r.Context(), tokens.CreateOrganizationTokenOptions{
Organization: org,
Expiry: opts.ExpiredAt,
})
if err != nil {
Error(w, err)
return
}

b, err := jsonapi.Marshal(&types.OrganizationToken{
ID: ot.ID,
CreatedAt: ot.CreatedAt,
Token: string(token),
ExpiredAt: ot.Expiry,
})
if err != nil {
Error(w, err)
return
}
w.Header().Set("Content-type", mediaType)
w.Write(b)
}

func (a *api) getOrganizationToken(w http.ResponseWriter, r *http.Request) {
org, err := decode.Param("organization_name", r)
if err != nil {
Error(w, err)
return
}

ot, err := a.GetOrganizationToken(r.Context(), org)
if err != nil {
Error(w, err)
return
}
if ot == nil {
Error(w, internal.ErrResourceNotFound)
return
}

b, err := jsonapi.Marshal(&types.OrganizationToken{
ID: ot.ID,
CreatedAt: ot.CreatedAt,
ExpiredAt: ot.Expiry,
})
if err != nil {
Error(w, err)
return
}
w.Header().Set("Content-type", mediaType)
w.Write(b)
}

func (a *api) deleteOrganizationToken(w http.ResponseWriter, r *http.Request) {
org, err := decode.Param("organization_name", r)
if err != nil {
Error(w, err)
return
}

err = a.DeleteOrganizationToken(r.Context(), org)
if err != nil {
Error(w, err)
return
}

w.WriteHeader(http.StatusNoContent)
}
21 changes: 21 additions & 0 deletions internal/api/types/organization_token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package types

import "time"

// OrganizationToken represents a Terraform Enterprise organization token.
type OrganizationToken struct {
ID string `jsonapi:"primary,authentication-tokens"`
CreatedAt time.Time `jsonapi:"attribute" json:"created-at"`
Token string `jsonapi:"attribute" json:"token"`
ExpiredAt *time.Time `jsonapi:"attribute" json:"expired-at"`
}

// OrganizationTokenCreateOptions contains the options for creating an organization token.
type OrganizationTokenCreateOptions struct {
// Optional: The token's expiration date.
// This feature is available in TFE release v202305-1 and later
ExpiredAt *time.Time `jsonapi:"attribute" json:"expired-at,omitempty"`
}
4 changes: 2 additions & 2 deletions internal/auth/team_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ func (a *service) CreateTeam(ctx context.Context, organization string, opts Crea
}

if err := a.db.createTeam(ctx, team); err != nil {
a.Error(err, "creating team", "name", opts.Name, "organization", organization, "subject", subject)
a.Error(err, "creating team", "name", team.Name, "organization", organization, "subject", subject)
return nil, err
}
a.V(0).Info("created team", "name", opts.Name, "organization", organization, "subject", subject)
a.V(0).Info("created team", "name", team.Name, "organization", organization, "subject", subject)

return team, nil
}
Expand Down
4 changes: 4 additions & 0 deletions internal/http/html/paths/funcmap.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

78 changes: 56 additions & 22 deletions internal/http/html/paths/gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,25 +66,34 @@ var defaultActions = []action{

// controllerSpec is a specification for a controller
type controllerSpec struct {
Name string // controller name, used in path names unless path is specified
nested []controllerSpec
path string
actions []action // additional actions
camel string
lowerCamel string
noprefix bool // disable site-wide prefix
// controller name, used in path names unless path is specified
Name string
nested []controllerSpec
path string
// additional actions
actions []action
// whether to skip default set of actions
skipDefaultActions bool
camel string
lowerCamel string
// disable site-wide prefix
noprefix bool

controllerType
}

type controller struct {
Name string
path string
Parent *controller
Actions []action // additional paths applying to individual members of collection
camel string
lowerCamel string
noprefix bool // disable site-wide prefix
Name string
path string
Parent *controller
// additional paths applying to individual members of collection
Actions []action
// whether to skip default set of actions
skipDefaultActions bool
camel string
lowerCamel string
// disable site-wide prefix
noprefix bool

controllerType
}
Expand Down Expand Up @@ -212,6 +221,26 @@ var specs = []controllerSpec{
Name: "agent_token",
controllerType: resourcePath,
},
{
Name: "organization_token",
controllerType: resourcePath,
skipDefaultActions: true,
path: "/token",
actions: []action{
{
name: "show",
collection: true,
},
{
name: "create",
collection: true,
},
{
name: "delete",
collection: true,
},
},
},
{
Name: "user",
controllerType: resourcePath,
Expand Down Expand Up @@ -432,17 +461,22 @@ func buildControllers(parent *controller, specs []controllerSpec) []controller {

for _, spec := range specs {
ctlr := controller{
Name: spec.Name,
camel: spec.camel,
lowerCamel: spec.lowerCamel,
path: spec.path,
Parent: parent,
controllerType: spec.controllerType,
noprefix: spec.noprefix,
Name: spec.Name,
camel: spec.camel,
lowerCamel: spec.lowerCamel,
path: spec.path,
Parent: parent,
controllerType: spec.controllerType,
noprefix: spec.noprefix,
skipDefaultActions: spec.skipDefaultActions,
}
switch spec.controllerType {
case resourcePath:
ctlr.Actions = append(defaultActions, spec.actions...)
if ctlr.skipDefaultActions {
ctlr.Actions = spec.actions
} else {
ctlr.Actions = append(defaultActions, spec.actions...)
}
case singlePath:
ctlr.Actions = []action{{name: "show"}}
}
Expand Down
17 changes: 17 additions & 0 deletions internal/http/html/paths/organization_token_paths.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions internal/http/html/static/css/output.css
Original file line number Diff line number Diff line change
Expand Up @@ -1123,10 +1123,6 @@ select {
padding-bottom: 0.5rem;
}

.pb-4 {
padding-bottom: 1rem;
}

.pl-10 {
padding-left: 2.5rem;
}
Expand Down Expand Up @@ -1199,6 +1195,11 @@ select {
color: rgb(107 114 128 / var(--tw-text-opacity));
}

.text-gray-600 {
--tw-text-opacity: 1;
color: rgb(75 85 99 / var(--tw-text-opacity));
}

.text-green-700 {
--tw-text-opacity: 1;
color: rgb(21 128 61 / var(--tw-text-opacity));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
</div>
</form>
<hr class="my-4">
<h3 class="font-semibold text-lg mb-2">Advanced</h3>
<form action="{{ deleteOrganizationPath .Name }}" method="POST">
<button id="delete-organization-button" class="btn-danger" {{ insufficient $canDelete }} onclick="return confirm('Are you sure you want to delete?')">
Delete organization
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
<span id="vcs_providers">
<a href="{{ vcsProvidersPath .Name }}">VCS providers</a>
</span>
<span id="organization_tokens">
<a href="{{ organizationTokenPath .Name }}">organization token</a>
</span>
{{ end }}
<span id="settings">
<a href="{{ editOrganizationPath .Name }}">settings</a>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{{ template "layout" . }}

{{ define "content-header-title" }}organization token{{ end }}

{{ define "content" }}
<span class="text-gray-600 text-sm">
The organization API token is used to manage teams, team membership and workspaces. This token does not have permission to perform plans and applies in workspaces.
</span>
{{ if .Token }}
<div class="widget">
<div>
<span>Token</span>
<span>{{ durationRound .Token.CreatedAt }} ago</span>
</div>
<div>
{{ template "identifier" .Token }}
<div class="flex gap-2">
<form action="{{ createOrganizationTokenPath .Organization }}" method="POST">
<button class="btn">regenerate</button>
</form>
<form action="{{ deleteOrganizationTokenPath .Organization }}" method="POST">
<button class="btn-danger" onclick="return confirm('Are you sure you want to delete?')">delete</button>
</form>
</div>
</div>
</div>
{{ else }}
<form class="mt-2" action="{{ createOrganizationTokenPath .Organization }}" method="POST">
<button class="btn w-72" >Create organization token</button>
</form>
{{ end }}
{{ end }}
Loading

0 comments on commit 7ddd416

Please sign in to comment.